├── .gitignore
├── README.md
├── docs
├── .nojekyll
├── BookProject.md
├── BookVault.md
├── BookViewer.md
├── Others.md
├── QuickStart.md
├── README.md
├── Thanks.md
├── ThirdPartyPlugins.md
├── _coverpage.md
├── _navbar.md
├── _sidebar.md
├── images
│ ├── add_bookvault.png
│ ├── book_explorer.png
│ ├── bookdata_setting.png
│ ├── bookvault_in_mobile.png
│ ├── bookvault_path_setting.jpg
│ ├── bookvaultpath.png
│ ├── bookvewer_setting.png
│ ├── bookviewer_deployment.png
│ ├── bookviewer_path_in_mobile.png
│ ├── bookviewer_server_setting_mobile.png
│ ├── dataview-example.png
│ ├── device_name.png
│ ├── front-matter-title-setting-1.png
│ ├── front-matter-title-setting-2.png
│ ├── open_bookvault_setting.png
│ └── vault_error.png
└── index.html
├── examples
└── dataview
│ └── display_books_table.md
├── manifest.json
├── package-lock.json
├── package.json
├── resources
└── config.js
├── src
├── Book.ts
├── BookProject.ts
├── BookVault.ts
├── RecentBooks.ts
├── components
│ ├── v-array-input.vue
│ ├── v-basic-setting.vue
│ ├── v-obtree-item.vue
│ └── v-obtree.vue
├── constants.ts
├── document_viewer
│ ├── Annotation.ts
│ ├── DocumentViewer.ts
│ ├── EPUBJSViewer.ts
│ ├── HtmlViewer.ts
│ ├── PDFTronViewer.ts
│ └── TxtViewer.ts
├── main.ts
├── settings.ts
├── styles.css
├── utils.ts
├── utils
│ └── PdfAnnotation.ts
└── view
│ ├── BasicBookSettingModal.ts
│ ├── BookExplorer.ts
│ ├── BookMasterSettingTab.ts
│ ├── BookProjectView.ts
│ ├── BookSuggestModal.ts
│ ├── BookTranslatorView.ts
│ ├── BookView.ts
│ ├── NavHeader.ts
│ └── RecentBookView.ts
├── tsconfig.json
└── vite.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | main.js
3 | /styles.css
4 | release
5 | data.json
6 | deploy.bat
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > 本插件正在持续开发中
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/BookProject.md:
--------------------------------------------------------------------------------
1 | - [ ] TODO
2 |
--------------------------------------------------------------------------------
/docs/BookVault.md:
--------------------------------------------------------------------------------
1 | - [ ] TODO
2 |
3 |
4 |
5 | ## 多书库支持
6 | ## 特殊的书库
7 | ## 云书库
--------------------------------------------------------------------------------
/docs/BookViewer.md:
--------------------------------------------------------------------------------
1 | - [ ] TODO
2 |
--------------------------------------------------------------------------------
/docs/Others.md:
--------------------------------------------------------------------------------
1 | - [ ] TODO
2 |
--------------------------------------------------------------------------------
/docs/QuickStart.md:
--------------------------------------------------------------------------------
1 |
2 | ## 添加书库
3 | 在第一次安装本插件并启用时,可能会弹出一个当前书库不存在的错误,因为插件此时还没有任何**书库**。
4 |
5 | 
6 |
7 | **书库**实际上是一个本地文件夹,这个文件夹可以在电脑上的任意位置,无需在obsidian库内。
8 |
9 | 打开设置面板,在 `BookMaster` 设置中找到`书库设置`,如下图所示,点击右侧的 `+` 号可以添加一个书库,其中:`1`为书库的唯一标识 `vid`, `2` 是可自定义的书库名称,`3` 是书库文件夹的绝对路径。点击 `5` 可以弹出窗口选择一个文件夹,选择确定后会自动将书库名设置为文件夹名。完成后点击 `4` 确认,此时即创建了一个书库。重复点击`书库设置`后侧的 `+` 号可以创建多个书库,注意每个书库都是相互独立的。
10 |
11 | 
12 |
13 |
14 |
15 | 关闭设置面板,在左侧边栏找到名为 `Book Explorer` 的icon,点击即可打开书库浏览器。当添加了多个书库时可以上边的工具栏中切换不同的书库。
16 |
17 | 
18 |
19 |
20 | 简单的看一下这个浏览器,在所有文件夹右侧有一个数字代表该文件夹下的文件数,每个文件右侧的颜色代表该文件的阅读状态:红色为`未读`,黄色为`在读`,绿色为`已读`,可以在文件的右键菜单设置阅读状态。
21 |
22 | ## 指定数据文件夹
23 |
24 | 需要为插件指定一个数据文件夹,这个文件夹必须在ob库内(这意味着你可以在ob的文件浏览器中看到这个文件夹),默认为`bookmaster`。插件产生的数据将存放在这个文件夹内。如果发现后面的设置和标注信息没有保存,请**仔细检查**这个路径,看看路径下有无文件。
25 |
26 | 
27 |
28 | 目前这个数据文件夹下主要会产生三个子文件夹
29 | - `book-data`: 存放一些`.md`文件,保存每个文件的设置,通过`.md`文件的yaml区设置每个文件的属性;
30 | - `book-annotations`: 存放标注文件;
31 | - `book-images`: 用于保存生成的标注截图;
32 |
33 | 每个子文件下都有以书库vid命名的文件夹,只保存该书库相关的文件。
34 |
35 | ## BookViewer部署
36 |
37 | 本插件附带了多种阅读器(目前只完善了pdf阅读器),提供了ob内的阅读、标注以及回链功能,下载插件提供的bookviewer 包后,需要创建web文件服务器提供对这个文件夹的访问功能。
38 |
39 | 本插件默认提供了一个测试服务器 `http://81.71.65.248:8866`,仅供测试,速度较慢,随时可能下线,请及时自行部署本地服务器。
40 |
41 | ### Windows系统下部署
42 | 最新的 `bookviewer` 包提供了`dufs.exe` 程序(tools下)用于快速搭建本地文件服务器(来自[dufs]()项目)。
43 |
44 | tools 文件夹下的 `run_dufs.vbs` 脚本用于快捷启动服务器,双击可完成后台运行服务器,该脚本文件内容为:
45 |
46 | ```vb
47 | set ws = WScript.CreateObject("WScript.Shell")
48 | ws.Run "dufs.exe --port 8866 ..\",0
49 | ```
50 |
51 | 可以看到默认的端口号为 *8866*,打开任意浏览器,输入 `http://127.0.0.1:8866`,如果看到了服务器界面,说明服务器部署成功,将地址 `http://127.0.0.1:8866` 输入到设置面板的 `BookViewer服务器地址` 即可。
52 |
53 | 
54 |
55 | 
56 |
57 | #### 实现开机自动运行
58 | 1. 在 `run_dufs.vbs` 上右键,选择创建快捷方式
59 | 2. 按 `Win + R` 键,打开运行窗口,输入 `shell:startup` 后回车打开`启动`文件夹
60 | 3. 将创建的快捷方式移动到这个文件夹下即可
61 |
62 |
63 | ### Linux 下部署
64 |
65 | > Linux使用者应该都懂的~~
66 |
67 | ### MacOS 下部署
68 | > dufs实际上也支持MacOS系统,由于本人没有条件测试,希望有条件的大佬测试后进行补充
69 |
70 | ## 阅读器基本使用
71 |
72 | TODO
73 |
74 | ## 其他
75 |
76 | ### 多设备支持
77 |
78 | 当在不同设备中使用插件时,书库文件夹的所在位置可能不同。此时可为不同设备上的软件设置对应的书库地址。
79 |
80 | 
81 |
82 | 在设置面板中的`当前设备名`下显示了当前设备的唯一 `id`,可为该设备自定义名称(只做展示用)。`id` 是从obsidian中获取的,不同设备一般不同,目前发现移动端以ob库路径为 `id`, 如果不同移动端下的ob库文件夹在同一路径,则插件无法分辨。
83 |
84 | 当在新设备中使用插件时,需要重新设置 `书库路径`以及 `BookViewer服务器地址`。
85 | 。
86 |
87 | ### 移动端部署(安卓)
88 |
89 | 由于插件对多设备的支持,可以在移动端单独设置不同的书库路径以及BookViewer服务器地址。重点是BookViewer服务器的部署,如果使用公网的服务器地址(如使用测试服务器),则像在pc端一样修改书库文件夹的地址即可。
90 |
91 | 
92 |
93 | 
94 |
95 |
96 | 由于obsidian安卓端有本地服务器的功能,实际上不需要另外部署文件服务器,只需要将bookviewer包放到移动端下, 如下图所示,服务器地址是文件夹地址前加 `http://localhost/_capacitor_file_`, 下面的图的实际地址为`http://localhost/_capacitor_file_/storage/emulated/0/Documents/bookviewer`
97 |
98 | 
99 |
100 | 
101 |
102 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | !> `obsidian-booknote-plugin` 从 0.3.0 开始更名为 `obsidian-bookmaster-plugin`,主要原因是 `booknote` 与软件 `bookxnote` 容易混淆,同时也因为 0.3.0 对插件进行了重构,是与之前版本完全不兼容的重大更新。
4 |
5 | 本插件尝试在 [obsidian](https://obsidian.md/) 上实现书籍管理,致力于构建书籍与笔记的良好关系。`Book`在本插件里已经扩展为一个十分宽泛的概念,它可以是一个文档(pdf,epub,txt等格式),也可以是一张图片,一个视频(如电影或视频教程),一段音频(如音乐)或者一个网页(html)等,在后面的文档中也将使用`Book`指代这些类型的文件。
6 | 插件通过标注,回链,BookProject等使`Book`与笔记形成相互联系的整体,也希望能实现优雅的管理与统计分析功能。重要的是,本插件坚持完全本地及安全的原则,由用户自行管理所有文档,所有管理文件也为通用的纯文本文件。
7 |
8 |
9 |
10 | 本插件实现了简单的文件/文献**管理**功能。专业的文档管理软件很多,如zotero,calibra等,本插件无意取代任何软件,但是管理功能是建立好`Book`与笔记纽带的前提。本插件的书籍管理单位是**书库(BookVault)**,一个书库实际上就是一个文件夹。插件目前可以为书籍提供阅读状态/进度,标签,作者,摘要等简单的管理,同时对于论文,专利,图片等常见类型的书籍也提供了特殊的支持。对书籍的记录是通过`.md`文件中的 `yaml` 实现的,每一个`Book` 文件都对应一个唯一的`.md`文件,因此通过 [dataview](https://github.com/blacksmithgu/obsidian-dataview) 插件,可以实现对所有文件进行汇总及统计。需要注意的是,书籍不必是一个实际的文件,也可以是一个虚拟的记录(只有一个`.md`文件)。
11 |
12 |
13 | `Book` 与笔记的联系是通过 **BookProject** 实现的。**Project** 实际上就是`Book`与笔记的组合(books+notes),一个 **Project** 里包含了若干的 `Book` 与若干笔记文件。一个 **BookProject** 可以是单个笔记文件,在`yaml`中通过 `bm-books` 指定与该笔记相关联的`Book`,也可以是一个文件夹,文件夹下的所有笔记都是 `Project`的笔记,此时则是在文件夹下的一个与文件夹同名的笔记文件里的`yaml`记录关联的 `Book`。
14 |
15 |
16 |
17 |
18 |
19 | 此外,插件提供了内置的**阅读器(BookViewer)**。插件只实现了有限的文件格式。内置阅读器还提供了必要的标注功能,理论上不提供编辑功能。更重要的是,内置阅读器无法与各种格式的专业阅读器相媲美,但是目前还没有找到使用专业阅读器提供回链的完美解决方案。
--------------------------------------------------------------------------------
/docs/Thanks.md:
--------------------------------------------------------------------------------
1 | - [ ] TODO
--------------------------------------------------------------------------------
/docs/ThirdPartyPlugins.md:
--------------------------------------------------------------------------------
1 | 可与本插件结合使用的一些插件
2 |
3 |
4 | ## Dataview
5 |
6 |
7 | 使用`dataviewjs`代码块显示指定书库与文件夹下所有book的信息表格,需要自行修改vid与path
8 |
9 | ```js
10 |
11 | // 插件
12 | const bmPlugin = dv.app.plugins.plugins['obsidian-bookmaster-plugin']
13 |
14 | // 目标书库和路径
15 | const vid = "00"
16 | const path = "/target/folder/path/"
17 |
18 | // 辅助变量和函数
19 | const statusMark = {
20 | unread:'未读',
21 | reading:'在读',
22 | finished:'已读'
23 | }
24 |
25 | const ratingMark = ['😐',"⭐","⭐⭐","⭐⭐⭐","⭐⭐⭐⭐","⭐⭐⭐⭐⭐"]
26 |
27 |
28 | // 获取所有book
29 | function getAllBooks(vid) {
30 | const books = []
31 | const _rec = (root) => {
32 | root.children.forEach((b) => {
33 | if (b.isFolder()) _rec(b);
34 | else books.push(b);
35 | })
36 | }
37 | _rec(bmPlugin.bookVaultManager.root.get(vid));
38 | return books
39 | }
40 |
41 | // 返回 markdown 链接: [title](link)
42 | function bookOpenLink(b) {
43 | if (b.bid) {
44 | return `[${b.meta.title || b.name}](obsidian://bookmaster?type=open-book&bid=${b.bid})`
45 | } else {
46 | return `[${b.meta.title || b.name}](obsidian://bookmaster?type=open-book&vid=${b.vid}&bpath=${encodeURIComponent(b.path)})`
47 | }
48 |
49 | }
50 |
51 | // 设置设置链接
52 | function bookSettingLink(b) {
53 | if (b.bid) {
54 | return `[设置](obsidian://bookmaster?type=basic-book-setting&bid=${b.bid})`
55 | } else {
56 | return `[设置](obsidian://bookmaster?type=basic-book-setting&vid=${b.vid}&bpath=${encodeURIComponent(b.path)})`
57 |
58 | }
59 | return `[设置](obsidian://bookmaster?type=basic-book-setting&bid=${b.bid})`
60 | }
61 |
62 | // 返回 meta 文件链接,
63 | // TODO: when bid is undefine
64 | function bookMetaLink(b) {
65 | return `[[${b.bid}.md|${b.title||b.name}]]`
66 | }
67 |
68 | // 获取第一个作者
69 | function bookFirstAuthor(b) {
70 | const authors = b.meta.authors;
71 | if (authors) {
72 | if (authors.length > 1) {
73 | return `${authors[0]} 等`
74 | } else {
75 | return `${authors} `
76 | }
77 | }
78 | }
79 |
80 |
81 | // 获取所有 books
82 | let books = getAllBooks(vid)
83 | .filter(b => b.path.startsWith(path))
84 | .sort(function (l, r) {
85 | const li = ["finished","reading","unread"].indexOf(l.meta.status);
86 | const ri = ["finished","reading","unread"].indexOf(r.meta.status);
87 | return li - ri;
88 | })
89 | // 生成表格
90 | dv.table(
91 | ["标题", "阅读状态", "作者","描述", "评分","设置"], // 标题
92 | books
93 | .map(b => [
94 | bookOpenLink(b),
95 | statusMark[b.meta.status||"unread"],
96 | bookFirstAuthor(b),
97 | b.meta.desc,
98 | ratingMark[b.meta.rating || 0],
99 | bookSettingLink(b)
100 | ]))
101 | ```
102 |
103 | 
104 |
105 |
106 | ## Front Matter Title
107 |
108 | 由于插件为每个文件的数据文件都使用一个唯一id命名,所以显示时难以分辨,可以使用 [Front Matter Title](https://github.com/snezhig/obsidian-front-matter-title) 插件在文件浏览器中显示实际名称,设置如下图(文件路径规则白名单需要改为自己的book-data路径):
109 |
110 | 
111 | 
112 |
113 | 此时可以在文件浏览器看到已经文件名变成有意义的名称,但不会改变实际文件名
--------------------------------------------------------------------------------
/docs/_coverpage.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # obsidian-bookmaster-plugin
6 |
7 |
8 |
9 |
12 |
13 | [GitHub](https://github.com/chenghongyao/obsidian-bookmaster-plugin/)
14 | [Get Started](#简介)
--------------------------------------------------------------------------------
/docs/_navbar.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/_navbar.md
--------------------------------------------------------------------------------
/docs/_sidebar.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | - [简介](/)
4 |
5 | - [快速使用](QuickStart.md)
6 |
7 | - [书库](BookVault.md)
8 |
9 | - [Book Project](BookProject.md)
10 |
11 | - [阅读器](BookViewer.md)
12 |
13 | - [第三方插件](ThirdPartyPlugins.md)
14 | - [其他](Others.md)
15 | - [致谢](Thanks.md)
16 |
17 |
18 |
--------------------------------------------------------------------------------
/docs/images/add_bookvault.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/add_bookvault.png
--------------------------------------------------------------------------------
/docs/images/book_explorer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/book_explorer.png
--------------------------------------------------------------------------------
/docs/images/bookdata_setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/bookdata_setting.png
--------------------------------------------------------------------------------
/docs/images/bookvault_in_mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/bookvault_in_mobile.png
--------------------------------------------------------------------------------
/docs/images/bookvault_path_setting.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/bookvault_path_setting.jpg
--------------------------------------------------------------------------------
/docs/images/bookvaultpath.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/bookvaultpath.png
--------------------------------------------------------------------------------
/docs/images/bookvewer_setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/bookvewer_setting.png
--------------------------------------------------------------------------------
/docs/images/bookviewer_deployment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/bookviewer_deployment.png
--------------------------------------------------------------------------------
/docs/images/bookviewer_path_in_mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/bookviewer_path_in_mobile.png
--------------------------------------------------------------------------------
/docs/images/bookviewer_server_setting_mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/bookviewer_server_setting_mobile.png
--------------------------------------------------------------------------------
/docs/images/dataview-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/dataview-example.png
--------------------------------------------------------------------------------
/docs/images/device_name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/device_name.png
--------------------------------------------------------------------------------
/docs/images/front-matter-title-setting-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/front-matter-title-setting-1.png
--------------------------------------------------------------------------------
/docs/images/front-matter-title-setting-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/front-matter-title-setting-2.png
--------------------------------------------------------------------------------
/docs/images/open_bookvault_setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/open_bookvault_setting.png
--------------------------------------------------------------------------------
/docs/images/vault_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenghongyao/obsidian-bookmaster-plugin/9d9bb45fa60866ee406e56a49fdf5716222b8362/docs/images/vault_error.png
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/examples/dataview/display_books_table.md:
--------------------------------------------------------------------------------
1 |
2 | 使用dataviewjs显示指定书库与文件夹下所有book的信息表格,需要自行修改vid与path
3 |
4 | ```dataviewjs
5 | // 插件
6 | const bmPlugin = dv.app.plugins.plugins['obsidian-bookmaster-plugin']
7 |
8 | // 目标书库和路径
9 | const vid = "00"
10 | const path = "/target/folder/path/"
11 |
12 | // 辅助变量和函数
13 | const statusMark = {
14 | unread:'未读',
15 | reading:'在读',
16 | finished:'已读'
17 | }
18 |
19 | const ratingMark = ['😐',"⭐","⭐⭐","⭐⭐⭐","⭐⭐⭐⭐","⭐⭐⭐⭐⭐"]
20 |
21 |
22 | // 获取所有book
23 | function getAllBooks(vid) {
24 | const books = []
25 | const _rec = (root) => {
26 | root.children.forEach((b) => {
27 | if (b.isFolder()) _rec(b);
28 | else books.push(b);
29 | })
30 | }
31 | _rec(bmPlugin.bookVaultManager.root.get(vid));
32 | return books
33 | }
34 |
35 | // 返回 markdown 链接: [title](link)
36 | function bookOpenLink(b) {
37 | if (b.bid) {
38 | return `[${b.meta.title || b.name}](obsidian://bookmaster?type=open-book&bid=${b.bid})`
39 | } else {
40 | return `[${b.meta.title || b.name}](obsidian://bookmaster?type=open-book&vid=${b.vid}&bpath=${encodeURIComponent(b.path)})`
41 | }
42 |
43 | }
44 |
45 | // 设置设置链接
46 | function bookSettingLink(b) {
47 | if (b.bid) {
48 | return `[设置](obsidian://bookmaster?type=basic-book-setting&bid=${b.bid})`
49 | } else {
50 | return `[设置](obsidian://bookmaster?type=basic-book-setting&vid=${b.vid}&bpath=${encodeURIComponent(b.path)})`
51 |
52 | }
53 | return `[设置](obsidian://bookmaster?type=basic-book-setting&bid=${b.bid})`
54 | }
55 |
56 | // 返回 meta 文件链接,
57 | // TODO: when bid is undefine
58 | function bookMetaLink(b) {
59 | return `[[${b.bid}.md|${b.title||b.name}]]`
60 | }
61 |
62 | // 获取第一个作者
63 | function bookFirstAuthor(b) {
64 | const authors = b.meta.authors;
65 | if (authors) {
66 | if (authors.length > 1) {
67 | return `${authors[0]} 等`
68 | } else {
69 | return `${authors} `
70 | }
71 | }
72 | }
73 |
74 |
75 | // 获取所有 books
76 | let books = getAllBooks(vid)
77 | .filter(b => b.path.startsWith(path))
78 | .sort(function (l, r) {
79 | const li = ["finished","reading","unread"].indexOf(l.meta.status);
80 | const ri = ["finished","reading","unread"].indexOf(r.meta.status);
81 | return li - ri;
82 | })
83 | // 生成表格
84 | dv.table(
85 | ["标题", "阅读状态", "作者","描述", "评分","设置"], // 标题
86 | books
87 | .map(b => [
88 | bookOpenLink(b),
89 | statusMark[b.meta.status||"unread"],
90 | bookFirstAuthor(b),
91 | b.meta.desc,
92 | ratingMark[b.meta.rating || 0],
93 | bookSettingLink(b)
94 | ]))
95 |
96 | ```
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "obsidian-bookmaster-plugin",
3 | "name": "BookMaster",
4 | "version": "0.3.7",
5 | "minAppVersion": "1.4.5",
6 | "description": "book manage plugin for Obsidian.",
7 | "author": "chenghongyao",
8 | "authorUrl": "https://github.com/chenghongyao",
9 | "isDesktopOnly": false
10 | }
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-bookmaster-plugin",
3 | "version": "0.1.0",
4 | "description": "",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "vite build --watch",
8 | "build": "vite build"
9 | },
10 | "keywords": [
11 | "obsidian",
12 | "book",
13 | "pdf",
14 | "annotation"
15 | ],
16 | "author": "chenghongyao",
17 | "devDependencies": {
18 | "@vitejs/plugin-vue": "^3.1.2",
19 | "tslib": "^2.4.0",
20 | "typescript": "^4.8.4",
21 | "vite": "^3.1.4",
22 | "vue": "^3.2.36"
23 | },
24 | "dependencies": {
25 | "@pdftron/webviewer": "^8.2.0",
26 | "annotpdf": "^1.0.15",
27 | "epubjs": "^0.3.93",
28 | "monkey-around": "^2.3.0",
29 | "obsidian": "1.4.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/resources/config.js:
--------------------------------------------------------------------------------
1 | // for bookmaster-v0.3.3
2 | if (!window.wvWindowMessageListener) {
3 | // console.error("add message EventListener");
4 | const eventHandlerMap = {
5 | openFile: function (data) {
6 | instance.UI.loadDocument(data.blob, { extension: data.extension });
7 | instance.xfdfString = data.xfdfString;
8 | instance.targetPage = data.page;
9 | },
10 | fitWidth: function (dat) {
11 | instance.UI.setFitMode(instance.UI.FitMode.FitWidth);
12 | },
13 | showAnnotation: function (data) {
14 | const anno = instance.Core.annotationManager.getAnnotationById(data);
15 | if (!anno) {
16 | console.error("annot:" + data + "doesn't exists");
17 | return;
18 | }
19 | instance.Core.annotationManager.deselectAllAnnotations();
20 | instance.Core.annotationManager.jumpToAnnotation(anno, { verticalOffset: "50%" });
21 | instance.Core.annotationManager.selectAnnotation(anno);
22 | },
23 | showBookPage: function (data) {
24 | instance.Core.documentViewer.setCurrentPage(data);
25 | },
26 | copyCurrentPageLink: function (data) {
27 | const page = instance.Core.documentViewer.getCurrentPage();
28 | window.postObsidianBookNoteMessage("copyCurrentPageLink", page);
29 | },
30 | setTheme: function(theme) {
31 | // console.log(theme.bgcolor);
32 | instance.docViewer.setDefaultPageColor(theme.bgcolor); // TODO?
33 | instance.UI.setTheme(theme.theme);
34 | }
35 | }
36 |
37 | window.wvWindowMessageListener = function (event) {
38 | const data = event.data;
39 | // console.log("message:",event)
40 | if (!(event.origin === "app://obsidian.md" || event.origin === "http://localhost") || !data["app"] || data["app"] !== "obsidian-book") return;
41 | if (eventHandlerMap[data.type]) {
42 | eventHandlerMap[data.type](data.data);
43 | }
44 | };
45 |
46 | this.addEventListener("message", window.wvWindowMessageListener);
47 |
48 | window.postObsidianBookNoteMessage = function (type, data) {
49 | window.parent.postMessage({
50 | app: instance.customData["id"],
51 | type: type,
52 | data: data,
53 | }, "*")
54 | }
55 | }
56 |
57 | if (!window.viewerLoadedListener) {
58 | // console.error("add viewerLoad EventListener");
59 | window.viewerLoadedListener = function () {
60 |
61 | instance.customData = JSON.parse(instance.UI.getCustomData());
62 |
63 | const { Actions, docViewer } = instance;
64 |
65 | const onTriggered = Actions.GoTo.prototype.onTriggered;
66 | Actions.GoTo.prototype.onTriggered = function(target, event) {
67 | if (target === docViewer.getDocument() && event.name === 'Open') {
68 | return;
69 | }
70 | onTriggered.apply(this, arguments);
71 | };
72 |
73 | if (instance.customData.theme) {
74 | docViewer.setDefaultPageColor(instance.customData.theme.bgcolor);
75 | instance.UI.setTheme(instance.customData.theme.theme);
76 | } else {
77 | instance.UI.setTheme("dark");
78 | }
79 |
80 |
81 | instance.UI.setAnnotationContentOverlayHandler(anno => {
82 | return null;
83 | });
84 | instance.UI.setLanguage("zh_cn");
85 |
86 | instance.UI.annotationPopup.add([
87 | {
88 | type: "actionButton",
89 | title: "复制回链",
90 | img: '',
91 | onClick: () => {
92 | annots = instance.Core.annotationManager.getSelectedAnnotations();
93 | if (annots.length) {
94 | window.postObsidianBookNoteMessage("copyAnnotationLink", {
95 | id: annots[annots.length - 1].Id,
96 | ctrlKey: window.event.ctrlKey,
97 | zoom: instance.Core.documentViewer.getZoom(),
98 | })
99 | }
100 | }
101 | }
102 | ])
103 |
104 |
105 | const { annotationManager } = instance.Core;
106 | annotationManager.setAnnotationDisplayAuthorMap((userId) => {
107 | return instance.customData.author;
108 | });
109 | annotationManager.addEventListener("annotationChanged", (annotations, action, { imported }) => {
110 | if (imported) return;
111 |
112 | instance.Core.annotationManager.exportAnnotCommand().then(xfdfString => {
113 | window.postObsidianBookNoteMessage("annotationChanged", {
114 | action: action,
115 | xfdf: xfdfString,
116 | zoom: instance.Core.documentViewer.getZoom(),
117 | })
118 | });
119 |
120 | });
121 |
122 |
123 | window.postObsidianBookNoteMessage("viewerLoaded");
124 | };
125 |
126 | window.addEventListener('viewerLoaded', window.viewerLoadedListener);
127 | }
128 |
129 | if (!window.documentLoadedListener) {
130 | // console.error("add documentLoaded EventListener");
131 | window.documentLoadedListener = function () {
132 | console.log("documentLoaded");
133 |
134 | if (instance.targetPage) {
135 | instance.Core.documentViewer.setCurrentPage(instance.targetPage);
136 | instance.targetPage = null;
137 | }
138 |
139 | if (instance.xfdfString) {
140 | instance.Core.annotationManager.importAnnotations(instance.xfdfString);
141 | }
142 |
143 | instance.Core.annotationManager.exportAnnotations({ links: false, widgets: false }).then((xfdfString) => {
144 | window.postObsidianBookNoteMessage("documentLoaded", xfdfString);
145 | });
146 | instance.UI.setFitMode(instance.UI.FitMode.FitWidth)
147 |
148 | instance.Core.documentViewer.addEventListener("pageNumberUpdated", (pageNum) => {
149 | window.postObsidianBookNoteMessage("pageNumberUpdated", pageNum);
150 | });
151 |
152 |
153 | };
154 |
155 | window.addEventListener('documentLoaded', window.documentLoadedListener);
156 | }
157 |
158 |
159 |
160 |
--------------------------------------------------------------------------------
/src/Book.ts:
--------------------------------------------------------------------------------
1 | import { BookView } from "./view/BookView";
2 | import * as utils from "./utils"
3 | import { normalizePath, TFile } from "obsidian";
4 | import { BOOK_TYPES, EXT2TYPE } from "./constants";
5 |
6 |
7 |
8 |
9 | export enum BookTreeSortType {
10 | PATH = "PATH",
11 | TAG = "TAG",
12 | AUTHOR = "AUTHOR",
13 | PUBLISH_YEAR = "PUBLISH_YEAR",
14 | }
15 | export enum BookStatus {
16 | UNREAD = "unread",
17 | READING = "reading",
18 | FINISHED = "finished",
19 | }
20 |
21 | export class BookMeta {
22 | // basic
23 | type: string; // book type
24 | title: string; // book title, default to filename
25 | desc: string; // book description
26 | authors: Array; // authors
27 | tags: Array; // tags
28 | rating: number; // rating: 0-5
29 | status: BookStatus; // read status
30 | "start-time": string // TODO: start time
31 | "finish-time": string; // TODO: finish time
32 | progress: number; // read progress,eg. reading page
33 | total?: number; // eg. total pages
34 | cover?: string; // cover address,url or image path,
35 | note?: string; // note file id or path for this book
36 |
37 | // others are depend on book type
38 | [key:string]: any;
39 |
40 | constructor() {
41 | this.status = BookStatus.UNREAD;
42 | this.tags = [];
43 | this.authors = [];
44 | this.type = "unknown"
45 | }
46 | }
47 |
48 |
49 |
50 | export interface BookMetaOptions {
51 | type: "text"|"number"|"text-array"|"text-choice",
52 | label?: string,
53 | choices?: Array;
54 | default?: any;
55 | // multiline?: boolean;
56 | }
57 | export interface BookMetaOptionMap {
58 | [name:string]:BookMetaOptions;
59 | }
60 | export const BookMetaMap : {[type:string]:BookMetaOptionMap}= {
61 | "basic": {
62 | "type": {
63 | type: "text",
64 | default: "unknown"
65 | },
66 | "title": {
67 | type: "text",
68 | // default: ""
69 | },
70 | "desc": {
71 | type: "text",
72 | // default: ""
73 | },
74 |
75 | "authors": {
76 | type: "text-array",
77 | default: [],
78 | },
79 | "tags": {
80 | type: "text-array",
81 | default: [],
82 | },
83 | "rating": {
84 | type: "number",
85 | default: 0
86 | },
87 | "status": {
88 | type: "text",
89 | default: BookStatus.UNREAD,
90 | },
91 | "progress": {
92 | type: "number",
93 | default: 0,
94 | },
95 | "total": {
96 | type: "number",
97 | // default: 0,
98 | },
99 | "cover": {
100 | type: "text",
101 | // default: "",
102 | },
103 | "note": {
104 | type: "text",
105 | // default: "",
106 | }
107 | },
108 | "book": {
109 | "publish-date": {
110 | type: "text",
111 | default: "",
112 | },
113 | "publisher": {
114 | type: "text",
115 | default: "",
116 | },
117 | "doi": {
118 | type: "text",
119 | default: "",
120 | }
121 | },
122 | "paper": {
123 | "publish-date": {
124 | type: "text",
125 | default: "",
126 | },
127 | "publisher": {
128 | type: "text",
129 | default: "",
130 | },
131 | "isbn": {
132 | type: "text",
133 | default: "",
134 | }
135 | },
136 | "image": {
137 |
138 | }
139 |
140 | };
141 |
142 |
143 |
144 | // create 16bit bid;
145 | function generateBid() {
146 |
147 | }
148 |
149 |
150 | export abstract class AbstractBook {
151 | parent: BookFolder;
152 | vid: string;
153 | visual: boolean;
154 |
155 | name: string;
156 | path: string;
157 |
158 | lost: boolean;
159 |
160 | children?: Array;
161 | existFlag: boolean;
162 | newFlag: boolean;
163 |
164 | constructor(parent: BookFolder, vid: string, name: string, path: string, lost: boolean = false) {
165 | this.parent = parent;
166 | this.vid = vid;
167 | this.lost = lost;
168 | this.visual = false;
169 |
170 | this.name = name;
171 | this.path = path;
172 |
173 | this.existFlag = false;
174 | this.newFlag = false;
175 | }
176 |
177 | isFolder() {
178 | return Boolean(this.children);
179 | }
180 |
181 |
182 | getEntry() {
183 | return `${this.vid}:${this.path}`
184 | }
185 |
186 |
187 | }
188 |
189 | export class Book extends AbstractBook {
190 | ext: string;
191 | bid: string;
192 | meta: BookMeta;
193 | view: BookView;
194 | metaFile: TFile;
195 |
196 | // parent: AbstractBook, vid: string, path: string, name: string,ext: string, bid?: string, visual: boolean = false, losted: boolean = false
197 | constructor(parent: BookFolder, vid: string, name: string, path: string, ext: string, bid?: string, lost: boolean = false) {
198 | super(parent, vid, name, path, lost);
199 | this.ext = ext;
200 | this.bid = bid;
201 | this.meta = new BookMeta();
202 |
203 |
204 | this.view = null;
205 | this.metaFile = null;
206 | }
207 |
208 |
209 | generateBid() {
210 | if (!this.bid) {
211 | const _all = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
212 | const N = 16;
213 | var res = '';
214 | for (var i = 0; i < N; i++) {
215 | res += _all[Math.floor(Math.random()* _all.length)]
216 | }
217 |
218 | this.bid = res;
219 | }
220 | return this.bid;
221 | }
222 |
223 | // private async getBookDescription(file: TFile) {
224 | // const ryaml = /^(---\r?\n[\s\S]*\r?\n---)\r?\n/;
225 | // const cont = await utils.app.vault.read(file);
226 | // const g = ryaml.exec(cont)
227 | // var len = 0;
228 | // if (g && g[1]) {
229 | // len = g[1].length;
230 | // }
231 | // return cont.substring(len+1).trim() ;
232 | // }
233 |
234 | async loadBookData(file: TFile) {
235 | const basicMeta = BookMetaMap['basic'];
236 | // TODO: correct meta type
237 | if (file) { // load from file
238 | return utils.app.fileManager.processFrontMatter(file, (inputMeta) => {
239 | this.bid = inputMeta["bid"];
240 | this.vid = inputMeta["vid"];
241 | this.path = inputMeta["path"];
242 | this.name = inputMeta["name"];
243 | this.ext = inputMeta["ext"];
244 | this.visual = inputMeta["visual"];
245 |
246 | for(const key in basicMeta) {
247 | const val = inputMeta[key];
248 | if (basicMeta[key].type === "text-array") {
249 | this.meta[key] = val === undefined ? new Array() : (typeof val === "string" ? [val] : val);
250 | } else if(val !== undefined){
251 | this.meta[key] = inputMeta[key];
252 | }
253 | }
254 |
255 | const typeMeta = BookMetaMap[inputMeta['type']];
256 | for(const key in typeMeta) {
257 | this.meta[key] = inputMeta[key];
258 | }
259 |
260 | // console.log(this.meta)
261 |
262 | })
263 |
264 | } else { // init book meta
265 |
266 | for(const key in basicMeta) {
267 | if (basicMeta[key].default !== undefined) {
268 | this.meta[key] = basicMeta[key].default instanceof Array ? Array.from(basicMeta[key].default) : basicMeta[key].default;
269 |
270 | }
271 | }
272 |
273 | if (this.meta.type === undefined) {
274 | this.meta.type = EXT2TYPE[this.ext] || "unknown";
275 | }
276 |
277 | if ( BookMetaMap[this.meta.type]) {
278 | const typeMeta = BookMetaMap[this.meta.type];
279 | for(const key in typeMeta) {
280 | if (typeMeta[key].default !== undefined) {
281 | this.meta[key] = typeMeta[key].default instanceof Array ? Array.from(typeMeta[key].default) : typeMeta[key].default;
282 | }
283 | }
284 | }
285 | // TODO: book desc
286 | // this.meta["desc"] = "";
287 | }
288 | }
289 |
290 | // make sure this book has bid
291 | // do NOT use this directly, use plugin.saveBookData() instead
292 | async saveBookData(datapath: string) {
293 | const filepath = normalizePath(datapath+`/${this.vid}` + "/"+this.bid+".md");
294 | return utils.safeWriteObFile(filepath, "", false).then(() => {
295 | const file = utils.app.vault.getAbstractFileByPath(filepath) as TFile;
296 | return utils.app.fileManager.processFrontMatter(file, (frontmatter) => {
297 | frontmatter["bm-meta"] = true;
298 | frontmatter["bid"] = this.bid;
299 | frontmatter["vid"] = this.vid;
300 | frontmatter["path"] = this.path;
301 | frontmatter["name"] = this.name;
302 | frontmatter["ext"] = this.ext;
303 | frontmatter["visual"] = this.visual;
304 |
305 | for(const key in this.meta) {
306 | var val = this.meta[key]; // TODO: correct type string
307 | if (val === undefined) continue;
308 | frontmatter[key] = val;
309 | }
310 | });
311 | });
312 |
313 | }
314 | }
315 |
316 | export enum BookFolderType {
317 | PATH = "path",
318 | TAG = "tag",
319 | PUBLISH_YEAR = "publish_year",
320 | AUTHOR = "author",
321 | }
322 | export class BookFolder extends AbstractBook {
323 | count: number;
324 | type: BookFolderType;
325 |
326 | constructor(parent: BookFolder, vid: string, name: string, path: string, lost: boolean = false, type: BookFolderType = BookFolderType.PATH, children?: Array) {
327 | super(parent, vid, name, path, lost);
328 | this.type = type;
329 | this.children = children ? children : new Array();
330 | }
331 |
332 | push(absbook: AbstractBook) {
333 | this.children.push(absbook);
334 | }
335 |
336 | clear() {
337 | this.children.length = 0;
338 | }
339 | }
340 |
341 |
342 |
343 | // import { BookView } from "./view/BookView";
344 | // import * as utils from "./utils"
345 | // import { normalizePath, TFile } from "obsidian";
346 | // import { EXT2TYPE } from "./constant";
347 | // import { reactive, Ref, ref } from "vue";
348 |
349 |
350 |
351 |
352 | // export enum BookTreeSortType {
353 | // PATH = "PATH",
354 | // TAG = "TAG",
355 | // AUTHOR = "AUTHOR",
356 | // PUBLISH_YEAR = "PUBLISH_YEAR",
357 | // }
358 | // export enum BookStatus {
359 | // UNREAD = "unread",
360 | // READING = "reading",
361 | // FINISHED = "finished",
362 | // }
363 |
364 | // export class BookMeta {
365 | // // basic
366 | // type: string; // book type
367 | // title: string; // book title, default to filename
368 | // desc: string; // book description
369 | // authors: Array; // authors
370 | // tags: Array; // tags
371 | // rating: number; // rating: 0-5
372 | // status: BookStatus; // read status
373 | // "start-time": string // TODO: start time
374 | // "finish-time": string; // TODO: finish time
375 | // progress: number; // read progress,eg. reading page
376 | // total?: number; // eg. total pages
377 | // cover?: string; // cover address,url or image path,
378 | // note?: string; // note file id or path for this book
379 |
380 | // // others are depend on book type
381 | // [key:string]: any;
382 |
383 | // constructor() {
384 | // this.status = BookStatus.UNREAD;
385 | // this.tags = [];
386 | // this.authors = [];
387 | // }
388 | // }
389 |
390 |
391 |
392 | // export interface BookMetaOptions {
393 | // type: "text"|"number"|"text-array"|"text-choice",
394 | // label?: string,
395 | // choices?: Array;
396 | // default?: any;
397 | // // multiline?: boolean;
398 | // }
399 | // export interface BookMetaOptionMap {
400 | // [name:string]:BookMetaOptions;
401 | // }
402 | // export const BookMetaMap : {[type:string]:BookMetaOptionMap}= {
403 | // "basic": {
404 | // "type": {
405 | // type: "text",
406 | // },
407 | // "title": {
408 | // type: "text",
409 | // // default: ""
410 | // },
411 | // "desc": {
412 | // type: "text",
413 | // // default: ""
414 | // },
415 | // "authors": {
416 | // type: "text-array",
417 | // default: [],
418 | // },
419 | // "tags": {
420 | // type: "text-array",
421 | // default: [],
422 | // },
423 | // "rating": {
424 | // type: "number",
425 | // default: 0
426 | // },
427 | // "status": {
428 | // type: "text",
429 | // default: BookStatus.UNREAD,
430 | // },
431 | // "progress": {
432 | // type: "number",
433 | // default: 0,
434 | // },
435 | // "total": {
436 | // type: "number",
437 | // // default: 0,
438 | // },
439 | // "cover": {
440 | // type: "text",
441 | // // default: "",
442 | // },
443 | // "note": {
444 | // type: "text",
445 | // // default: "",
446 | // }
447 | // },
448 | // "book": {
449 | // "publish-date": {
450 | // type: "text",
451 | // default: "",
452 | // },
453 | // "publisher": {
454 | // type: "text",
455 | // default: "",
456 | // },
457 | // "doi": {
458 | // type: "text",
459 | // default: "",
460 | // }
461 | // },
462 | // "paper": {
463 | // "publish-date": {
464 | // type: "text",
465 | // default: "",
466 | // },
467 | // "publisher": {
468 | // type: "text",
469 | // default: "",
470 | // },
471 | // "isbn": {
472 | // type: "text",
473 | // default: "",
474 | // }
475 | // },
476 | // "image": {
477 |
478 | // }
479 |
480 | // };
481 |
482 |
483 |
484 | // // create 16bit bid;
485 | // function generateBid() {
486 |
487 | // }
488 |
489 |
490 | // export abstract class AbstractBook {
491 | // parent: BookFolder;
492 | // vid: string;
493 | // visual: Ref;
494 |
495 | // name: Ref;
496 | // path: Ref;
497 |
498 | // lost: Ref;
499 |
500 | // children?: Array;
501 | // existFlag: boolean;
502 |
503 | // constructor(parent: BookFolder, vid: string, name: string, path: string, lost: boolean = false) {
504 | // this.parent = parent;
505 | // this.vid = vid;
506 | // this.lost = ref(lost);
507 | // this.visual = ref(false);
508 |
509 | // this.name = ref(name);
510 | // this.path = ref(path);
511 |
512 | // this.existFlag = false;
513 | // }
514 |
515 | // isFolder() {
516 | // return Boolean(this.children);
517 | // }
518 |
519 |
520 | // getEntry() {
521 | // return `${this.vid}:${this.path}`
522 | // }
523 |
524 |
525 | // }
526 |
527 | // export class Book extends AbstractBook {
528 | // ext: Ref;
529 | // bid: Ref;
530 | // meta: any;
531 | // view: BookView;
532 | // metaFile: TFile;
533 |
534 | // // parent: AbstractBook, vid: string, path: string, name: string,ext: string, bid?: string, visual: boolean = false, losted: boolean = false
535 | // constructor(parent: BookFolder, vid: string, name: string, path: string, ext: string, bid?: string, lost: boolean = false) {
536 | // super(parent, vid, name, path, lost);
537 | // this.ext = ref(ext);
538 | // this.bid = ref(bid);
539 | // this.meta = reactive(new BookMeta());
540 |
541 |
542 | // this.view = null;
543 | // this.metaFile = null;
544 | // }
545 |
546 |
547 | // generateBid() {
548 | // if (!this.bid) {
549 | // const _all = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
550 | // const N = 16;
551 | // var res = '';
552 | // for (var i = 0; i < N; i++) {
553 | // res += _all[Math.floor(Math.random()* _all.length)]
554 | // }
555 |
556 | // this.bid.value = res;
557 | // this.bid = res;
558 | // }
559 | // return this.bid;
560 | // }
561 |
562 |
563 | // loadBookData(file: TFile | any) {
564 | // const basicMeta = BookMetaMap['basic'];
565 |
566 | // // TODO: correct meta type
567 | // if (file) { // load from file
568 |
569 | // const inputMeta: any = file instanceof TFile ? utils.app.metadataCache.getFileCache(file).frontmatter : file;
570 | // const typeMeta = BookMetaMap[inputMeta['type']];
571 |
572 | // this.bid = inputMeta["bid"];
573 | // this.vid = inputMeta["vid"];
574 | // this.path = inputMeta["path"];
575 | // this.name = inputMeta["name"];
576 | // this.ext = inputMeta["ext"];
577 | // this.visual = inputMeta["visual"];
578 | // // this.hash = inputMeta["hash"];
579 | // // this.citekey = inputMeta["citekey"];
580 |
581 | // for(const key in basicMeta) {
582 | // const val = inputMeta[key];
583 | // if (basicMeta[key].type === "text-array") {
584 | // this.meta[key] = val === undefined ? new Array() : (typeof val === "string" ? [val] : val);
585 | // } else if(val !== undefined){
586 | // this.meta[key] = inputMeta[key];
587 | // }
588 | // }
589 |
590 | // for(const key in typeMeta) {
591 | // this.meta[key] = inputMeta[key];
592 | // }
593 |
594 | // } else { // init book meta
595 |
596 | // for(const key in basicMeta) {
597 | // if (basicMeta[key].default !== undefined) {
598 | // this.meta[key] = basicMeta[key].default instanceof Array ? Array.from(basicMeta[key].default) : basicMeta[key].default;
599 |
600 | // }
601 | // }
602 |
603 | // if (this.meta.type === undefined) {
604 | // this.meta.type = EXT2TYPE[this.ext] || "unknown";
605 | // }
606 |
607 | // if ( BookMetaMap[this.meta.type]) {
608 | // const typeMeta = BookMetaMap[this.meta.type];
609 | // for(const key in typeMeta) {
610 | // if (typeMeta[key].default !== undefined) {
611 | // this.meta[key] = typeMeta[key].default instanceof Array ? Array.from(typeMeta[key].default) : typeMeta[key].default;
612 | // }
613 | // }
614 | // }
615 |
616 |
617 | // }
618 | // }
619 |
620 | // // make sure this book has bid
621 | // // do NOT use this directly, use plugin.saveBookData() instead
622 | // async saveBookData(datapath: string) {
623 | // const filepath = normalizePath(datapath+"/"+this.bid+".md");
624 |
625 | // const rawMeta = (utils.app.metadataCache.getCache(filepath)?.frontmatter as any) || {};
626 | // // TODO: load from map
627 | // delete rawMeta['position'];
628 |
629 | // const basicMeta = BookMetaMap['basic'];
630 | // const typeMeta = this.meta.type && BookMetaMap[this.meta.type];
631 |
632 | // for(const key in basicMeta) {
633 | // if (this.meta[key] !== undefined) {
634 | // rawMeta[key] = this.meta[key];
635 | // }
636 | // }
637 |
638 | // if (typeMeta) {
639 | // for(const key in typeMeta) {
640 | // if (this.meta[key] !== undefined) {
641 | // rawMeta[key] = this.meta[key];
642 | // }
643 | // }
644 | // }
645 |
646 | // const content = this.getBookMetaString();
647 | // return utils.safeWriteObFile(filepath,content,true);
648 | // }
649 |
650 | // private getBookMetaString() {
651 | // var content = "";
652 | // content += "---\n";
653 | // content += "bm-meta: true\n";
654 | // content += `bid: "${this.bid}"\n`;
655 | // content += `vid: "${this.vid}"\n`;
656 | // content += `path: "${this.path}"\n`;
657 | // content += `name: "${this.name}"\n`;
658 | // content += `ext: ${this.ext}\n`;
659 | // content += `visual: ${this.visual}\n`;
660 | // // if (this.hash) content += `hash: ${this.hash}\n`;
661 | // // if (this.citekey) content += `citekey: ${this.citekey}\n`;
662 |
663 | // for(const key in this.meta) {
664 | // const val = this.meta[key]; // TODO: correct type string
665 |
666 | // if (val === undefined) continue;
667 |
668 | // if (typeof val === "string") {
669 | // if (!val) continue;
670 | // content += `${key}: "${val}"\n`;
671 | // } else if (typeof val === "object") { // array
672 | // if (val.length === 0) continue;
673 | // content += `${key}:\n`;
674 | // val.forEach((v: string) => {
675 | // content += ` - ${v}\n`
676 | // })
677 | // } else {
678 | // content += `${key}: ${val}\n`;
679 | // }
680 | // }
681 | // content += "---\n"
682 | // return content;
683 | // }
684 |
685 | // }
686 |
687 | // export class BookFolder extends AbstractBook {
688 | // count: number;
689 | // constructor(parent: BookFolder, vid: string, name: string, path: string, lost: boolean = false, children?: Array) {
690 | // super(parent, vid, name, path, lost);
691 | // this.children = children ? children : new Array();
692 | // }
693 |
694 | // push(absbook: AbstractBook) {
695 | // this.children.push(absbook);
696 | // }
697 |
698 | // clear() {
699 | // this.children.length = 0;
700 | // }
701 | // }
--------------------------------------------------------------------------------
/src/BookProject.ts:
--------------------------------------------------------------------------------
1 | import { normalizePath, TFile } from "obsidian";
2 | import { reactive } from "vue";
3 | import { Book, BookFolder } from "./Book";
4 | import { BookVaultManager } from "./BookVault";
5 | import BookMasterPlugin from "./main";
6 | import * as utils from './utils'
7 |
8 |
9 |
10 | export default class BookProjecetManager {
11 | plugin: BookMasterPlugin;
12 | bookVaultManager: BookVaultManager;
13 | projectBooks: BookFolder;
14 | projectFile: TFile;
15 |
16 | constructor(plugin: BookMasterPlugin) {
17 | this.plugin = plugin;
18 | this.bookVaultManager = plugin.bookVaultManager;
19 | this.projectBooks = reactive(new BookFolder(null, "", "","/"));
20 | }
21 |
22 |
23 | async loadProjectFile(file: TFile) {
24 |
25 | // TODO: project file is deleted
26 | this.projectFile = file;
27 |
28 | const projectName = utils.getPropertyValue(this.projectFile, "bm-name") || this.projectFile.basename
29 | this.projectBooks.name = projectName;
30 | this.projectBooks.clear();
31 |
32 |
33 | let books = utils.getPropertyValue(this.projectFile, "bm-books");
34 | if (!books) return;
35 |
36 | if (typeof books === "string") books = [books];
37 |
38 | for (let i = 0; i < books.length; i++) {
39 | const regIdPath = /[a-zA-Z0-9]{16}/;
40 | const IdPathGroup = regIdPath.exec(books[i]);
41 | if (IdPathGroup) {
42 | const book = await this.bookVaultManager.getBookById(IdPathGroup[0]);
43 | if (book) {
44 | this.projectBooks.push(book);
45 | }
46 | continue;
47 | }
48 |
49 | const regUrl = /^\[(.*)\]\((https?:\/\/[\w\-_]+(?:\.[\w\-_]+)+[\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?\)$/
50 | const urlGroup = regUrl.exec(books[i]);
51 | if (urlGroup) {
52 | const book = new Book(null,null,urlGroup[2],urlGroup[1],"url",null);
53 | this.projectBooks.push(book);
54 | continue;
55 | }
56 |
57 |
58 | }
59 |
60 | }
61 |
62 | async loadProjectFromFile(file: TFile) {
63 | const projFile = this.searchProjectFile(file);
64 | if (projFile) {
65 | this.loadProjectFile(projFile);
66 | } else {
67 | this.projectBooks.clear();
68 | }
69 | }
70 |
71 | isProjectFile(file: TFile) {
72 | return file && (utils.getPropertyValue(file, "bookmaster-plugin") || utils.getPropertyValue(file,"bm-books"));
73 | }
74 |
75 | searchProjectFile(file: TFile) {
76 | if (this.isProjectFile(file)) {
77 | return file;
78 | }
79 | if (!file.parent.name) {
80 | return null;
81 | }
82 | const folderFilePath = normalizePath(file.parent.path + `/${file.parent.name}.md`);
83 | const folderFile = utils.app.vault.getAbstractFileByPath(folderFilePath) as TFile
84 | if (folderFile && this.isProjectFile(folderFile)) {
85 | return folderFile;
86 | } else {
87 | return null;
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/src/RecentBooks.ts:
--------------------------------------------------------------------------------
1 | import { TFile, normalizePath, debounce } from "obsidian";
2 | import { reactive } from "vue";
3 | import { Book, BookFolder } from "./Book";
4 | import { BookVaultManager } from "./BookVault";
5 | import BookMasterPlugin from "./main";
6 | import * as utils from "./utils";
7 |
8 |
9 | // TODO: remove books that are removed from book vault
10 |
11 | export default class RecentBooksManager {
12 | recentBooks: BookFolder
13 | plugin: BookMasterPlugin;
14 | bookVaultManager: BookVaultManager
15 | private debounceSaveRecentBooks: any;
16 |
17 | constructor(plugin: BookMasterPlugin) {
18 | this.plugin = plugin;
19 | this.bookVaultManager = this.plugin.bookVaultManager;
20 |
21 | this.recentBooks = reactive(new BookFolder(null, "", "recent", "/"));
22 |
23 | this.debounceSaveRecentBooks = debounce(() => {
24 | this.saveRecentBooks()
25 | },1000,true);
26 |
27 | }
28 |
29 | async setup() {
30 | this.recentBooks.children = []
31 |
32 | const content = await utils.safeReadObFile(this.plugin.settings.dataPath + "/recent.md");
33 | if (!content) return;
34 |
35 | const r = /\[.*\]\((.*)\)/g
36 | var item = null;
37 | do {
38 | item = r.exec(content)?.[1];
39 | if (!item) continue;
40 | const url = new URL(item);
41 | if (!url) continue;
42 | const bid = url.searchParams.get("bid");
43 | if (!url) continue;
44 | this.bookVaultManager.getBookById(bid).then((book) => {
45 | if (book) {
46 | this.recentBooks.push(book);
47 | }
48 | })
49 | } while(item)
50 | }
51 |
52 | private async saveRecentBooks() {
53 | var content = "";
54 | for (var i = 0; i !== this.recentBooks.children.length;i++) {
55 | const book = this.recentBooks.children[i] as Book;
56 | content += `- [${book.meta.title||book.name}](obsidian://bookmaster?type=open-book&bid=${book.bid})\r\n`;
57 | }
58 | return utils.safeWriteObFile(normalizePath(this.plugin.settings.dataPath + "/recent.md"),content,true);
59 | }
60 |
61 | async addBook(book: Book) {
62 | const ind = this.recentBooks.children.indexOf(book);
63 | if (ind == 0) return;
64 | else if (ind > 0) {
65 | this.recentBooks.children.splice(ind,1);
66 | } else if (this.recentBooks.children.length >= this.plugin.settings.recentBookNumberLimit) {
67 | this.recentBooks.children.pop();
68 | }
69 | this.recentBooks.children.splice(0,0,book);
70 | return this.debounceSaveRecentBooks();
71 | }
72 |
73 | async removeBook(book: Book) {
74 | this.recentBooks.children.remove(book);
75 | return this.debounceSaveRecentBooks();
76 | }
77 |
78 | clear() {
79 | this.recentBooks.clear();
80 | return this.debounceSaveRecentBooks();
81 | }
82 | }
--------------------------------------------------------------------------------
/src/components/v-array-input.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
45 |
46 |
--------------------------------------------------------------------------------
/src/components/v-basic-setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
类型:
6 |
7 |
15 |
16 |
17 |
18 |
状态:
19 |
20 |
24 | {{item.value}}
25 |
26 |
27 |
28 |
29 |
35 |
36 |
37 |
标题:
38 |
39 |
42 |
43 |
44 |
45 |
46 |
作者:
47 |
48 |
49 |
50 |
51 |
52 |
53 |
标签:
54 |
55 |
56 |
57 |
58 |
59 |
描述:
60 |
61 |
62 |
65 |
66 |
67 |
74 |
75 |
76 |
77 |
78 |
79 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/src/components/v-obtree-item.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
{{item.name}}
12 |
13 |
14 |
15 | $emit('open-file',item)"
19 | @context-menu="(e,node) => $emit('context-menu',e,node)"
20 | @select-file="(node,ctrlKey) => $emit('select-file',node,ctrlKey)"
21 | />
22 |
23 |
24 |
25 |
26 |
40 |
41 |
42 | {{(item.meta && item.meta.title) || item.name}}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
96 |
97 |
--------------------------------------------------------------------------------
/src/components/v-obtree.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
{{root.name || "???"}}
6 |
7 |
8 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 |
2 | export const EXT2TYPE : {[ext:string]:any}= {
3 | pdf: "book",
4 | epub: "book",
5 | jpg: "image",
6 | png: "image",
7 | jpeg: "image",
8 | bmp: "image",
9 | gif: "image",
10 | }
11 |
12 |
13 | export const ImageExts = ["jpg","jpeg","png","bmp","jfif"]
14 | export const AudioExts = ["wav","mp3","ogg"] // support by