├── .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 | ![](images/open_bookvault_setting.png ':size=850') 6 | 7 | **书库**实际上是一个本地文件夹,这个文件夹可以在电脑上的任意位置,无需在obsidian库内。 8 | 9 | 打开设置面板,在 `BookMaster` 设置中找到`书库设置`,如下图所示,点击右侧的 `+` 号可以添加一个书库,其中:`1`为书库的唯一标识 `vid`, `2` 是可自定义的书库名称,`3` 是书库文件夹的绝对路径。点击 `5` 可以弹出窗口选择一个文件夹,选择确定后会自动将书库名设置为文件夹名。完成后点击 `4` 确认,此时即创建了一个书库。重复点击`书库设置`后侧的 `+` 号可以创建多个书库,注意每个书库都是相互独立的。 10 | 11 | ![](images/add_bookvault.png ':size=850') 12 | 13 | 14 | 15 | 关闭设置面板,在左侧边栏找到名为 `Book Explorer` 的icon,点击即可打开书库浏览器。当添加了多个书库时可以上边的工具栏中切换不同的书库。 16 | 17 | ![书库浏览器](images/book_explorer.png ':size=850') 18 | 19 | 20 | 简单的看一下这个浏览器,在所有文件夹右侧有一个数字代表该文件夹下的文件数,每个文件右侧的颜色代表该文件的阅读状态:红色为`未读`,黄色为`在读`,绿色为`已读`,可以在文件的右键菜单设置阅读状态。 21 | 22 | ## 指定数据文件夹 23 | 24 | 需要为插件指定一个数据文件夹,这个文件夹必须在ob库内(这意味着你可以在ob的文件浏览器中看到这个文件夹),默认为`bookmaster`。插件产生的数据将存放在这个文件夹内。如果发现后面的设置和标注信息没有保存,请**仔细检查**这个路径,看看路径下有无文件。 25 | 26 | ![数据文件夹设置](images/bookdata_setting.png ':size=850') 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 | ![服务器部署结果](images/bookviewer_deployment.png ':size=850') 54 | 55 | ![服务器地址设置](images/bookvewer_setting.png ':size=850') 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 | ![设备名](images/device_name.png ':size=850') 81 | 82 | 在设置面板中的`当前设备名`下显示了当前设备的唯一 `id`,可为该设备自定义名称(只做展示用)。`id` 是从obsidian中获取的,不同设备一般不同,目前发现移动端以ob库路径为 `id`, 如果不同移动端下的ob库文件夹在同一路径,则插件无法分辨。 83 | 84 | 当在新设备中使用插件时,需要重新设置 `书库路径`以及 `BookViewer服务器地址`。 85 | ![书库路径](images/bookvaultpath.png ':size=850')。 86 | 87 | ### 移动端部署(安卓) 88 | 89 | 由于插件对多设备的支持,可以在移动端单独设置不同的书库路径以及BookViewer服务器地址。重点是BookViewer服务器的部署,如果使用公网的服务器地址(如使用测试服务器),则像在pc端一样修改书库文件夹的地址即可。 90 | 91 | ![](images/bookvault_in_mobile.png ':size=400') 92 | 93 | ![](images/bookvault_path_setting.jpg ':size=400') 94 | 95 | 96 | 由于obsidian安卓端有本地服务器的功能,实际上不需要另外部署文件服务器,只需要将bookviewer包放到移动端下, 如下图所示,服务器地址是文件夹地址前加 `http://localhost/_capacitor_file_`, 下面的图的实际地址为`http://localhost/_capacitor_file_/storage/emulated/0/Documents/bookviewer` 97 | 98 | ![](images/bookviewer_path_in_mobile.png ':size=400') 99 | 100 | ![](images/bookviewer_server_setting_mobile.png ':size=400') 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 | ![](images/dataview-example.png ':size=850') 104 | 105 | 106 | ## Front Matter Title 107 | 108 | 由于插件为每个文件的数据文件都使用一个唯一id命名,所以显示时难以分辨,可以使用 [Front Matter Title](https://github.com/snezhig/obsidian-front-matter-title) 插件在文件浏览器中显示实际名称,设置如下图(文件路径规则白名单需要改为自己的book-data路径): 109 | 110 | ![](images/front-matter-title-setting-1.png ':size=850') 111 | ![](images/front-matter-title-setting-2.png ':size=850') 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 | 13 | 45 | 46 | -------------------------------------------------------------------------------- /src/components/v-basic-setting.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /src/components/v-obtree-item.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 50 | 96 | 97 | -------------------------------------------------------------------------------- /src/components/v-obtree.vue: -------------------------------------------------------------------------------- 1 | 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