├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── bug_report_zh_cn.yml └── workflows │ ├── mark_stale.yml │ ├── release.yml │ └── thank_feedback.yml ├── .gitignore ├── .release.py ├── CHANGELOG.md ├── DEV_INTRO.md ├── LICENSE ├── README.md ├── README_zh_CN.md ├── icon.png ├── package.json ├── plugin.json ├── preview.png ├── scripts ├── .gitignore ├── make_dev_link.js └── reset_dev_loc.js ├── src ├── components │ ├── dialog │ │ ├── outdatedSetting.vue │ │ └── switchPanel.vue │ └── settings │ │ ├── block.vue │ │ ├── column.vue │ │ ├── item.vue │ │ ├── items │ │ ├── button.vue │ │ ├── input.vue │ │ ├── nameDivider.vue │ │ ├── order.vue │ │ ├── select.vue │ │ ├── switch.vue │ │ └── textarea.vue │ │ ├── page.vue │ │ └── setting.vue ├── constants.ts ├── hello.vue ├── i18n │ ├── en_US.json │ └── zh_CN.json ├── index.scss ├── index.ts ├── logger │ └── index.ts ├── manager │ └── settingManager.ts ├── syapi │ ├── custom.ts │ ├── index.ts │ └── interface.d.ts ├── types │ ├── index.d.ts │ ├── onlyThis.d.ts │ └── settings.d.ts ├── utils │ ├── common.ts │ ├── commonCheck.ts │ ├── docSortUtils.ts │ ├── getInstance.ts │ ├── lang.ts │ ├── mutex.ts │ ├── onlyThisUtil.ts │ └── settings.ts └── worker │ ├── commonProvider.ts │ ├── contentApplyer.ts │ ├── contentPrinter.ts │ ├── eventHandler.ts │ ├── pluginHelper.ts │ ├── setStyle.ts │ └── shortcutHandler.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a report to help us improve 3 | assignees: [] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for helping us improve by reporting this bug. Please provide as much detail as possible. 9 | 10 | - type: textarea 11 | id: describe-bug 12 | attributes: 13 | label: Describe the Bug 14 | description: Provide a detailed description of the bug's behavior. 15 | placeholder: Describe the bug here... 16 | validations: 17 | required: true 18 | 19 | - type: markdown 20 | attributes: 21 | value: | 22 | If the issue cannot be reproduced consistently, please go to the plugin settings, select "About," enable "Show detailed logs and Join alpha test". Check for any relevant messages in the `Console` of the developer tools (`Ctrl+Shift+I`) when bug occured. 23 | 24 | - type: textarea 25 | id: steps-to-reproduce 26 | attributes: 27 | label: Steps to Reproduce 28 | description: Describe the steps or settings needed to reproduce the issue. If it cannot be consistently reproduced, indicate the frequency of occurrence and upload relavent logs in the developer tools (if available). 29 | placeholder: 1. Step one\n2. Step two\n3. Step three 30 | validations: 31 | required: true 32 | 33 | - type: textarea 34 | id: expected-behavior 35 | attributes: 36 | label: Expected Behavior 37 | description: Describe what you expected to happen. 38 | placeholder: Describe the expected behavior here... 39 | 40 | - type: textarea 41 | id: screenshots-or-recordings 42 | attributes: 43 | label: Screenshots or Recordings 44 | description: If possible, upload screenshots or recordings to demonstrate the bug. 45 | placeholder: Upload to screenshot or recording here... 46 | 47 | - type: input 48 | id: operating-system 49 | attributes: 50 | label: Device and Version Information 51 | description: Specify your OS, plugin version, SiYuan version, and other relevant details. 52 | placeholder: "e.g., Operating System: iOS; Plugin Version: latest; SiYuan Version: v3.1.11" 53 | validations: 54 | required: true 55 | 56 | - type: textarea 57 | id: additional-information 58 | attributes: 59 | label: Additional Information 60 | description: Add any other relevant information here. 61 | placeholder: Additional context here... 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_zh_cn.yml: -------------------------------------------------------------------------------- 1 | name: 问题反馈 2 | description: 提交非预期行为、错误或缺陷报告 3 | assignees: [] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | 感谢提交问题反馈!请尽可能详细地填写以下内容,以帮助我们理解和解决问题。 9 | 10 | - type: textarea 11 | id: problem-description 12 | attributes: 13 | label: 问题现象 14 | description: 尽可能详细地描述问题的表现。 15 | placeholder: 在此描述问题... 16 | validations: 17 | required: true 18 | 19 | - type: markdown 20 | attributes: 21 | value: | 22 | 如果问题不能稳定重现,请打开插件设置-关于-显示详细日志并加入测试计划,在错误发生时关注`Ctrl+Shift+I`开发者工具中的`Console`/`控制台`中的有关提示信息。 23 | 24 | 25 | - type: textarea 26 | id: reproduce-steps 27 | attributes: 28 | label: 复现操作 29 | description: 描述重现问题所需要的步骤或设置项。如果不能稳定重现,请说明问题的发生频率,并上传错误提示信息。 30 | placeholder: 1. 步骤一\n2. 步骤二\n3. 步骤三 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | id: expected-behavior 36 | attributes: 37 | label: 预期行为 38 | description: 描述预期发生的情况。 39 | placeholder: 在此描述预期行为... 40 | 41 | - type: textarea 42 | id: screenshots-or-recordings 43 | attributes: 44 | label: 截图或录屏说明 45 | description: 如果可能,请上传截图或录屏来演示问题。 46 | placeholder: 在此提供截图或录屏... 47 | 48 | - type: input 49 | id: device-info 50 | attributes: 51 | label: 设备与版本信息 52 | description: 请提供操作系统、插件和思源版本等信息。 53 | placeholder: 例:操作系统:iOS;插件版本:最新;思源版本:v3.1.11 54 | validations: 55 | required: true 56 | 57 | - type: textarea 58 | id: additional-info 59 | attributes: 60 | label: 其他补充信息 61 | description: 如有其他相关信息,请在此提供。 62 | placeholder: 在此提供补充信息... 63 | -------------------------------------------------------------------------------- /.github/workflows/mark_stale.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | 10 | jobs: 11 | close-issues: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/stale@v9 15 | with: 16 | days-before-issue-stale: 45 17 | days-before-issue-close: 14 18 | stale-issue-label: "stale" 19 | stale-issue-message: "This issue has been marked as stale because it has been open for 45 days with no activity. If you believe this issue is still relevant, please comment to keep it active. Otherwise, it will be closed in 14 days. Thank you for your contributions!" 20 | # close-issue-message: "This issue is about to be closed because it has been inactive for 14 days since being marked as stale. If you still have questions or concerns, please leave a comment." 21 | exempt-issue-labels: "in-progress,working on,working_on,stage/working on,stage/queue,stage/queue(next)" 22 | exempt-all-milestones: true 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release on Tag Push 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | # Checkout 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | 19 | # Install Node.js 20 | - name: Install Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 18 24 | registry-url: "https://registry.npmjs.org" 25 | 26 | # Install pnpm 27 | - name: Install pnpm 28 | uses: pnpm/action-setup@v4 29 | id: pnpm-install 30 | with: 31 | version: 8 32 | run_install: false 33 | 34 | # Get pnpm store directory 35 | - name: Get pnpm store directory 36 | id: pnpm-cache 37 | shell: bash 38 | run: | 39 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 40 | 41 | # Setup pnpm cache 42 | - name: Setup pnpm cache 43 | uses: actions/cache@v3 44 | with: 45 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 46 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 47 | restore-keys: | 48 | ${{ runner.os }}-pnpm-store- 49 | 50 | # Install dependencies 51 | - name: Install dependencies 52 | run: pnpm install 53 | 54 | # Build for production, 这一步会生成一个 package.zip 55 | - name: Build for production 56 | run: pnpm build 57 | 58 | - name: Set up Python 59 | uses: actions/setup-python@v2 60 | with: 61 | python-version: '3.8' 62 | 63 | - name: Get CHANGELOGS 64 | run: python .release.py 65 | 66 | - name: Release 67 | uses: softprops/action-gh-release@v1 68 | with: 69 | body_path: ./result.txt 70 | files: package.zip 71 | prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') || contains(github.ref, 'dev') }} 72 | token: ${{ secrets.GITHUB_TOKEN }} 73 | -------------------------------------------------------------------------------- /.github/workflows/thank_feedback.yml: -------------------------------------------------------------------------------- 1 | name: Issue Commenter 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | permissions: 8 | issues: write 9 | pull-requests: write 10 | 11 | jobs: 12 | comment: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check author 16 | uses: actions/github-script@v7 17 | id: checkskip 18 | with: 19 | script: | 20 | const issueAuthor = context.payload.issue.user.login; 21 | const excludedUser = [context.repo.owner]; 22 | if (-1 !== excludedUser.indexOf(issueAuthor)) { 23 | core.setOutput('skip', 'true'); 24 | } else { 25 | core.setOutput('skip', 'false'); 26 | } 27 | 28 | - uses: gacts/is-stargazer@v1 29 | if: steps.checkskip.outputs.skip != 'true' 30 | id: check-star 31 | 32 | - name: Comment 1 33 | if: steps.check-star.outputs.is-stargazer == 'true' && steps.checkskip.outputs.skip != 'true' 34 | uses: actions/github-script@v7 35 | with: 36 | script: | 37 | github.rest.issues.createComment({ 38 | issue_number: context.issue.number, 39 | owner: context.repo.owner, 40 | repo: context.repo.repo, 41 | body: `Thank you for your feedback! We appreciate you taking the time to contribute. 42 | 43 | We usually respond within 7 days. To ensure you don't miss any updates, we recommend subscribing to email notifications or checking back regularly. 44 | 45 | Also, Thank you for starring our repository!🌟` 46 | }) 47 | 48 | - name: Comment 2 49 | if: steps.check-star.outputs.is-stargazer == 'false' && steps.checkskip.outputs.skip != 'true' 50 | uses: actions/github-script@v7 51 | with: 52 | script: | 53 | github.rest.issues.createComment({ 54 | issue_number: context.issue.number, 55 | owner: context.repo.owner, 56 | repo: context.repo.repo, 57 | body: `Thank you for your feedback. We appreciate you taking the time to contribute. 58 | 59 | We usually respond within 7 days. To ensure you don't miss any updates, we recommend subscribing to email notifications or checking back regularly.` 60 | }) 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | pnpm-lock.yaml 5 | package.zip 6 | node_modules 7 | dev 8 | dist 9 | build 10 | tmp 11 | notSync/* 12 | !notSync/ -------------------------------------------------------------------------------- /.release.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open('CHANGELOG.md', 'r', encoding='utf-8') as f: 4 | readme_str = f.read() 5 | 6 | match_obj = re.search(r'(?<=### )[\s\S]*?(?=#)', readme_str, re.DOTALL) 7 | if match_obj: 8 | h3_title = match_obj.group(0) 9 | with open('result.txt', 'w') as f: 10 | f.write(h3_title) 11 | else: 12 | with open('result.txt', 'w') as f: 13 | f.write("") -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 更新日志 2 | 3 | ### v1.6.1 (2025年6月1日) 4 | 5 | - 修复:笔记本下的第一层文档,上一篇下一篇、同级文档不显示的问题; 6 | - 改进:样式调整; 7 | - 父文档、相邻文档均纳入“文档链接等宽分列数”控制范围,如要修改为原状,请使用 8 | ```css 9 | .og-hierachy-navigate-parent-doc-container docLinksWrapper, .og-hierachy-navigate-next-doc-container docLinksWrapper { 10 | width: unset; 11 | } 12 | ``` 13 | - 预览方格内容区不再纳入`最大高度限制`控制范围,现在是单独控制的`max-height: 30vh`;对应有选择器调整,请使用`.og-hn-pb-container`选中预览方格内容区; 14 | - 宽度一致性:带预览的相邻文档,改为`overflow: scroll`; 15 | - 移除:调整“所见即所得”编辑器padding-bottom的行为; 16 | 17 | ### v1.6.0 (2025年5月29日) 18 | 19 | - 新增:🧪将内容区显示在文档末尾; 20 | - 新增:🧪带预览的上一篇下一篇内容区; 21 | - 新增:🧪预览方格内容区; 22 | - 改进:在内容区数量不足时,不再显示”更多/更少“; 23 | - 改进:性能模式开启时,文档信息行将不统计字符个数、所有下层文档个数; 24 | - 调整:移动端内容最大高度改为固定25vh; 25 | 26 | ### v1.5.3 (2025年5月10日) 27 | 28 | - 调整:“移动端返回键触发更新”设置项即使开启,也会在思源v3.1.25及之后版本不生效;[相关issue](https://github.com/siyuan-note/siyuan/issues/14296) 29 | 30 | 该设置项将在插件下次更新时移除。 31 | 如仍遇到移动端返回上一篇文档导航未刷新的问题,请更新至思源v3.1.25及以上版本。 32 | - 改进:新增设置项“性能模式”; 33 | 34 | 子文档或同级文档数量大于512后,将不再显示部分内容区; 35 | - 改进:性能调整,对于文档树列表结果,由Printer获取并存储在公共区域,以避免在未启用内容区时仍然获取相应信息; 36 | - 新增:用于控制文档名是否居中的设置项; 37 | 38 | ### v1.5.2 (2025年2月1日) 39 | 40 | - 修复:“往年今日”等内容区,单击链接可能重复打开文档的问题; 41 | 42 | ### v1.5.1 (2025年1月5日) 43 | 44 | - 修复:设置“文档链接等宽分列数”、“隐藏导航区提示词”后,部分情况下右侧出现较大空白、并没有完成分列的问题;修复了另一个相关问题; 45 | 46 | ### v1.5.0 (2024年12月31日) 47 | 48 | - 修复:尝试修复发布模式下,插件导致反复进行Basic认证的问题; 49 | - 改进:增加设置项 及时更新;(在文档重命名、移动等操作后更新导航区); 50 | - 改进:临时置顶内容区(测试结束); 51 | - 其他:前缀为空情况处理、`.og-hn-ref-link-simulate`class识别插件ref、无笔记本图标bug; 52 | 53 | ### v1.4.3 (2024年11月19日) 54 | 55 | - 修复:尝试修复MacOS下快捷键问题; 56 | - 改进:兼容思源v3.1.12中动态文档图标的更改; 57 | - 修复:相关文档面板排除重复文档; 58 | - 新增:临时置顶内容区(测试); 59 | 60 | ### v1.4.2 (2024年11月7日) 61 | 62 | - 修复:文档排序不是文档名升序时,错误补充日记上一篇下一篇的问题; 63 | 涉及设置项变动:“相邻文档对于日记(dailynote)按日记对应日期升序获取”; 64 | - 修复:“文档上级为笔记本时,父文档区域显示同级文档”设置项无效的问题; 65 | - 修复:“父文档”/“上层文档”图标错误的问题; 66 | - 修复:无法一次获取图标时,显示文档图标错误的问题; 67 | 涉及设置项变动:“尽可能地显示文档图标”; 68 | 69 | ### v1.4.1 (2024年11月2日) 70 | 71 | - 修复:当编辑器中存在选区时,无法通过层级导航打开文档的问题;(重构) 72 | - 新增:`Ctrl+点击`在后台标签页中打开; 73 | alt、shift在分屏打开原本就支持; 74 | - 新增:🧪打开文档后关闭上一个文档(设置项); 75 | - 新增:🧪连续打开文档则关闭之前的文档(设置项); 76 | - 改进:文档信息内容区加入所有下方文档计数(括号中表示,依赖数据库,部分情况下需要重建索引才准确); 77 | - 移除:禁用移动端面板入口; 78 | 79 | ### v1.4.0-rc1 (2024年10月21日) 80 | 81 | - 修复:“自第x个启用的内容区起,隐藏内容区”启用时,不参与刷新的内容区刷新后不显示的问题; 82 | - 新增:相邻文档跳转对话框;默认快捷键Ctrl+Alt+E 83 | - 支持通过快捷键创建文档; 84 | - 如果没有找到想要的结果,支持跳转到“基于文档搜索”插件继续搜索; 85 | - 改进:调整与`listChlidDocs`的兼容性; 86 | - 修复:启动思源但保留之前文档页签时,分屏后新出现的页签没有刷新的问题; 87 | - 移除:移除设置项:icon调节、其他情况下显示、同级文档忽略隐藏文档; 88 | 89 | ### v1.3.1 (2024年8月25日) 90 | 91 | - 修复:“编辑器宽度”插件启用时无法与标题对齐的问题; 92 | 93 | ### v1.3.0(2024年8月18日) 94 | 95 | - 修复:“导航区不再插在文档标题所在HTML元素”开启且“文档自适应宽度”关闭,插件没有处理宽度变化的问题; 96 | - 改进:使用思源的悬停样式替换部分悬停效果; 97 | - 新增:反向链接排除和指定正则匹配设置; 98 | - 新增:反链链接排序方式; 99 | - 改进:设置页; 100 | 101 | 设置项移除提示将在新版本安装后提示一次: 102 | 部分在“试验田”结束测试的设置已经移动到“内容展示逻辑”或“通用”选项卡; 103 | 104 | ### v1.3.0-beta1 (2024年8月16日) 105 | 106 | - 修复:开启“导航区不再插在文档标题所在HTML元素”且“文档自适应”宽度关闭,插件没有处理宽度变化的问题; 107 | - 改进:使用思源的悬停样式替换部分悬停效果; 108 | - 新增:反向链接排除和指定正则匹配设置; 109 | - 新增:反链链接排序方式; 110 | 111 | ### v1.2.0 (2024年7月22日) 112 | 113 | - 新增:(设置项)自第x个内容区起,隐藏内容区; 114 | - 新增:(设置项)在面包屑前加入当前笔记本层级; 115 | - 回滚:撤销v1.1.0中“修复:安卓端导航区被文档图标遮挡的问题”的更改; 116 | - 修复:面包屑部分鼠标悬停时显示undefined的问题; 117 | - 修复:设置面板关闭后未销毁的问题(感谢 Frostime https://ld246.com/article/1721278971170 ); 118 | 119 | 相较于 beta1: 120 | - 调整:面包屑中笔记本层级的背景色; 121 | - 调整:隐藏内容区功能改为各个内容区独立隐藏; 122 | 123 | ### v1.2.0-beta1 (2024年7月19日) 124 | 125 | - 新增:(设置项)自第x个内容区起,隐藏内容区; 126 | - 新增:(设置项)在面包屑前加入当前笔记本层级; 127 | - 回滚:撤销v1.1.0中“修复:安卓端导航区被文档图标遮挡的问题”的更改; 128 | - 修复:面包屑部分鼠标悬停时显示undefined的问题; 129 | - 修复:设置面板关闭后未销毁的问题(感谢 Frostime https://ld246.com/article/1721278971170 ); 130 | 131 | ### v1.1.0 (2024年6月16日) 132 | 133 | - 新增:正向链接内容区; 134 | - 修复:安卓端导航区被文档图标遮挡的问题; 135 | - 新增:(设置项)显示详细日志,帮助定位问题; 136 | - 调整:禁用读取设置后立即保存,尝试避免潜在的设置覆盖; 137 | - 改进:鼠标悬停在内容区空白位置,将通过title显示内容区名称; 138 | 139 | ### v1.0.6 (2024年5月31日) 140 | 141 | - 改进:增大内容区间距; 142 | - 新增:(设置项-外观)悬停显示内容区边框; 143 | - 改进:右键内容区,将临时展开内容区高度; 144 | - 调整:升级至此版本,将默认开启“导航区不再插在文档标题所在的HTML元素”、默认禁用“调整文档图标位置”; 145 | (原:内容区插入在“标题”元素内部下方;调整为:内容区插在“标题”元素外部下方) 146 | 147 | ### v1.0.5 (2024年5月15日) 148 | - 修复:“不存在文档时隐藏”对挂件、上一篇下一篇、往年今日内容区无效的问题; 149 | - 新增:(设置项)🧪导航区不再插在文档标题所在的HTML元素; 150 | - 修复:非https、localhost环境下,无法打开插件设置页的问题; 151 | - 调整:等宽分列数的生效范围; 152 | 153 | 增加生效范围:反链、往年今日内容区; 154 | 155 | ### v1.0.4 (2024年4月25日) 156 | 157 | - 修复:“往年今日”在亿些情况下无法显示的问题; 158 | - 新增:(设置项)🧪移动端侧滑返回时更新; 159 | - 新增:(设置项)🧪移动端更新时移除全部内容区; 160 | - 此项在v1.0.2、v1.0.3是默认启用的,但自此版本起默认禁用; 161 | 162 | 163 | ### v1.0.3 (2024年4月2日) 164 | 165 | - 修复:移动端闪卡面包屑未能显示的问题; 166 | 167 | ### v1.0.2 (2024年4月2日) 168 | - 改进:调整部分代码逻辑,加快响应速度; 169 | - 修复:没有相邻文档时,区域未显示“无”的问题; 170 | - 设置项: 171 | > 本次调整了样式CSS的实现,请自定义CSS的用户注意修改代码片段或插件外观设置; 172 | 173 | - 调整:文档名最大长度的实现,现在此设置仅对子文档菜单生效,对页面内容不生效; 174 | - 调整:`sameWidth`文档链接相同宽度的实现更改为`min-width`,用于等宽分列下控制最小列宽; 175 | - 新增:子文档、同级文档区域等宽分列显示控制; 176 | - 新增:🧪上一篇、下一篇支持跟随日记跳转(内容区可选项,文档/块较多时,将导致显示延迟); 177 | - 改进:上一篇下一篇快捷键在识别为日记且文档树上下一篇获取失败时,跟随日记属性`custom-dailynote-yyyyMMdd`跳转; 178 | - 修复:移动端内容区重复显示(切换时未清空内容区); 179 | - 修复:移动端插件冲突导致导航区显示错误的问题;(关于侧滑返回不更新的问题,请升级到思源v3.0.6或更高版本) 180 | - 新增:🧪往年今日内容区; 181 | 182 | ### v1.0.1 (2024年3月14日) 183 | - 修复:一些情况下设置项回滚为默认设置的问题; 184 | - 新增:“间隔复习”界面新增“块面包屑”内容区; 185 | - 改进:部分内容区获取改为异步进行,提升响应速度; 186 | - 改进:对隐藏文档的处理,详见试验田中设置项; 187 | - 修复:对一些常见的字符实体进行转义; 188 | - 移除:出错重试; 189 | - 改进:跟随fdb更新:面包屑子文档菜单加入图标; 190 | 191 | ### v1.0.0 (2024年3月4日) 192 | - 重构:使用`vite-vue-ts`重构项目,**这带来了一些尚未被发现的bugs(缺陷),导致原本能够使用的功能未被迁移或出错**; 193 | - 新增: 194 | - 内容区显示控制,支持自定义排序; 195 | - 支持在“间隔复习”界面显示文档导航,内容独立控制(在全屏下点击将在后台打开文档!); 196 | - 支持文档自定义显示顺序和显示内容(原有的og文档导航忽略参数优先生效)(设置方法:文档上增加属性`og-hn-content`,属性值为数组,例`["parent", "child", "info"]`); 197 | - 改进: 198 | - 一定程度上加快显示速度,现在使用`loaded-protyle`事件调起; 199 | - 跟随文档面包屑变更,在不存在子文档时不显示最后一个`>`; 200 | - 修复: 201 | - `listChildDocs`部分不能正常显示的问题; 202 | - 有时安卓端不能显示的问题; 203 | - 部分分屏情况下不显示的问题; 204 | - 分屏情况下,点击导航打开的文档不在所在分屏区的问题; 205 | - UI: 206 | - 新的设置项页面,希望能够使得设置项更好找; 207 | - 移除: 208 | - 设置项:立即更新(由`ws-main`触发无protyle信息,重构后不兼容使用`id`定位); 209 | 210 | 211 | ### v0.2.12 (2024年2月4日) 212 | - 修复:“文档最大数量”设置为0时,子文档、同级文档不显示的问题; 213 | 214 | ### v0.2.11 (2024年2月1日) 215 | - 修复:“文档最大数量”设置较小时,导致思源提示,并可能导致无法显示、相邻文档跳转失败的问题; 216 | - 新增:快捷键打开第一个子文档; 217 | 218 | ### v0.2.10 (2023年11月28日) 219 | - 修复:禁用“隐藏导航区提示词”时,导航区域宽度溢出的问题; 220 | - 修复:移动端lcd挂件不能在文档切换后刷新的问题; 221 | - 新增:🧪支持显示反链区域; 222 | 223 | ### v0.2.9 (2023年11月14日) 224 | 225 | - 界面: 226 | - 设置页面高度增加; 227 | - 上下文档加入提示词“邻”; 228 | - 设置最大最小值判定(同步面包屑插件更改); 229 | - 在显示提示词时,第二行起与提示词结尾对齐缩进; 230 | - 改进:尽快响应新分屏中的页签; 231 | - 改进:🧪(设置项)出错重试次数,默认为5,减少不显示的概率; 232 | - 修复:反复按下“返回父文档”快捷键后,有概率不能连续响应的问题;(快捷键绑定换用callback实现); 233 | - 新增:快捷键:打开同层级上一篇、下一篇文档; 234 | - 新增:🧪(设置项)文档包含数据库时,不显示子文档区域; 235 | - 新增:🧪(设置项)空白文档判定阈值(用于控制lcd挂件的显示); 236 | 237 | ### v0.2.8 (2023年10月18日) 238 | - 新增:引入思源编辑器内快捷键,用于在当前位置(当前块下方)插入listChildDocs挂件; 239 | (为防止用户误触,默认不绑定组合键,需要在`设置`-`快捷键`-`插件`-`文档层级导航`-`插入lcd挂件`处自行设置对应的组合键) 240 | - 新增:设置项“始终显示同级文档”; 241 | - 改进:同级文档中,当前文档对应的链接样式调整(对应css类`og-hn-docLinksWrapper-hl`); 242 | 243 | ### v0.2.7 (2023年9月11日) 244 | - 修复:同层级上一篇、下一篇样式问题; 245 | - 改进:容器样式,缩短与正文的间距; 246 | - 新增:思源内快捷键`Ctrl + Alt + ←`打开父文档; 247 | ### v0.2.6 (2023年9月3日) 248 | - 新增:🧪(设置项)显示同级文档的上一篇、下一篇; 249 | - 新增:🧪对于空白父文档,支持使用listChildDocs替代子文档部分; 250 | - 修复:(设置项:使用文档面包屑替代父文档部分)尝试兼容Tsundoku主题; 251 | - 改进:缓解快速切换页签导致短暂显示错误的问题; 252 | - 移除:出错重试;改用`onLayoutReady`尝试一次; 253 | - 改进:关闭插件后移除已经插入的文档导航容器; 254 | 255 | ### v0.2.6-beta3 (2023年8月29日) 256 | - 修复:上一篇下一篇中未处理`.sy`后缀的问题; 257 | - 修复:尝试与Tsundoku主题兼容; 258 | 259 | ### v0.2.6-beta2 (2023年8月28日) 260 | - 新增:🧪同级文档的上一篇、下一篇; 261 | 262 | ### v0.2.6-beta1 (2023年8月24日) 263 | - 新增:🧪对于空白父文档,支持使用listChildDocs替代子文档部分; 264 | - 改进:缓解快速切换页签导致短暂显示错误的问题; 265 | - 移除:出错重试;改用`onLayoutReady`尝试一次; 266 | - 改进:关闭插件后移除已经插入的文档导航容器; 267 | 268 | ### v0.2.5 (2023年7月10日) 269 | - 修复:部分情况下,提示词和链接没有对齐的问题; 270 | - 改进:“无”的样式; 271 | - 改进:没有子文档时,不显示子文档字符总数; 272 | - 修复:子文档总字符数重复统计的问题; 273 | 274 | ### v0.2.4 (2023年7月9日) 275 | - 新增:在首行显示当前文档的相关信息; 276 | - 修复:对于刚创建的文档,文档导航显示错误的问题(现在最差的情况是不显示); 277 | - 新增:🧪使用文档面包屑替代父文档部分; 278 | 279 | ### v0.2.3 (2023年6月20日) 280 | - 改进:新增设置项以在无文档时不显示“无”; 281 | 282 | ### v0.2.2(2023年6月9日) 283 | - 改进:切换页签后更新文档导航; 284 | - 改进:检查文档变化并立即更新文档导航; 285 | 286 | ### v0.2.1(2023年5月31日) 287 | 288 | - 修复:在v2.8.10-dev5出现错误的问题; 289 | - 改进:默认调整图标位置; 290 | 291 | ### v0.2.0 (2023年5月26日) 292 | 293 | - 重构:迁移至官方插件系统; 294 | - 改进:支持设定最大高度,控制占用的空间; 295 | - 改进:带有`og-hn-ignore`或`og文档导航忽略`属性或属性值的文档将被忽略; 296 | 297 | ### v0.1.1 (2023年4月26日) 298 | 299 | - 新增:设置项配置; 300 | - 改进:默认样式; 301 | - 修复:快速切换标签页插入错误的问题; 302 | - 修复:导致选项`转移引用`无法显示; 303 | - 修复:安卓端问题; 304 | 305 | ### v0.1.0 306 | 307 | 首次发布。 -------------------------------------------------------------------------------- /DEV_INTRO.md: -------------------------------------------------------------------------------- 1 | ## dev introduction 参与开发 / 自行打包 2 | 3 | > 文档更新可能有所落后。 4 | 5 | 本项目基于[siyuan/plugin-sample-vite-svelte](https://github.com/siyuan-note/plugin-sample-vite-svelte),整体开发方式相同; 6 | 7 | 这里摘录如下: 8 | 9 | > 1. 安装 [NodeJS](https://nodejs.org/en/download) 和 [pnpm](https://pnpm.io/installation),然后在开发文件夹下执行 `pnpm i` 安装所需要的依赖; 10 | > 11 | > 开发者所使用的版本分别为:`v16.20.2`和`v8.7.5`; 12 | > 2. **自动创建符号链接** 13 | > - 打开思源笔记, 确保思源内核正在运行 14 | > - 运行 `pnpm run make-link`, 脚本会自动检测所有思源的工作空间, 请在命令行中手动输入序号以选择工作空间 15 | > ```bash 16 | > >>> pnpm run make-link 17 | > > plugin-sample-vite-svelte@0.0.3 make-link H:\SrcCode\开源项目\plugin-sample-vite-svelte 18 | > > node --no-warnings ./scripts/make_dev_link.js 19 | > 20 | > "targetDir" is empty, try to get SiYuan directory automatically.... 21 | > Got 2 SiYuan workspaces 22 | > [0] H:\Media\SiYuan 23 | > [1] H:\临时文件夹\SiYuanDevSpace 24 | > Please select a workspace[0-1]: 0 25 | > Got target directory: H:\Media\SiYuan/data/plugins 26 | > Done! Created symlink H:\Media\SiYuan/data/plugins/plugin-sample-vite-svelte 27 | > ``` 28 | > 3. **手动创建符号链接** 29 | > - 打开 `./scripts/make_dev_link.js` 文件,更改 `targetDir` 为思源的插件目录 `/data/plugins` 30 | > - 运行 `pnpm run make-link` 命令, 如果看到类似以下的消息,说明创建成功: 31 | > ```bash 32 | > ❯❯❯ pnpm run make-link 33 | > > plugin-sample-vite-svelte@0.0.1 make-link H:\SrcCode\plugin-sample-vite-svelte 34 | > > node ./scripts/make_dev_link.js 35 | > 36 | > Done! Created symlink H:/SiYuanDevSpace/data/plugins/plugin-sample-vite-svelte 37 | > ``` 38 | > 4. 执行 `pnpm run dev` 进行实时编译 39 | > 5. 在思源中打开集市并在下载选项卡中启用插件 40 | > 6. 打包发布时,可以使用action自动构建,或使用`pnpm build`完成构建; 41 | 42 | 请注意,此项目基于AGPLv3.0协议开源,如果再分发,需要满足协议要求;如果要提交PR,请优先提交到dev分支; 43 | 44 | ## 项目结构大致介绍 45 | 46 | ``` 47 | +---.github GitHub使用的相关信息 48 | | \---workflows 49 | +---asset README使用的附件 50 | +---dev 使用pnpm run dev时创建的编译内容 51 | | \---i18n 52 | +---scripts pnpm run make-link 使用的脚本 53 | \---src 插件源代码 54 | +---components vue组件,主要是设置页面组件 55 | | \---settings 56 | | \---items 57 | +---i18n 多语言文件 58 | +---logger 日志管理 59 | +---manager 设置项管理 60 | +---printer 【没有使用】 61 | +---types 类型定义 62 | +---utils 通用工具 63 | \---worker 插件的主要功能逻辑 64 | ``` 65 | 66 | ``` 67 | | constants.ts 所使用的常量 68 | | hello.vue 无用,原用作vue测试 69 | | index.scss 无用 70 | | index.ts plugin入口和Plugin类定义 71 | | 72 | +---components 73 | | \---settings 设置项组件 74 | | | block.vue 块类型设置项 75 | | | item.vue 单项设置项 76 | | | page.vue 设置项单页面 77 | | | setting.vue 设置项总页面 78 | | | 79 | | \---items 设置项元素组件: 80 | | button.vue 按钮 81 | | input.vue 输入 82 | | nameDivider.vue 目前无效 83 | | order.vue 拖拽排序 84 | | select.vue 选择 85 | | switch.vue 开关(启用/禁用) 86 | | textarea.vue 文本框输入区 87 | | 88 | +---i18n 文本翻译和多语言 89 | | en_US.json 90 | | zh_CN.json 91 | | 92 | +---logger 日志 93 | | index.ts 94 | | 95 | +---manager 96 | | settingManager.ts 设置项管理,包括一个新设置项的声明和保存逻辑 97 | | 98 | +---syapi 99 | | custom.ts 思源笔记API(仅本插件使用,有修改) 100 | | index.ts 思源笔记API(常用) 101 | | interface.d.ts 思源笔记API接口 102 | | 103 | +---types 104 | | index.d.ts 通用类型声明 105 | | onlyThis.d.ts 仅本插件使用的类型声明 106 | | settings.d.ts 设置项所使用的类型声明 107 | | 108 | +---utils 109 | | common.ts 通用工具 110 | | commonCheck.ts 通用检查工具 111 | | getInstance.ts 获得类示例 112 | | lang.ts 获得i18n语言信息 113 | | mutex.ts 互斥量 114 | | onlyThisUtil.ts 仅在本插件中使用的工具类 115 | | settings.ts 设置项工具 116 | | 117 | \---worker 118 | commonProvider.ts 通用内容提供 119 | contentApplyer.ts 内容区DOM写入/更新操作 120 | contentPrinter.ts 内容区HTMLElement生成 121 | eventHandler.ts 思源事件处理 122 | pluginHelper.ts 目前无用 123 | setStyle.ts 设置样式 124 | shortcutHandler.ts 快捷键绑定处理 125 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Hierarchy Navigate 2 | 3 | [中文](README_zh_CN.md) 4 | 5 | > Most of this document was translated by Google Translate. 6 | 7 | > A [Siyuan-note](https://github.com/siyuan-note/siyuan) plugin that adds parent and children document links under the document title. 8 | 9 | ### Quick Start 10 | 11 | - Download from marketplace OR 1: Unzip the `package.zip` file in Release, 2: Move the folder to `{workplace}/data/plugins/`, 3: Rename the folder to `syplugin-hierarchyNavigate`; 12 | - Just turn on the plugin; (`Marketplace`--`Downloaded`--`Plugin`--Find this plugin, click switch icon) 13 | - For more information, please refer to the plugin setting page; (`Marketplace`--`Downloaded`--`Plugin`--`HierarchyNavigate`-- Click setting icon) 14 | - Documents with the `og-hn-ignore` attribute or attribute value will be ignored and the hierarchical navigation will not be displayed. 15 | - By using the `og-hn-content` attribute in the document, you can customize the display order of the current document's content. The attribute value is an array of strings, for example: `["info", "breadcrumb", "parent", "sibling", "previousAndNext", "backlinks", "child", "widget"]`. 16 | 17 | #### Shortcut 18 | 19 | | Func | Default Shortcut | Recommended Shortcut | Note | 20 | | --- | --- | --- | --- | 21 | | Open upper doc | `⌥⌘←` or `Ctrl + Alt + ←` | | | 22 | | Open first subdocument | `⌥⌘→` or `Ctrl + Alt + →` | | | 23 | | Open previous doc | `⌥⌘↑` or `Ctrl + Alt + ↑` | | | 24 | | Open next doc | `⌥⌘↓` or `Ctrl + Alt + ↓` | | | 25 | | Insert the `listChildDocs` widget | / | `⌥⇧L` or `Shift + Alt + L` | Need download `listChildDocs` first | 26 | | Display navigation related to the current document | `⌥⌘E` or `Ctrl + Alt + E` | | | 27 | 28 | #### Other explanation 29 | 30 | - Maybe available in siyuan Android App (in testing); 31 | 32 | - If there is lag when inputting large amounts of text after enabling the plugin, please check the `Immediate Update` setting item; if there is a display delay, please check setting items such as `Display Document Icons Whenever Possible` and "Backlinks: Pin document names that match the regular expression". 33 | 34 | 35 | 36 | ### Feedback bugs 37 | 38 | Please go to [github repository](https://github.com/OpaqueGlass/syplugin-my-plugin-collection) to report problems. 39 | 40 | ### References & Appreciations 41 | 42 | For dependencies, please refer[package.json](./package.json). 43 | 44 | | Developer/Project | Description | Illustration | 45 | | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | 46 | | [UFDXD](https://github.com/UFDXD)/[HBuilderX-Light](https://github.com/UFDXD/HBuilderX-Light) | A borderless eye protection theme | This plug-in is implemented by referring to the parent-child document information under its title | 47 | | [leolee9086](https://github.com/leolee9086) / [cc-template](https://github.com/leolee9086/cc-template) | Render template in widget; [Mulan Permissive Software License,Version 2](https://github.com/leolee9086/cc-template/blob/main/LICENSE) | Click to open the doc. | 48 | | [zuoez02](https://github.com/zuoez02)/[siyuan-plugin-system](https://github.com/zuoez02/siyuan-plugin-system) | A 3-rd plugin system for siyuan | | 49 | | [zxhd863943427](https://github.com/zxhd863943427)&[mozhux (赐我一胖)](https://github.com/mozhux) | | Suggestions or contributions about default style. | 50 | |[wetoria](https://github.com/Wetoria)/[DailyNotesWalker](https://github.com/Wetoria/siyuan-plugin-DailyNotesWalker)|Shortcuts Quick View Previous Next Diary|Refer to its idea and shortcut binding method| 51 | | (qq) 八面风, 与路同飞, (Github) [QQQOrange](https://github.com/QQQOrange) | | Assist in locating the issue | 52 | | [Trilium](https://github.com/zadam/trilium) / note-list-widget | | Preview box content area css style, and functional design | -------------------------------------------------------------------------------- /README_zh_CN.md: -------------------------------------------------------------------------------- 1 | ## hierarchyNavigate 文档层级导航 2 | 3 | [English](README.md) 4 | 5 | > 在文档标题下添加上下层文档导航的[思源笔记](https://github.com/siyuan-note/siyuan)插件。 6 | 7 | > 当前版本:v1.6.1 8 | > 9 | > 修复:笔记本下的第一层文档,上一篇下一篇、同级文档不显示的问题; 10 | > 改进:样式调整; 11 | > 12 | > 其他详见 [更新日志](CHANGELOG.md)。 13 | 14 | ### 快速开始 15 | 16 | - 从集市下载 或 1、解压Release中的`package.zip`,2、将文件夹移动到`工作空间/data/plugins/`,3、并将文件夹重命名为`syplugin-hierarchyNavigate`; 17 | - 开启插件即可; 18 | - 其他请浏览插件设置页面(`设置`→`集市`→`已下载`→`插件`→`文档层级导航`→“设置图标”),**提示:设置页可以上下滑动哦**; 19 | - 带有`og-hn-ignore`或`og文档导航忽略`属性或属性值的文档将被忽略,不显示层级导航; 20 | - 通过文档上的`og-hn-content`属性,可自定义当前文档的内容显示顺序,属性值为字符串数组,例`["info","breadcrumb","parent","sibling","previousAndNext","backlinks","child","widget"]`; 21 | - 每个内容区默认限制高度(设置-外观、CSS-最大高度限制),右键内容区,可临时解除内容区高度限制; 22 | 23 | #### 快捷键说明 24 | 25 | > 插件注册的快捷键可以到 思源设置-快捷键 处更改; 26 | 27 | | 功能 | 默认快捷键 | 快捷键设置参考| 备注 | 28 | | --- | --- | --- | --- | 29 | | 跳转到父文档(向上) | `⌥⌘←` 或 `Ctrl + Alt + ←` | | 默认快捷键可能和网易云音乐全局快捷键冲突,以下3个快捷键也有相同的问题 | 30 | | 跳转到首个子文档(向下) | `⌥⌘→` 或 `Ctrl + Alt + →` | | | 31 | | 跳转到上一篇文档(同层级) | `⌥⌘↑` 或 `Ctrl + Alt + ↑` | | | 32 | | 跳转到下一篇文档(同层级) | `⌥⌘↓` 或 `Ctrl + Alt + ↓` | | | 33 | | 当前块下方插入listChildDocs挂件 | 无 | `⌥⇧L` 或 `Shift + Alt + L` | 需事先下载挂件 | 34 | | 显示与当前文档相关文档导航 | `⌥⌘E` 或 `Ctrl + Alt + E` | | | 35 | | 置顶当前文档的层级导航区域 | 无 | `⌥⌘Q` 或 `Ctrl + Alt + Q` | 再次按下快捷键取消临时置顶 | 36 | 37 | 点击打开文档的同时按下快捷键: 38 | 39 | - `Ctrl`:在后台标签页打开; 40 | - `Shift`:在下侧分屏打开; 41 | - `Alt`: 在右侧分屏打开; 42 | 43 | #### 说明 44 | 45 | - 对于刚创建的文档,由于数据库更新延迟,可能不会显示导航区,切换页签之后将正常显示; 46 | 47 | - 如果启用插件后,在输入大量文字时卡顿,请检查`及时更新`设置项;出现显示延迟,请检查`尽可能地显示文档图标`、“反向链接文档名匹配正则表达式”等设置项; 48 | 49 | 50 | ### 反馈bug 51 | 52 | (推荐)请前往[github仓库](https://github.com/OpaqueGlass/syplugin-hierarchyNavigate)反馈问题。 53 | 54 | 如果您无法访问github,请[在此反馈](https://wj.qq.com/s2/12395364/b69f/)。 55 | 56 | ### 参考&感谢 57 | 58 | 代码贡献者(开发者)详见[贡献者列表](https://github.com/OpaqueGlass/syplugin-hierarchyNavigate/graphs/contributors)。 59 | 60 | 依赖项详见[package.json](./package.json)。 61 | 62 | | 开发者/项目 | 描述 | 说明 | 63 | | ------------------------------------------------------------ | ------------------------------------------------------------ | ---------------------------- | 64 | | [UFDXD](https://github.com/UFDXD)/[HBuilderX-Light](https://github.com/UFDXD/HBuilderX-Light) | 一个无边距的护眼主题 | 参考其标题下父子文档信息实现 | 65 | | [leolee9086](https://github.com/leolee9086) / [cc-template](https://github.com/leolee9086/cc-template) | 使用挂件渲染模板;[木兰宽松许可证, 第2版](https://github.com/leolee9086/cc-template/blob/main/LICENSE) | 点击打开文档 | 66 | | [zuoez02](https://github.com/zuoez02)/[siyuan-plugin-system](https://github.com/zuoez02/siyuan-plugin-system) | 插件系统 | | 67 | | [zxhd863943427](https://github.com/zxhd863943427)&[mozhux (赐我一胖) (github.com)](https://github.com/mozhux) | | 样式建议等 | 68 | |[wetoria](https://github.com/Wetoria)/[DailyNotesWalker](https://github.com/Wetoria/siyuan-plugin-DailyNotesWalker)|快捷键快速查看上下一篇日记|参考其idea和快捷键绑定方式| 69 | | (qq) 八面风, 与路同飞, (Github) [QQQOrange](https://github.com/QQQOrange) | | 帮助定位问题 | 70 | |[Trilium](https://github.com/zadam/trilium) / note-list-widget | | 预览方格内容区css样式,和功能设计 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpaqueGlass/syplugin-hierarchyNavigate/3d0b081a60cfa381729d532c5f8580de05ab953e/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "syplugin-hierarchy-navigate", 3 | "version": "1.0.0-SNAPSHOT", 4 | "type": "module", 5 | "description": "", 6 | "repository": "https://github.com/OpaqueGlass/syplugin-hierarchyNavigate", 7 | "homepage": "", 8 | "author": "OpaqueGlass", 9 | "license": "AGPL-3.0", 10 | "scripts": { 11 | "make-link": "node --no-warnings ./scripts/make_dev_link.js", 12 | "change-dir": "node --no-warnings ./scripts/reset_dev_loc.js", 13 | "dev": "vite build --watch", 14 | "build": "vite build" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^20.6.2", 18 | "@types/sortablejs": "^1.15.8", 19 | "fast-glob": "^3.3.1", 20 | "glob": "^7.2.3", 21 | "minimist": "^1.2.8", 22 | "rollup-plugin-livereload": "^2.0.5", 23 | "sass": "^1.67.0", 24 | "siyuan": "^1.0.6", 25 | "ts-node": "^10.9.1", 26 | "typescript": "^5.2.2", 27 | "vite": "^4.4.9", 28 | "vite-plugin-banner": "^0.7.1", 29 | "vite-plugin-static-copy": "^0.15.0", 30 | "vite-plugin-zip-pack": "^1.0.6" 31 | }, 32 | "dependencies": { 33 | "@vitejs/plugin-vue": "^4.5.0", 34 | "@vue/tsconfig": "^0.4.0", 35 | "natsort": "^2.0.3", 36 | "sortablejs": "^1.15.2", 37 | "vue": "^3.4.15", 38 | "vue-tsc": "^1.8.22" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "syplugin-hierarchyNavigate", 3 | "author": "OpaqueGlass", 4 | "url": "https://github.com/OpaqueGlass/syplugin-hierarchyNavigate", 5 | "version": "1.6.1", 6 | "displayName": { 7 | "default": "HierarchyNavigate", 8 | "zh_CN": "文档层级导航", 9 | "en_US": "HierarchyNavigate" 10 | }, 11 | "description": { 12 | "default": "Insert doc-links under current doc title", 13 | "zh_CN": "在标题下方显示上下层级文档链接", 14 | "en_US": "Insert doc-links under current doc title" 15 | }, 16 | "readme": { 17 | "default": "README.md", 18 | "zh_CN": "README_zh_CN.md", 19 | "en_US": "README.md" 20 | }, 21 | "i18n": [ 22 | "zh_CN", 23 | "en_US" 24 | ], 25 | "funding": { 26 | "custom": [ 27 | "https://wj.qq.com/s2/12395364/b69f/" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpaqueGlass/syplugin-hierarchyNavigate/3d0b081a60cfa381729d532c5f8580de05ab953e/preview.png -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | build 3 | dist 4 | *.exe 5 | *.spec 6 | -------------------------------------------------------------------------------- /scripts/make_dev_link.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import http from 'node:http'; 3 | import readline from 'node:readline'; 4 | 5 | 6 | //************************************ Write you dir here ************************************ 7 | 8 | //Please write the "workspace/data/plugins" directory here 9 | //请在这里填写你的 "workspace/data/plugins" 目录 10 | let targetDir = ''; 11 | //Like this 12 | // let targetDir = `H:\\SiYuanDevSpace\\data\\plugins`; 13 | //******************************************************************************************** 14 | 15 | const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info); 16 | const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info); 17 | 18 | let POST_HEADER = { 19 | // "Authorization": `Token ${token}`, 20 | "Content-Type": "application/json", 21 | } 22 | 23 | async function myfetch(url, options) { 24 | //使用 http 模块,从而兼容那些不支持 fetch 的 nodejs 版本 25 | return new Promise((resolve, reject) => { 26 | let req = http.request(url, options, (res) => { 27 | let data = ''; 28 | res.on('data', (chunk) => { 29 | data += chunk; 30 | }); 31 | res.on('end', () => { 32 | resolve({ 33 | ok: true, 34 | status: res.statusCode, 35 | json: () => JSON.parse(data) 36 | }); 37 | }); 38 | }); 39 | req.on('error', (e) => { 40 | reject(e); 41 | }); 42 | req.end(); 43 | }); 44 | } 45 | 46 | async function getSiYuanDir() { 47 | let url = 'http://127.0.0.1:6806/api/system/getWorkspaces'; 48 | let conf = {}; 49 | try { 50 | let response = await myfetch(url, { 51 | method: 'POST', 52 | headers: POST_HEADER 53 | }); 54 | if (response.ok) { 55 | conf = await response.json(); 56 | } else { 57 | error(`HTTP-Error: ${response.status}`); 58 | return null; 59 | } 60 | } catch (e) { 61 | error("Error:", e); 62 | error("Please make sure SiYuan is running!!!"); 63 | return null; 64 | } 65 | return conf.data; 66 | } 67 | 68 | async function chooseTarget(workspaces) { 69 | let count = workspaces.length; 70 | log(`Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`) 71 | for (let i = 0; i < workspaces.length; i++) { 72 | log(`[${i}] ${workspaces[i].path}`); 73 | } 74 | 75 | if (count == 1) { 76 | return `${workspaces[0].path}/data/plugins`; 77 | } else { 78 | const rl = readline.createInterface({ 79 | input: process.stdin, 80 | output: process.stdout 81 | }); 82 | let index = await new Promise((resolve, reject) => { 83 | rl.question(`Please select a workspace[0-${count-1}]: `, (answer) => { 84 | resolve(answer); 85 | }); 86 | }); 87 | rl.close(); 88 | return `${workspaces[index].path}/data/plugins`; 89 | } 90 | } 91 | 92 | if (targetDir === '') { 93 | log('"targetDir" is empty, try to get SiYuan directory automatically....') 94 | let res = await getSiYuanDir(); 95 | 96 | if (res === null) { 97 | log('Failed! You can set the plugin directory in scripts/make_dev_link.js and try again'); 98 | process.exit(1); 99 | } 100 | 101 | targetDir = await chooseTarget(res); 102 | log(`Got target directory: ${targetDir}`); 103 | } 104 | 105 | //Check 106 | if (!fs.existsSync(targetDir)) { 107 | error(`Failed! plugin directory not exists: "${targetDir}"`); 108 | error(`Please set the plugin directory in scripts/make_dev_link.js`); 109 | process.exit(1); 110 | } 111 | 112 | 113 | //check if plugin.json exists 114 | if (!fs.existsSync('./plugin.json')) { 115 | //change dir to parent 116 | process.chdir('../'); 117 | if (!fs.existsSync('./plugin.json')) { 118 | error('Failed! plugin.json not found'); 119 | process.exit(1); 120 | } 121 | } 122 | 123 | //load plugin.json 124 | const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8')); 125 | const name = plugin?.name; 126 | if (!name || name === '') { 127 | error('Failed! Please set plugin name in plugin.json'); 128 | process.exit(1); 129 | } 130 | 131 | //dev directory 132 | const devDir = `${process.cwd()}/dev`; 133 | //mkdir if not exists 134 | if (!fs.existsSync(devDir)) { 135 | fs.mkdirSync(devDir); 136 | } 137 | 138 | function cmpPath(path1, path2) { 139 | path1 = path1.replace(/\\/g, '/'); 140 | path2 = path2.replace(/\\/g, '/'); 141 | // sepertor at tail 142 | if (path1[path1.length - 1] !== '/') { 143 | path1 += '/'; 144 | } 145 | if (path2[path2.length - 1] !== '/') { 146 | path2 += '/'; 147 | } 148 | return path1 === path2; 149 | } 150 | 151 | const targetPath = `${targetDir}/${name}`; 152 | //如果已经存在,就退出 153 | if (fs.existsSync(targetPath)) { 154 | let isSymbol = fs.lstatSync(targetPath).isSymbolicLink(); 155 | 156 | if (isSymbol) { 157 | let srcPath = fs.readlinkSync(targetPath); 158 | 159 | if (cmpPath(srcPath, devDir)) { 160 | log(`Good! ${targetPath} is already linked to ${devDir}`); 161 | } else { 162 | error(`Error! Already exists symbolic link ${targetPath}\nBut it links to ${srcPath}`); 163 | } 164 | } else { 165 | error(`Failed! ${targetPath} already exists and is not a symbolic link`); 166 | } 167 | 168 | } else { 169 | //创建软链接 170 | fs.symlinkSync(devDir, targetPath, 'junction'); 171 | let devInfo = { 172 | "devDir": null 173 | } 174 | fs.writeFileSync(`${process.cwd()}\\notSync\\devInfo.json`, JSON.stringify(devInfo), 'utf-8'); 175 | log(`Done! Created symlink ${targetPath}`); 176 | } 177 | 178 | -------------------------------------------------------------------------------- /scripts/reset_dev_loc.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import http from 'node:http'; 3 | import readline from 'node:readline'; 4 | 5 | 6 | //************************************ Write you dir here ************************************ 7 | 8 | //Please write the "workspace/data/plugins" directory here 9 | //请在这里填写你的 "workspace/data/plugins" 目录 10 | let targetDir = ''; 11 | //Like this 12 | // let targetDir = `H:\\SiYuanDevSpace\\data\\plugins`; 13 | //******************************************************************************************** 14 | 15 | const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info); 16 | const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info); 17 | 18 | let POST_HEADER = { 19 | // "Authorization": `Token ${token}`, 20 | "Content-Type": "application/json", 21 | } 22 | 23 | async function myfetch(url, options) { 24 | //使用 http 模块,从而兼容那些不支持 fetch 的 nodejs 版本 25 | return new Promise((resolve, reject) => { 26 | let req = http.request(url, options, (res) => { 27 | let data = ''; 28 | res.on('data', (chunk) => { 29 | data += chunk; 30 | }); 31 | res.on('end', () => { 32 | resolve({ 33 | ok: true, 34 | status: res.statusCode, 35 | json: () => JSON.parse(data) 36 | }); 37 | }); 38 | }); 39 | req.on('error', (e) => { 40 | reject(e); 41 | }); 42 | req.end(); 43 | }); 44 | } 45 | 46 | async function getSiYuanDir() { 47 | let url = 'http://127.0.0.1:6806/api/system/getWorkspaces'; 48 | let conf = {}; 49 | try { 50 | let response = await myfetch(url, { 51 | method: 'POST', 52 | headers: POST_HEADER 53 | }); 54 | if (response.ok) { 55 | conf = await response.json(); 56 | } else { 57 | error(`HTTP-Error: ${response.status}`); 58 | return null; 59 | } 60 | } catch (e) { 61 | error("Error:", e); 62 | error("Please make sure SiYuan is running!!!"); 63 | return null; 64 | } 65 | return conf.data; 66 | } 67 | 68 | async function chooseTarget(workspaces) { 69 | let count = workspaces.length; 70 | log(`Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`) 71 | for (let i = 0; i < workspaces.length; i++) { 72 | log(`[${i}] ${workspaces[i].path}`); 73 | } 74 | 75 | if (count == 1) { 76 | return `${workspaces[0].path}/data/plugins`; 77 | } else { 78 | const rl = readline.createInterface({ 79 | input: process.stdin, 80 | output: process.stdout 81 | }); 82 | let index = await new Promise((resolve, reject) => { 83 | rl.question(`Please select a workspace[0-${count-1}]: `, (answer) => { 84 | resolve(answer); 85 | }); 86 | }); 87 | rl.close(); 88 | return `${workspaces[index].path}/data/plugins`; 89 | } 90 | } 91 | 92 | if (targetDir === '') { 93 | log('"targetDir" is empty, try to get SiYuan directory automatically....') 94 | let res = await getSiYuanDir(); 95 | 96 | if (res === null) { 97 | log('Failed! You can set the plugin directory in scripts/make_dev_link.js and try again'); 98 | process.exit(1); 99 | } 100 | 101 | targetDir = await chooseTarget(res); 102 | log(`Got target directory: ${targetDir}`); 103 | } 104 | 105 | //Check 106 | if (!fs.existsSync(targetDir)) { 107 | error(`Failed! plugin directory not exists: "${targetDir}"`); 108 | error(`Please set the plugin directory in scripts/make_dev_link.js`); 109 | process.exit(1); 110 | } 111 | 112 | 113 | //check if plugin.json exists 114 | if (!fs.existsSync('./plugin.json')) { 115 | //change dir to parent 116 | process.chdir('../'); 117 | if (!fs.existsSync('./plugin.json')) { 118 | error('Failed! plugin.json not found'); 119 | process.exit(1); 120 | } 121 | } 122 | 123 | //load plugin.json 124 | const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8')); 125 | const name = plugin?.name; 126 | if (!name || name === '') { 127 | error('Failed! Please set plugin name in plugin.json'); 128 | process.exit(1); 129 | } 130 | 131 | //dev directory 132 | const devDir = `${process.cwd()}/dev`; 133 | //mkdir if not exists 134 | if (!fs.existsSync(devDir)) { 135 | fs.mkdirSync(devDir); 136 | } 137 | 138 | function cmpPath(path1, path2) { 139 | path1 = path1.replace(/\\/g, '/'); 140 | path2 = path2.replace(/\\/g, '/'); 141 | // sepertor at tail 142 | if (path1[path1.length - 1] !== '/') { 143 | path1 += '/'; 144 | } 145 | if (path2[path2.length - 1] !== '/') { 146 | path2 += '/'; 147 | } 148 | return path1 === path2; 149 | } 150 | 151 | const targetPath = `${targetDir}/${name}`; 152 | //如果已经存在,就退出 153 | if (fs.existsSync(targetPath)) { 154 | let isSymbol = fs.lstatSync(targetPath).isSymbolicLink(); 155 | 156 | if (isSymbol) { 157 | let srcPath = fs.readlinkSync(targetPath); 158 | 159 | if (cmpPath(srcPath, devDir)) { 160 | log(`Good! ${targetPath} is already linked to ${devDir}`); 161 | } else { 162 | error(`Error! Already exists symbolic link ${targetPath}\nBut it links to ${srcPath}`); 163 | } 164 | } else { 165 | error(`Failed! ${targetPath} already exists and is not a symbolic link`); 166 | } 167 | 168 | } else { 169 | //创建软链接 170 | // fs.symlinkSync(devDir, targetPath, 'junction'); 171 | let devInfo = { 172 | "devDir": targetPath 173 | } 174 | fs.writeFileSync(`${process.cwd()}\\notSync\\devInfo.json`, JSON.stringify(devInfo), 'utf-8'); 175 | log(`Done! Changed dev save path ${targetPath}`); 176 | } 177 | 178 | -------------------------------------------------------------------------------- /src/components/dialog/outdatedSetting.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/components/dialog/switchPanel.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 383 | 384 | -------------------------------------------------------------------------------- /src/components/settings/block.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/components/settings/column.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /src/components/settings/item.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/components/settings/items/button.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/settings/items/input.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/settings/items/nameDivider.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/settings/items/order.vue: -------------------------------------------------------------------------------- 1 | 33 | 151 | -------------------------------------------------------------------------------- /src/components/settings/items/select.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/components/settings/items/switch.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/settings/items/textarea.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/settings/page.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/settings/setting.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 105 | 106 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export class CONSTANTS { 2 | public static readonly RANDOM_DELAY: number = 300; 3 | public static readonly STYLE_ID: string = "hierarchy-navigate-plugin-style"; 4 | public static readonly ICON_ALL: string = "all"; // 2 5 | public static readonly ICON_NONE: string = "none"; // 0 6 | public static readonly ICON_CUSTOM_ONLY: string = "custom"; // 1 7 | public static readonly PLUGIN_NAME: string = "og_hierachy_navigate"; 8 | public static readonly SAVE_TIMEOUT: number = 900; 9 | public static readonly TOP_CONTAINER_CLASS_NAME: string = "og-hn-heading-docs-container"; // 最上层的 10 | public static readonly HEADING_CLASS_NAME: string = "og-hn-at-doc-top"; 11 | public static readonly FOOTER_CLASS_NAME: string = "og-hn-at-doc-end"; 12 | public static readonly CONTAINER_CLASS_NAME: string = "og-hierachy-navigate-doc-container"; // 包括链接的子容器 13 | public static readonly ARROW_CLASS_NAME: string = "og-hierachy-navigate-breadcrumb-arrow"; 14 | public static readonly INFO_CONTAINER_CLASS: string = "og-hierachy-navigate-info-container"; 15 | public static readonly PARENT_CONTAINER_ID: string = "og-hierachy-navigate-parent-doc-container"; 16 | public static readonly CHILD_CONTAINER_ID: string = "og-hierachy-navigate-children-doc-container"; 17 | public static readonly SIBLING_CONTAINER_ID: string = "og-hierachy-navigate-sibling-doc-container"; 18 | public static readonly ON_THIS_DAY_CONTAINER_CLASS_NAME: string = "og-hierachy-navigate-onthisday-doc-container"; 19 | public static readonly INDICATOR_CLASS_NAME: string = "og-hierachy-navigate-doc-indicator"; 20 | public static readonly BREADCRUMB_CONTAINER_CLASS_NAME: string = "og-hierachy-navigate-breadcrumb-container"; 21 | public static readonly MORE_OR_LESS_CONTAINER_CLASS_NAME: string = "og-hierachy-navigate-moreorless-container"; 22 | public static readonly CONTAINER_MULTILINE_STYLE_CLASS_NAME: string = "og-hn-container-multiline"; 23 | public static readonly COULD_FOLD_CLASS_NAME: string = "og-hn-container-could-fold"; 24 | public static readonly IS_FOLDING_CLASS_NAME: string = "og-hn-is-folding"; 25 | public static readonly HIDE_COULD_FOLD_STYLE_ID: string = "og-hn-hide-could-fold"; 26 | public static readonly REF_LINK_FOR_POP_OUT_CLASS_NAME: string = "og-hn-ref-link-simulate"; 27 | public static readonly PLACEHOLDER_FOR_POP_OUT_CLASS_NAME: string = "og-hn-nav-top-placeholder"; 28 | 29 | 30 | public static readonly AREA_NOT_FOLD_CLASS_NAME: string = "og-hn-not-fold"; 31 | 32 | public static readonly MENU_ITEM_CLASS_NAME: string = "og-hn-breadcrumb-menu-item-container"; 33 | 34 | public static readonly NONE_CLASS_NAME: string = "og-hierachy-navigate-doc-not-exist"; 35 | public static readonly NEXT_CONTAINER_CLASS_NAME: string = "og-hierachy-navigate-next-doc-container"; 36 | public static readonly NEXT_PREVIEW_CONSTAINER_CLASS_NAME: string = "og-hierachy-navigate-next-preview-doc-container"; 37 | public static readonly BACKLINK_CONTAINER_CLASS_NAME: string = "og-hierachy-navigate-backlink-doc-container"; 38 | public static readonly FOWARDLINK_CONTAINER_CLASS_NAME: string = "og-hierachy-navigate-forwardlink-doc-container"; 39 | public static readonly TO_THE_TOP_CLASS_NAME: string = "og-hn-container-to-top"; 40 | public static readonly POP_NONE: string = "disable"; // 0 41 | public static readonly POP_LIMIT: string = "icon_only"; // 1 42 | public static readonly POP_ALL: string = "all"; // 2 43 | public static readonly BACKLINK_NONE: string = "disable"; 44 | public static readonly BACKLINK_NORMAL: string = "show_all_as_doc"; 45 | public static readonly BACKLINK_DOC_ONLY: string = "doc_only"; 46 | public static readonly REMOVE_CURRENT_TAB_DEFAULT: string = "default"; // 2 47 | public static readonly REMOVE_CURRENT_TAB_TRUE: string = "true"; // 2 48 | public static readonly REMOVE_CURRENT_TAB_FALSE: string = "false"; // 2 49 | } 50 | 51 | export class LINK_SORT_TYPES { 52 | public static readonly NAME_ALPHABET_ASC:string = "alphabet_asc"; 53 | public static readonly NAME_ALPHABET_DESC:string = "alphabet_desc"; 54 | public static readonly NAME_NATURAL_ASC:string = "natural_asc"; 55 | public static readonly NAME_NATURAL_DESC:string = "natural_desc"; 56 | public static readonly CREATE_TIME_ASC:string = "create_asc"; 57 | public static readonly CREATE_TIME_DESC:string = "create_desc"; 58 | public static readonly UPDATE_TIME_ASC:string = "update_asc"; 59 | public static readonly UPDATE_TIME_DESC:string = "update_desc"; 60 | } 61 | 62 | export class PRINTER_NAME { 63 | public static readonly PARENT: string = "parent"; 64 | public static readonly CHILD: string = "child"; 65 | public static readonly SIBLING: string = "sibling"; 66 | public static readonly PREV_NEXT: string = "previousAndNext"; 67 | public static readonly BACKLINK: string = "backlinks"; 68 | public static readonly BREADCRUMB: string = "breadcrumb"; 69 | public static readonly INFO: string = "info"; 70 | public static readonly WIDGET: string = "widget"; 71 | public static readonly BLOCK_BREADCRUMB: string = "blockBreadcrumb"; 72 | public static readonly ON_THIS_DAY: string = "onThisDay"; 73 | public static readonly FORWARDLINK: string = "forwardlinks"; 74 | public static readonly MORE_OR_LESS: string = "moreorless"; 75 | public static readonly PREV_NEXT_PREVIEW: string = "previousAndNextPreview"; 76 | public static readonly PREVIEW_BOX:string = "previewBox"; 77 | } 78 | -------------------------------------------------------------------------------- /src/hello.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 45 | 46 | 72 | 73 | 101 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | #helloPanel { 2 | border: 1px rgb(189, 119, 119) dashed; 3 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2024 OpaqueGlass 3 | This program is released under the AGPLv3 license. 4 | For details, see: 5 | - License Text: https://www.gnu.org/licenses/agpl-3.0.html 6 | - License Summary: https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0) 7 | 8 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 9 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM 10 | "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 11 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK 12 | AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, 13 | YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 14 | 15 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, 16 | OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU 17 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF 18 | THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 19 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO 20 | OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 21 | POSSIBILITY OF SUCH DAMAGES. 22 | 23 | */ 24 | /** 25 | * syplugin-hierarchy-navigate 26 | * Copyright (C) 2024 OpaqueGlass 27 | * @license AGPL-3.0 28 | */ 29 | import { 30 | Plugin, 31 | showMessage, 32 | openTab, 33 | getFrontend, 34 | } from "siyuan"; 35 | import * as siyuan from "siyuan"; 36 | import "@/index.scss"; 37 | 38 | import { createApp } from "vue"; 39 | import settingVue from "./components/settings/setting.vue"; 40 | import { setLanguage } from "./utils/lang"; 41 | import { debugPush, errorPush, logPush } from "./logger"; 42 | import { initSettingProperty } from './manager/settingManager'; 43 | import { setPluginInstance } from "./utils/getInstance"; 44 | import { loadSettings } from "./manager/settingManager"; 45 | import EventHandler from "./worker/eventHandler"; 46 | import { removeCouldHideStyle, removeStyle, setCouldHideStyle, setStyle } from "./worker/setStyle"; 47 | import { bindCommand } from "./worker/shortcutHandler"; 48 | import { CONSTANTS } from "./constants"; 49 | import { generateUUID } from "./utils/common"; 50 | // import "source-map-support/register"; 51 | 52 | const STORAGE_NAME = "menu-config"; 53 | const TAB_TYPE = "custom_tab"; 54 | const DOCK_TYPE = "dock_tab"; 55 | 56 | 57 | 58 | 59 | export default class OGPluginTemplate extends Plugin { 60 | 61 | private isMobile: boolean; 62 | private settingPanel; 63 | private myEventHandler: EventHandler; 64 | 65 | async onload() { 66 | this.data[STORAGE_NAME] = {readonlyText: "Readonly"}; 67 | logPush("测试", this.i18n); 68 | setLanguage(this.i18n); 69 | setPluginInstance(this); 70 | initSettingProperty(); 71 | bindCommand(this); 72 | // 载入设置项,此项必须在setPluginInstance之后被调用 73 | this.myEventHandler = new EventHandler(); 74 | 75 | const frontEnd = getFrontend(); 76 | this.isMobile = frontEnd === "mobile" || frontEnd === "browser-mobile"; 77 | const textareaElement = document.createElement("textarea"); 78 | setCouldHideStyle(); 79 | 80 | // 81 | // const server = http.createServer(); 82 | // 不大行, socketio 的transport extend的对象不存在或找不到,可能浏览器环境没有 83 | 84 | // const server = http.createServer(); 85 | // server.listen(25565, () => { 86 | // console.log('yunchu Server listening on port 25565'); 87 | // }); 88 | // server.on("connection", (socket) => { 89 | // console.log("yunchu connection", socket.remoteAddress, socket.remotePort); 90 | // // socket.setEncoding('utf-8') 91 | // socket.write('yunchu') 92 | // // 收到客户端数据时 93 | // socket.on('connect', (data) => { 94 | // console.info('yunchu 接收到客户端消息:【'+ `${data}`+"】"); 95 | // // 定义接收到消息的事件处理 96 | // // 。。。。 97 | // }); 98 | // }); 99 | } 100 | 101 | onLayoutReady(): void { 102 | loadSettings().then(()=>{ 103 | this.myEventHandler.bindHandler(); 104 | logPush("绑定Handler结束"); 105 | setStyle(); 106 | }).catch((e)=>{ 107 | showMessage("文档层级导航插件载入设置项失败。Load plugin settings faild. syplugin-hierarchy-navigate"); 108 | errorPush(e); 109 | }); 110 | } 111 | 112 | onunload(): void { 113 | logPush("正在卸载插件"); 114 | // 善后 115 | this.myEventHandler.unbindHandler(); 116 | // 移除所有已经插入的导航区 117 | document.querySelectorAll(`.${CONSTANTS.TOP_CONTAINER_CLASS_NAME}`).forEach((elem)=>{ 118 | elem.remove(); 119 | }); 120 | removeStyle(); 121 | removeCouldHideStyle(); 122 | // 清理绑定的宽度监听 123 | if (window["og_hn_observe"]) { 124 | for (const key in window["og_hn_observe"]) { 125 | debugPush("插件卸载清理observer", key); 126 | window["og_hn_observe"][key]?.disconnect(); 127 | } 128 | delete window["og_hn_observe"]; 129 | } 130 | } 131 | 132 | openSetting() { 133 | // 生成Dialog内容 134 | const uid = generateUUID(); 135 | // 创建dialog 136 | const app = createApp(settingVue); 137 | const settingDialog = new siyuan.Dialog({ 138 | "title": this.i18n["setting_panel_title"], 139 | "content": ` 140 |
141 | `, 142 | "width": isMobile() ? "92vw":"1040px", 143 | "height": isMobile() ? "50vw":"80vh", 144 | "destroyCallback": ()=>{app.unmount(); }, 145 | }); 146 | app.mount(`#og_plugintemplate_${uid}`); 147 | 148 | } 149 | 150 | private openStatisticTab() { 151 | openTab({ 152 | app: this.app, 153 | custom: { 154 | icon: "iconTestStatistics", 155 | title: "Custom Tab", 156 | 157 | id: this.name + TAB_TYPE 158 | }, 159 | }); 160 | } 161 | 162 | 163 | } 164 | 165 | function isMobile() { 166 | return window.top.document.getElementById("sidebar") ? true : false; 167 | }; 168 | -------------------------------------------------------------------------------- /src/logger/index.ts: -------------------------------------------------------------------------------- 1 | 2 | // debug push 3 | let g_DEBUG = 2; 4 | const g_NAME = "hn"; 5 | const g_FULLNAME = "层级导航"; 6 | 7 | /* 8 | LEVEL 0 忽略所有 9 | LEVEL 1 仅Error 10 | LEVEL 2 Err + Warn 11 | LEVEL 3 Err + Warn + Info 12 | LEVEL 4 Err + Warn + Info + Log 13 | LEVEL 5 Err + Warn + Info + Log + Debug 14 | 请注意,基于代码片段加入window下的debug设置,可能在刚载入挂件时无效 15 | */ 16 | export function commonPushCheck() { 17 | if (window.top["OpaqueGlassDebugV2"] == undefined || window.top["OpaqueGlassDebugV2"][g_NAME] == undefined) { 18 | return g_DEBUG; 19 | } 20 | return window.top["OpaqueGlassDebugV2"][g_NAME]; 21 | } 22 | 23 | export function isDebugMode() { 24 | return commonPushCheck() > g_DEBUG; 25 | } 26 | 27 | export function debugPush(str: string, ...args: any[]) { 28 | if (commonPushCheck() >= 5) { 29 | console.debug(`${g_FULLNAME}[D] ${new Date().toLocaleTimeString()} ${str}`, ...args); 30 | } 31 | } 32 | 33 | export function infoPush(str: string, ...args: any[]) { 34 | if (commonPushCheck() >= 3) { 35 | console.info(`${g_FULLNAME}[I] ${new Date().toLocaleTimeString()} ${str}`, ...args); 36 | } 37 | } 38 | 39 | export function logPush(str: string, ...args: any[]) { 40 | if (commonPushCheck() >= 4) { 41 | console.log(`${g_FULLNAME}[L] ${new Date().toLocaleTimeString()} ${str}`, ...args); 42 | } 43 | } 44 | 45 | export function errorPush(str: string, ... args: any[]) { 46 | if (commonPushCheck() >= 1) { 47 | console.error(`${g_FULLNAME}[E] ${new Date().toLocaleTimeString()} ${str}`, ...args); 48 | console.trace(args[0] ?? undefined); 49 | } 50 | } 51 | 52 | export function warnPush(str: string, ... args: any[]) { 53 | if (commonPushCheck() >= 2) { 54 | console.warn(`${g_FULLNAME}[W] ${new Date().toLocaleTimeString()} ${str}`, ...args); 55 | } 56 | } -------------------------------------------------------------------------------- /src/syapi/custom.ts: -------------------------------------------------------------------------------- 1 | import { debugPush } from "@/logger"; 2 | import { queryAPI, listDocsByPathT, getTreeStat, getCurrentDocIdF } from "."; 3 | 4 | /** 5 | * 统计子文档字符数 6 | * @param {*} childDocs 7 | * @returns 8 | */ 9 | export async function getChildDocumentsWordCount(docId:string) { 10 | const sqlResult = await queryAPI(` 11 | SELECT SUM(length) AS count 12 | FROM blocks 13 | WHERE 14 | path like "%/${docId}/%" 15 | AND 16 | type in ("p", "h", "c", "t") 17 | `); 18 | if (sqlResult[0].count) { 19 | return sqlResult[0].count; 20 | } 21 | return 0; 22 | // let totalWords = 0; 23 | // let docCount = 0; 24 | // for (let childDoc of childDocs) { 25 | // let tempWordsResult = await getTreeStat(childDoc.id); 26 | // totalWords += tempWordsResult.wordCount; 27 | // childDoc["wordCount"] = tempWordsResult.wordCount; 28 | // docCount++; 29 | // if (docCount > 128) { 30 | // totalWords = `${totalWords}+`; 31 | // break; 32 | // } 33 | // } 34 | // return [childDocs, totalWords]; 35 | } 36 | 37 | export async function getChildDocuments(sqlResult:SqlResult, maxListCount: number): Promise { 38 | let childDocs = await listDocsByPathT({path: sqlResult.path, notebook: sqlResult.box, maxListCount: maxListCount}); 39 | return childDocs; 40 | } 41 | 42 | export async function isChildDocExist(id: string) { 43 | const sqlResponse = await queryAPI(` 44 | SELECT * FROM blocks WHERE path like '%${id}/%' LIMIT 3 45 | `); 46 | if (sqlResponse && sqlResponse.length > 0) { 47 | return true; 48 | } 49 | return false; 50 | } 51 | 52 | export async function isDocHasAv(docId: string) { 53 | let sqlResult = await queryAPI(` 54 | SELECT count(*) as avcount FROM blocks WHERE root_id = '${docId}' 55 | AND type = 'av' 56 | `); 57 | if (sqlResult.length > 0 && sqlResult[0].avcount > 0) { 58 | return true; 59 | } else { 60 | 61 | return false; 62 | } 63 | } 64 | 65 | export async function isDocEmpty(docId: string, blockCountThreshold = 0) { 66 | // 检查父文档是否为空 67 | let treeStat = await getTreeStat(docId); 68 | if (blockCountThreshold == 0 && treeStat.wordCount != 0 && treeStat.imageCount != 0) { 69 | debugPush("treeStat判定文档非空,不插入挂件"); 70 | return false; 71 | } 72 | if (blockCountThreshold != 0) { 73 | let blockCountSqlResult = await queryAPI(`SELECT count(*) as bcount FROM blocks WHERE root_id like '${docId}' AND type in ('p', 'c', 'iframe', 'html', 'video', 'audio', 'widget', 'query_embed', 't')`); 74 | if (blockCountSqlResult.length > 0) { 75 | if (blockCountSqlResult[0].bcount > blockCountThreshold) { 76 | return false; 77 | } else { 78 | return true; 79 | } 80 | } 81 | } 82 | 83 | let sqlResult = await queryAPI(`SELECT markdown FROM blocks WHERE 84 | root_id like '${docId}' 85 | AND type != 'd' 86 | AND (type != 'p' 87 | OR (type = 'p' AND length != 0) 88 | ) 89 | LIMIT 5`); 90 | if (sqlResult.length <= 0) { 91 | return true; 92 | } else { 93 | debugPush("sql判定文档非空,不插入挂件"); 94 | return false; 95 | } 96 | } 97 | 98 | export function getActiveDocProtyle() { 99 | const allProtyle = {}; 100 | window.siyuan.layout.centerLayout?.children?.forEach((wndItem) => { 101 | wndItem?.children?.forEach((tabItem) => { 102 | if (tabItem?.model) { 103 | allProtyle[tabItem?.id](tabItem.model?.editor?.protyle); 104 | } 105 | }); 106 | }); 107 | } 108 | 109 | export function getActiveEditorIds() { 110 | let result = []; 111 | let id = window.document.querySelector(`.layout__wnd--active [data-type="tab-header"].item--focus`)?.getAttribute("data-id"); 112 | if (id) return [id]; 113 | window.document.querySelectorAll(`[data-type="tab-header"].item--focus`).forEach(item=>{ 114 | let uid = item.getAttribute("data-id"); 115 | if (uid) result.push(uid); 116 | }); 117 | return result; 118 | } 119 | 120 | 121 | 122 | /** 123 | * 获取当前更新时间字符串 124 | * @returns 125 | */ 126 | export function getUpdateString(){ 127 | let nowDate = new Date(); 128 | let hours = nowDate.getHours(); 129 | let minutes = nowDate.getMinutes(); 130 | let seconds = nowDate.getSeconds(); 131 | hours = formatTime(hours); 132 | minutes = formatTime(minutes); 133 | seconds = formatTime(seconds); 134 | let timeStr = nowDate.toJSON().replace(new RegExp("-", "g"),"").substring(0, 8) + hours + minutes + seconds; 135 | return timeStr; 136 | function formatTime(num) { 137 | return num < 10 ? '0' + num : num; 138 | } 139 | } 140 | 141 | /** 142 | * 生成一个随机的块id 143 | * @returns 144 | */ 145 | export function generateBlockId(){ 146 | // @ts-ignore 147 | if (window?.Lute?.NewNodeID) { 148 | // @ts-ignore 149 | return window.Lute.NewNodeID(); 150 | } 151 | let timeStr = getUpdateString(); 152 | let alphabet = new Array(); 153 | for (let i = 48; i <= 57; i++) alphabet.push(String.fromCharCode(i)); 154 | for (let i = 97; i <= 122; i++) alphabet.push(String.fromCharCode(i)); 155 | let randomStr = ""; 156 | for (let i = 0; i < 7; i++){ 157 | randomStr += alphabet[Math.floor(Math.random() * alphabet.length)]; 158 | } 159 | let result = timeStr + "-" + randomStr; 160 | return result; 161 | } 162 | 163 | /** 164 | * 转换块属性对象为{: }格式IAL字符串 165 | * @param {*} attrData 其属性值应当为String类型 166 | * @returns 167 | */ 168 | export function transfromAttrToIAL(attrData) { 169 | let result = "{:"; 170 | for (let key in attrData) { 171 | result += ` ${key}=\"${attrData[key]}\"`; 172 | } 173 | result += "}"; 174 | if (result == "{:}") return null; 175 | return result; 176 | } -------------------------------------------------------------------------------- /src/syapi/interface.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | echarts: { 3 | init(element: HTMLElement, theme?: string, options?: { 4 | width: number 5 | }): { 6 | setOption(option: any): void; 7 | getZr(): any; 8 | on(name: string, event: (e: any) => void): any; 9 | containPixel(name: string, position: number[]): any; 10 | resize(): void; 11 | }; 12 | dispose(element: Element): void; 13 | getInstanceById(id: string): { 14 | resize: () => void 15 | }; 16 | } 17 | ABCJS: { 18 | renderAbc(element: Element, text: string, options: { 19 | responsive: string 20 | }): void; 21 | } 22 | hljs: { 23 | listLanguages(): string[]; 24 | highlight(text: string, options: { 25 | language?: string, 26 | ignoreIllegals: boolean 27 | }): { 28 | value: string 29 | }; 30 | getLanguage(text: string): { 31 | name: string 32 | }; 33 | }; 34 | katex: { 35 | renderToString(math: string, option: { 36 | displayMode: boolean; 37 | output: string; 38 | macros: IObject; 39 | trust: boolean; 40 | strict: (errorCode: string) => "ignore" | "warn"; 41 | }): string; 42 | } 43 | mermaid: { 44 | initialize(options: any): void, 45 | init(options: any, element: Element): void 46 | }; 47 | plantumlEncoder: { 48 | encode(options: string): string, 49 | }; 50 | pdfjsLib: any 51 | 52 | dataLayer: any[] 53 | 54 | siyuan: ISiyuan 55 | webkit: any 56 | html2canvas: (element: Element, opitons: { 57 | useCORS: boolean, 58 | scale?: number 59 | }) => Promise; 60 | JSAndroid: { 61 | returnDesktop(): void 62 | openExternal(url: string): void 63 | changeStatusBarColor(color: string, mode: number): void 64 | writeClipboard(text: string): void 65 | writeImageClipboard(uri: string): void 66 | readClipboard(): string 67 | getBlockURL(): string 68 | } 69 | 70 | Protyle: any 71 | 72 | goBack(): void 73 | 74 | reconnectWebSocket(): void 75 | 76 | showKeyboardToolbar(height: number): void 77 | 78 | hideKeyboardToolbar(): void 79 | 80 | openFileByURL(URL: string): boolean 81 | 82 | destroyTheme(): Promise 83 | } 84 | 85 | interface IFile { 86 | icon: string; 87 | name1: string; 88 | alias: string; 89 | memo: string; 90 | bookmark: string; 91 | path: string; 92 | name: string; 93 | hMtime: string; 94 | hCtime: string; 95 | hSize: string; 96 | dueFlashcardCount?: string; 97 | newFlashcardCount?: string; 98 | flashcardCount?: string; 99 | id: string; 100 | count: number; 101 | subFileCount: number; 102 | } 103 | 104 | interface ISiyuan { 105 | zIndex: number 106 | storage?: { 107 | [key: string]: any 108 | }, 109 | transactions?: { 110 | protyle: IProtyle, 111 | doOperations: IOperation[], 112 | undoOperations: IOperation[] 113 | }[] 114 | reqIds: { 115 | [key: string]: number 116 | }, 117 | editorIsFullscreen?: boolean, 118 | hideBreadcrumb?: boolean, 119 | notebooks?: INotebook[], 120 | emojis?: IEmoji[], 121 | backStack?: IBackStack[], 122 | mobile?: { 123 | editor?: any 124 | popEditor?: any 125 | files?: any 126 | }, 127 | user?: { 128 | userId: string 129 | userName: string 130 | userAvatarURL: string 131 | userHomeBImgURL: string 132 | userIntro: string 133 | userNickname: string 134 | userSiYuanOneTimePayStatus: number // 0 未付费;1 已付费 135 | userSiYuanProExpireTime: number // -1 终身会员;0 普通用户;> 0 过期时间 136 | userSiYuanSubscriptionPlan: number // 0 年付订阅/终生;1 教育优惠;2 订阅试用 137 | userSiYuanSubscriptionType: number // 0 年付;1 终生;2 月付 138 | userSiYuanSubscriptionStatus: number // -1:未订阅,0:订阅可用,1:订阅封禁,2:订阅过期 139 | userToken: string 140 | userTitles: { 141 | name: string, 142 | icon: string, 143 | desc: string 144 | }[] 145 | }, 146 | dragElement?: HTMLElement, 147 | layout?: { 148 | layout?: any, 149 | centerLayout?: any, 150 | leftDock?: any, 151 | rightDock?: any, 152 | bottomDock?: any, 153 | } 154 | config?: any; 155 | ws: any, 156 | ctrlIsPressed?: boolean, 157 | altIsPressed?: boolean, 158 | shiftIsPressed?: boolean, 159 | coordinates?: { 160 | pageX: number, 161 | pageY: number, 162 | clientX: number, 163 | clientY: number, 164 | screenX: number, 165 | screenY: number, 166 | }, 167 | menus?: any, 168 | languages?: { 169 | [key: string]: any; 170 | } 171 | bookmarkLabel?: string[] 172 | blockPanels: any, 173 | dialogs: any, 174 | viewer?: any 175 | } 176 | 177 | interface SqlResult { 178 | alias: string; 179 | box: string; 180 | content: string; 181 | created: string; 182 | fcontent: string; 183 | hash: string; 184 | hpath: string; 185 | ial: string; 186 | id: string; 187 | length: number; 188 | markdown: string; 189 | memo: string; 190 | name: string; 191 | parent_id: string; 192 | path: string; 193 | root_id: string; 194 | sort: number; 195 | subtype: SqlBlockSubType; 196 | tag: string; 197 | type: SqlBlockType; 198 | updated: string; 199 | } 200 | 201 | type SqlBlockType = "d" | "p" | "h" | "l" | "i" | "b" | "html" | "widget" | "tb" | "c" | "s" | "t" | "iframe" | "av" | "m" | "query_embed" | "video" | "audio"; 202 | 203 | type SqlBlockSubType = "o" | "u" | "t" | "" |"h1" | "h2" | "h3" | "h4" | "h5" | "h6" 204 | 205 | interface FullTextSearchQuery { 206 | query: string; 207 | method?: number; 208 | types?: BlockTypeFilter; 209 | paths?: string[]; 210 | groupBy?: number; 211 | orderBy?: number; 212 | page?: number; 213 | reqId?: number; 214 | pageSize?: number; 215 | } 216 | 217 | 218 | interface ExportMdContentBody { 219 | id: string, 220 | refMode: number, 221 | // 内容块引用导出模式 222 | // 2:锚文本块链 223 | // 3:仅锚文本 224 | // 4:块引转脚注+锚点哈希 225 | // (5:锚点哈希 https://github.com/siyuan-note/siyuan/issues/10265 已经废弃 https://github.com/siyuan-note/siyuan/issues/13331) 226 | // (0:使用原始文本,1:使用 Blockquote,都已经废弃 https://github.com/siyuan-note/siyuan/issues/3155) 227 | embedMode: number, 228 | // 内容块引用导出模式,0:使用原始文本,1:使用 Blockquote 229 | yfm: boolean, 230 | // Markdown 导出时是否添加 YAML Front Matter 231 | } -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2023 frostime. All rights reserved. 3 | */ 4 | 5 | /** 6 | * Frequently used data structures in SiYuan 7 | */ 8 | type DocumentId = string; 9 | type BlockId = string; 10 | type NotebookId = string; 11 | type PreviousID = BlockId; 12 | type ParentID = BlockId | DocumentId; 13 | 14 | type Notebook = { 15 | id: NotebookId; 16 | name: string; 17 | icon: string; 18 | sort: number; 19 | closed: boolean; 20 | } 21 | 22 | type NotebookConf = { 23 | name: string; 24 | closed: boolean; 25 | refCreateSavePath: string; 26 | createDocNameTemplate: string; 27 | dailyNoteSavePath: string; 28 | dailyNoteTemplatePath: string; 29 | } 30 | 31 | type BlockType = "d" | "s" | "h" | "t" | "i" | "p" | "f" | "audio" | "video" | "other"; 32 | 33 | type BlockSubType = "d1" | "d2" | "s1" | "s2" | "s3" | "t1" | "t2" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "table" | "task" | "toggle" | "latex" | "quote" | "html" | "code" | "footnote" | "cite" | "collection" | "bookmark" | "attachment" | "comment" | "mindmap" | "spreadsheet" | "calendar" | "image" | "audio" | "video" | "other"; 34 | 35 | type Block = { 36 | id: BlockId; 37 | parent_id?: BlockId; 38 | root_id: DocumentId; 39 | hash: string; 40 | box: string; 41 | path: string; 42 | hpath: string; 43 | name: string; 44 | alias: string; 45 | memo: string; 46 | tag: string; 47 | content: string; 48 | fcontent?: string; 49 | markdown: string; 50 | length: number; 51 | type: BlockType; 52 | subtype: BlockSubType; 53 | ial?: { [key: string]: string }; 54 | sort: number; 55 | created: string; 56 | updated: string; 57 | } 58 | 59 | type doOperation = { 60 | action: string; 61 | data: string; 62 | id: BlockId; 63 | parentID: BlockId | DocumentId; 64 | previousID: BlockId; 65 | retData: null; 66 | } 67 | 68 | /** 69 | * By OpaqueGlass. Copy from https://github.com/siyuan-note/siyuan/blob/master/app/src/types/index.d.ts 70 | */ 71 | interface IFile { 72 | icon: string; 73 | name1: string; 74 | alias: string; 75 | memo: string; 76 | bookmark: string; 77 | path: string; 78 | name: string; 79 | hMtime: string; 80 | hCtime: string; 81 | hSize: string; 82 | dueFlashcardCount?: string; 83 | newFlashcardCount?: string; 84 | flashcardCount?: string; 85 | id: string; 86 | count: number; 87 | subFileCount: number; 88 | } 89 | -------------------------------------------------------------------------------- /src/types/onlyThis.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 此文件中包含 仅在当前插件中使用的interface接口定义 3 | */ 4 | interface IBasicInfo { 5 | success: boolean, 6 | docBasicInfo: ISimpleDocInfoResult, 7 | parentDocBasicInfo: ISimpleDocInfoResult, 8 | allSiblingDocInfoList: IFile[], 9 | childDocInfoList: IFile[], 10 | userDemandSiblingDocInfoList: IFile[], 11 | currentDocId: string, 12 | currentDocAttrs: any, 13 | siblingDocLimited: boolean, 14 | subDocLimited: boolean, 15 | } 16 | 17 | interface IFileTreeListNeeded { 18 | userDemandSiblingDocInfoList: boolean, 19 | allSiblingDocInfoList: boolean, 20 | childDocInfoList: boolean, 21 | } 22 | 23 | interface ISimpleDocInfoResult { 24 | id: string, 25 | path: string, 26 | ial: any, 27 | attrViews: any, 28 | // rootID: string, // 根块id,不是笔记本id 29 | name: string, 30 | refCount: number, 31 | refIDs: string[], 32 | subFileCount: number, 33 | createTime: Date, 34 | updateTime: Date, 35 | icon: string|undefined, 36 | box: string 37 | // hpath: string, 38 | } 39 | 40 | interface IProtyleEnvInfo { 41 | mobile: boolean, 42 | flashCard: boolean, 43 | notTraditional: boolean, 44 | originProtyle: any 45 | } 46 | 47 | interface IDocLinkGenerateInfo { 48 | icon?: string; 49 | // alias: string; 50 | path: string; 51 | name: string; 52 | id: string; 53 | count?: number; 54 | subFileCount?: number; // 请注意,不指出此项将是认为有子文档,但数量未知 55 | content?: string; 56 | } 57 | 58 | interface IAllPrinterResult { 59 | elements: Array, // 最终生成的各个部分元素 60 | onlyOnce: Array, // 如果有,则该部分不做替换 61 | relateContentKeys: Array, // 相关的各个部分内容key 62 | } -------------------------------------------------------------------------------- /src/types/settings.d.ts: -------------------------------------------------------------------------------- 1 | type IConfigProperty = { 2 | key: string, 3 | type: IConfigPropertyType, // 设置项类型 4 | min?: number, // 设置项最小值 5 | max?: number, // 设置项最大值 6 | btndo?: Function, // 按钮设置项的调用函数(callback) 7 | // defaultValue?: any, //默认值 8 | options?: number, // 选项数量,选项名称由语言文件中_option_i决定 9 | }; 10 | 11 | type IConfigPropertyType = 12 | "SELECT" | 13 | "TEXT" | 14 | "NUMBER" | 15 | "BUTTON" | 16 | "TEXTAREA" | 17 | "SWITCH" | 18 | "ORDER" | 19 | "TIPS"; 20 | 21 | type ITabProperty = { 22 | nameKey: string, // 标签页名称对应的语言文件关键字 23 | iconKey: string, // 设置项描述对应的语言关键字 24 | properties: Array // 设置项列表 25 | }; 26 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import { getBackend, IProtyle, openMobileFileById, openTab } from "siyuan"; 2 | import { isEventCtrlKey, isValidStr } from "./commonCheck"; 3 | import { debugPush, logPush, warnPush } from "@/logger"; 4 | import { getPluginInstance } from "./getInstance"; 5 | import { getCurrentDocIdF, isMobile } from "@/syapi"; 6 | import { removeCurrentTabF } from "./onlyThisUtil"; 7 | 8 | export function getToken(): string { 9 | return ""; 10 | } 11 | 12 | /** 13 | * 在protyle所在的分屏中打开 14 | * @param event 15 | * @param protyleElem 16 | * @deprecated 17 | */ 18 | export function openRefLinkInProtyleWnd(protyleElem: IProtyle, openInFocus: boolean, event: MouseEvent) { 19 | logPush("debug", event, protyleElem); 20 | openRefLink(event, null, null, protyleElem, openInFocus); 21 | } 22 | 23 | /** 24 | * 休息一下,等待 25 | * @param time 单位毫秒 26 | * @returns 27 | */ 28 | export function sleep(time:number){ 29 | return new Promise((resolve) => setTimeout(resolve, time)); 30 | } 31 | 32 | export function getFocusedBlockId() { 33 | const focusedBlock = getFocusedBlock(); 34 | if (focusedBlock == null) { 35 | return null; 36 | } 37 | return focusedBlock.dataset.nodeId; 38 | } 39 | 40 | 41 | export function getFocusedBlock() { 42 | if (document.activeElement.classList.contains('protyle-wysiwyg')) { 43 | /* 光标在编辑区内 */ 44 | let block = window.getSelection()?.focusNode?.parentElement; // 当前光标 45 | while (block != null && block?.dataset?.nodeId == null) block = block.parentElement; 46 | return block; 47 | } 48 | else return null; 49 | } 50 | 51 | /** 52 | * 在点击时打开思源块/文档 53 | * 为引入本项目,和原代码相比有更改 54 | * @refer https://github.com/leolee9086/cc-template/blob/6909dac169e720d3354d77685d6cc705b1ae95be/baselib/src/commonFunctionsForSiyuan.js#L118-L141 55 | * @license 木兰宽松许可证 56 | * @param {MouseEvent} event 当给出event时,将寻找event.currentTarget的data-node-id作为打开的文档id 57 | * @param {string} docId,此项仅在event对应的发起Elem上找不到data node id的情况下使用 58 | * @param {any} keyParam event的Key,主要是ctrlKey shiftKey等,此项仅在event无效时使用 59 | * @param {IProtyle} protyleElem 如果不为空打开文档点击事件将在该Elem上发起 60 | * @param {boolean} openInFocus 在当前聚焦的窗口中打开,给定此项为true,则优于protyle选项生效 61 | * @deprecated 请使用openRefLinkByAPI 62 | */ 63 | export function openRefLink(event: MouseEvent, paramId = "", keyParam = undefined, protyleElem = undefined, openInFocus = false){ 64 | let syMainWndDocument= window.parent.document 65 | let id; 66 | if (event && (event.currentTarget as HTMLElement)?.getAttribute("data-node-id")) { 67 | id = (event.currentTarget as HTMLElement)?.getAttribute("data-node-id"); 68 | } else if ((event?.currentTarget as HTMLElement)?.getAttribute("data-id")) { 69 | id = (event.currentTarget as HTMLElement)?.getAttribute("data-id"); 70 | } else { 71 | id = paramId; 72 | } 73 | // 处理笔记本等无法跳转的情况 74 | if (!isValidStr(id)) { 75 | debugPush("错误的id", id) 76 | return; 77 | } 78 | event?.preventDefault(); 79 | event?.stopPropagation(); 80 | debugPush("openRefLinkEvent", event); 81 | let simulateLink = syMainWndDocument.createElement("span") 82 | simulateLink.setAttribute("data-type","a") 83 | simulateLink.setAttribute("data-href", "siyuan://blocks/" + id) 84 | simulateLink.style.display = "none";//不显示虚拟链接,防止视觉干扰 85 | let tempTarget = null; 86 | // 如果提供了目标protyle,在其中插入 87 | if (protyleElem && !openInFocus) { 88 | tempTarget = protyleElem.querySelector(".protyle-wysiwyg div[data-node-id] div[contenteditable]") ?? protyleElem; 89 | debugPush("openRefLink使用提供窗口", tempTarget); 90 | } 91 | debugPush("openInFocus?", openInFocus); 92 | if (openInFocus) { 93 | // 先确定Tab 94 | const dataId = syMainWndDocument.querySelector(".layout__wnd--active .layout-tab-bar .item--focus")?.getAttribute("data-id"); 95 | debugPush("openRefLink尝试使用聚焦窗口", dataId); 96 | // 再确定Protyle 97 | if (isValidStr(dataId)) { 98 | tempTarget = window.document.querySelector(`.fn__flex-1.protyle[data-id='${dataId}'] 99 | .protyle-wysiwyg div[data-node-id] div[contenteditable]`); 100 | debugPush("openRefLink使用聚焦窗口", tempTarget); 101 | } 102 | } 103 | if (!isValidStr(tempTarget)) { 104 | tempTarget = syMainWndDocument.querySelector(".protyle-wysiwyg div[data-node-id] div[contenteditable]"); 105 | debugPush("openRefLink未能找到指定窗口,更改为原状态"); 106 | } 107 | tempTarget.appendChild(simulateLink); 108 | let clickEvent = new MouseEvent("click", { 109 | ctrlKey: event?.ctrlKey ?? keyParam?.ctrlKey, 110 | shiftKey: event?.shiftKey ?? keyParam?.shiftKey, 111 | altKey: event?.altKey ?? keyParam?.altKey, 112 | metaKey: event?.metaKey ?? keyParam?.metaKey, 113 | bubbles: true 114 | }); 115 | // 存在选区时,ref相关点击是不执行的,这里暂存、清除,并稍后恢复 116 | const tempSaveRanges = []; 117 | const selection = window.getSelection(); 118 | for (let i = 0; i < selection.rangeCount; i++) { 119 | tempSaveRanges.push(selection.getRangeAt(i)); 120 | } 121 | window.getSelection()?.removeAllRanges(); 122 | 123 | simulateLink.dispatchEvent(clickEvent); 124 | simulateLink.remove(); 125 | 126 | // // 恢复选区,不确定恢复选区是否会导致其他问题 127 | // if (selection.isCollapsed) { 128 | // tempSaveRanges.forEach(range => selection.addRange(range)); // 恢复选区 129 | // } 130 | } 131 | 132 | let lastClickTime_openRefLinkByAPI = 0; 133 | /** 134 | * 基于API的打开思源块/文档 135 | * @param mouseEvent 鼠标点击事件,如果存在,优先使用 136 | * @param paramDocId 如果没有指定 event,使用此参数作为文档id 137 | * @param keyParam 如果没有event,使用此次数指定ctrlKey后台打开、shiftKey下方打开、altKey右侧打开 138 | * @param openInFocus 是否以聚焦块的方式打开(此参数有变动) 139 | * @param removeCurrentTab 是否移除当前Tab 140 | * @param autoRemoveJudgeMiliseconds 自动判断是否移除当前Tab的时间间隔(0则 不自动判断) 141 | * @returns 142 | */ 143 | export function openRefLinkByAPI({mouseEvent, paramDocId = "", keyParam = {}, openInFocus = undefined, removeCurrentTab = undefined, autoRemoveJudgeMiliseconds = 0}: {mouseEvent?: MouseEvent, paramDocId?: string, keyParam?: any, openInFocus?: boolean, removeCurrentTab?: boolean, autoRemoveJudgeMiliseconds?: number}) { 144 | let docId: string; 145 | if (mouseEvent && (mouseEvent.currentTarget as HTMLElement)?.getAttribute("data-node-id")) { 146 | docId = (mouseEvent.currentTarget as HTMLElement)?.getAttribute("data-node-id"); 147 | } else if ((mouseEvent?.currentTarget as HTMLElement)?.getAttribute("data-id")) { 148 | docId = (mouseEvent.currentTarget as HTMLElement)?.getAttribute("data-id"); 149 | } else { 150 | docId = paramDocId; 151 | } 152 | // 处理笔记本等无法跳转的情况 153 | if (!isValidStr(docId)) { 154 | debugPush("错误的id", docId) 155 | return; 156 | } 157 | // 需要冒泡,否则不能在所在页签打开 158 | // event?.preventDefault(); 159 | // event?.stopPropagation(); 160 | if (isMobile()) { 161 | openMobileFileById(getPluginInstance().app, docId); 162 | return; 163 | } 164 | debugPush("openRefLinkEventAPIF", mouseEvent); 165 | if (mouseEvent) { 166 | keyParam = {}; 167 | keyParam["ctrlKey"] = mouseEvent.ctrlKey; 168 | keyParam["shiftKey"] = mouseEvent.shiftKey; 169 | keyParam["altKey"] = mouseEvent.altKey; 170 | keyParam["metaKey"] = mouseEvent.metaKey; 171 | } 172 | let positionKey = undefined; 173 | if (keyParam["altKey"]) { 174 | positionKey = "right"; 175 | } else if (keyParam["shiftKey"]) { 176 | positionKey = "bottom"; 177 | } 178 | if (autoRemoveJudgeMiliseconds > 0) { 179 | if (Date.now() - lastClickTime_openRefLinkByAPI < autoRemoveJudgeMiliseconds) { 180 | removeCurrentTab = true; 181 | } 182 | lastClickTime_openRefLinkByAPI = Date.now(); 183 | } 184 | // 手动关闭 185 | const needToCloseDocId = getCurrentDocIdF(true); 186 | 187 | const finalParam = { 188 | app: getPluginInstance().app, 189 | doc: { 190 | id: docId, 191 | zoomIn: openInFocus 192 | }, 193 | position: positionKey, 194 | keepCursor: isEventCtrlKey(keyParam) ? true : undefined, 195 | removeCurrentTab: removeCurrentTab, // 目前这个选项的行为是:true,则当前页签打开;false,则根据思源设置:新页签打开 196 | }; 197 | debugPush("打开文档执行参数", finalParam); 198 | openTab(finalParam); 199 | // 后台打开页签不可移除 200 | if (removeCurrentTab && !isEventCtrlKey(keyParam)) { 201 | debugPush("插件自行移除页签"); 202 | removeCurrentTabF(needToCloseDocId); 203 | removeCurrentTab = false; 204 | } 205 | } 206 | 207 | 208 | 209 | export function parseDateString(dateString: string): Date | null { 210 | if (dateString.length !== 14) { 211 | warnPush("Invalid date string length. Expected format: 'YYYYMMDDHHmmss'"); 212 | return null; 213 | } 214 | 215 | const year = parseInt(dateString.slice(0, 4), 10); 216 | const month = parseInt(dateString.slice(4, 6), 10) - 1; // 月份从 0 开始 217 | const day = parseInt(dateString.slice(6, 8), 10); 218 | const hours = parseInt(dateString.slice(8, 10), 10); 219 | const minutes = parseInt(dateString.slice(10, 12), 10); 220 | const seconds = parseInt(dateString.slice(12, 14), 10); 221 | 222 | const date = new Date(year, month, day, hours, minutes, seconds); 223 | 224 | if (isNaN(date.getTime())) { 225 | warnPush("Invalid date components."); 226 | return null; 227 | } 228 | 229 | return date; 230 | } 231 | 232 | export function formatDateStringLikeFileTree(dateString: string): string { 233 | if (dateString.length !== 14) { 234 | warnPush("Invalid date string length. Expected format: 'YYYYMMDDHHmmss'"); 235 | return ""; 236 | } 237 | 238 | const year = dateString.substring(0, 4); 239 | const month = dateString.substring(4, 6); 240 | const day = dateString.substring(6, 8); 241 | const hour = dateString.substring(8, 10); 242 | const minute = dateString.substring(10, 12); 243 | const second = dateString.substring(12, 14); 244 | 245 | return `${year}-${month}-${day} ${hour}:${minute}:${second}`; 246 | } 247 | 248 | export function generateUUID() { 249 | let uuid = ''; 250 | let i = 0; 251 | let random = 0; 252 | 253 | for (i = 0; i < 36; i++) { 254 | if (i === 8 || i === 13 || i === 18 || i === 23) { 255 | uuid += '-'; 256 | } else if (i === 14) { 257 | uuid += '4'; 258 | } else { 259 | random = Math.random() * 16 | 0; 260 | if (i === 19) { 261 | random = (random & 0x3) | 0x8; 262 | } 263 | uuid += (random).toString(16); 264 | } 265 | } 266 | 267 | return uuid; 268 | } 269 | 270 | export function isPluginExist(pluginName: string) { 271 | const plugins = window.siyuan.ws.app.plugins; 272 | return plugins?.some((plugin) => plugin.name === pluginName); 273 | } 274 | 275 | export function isAnyPluginExist(pluginNames: string[]) { 276 | return pluginNames.some(isPluginExist); 277 | } 278 | 279 | export function replaceShortcutString(shortcut:string) { 280 | const backend = getBackend(); 281 | 282 | if (backend !== "darwin") { 283 | return shortcut 284 | .replace(/⌥/g, 'Alt ') // 替换 Option 键 285 | .replace(/⌘/g, 'Ctrl ') // 替换 Command 键 286 | .replace(/⇧/g, 'Shift ') // 替换 Shift 键 287 | .replace(/⇪/g, 'CapsLock ') // 替换 Caps Lock 键 288 | .replace(/⌃/g, 'Ctrl '); // 替换 Control 键 289 | } 290 | 291 | return shortcut; 292 | } -------------------------------------------------------------------------------- /src/utils/commonCheck.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 判定字符串是否有效 3 | * @param s 需要检查的字符串(或其他类型的内容) 4 | * @returns true / false 是否为有效的字符串 5 | */ 6 | export function isValidStr(s: any): boolean { 7 | if (s == undefined || s == null || s === '') { 8 | return false; 9 | } 10 | return true; 11 | } 12 | 13 | /** 14 | * 判断字符串是否为空白 15 | * @param s 字符串 16 | * @returns true 字符串为空或无效或只包含空白字符 17 | */ 18 | export function isBlankStr(s: any): boolean { 19 | if (!isValidStr(s)) return true; 20 | const clearBlankStr = s.replace(/\s+/g, ''); 21 | if (clearBlankStr === '') { 22 | return true; 23 | } 24 | return false; 25 | } 26 | 27 | let cacheIsMacOs = undefined; 28 | export function isMacOs() { 29 | let platform = window.top.siyuan.config.system.os ?? navigator.platform ?? "ERROR"; 30 | platform = platform.toUpperCase(); 31 | let isMacOSFlag = cacheIsMacOs; 32 | if (cacheIsMacOs == undefined) { 33 | for (let platformName of ["DARWIN", "MAC", "IPAD", "IPHONE", "IOS"]) { 34 | if (platform.includes(platformName)) { 35 | isMacOSFlag = true; 36 | break; 37 | } 38 | } 39 | cacheIsMacOs = isMacOSFlag; 40 | } 41 | if (isMacOSFlag == undefined) { 42 | isMacOSFlag = false; 43 | } 44 | return isMacOSFlag; 45 | } 46 | 47 | export function isEventCtrlKey(event) { 48 | if (isMacOs()) { 49 | return event.metaKey; 50 | } 51 | return event.ctrlKey; 52 | } 53 | 54 | /** 55 | * 是否小于输入的版本号 56 | * @param version 输入的版本号,形如"3.1.23" 57 | * @returns boolean 表示是否小于,等于也false 58 | */ 59 | export function isCurrentVersionLessThan(version:string) { 60 | const parsedInputVersion = parseVersion(version); 61 | const parsedCurrentVersion = parseVersion(window.siyuan.config.system.kernelVersion); 62 | 63 | // 比较每个部分 64 | for (let i = 0; i < 3; i++) { 65 | if ((parsedCurrentVersion[i] || 0) < (parsedInputVersion[i] || 0)) { 66 | return true; 67 | } else if ((parsedCurrentVersion[i] || 0) > (parsedInputVersion[i] || 0)) { 68 | return false; 69 | } 70 | } 71 | // 版本号相同 72 | return false; 73 | } 74 | 75 | /** 76 | * 是否大于输入的版本号 77 | * @param version 输入的版本号,形如"3.1.23" 78 | * @returns boolean 表示是否大于,等于也false 79 | */ 80 | export function isCurrentVersionGreaterThan(version:string) { 81 | const currentVersion = window.siyuan.config.system.kernelVersion; 82 | 83 | const parsedInputVersion = parseVersion(version); 84 | const parsedCurrentVersion = parseVersion(currentVersion); 85 | 86 | for (let i = 0; i < 3; i++) { 87 | if ((parsedCurrentVersion[i] || 0) > (parsedInputVersion[i] || 0)) { 88 | return true; 89 | } else if ((parsedCurrentVersion[i] || 0) < (parsedInputVersion[i] || 0)) { 90 | return false; 91 | } 92 | } 93 | return false; 94 | } 95 | 96 | // 移除除了.数字的部分,分组 97 | const parseVersion = (version: string) => { 98 | return version.replace(/[^0-9.]/g, '').split('.').map(Number); 99 | }; -------------------------------------------------------------------------------- /src/utils/docSortUtils.ts: -------------------------------------------------------------------------------- 1 | import { debugPush, errorPush, logPush, warnPush } from "@/logger"; 2 | import { getReadOnlyGSettings } from "@/manager/settingManager"; 3 | import { isBlankStr, isValidStr } from "./commonCheck"; 4 | import { LINK_SORT_TYPES } from "@/constants"; 5 | import natsort from "natsort"; 6 | 7 | export function removeDocByDocName(docList: IFile[], regExpStrList: string[]) { 8 | let regExpList = regExpStrList.map(turnRegStr2Reg).filter(reg => reg != null); 9 | let newDocList: IFile[] = []; 10 | for (let doc of docList) { 11 | let isRemove = false; 12 | for (let reg of regExpList) { 13 | // 全局匹配g或粘性匹配y后,再次test从上次匹配位置之后开始匹配,这里清空 14 | reg.lastIndex = 0; 15 | if (reg.test(doc.name.slice(0, -3))) { 16 | isRemove = true; 17 | break; 18 | } 19 | } 20 | if (!isRemove) { 21 | newDocList.push(doc); 22 | } 23 | } 24 | return newDocList; 25 | } 26 | 27 | /** 28 | * 根据正则字符串,将匹配的文档固定在列表前部 29 | * @param docList 请注意 文档名称以.sy结尾!匹配时会移除 30 | * @param regExpStrList 31 | */ 32 | export function pinDocByDocName(docList: IFile[], regExpStrList: string[]) { 33 | let regExpList = regExpStrList.map(turnRegStr2Reg).filter(reg => reg != null); 34 | let pinDocList: IFile[] = []; 35 | let otherDocList: IFile[] = []; 36 | let addedFlagList: boolean[] = new Array(docList.length).fill(false); 37 | // 按照正则的顺序匹配,第一个正则匹配到的文档固定在前部,第二个正则匹配到的文档固定在第一个正则匹配到的文档后部,以此类推 38 | for (let reg of regExpList) { 39 | for (let [index, doc] of docList.entries()) { 40 | // 全局匹配g或粘性匹配y后,再次test从上次匹配位置之后开始匹配,这里清空 41 | reg.lastIndex = 0; 42 | if (reg.test(doc.name.slice(0, -3)) && !addedFlagList[index]) { 43 | pinDocList.push(doc); 44 | addedFlagList[docList.indexOf(doc)] = true; 45 | } 46 | } 47 | } 48 | // 将addedFlag为False的,整理为 otherDocList 49 | for (let [index, doc] of docList.entries()) { 50 | if (!addedFlagList[index]) { 51 | otherDocList.push(doc); 52 | } 53 | } 54 | return pinDocList.concat(otherDocList); 55 | } 56 | 57 | export function pinAndRemoveByDocNameForBackLinks(docList: IFile[]) { 58 | const g_setting = getReadOnlyGSettings(); 59 | 60 | const removeRegStrListStr = g_setting.removeRegStrListForLinks; 61 | const removeRegStrList = isBlankStr(removeRegStrListStr) ? [] : removeRegStrListStr.split("\n"); 62 | 63 | const pinRegStrListStr = g_setting.pinRegStrListForLinks; 64 | const pinRegStrList = isBlankStr(pinRegStrListStr) ? [] : pinRegStrListStr.split("\n"); 65 | 66 | const removedDocList = removeDocByDocName(docList, removeRegStrList); 67 | return pinDocByDocName(removedDocList, pinRegStrList); 68 | } 69 | 70 | function turnRegStr2Reg(regStr: string): RegExp { 71 | try { 72 | if (isBlankStr(regStr)) { 73 | warnPush("跳过正则表达式,因为正则字符串为空"); 74 | return null; 75 | } 76 | return new RegExp(regStr, "gm"); 77 | } catch (error) { 78 | warnPush("正则表达式无法解析,正则字符串为:" + regStr, ",错误为:" + error); 79 | return null; 80 | } 81 | } 82 | 83 | /** 84 | * (反链)内部排序类型转换为API接口所需的排序类型 85 | * @param sortType 插件内部的排序类型 86 | * @returns getBacklink2 API接口所需的sort 87 | */ 88 | export function linkSortTypeToBackLinkApiSortNum(sortType: string): string { 89 | switch (sortType) { 90 | case LINK_SORT_TYPES.NAME_ALPHABET_ASC: 91 | return "0"; 92 | case LINK_SORT_TYPES.NAME_ALPHABET_DESC: 93 | return "1"; 94 | case LINK_SORT_TYPES.NAME_NATURAL_ASC: 95 | return "4"; 96 | case LINK_SORT_TYPES.NAME_NATURAL_DESC: 97 | return "5"; 98 | case LINK_SORT_TYPES.CREATE_TIME_ASC: 99 | return "9"; 100 | case LINK_SORT_TYPES.CREATE_TIME_DESC: 101 | return "10"; 102 | case LINK_SORT_TYPES.UPDATE_TIME_ASC: 103 | return "2"; 104 | case LINK_SORT_TYPES.UPDATE_TIME_DESC: 105 | return "3"; 106 | default: 107 | warnPush("反链未知的排序类型:" + sortType); 108 | return "3"; 109 | } 110 | } 111 | 112 | /** 113 | * 对文档信息列表进行自然排序 114 | * @param docList 对象列表 115 | * @param attributeName 需要排序的对象属性 116 | * @param desc 是否为降序 117 | * @returns 排序后的结果 118 | */ 119 | export function sortIFileWithNatural(docList: IFile[], attributeName: string, desc: boolean): IFile[] { 120 | const sorter = natsort({ insensitive: true }); 121 | return docList.sort((a, b) => { 122 | if (desc) { 123 | return sorter(b[attributeName], a[attributeName]); 124 | } 125 | return sorter(a[attributeName], b[attributeName]); 126 | }); 127 | } -------------------------------------------------------------------------------- /src/utils/getInstance.ts: -------------------------------------------------------------------------------- 1 | let pluginInstance: any = null; 2 | 3 | export function setPluginInstance(instance:any) { 4 | pluginInstance = instance; 5 | } 6 | export function getPluginInstance() { 7 | return pluginInstance; 8 | } -------------------------------------------------------------------------------- /src/utils/lang.ts: -------------------------------------------------------------------------------- 1 | let language = null; 2 | let emptyLanguageKey: Array = []; 3 | 4 | export function setLanguage(lang:any) { 5 | language = lang; 6 | } 7 | 8 | export function lang(key: string) { 9 | if (language != null && language[key] != null) { 10 | return language[key]; 11 | } 12 | if (language == null) { 13 | emptyLanguageKey.push(key); 14 | console.error("语言文件未定义该Key", JSON.stringify(emptyLanguageKey)); 15 | } 16 | return key; 17 | } 18 | 19 | /** 20 | * 21 | * @param key key 22 | * @returns [设置项名称,设置项描述,设置项按钮名称(如果有)] 23 | */ 24 | export function settingLang(key: string) { 25 | let settingName: string = lang(`setting_${key}_name`); 26 | let settingDesc: string = lang(`setting_${key}_desp`); 27 | let settingBtnName: string = lang(`setting_${key}_btn`) 28 | if (settingName == "Undefined" || settingDesc == "Undefined") { 29 | throw new Error(`设置文本${key}未定义`); 30 | } 31 | return [settingName, settingDesc, settingBtnName]; 32 | } 33 | 34 | export function settingPageLang(key: string) { 35 | let pageSettingName: string = lang(`settingpage_${key}_name`); 36 | return [pageSettingName]; 37 | } -------------------------------------------------------------------------------- /src/utils/mutex.ts: -------------------------------------------------------------------------------- 1 | export default class Mutex { 2 | private isLocked: boolean = false; 3 | private queue: (() => void)[] = []; 4 | 5 | async lock(): Promise { 6 | return new Promise((resolve) => { 7 | const acquireLock = async () => { 8 | if (!this.isLocked) { 9 | this.isLocked = true; 10 | resolve(); 11 | } else { 12 | this.queue.push(() => { 13 | this.isLocked = true; 14 | resolve(); 15 | }); 16 | } 17 | }; 18 | 19 | acquireLock(); 20 | }); 21 | } 22 | 23 | tryLock(): boolean { 24 | if (!this.isLocked) { 25 | this.isLocked = true; 26 | return true; 27 | } else { 28 | return false; 29 | } 30 | } 31 | 32 | unlock(): void { 33 | this.isLocked = false; 34 | const next = this.queue.shift(); 35 | if (next) { 36 | next(); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/utils/onlyThisUtil.ts: -------------------------------------------------------------------------------- 1 | import { debugPush, errorPush } from "@/logger"; 2 | import { DOC_SORT_TYPES, getblockAttr, getCurrentDocIdF, isMobile, queryAPI } from "@/syapi"; 3 | import { IProtyle } from "siyuan"; 4 | import * as siyuanAPIs from "siyuan"; 5 | import { isValidStr } from "./commonCheck"; 6 | import { openRefLinkByAPI } from "./common"; 7 | 8 | export function getProtyleInfo(protyle: IProtyle):IProtyleEnvInfo { 9 | let result:IProtyleEnvInfo = { 10 | mobile: false, 11 | flashCard: false, 12 | notTraditional: false, 13 | originProtyle: protyle 14 | }; 15 | if (protyle.model == null) { 16 | result["notTraditional"] = true; 17 | } 18 | if (isMobile()) { 19 | result["mobile"] = true; 20 | } 21 | if (protyle.element.classList.contains("card__block")) { 22 | result["flashCard"] = true; 23 | } 24 | return result; 25 | } 26 | 27 | /** 28 | * html字符转义 29 | * 目前仅emoji使用 30 | * 对常见的html字符实体换回原符号 31 | * @param {*} inputStr 32 | * @returns 33 | */ 34 | export function htmlTransferParser(inputStr:string): string { 35 | if (inputStr == null || inputStr == "") return ""; 36 | let transfer = ["<", ">", " ", """, "&"]; 37 | let original = ["<", ">", " ", `"`, "&"]; 38 | for (let i = 0; i < transfer.length; i++) { 39 | inputStr = inputStr.replace(new RegExp(transfer[i], "g"), original[i]); 40 | } 41 | return inputStr; 42 | } 43 | 44 | 45 | export function getListItemEmojiHtmlStr(iconString:string, hasChild:boolean) { 46 | // 无emoji的处理 47 | if (!isValidStr(iconString)) { 48 | return hasChild ? `📑` : `📄`; 49 | } 50 | let result = iconString; 51 | // emoji地址判断逻辑为出现.,但请注意之后的补全 52 | if (iconString.startsWith("api/icon/getDynamicIcon")) { 53 | result = ``; 54 | } else if (iconString.indexOf(".") != -1) { 55 | result = ``; 56 | } else { 57 | result = `${emojiIconHandler(iconString, hasChild)}`; 58 | } 59 | return result; 60 | function emojiIconHandler(iconString:string, hasChild = false) { 61 | //确定是emojiIcon 再调用,printer自己加判断 62 | try { 63 | let result = ""; 64 | iconString.split("-").forEach(element => { 65 | //TODO: 确定是否正常 66 | debugPush("element", element); 67 | result += String.fromCodePoint(Number("0x" + element)); 68 | }); 69 | return result; 70 | } catch (err) { 71 | errorPush("emoji处理时发生错误", iconString, err); 72 | return hasChild ? "📑" : "📄"; 73 | } 74 | } 75 | } 76 | 77 | export function emojiIconHandler(iconString:string, hasChild = false) { 78 | if (!isValidStr(iconString)) { 79 | if (window.siyuan.storage["local-images"]) { 80 | if (hasChild) { 81 | return emojiIconHandler(window.siyuan.storage["local-images"].folder, hasChild); 82 | } else { 83 | return emojiIconHandler(window.siyuan.storage["local-images"].file, hasChild); 84 | } 85 | } 86 | return hasChild ? "📑" : "📄"; 87 | } 88 | //确定是emojiIcon 再调用,printer自己加判断 89 | try { 90 | let result = ""; 91 | iconString.split("-").forEach(element => { 92 | result += String.fromCodePoint(Number("0x" + element)); 93 | }); 94 | return result; 95 | } catch (err) { 96 | errorPush("emoji处理时发生错误", iconString, err); 97 | return hasChild ? "📑" : "📄"; 98 | } 99 | } 100 | 101 | /** 102 | * 使用设置中的参数处理文档 103 | * @param param0 104 | */ 105 | export function openRefLinkByAPIWithConfig({mouseEvent, paramDocId = "", keyParam = undefined, openInFocus = undefined, g_setting}: {mouseEvent?: MouseEvent, paramDocId?: string, keyParam?: any, openInFocus?: boolean, g_setting: any}) { 106 | let removeCurrentTab = undefined; 107 | let autoRemoveJudgeMiliseconds = 0; 108 | if (g_setting.openDocRemoveCurrentTab == "true") { 109 | removeCurrentTab = true; 110 | } 111 | if (g_setting.openDocRemoveCurrentTab == "false") { 112 | removeCurrentTab = false; 113 | } 114 | if (g_setting.autoRemoveOldTabJudgeMiliseconds != 0 && Number.isInteger(g_setting.autoRemoveOldTabJudgeMiliseconds)) { 115 | autoRemoveJudgeMiliseconds = g_setting.autoRemoveOldTabJudgeMiliseconds; 116 | } 117 | // TODO: 集中处理,以防止嵌套触发;不stopProp是为了分屏情况在正确的分屏区打开 118 | // if (mouseEvent.currentTarget != mouseEvent.target && mouseEvent.currentTarget.classList.contains("refLinks") && mouseEvent.target.classList.contains("refLinks")) { 119 | // debugPush("WARN"); 120 | // } else { 121 | // debugPush("WARNCliked", mouseEvent.currentTarget, mouseEvent.target); 122 | // } 123 | openRefLinkByAPI({mouseEvent, paramDocId, keyParam, openInFocus, removeCurrentTab, autoRemoveJudgeMiliseconds}); 124 | } 125 | 126 | export function removeCurrentTabF(docId?:string) { 127 | // 获取tabId 128 | if (!isValidStr(docId)) { 129 | docId = getCurrentDocIdF(true); 130 | } 131 | if (!isValidStr(docId)) { 132 | debugPush("错误的id或多个匹配id"); 133 | return; 134 | } 135 | // v3.1.11或以上 136 | if (siyuanAPIs?.getAllEditor) { 137 | const editor = siyuanAPIs.getAllEditor(); 138 | let protyle = null; 139 | for (let i = 0; i < editor.length; i++) { 140 | if (editor[i].protyle.block.rootID === docId) { 141 | protyle = editor[i].protyle; 142 | break; 143 | } 144 | } 145 | if (protyle) { 146 | if (protyle.model.headElement) { 147 | if (protyle.model.headElement.classList.contains("item--pin")) { 148 | debugPush("Pin页面,不关闭存在页签"); 149 | return; 150 | } 151 | } 152 | //id: string, closeAll = false, animate = true, isSaveLayout = true 153 | debugPush("关闭存在页签", protyle?.model?.parent?.parent, protyle.model?.parent?.id); 154 | protyle?.model?.parent?.parent?.removeTab(protyle.model?.parent?.id, false, false); 155 | } else { 156 | debugPush("没有找到对应的protyle,不关闭存在的页签"); 157 | return; 158 | } 159 | } else { // v3.1.10或以下 160 | return; 161 | } 162 | 163 | } 164 | 165 | /** 166 | * 获取临近的日记 167 | * @param param0 sqlResult^: 查询结果,docId&: 文档id,boxId&: 笔记本id,getNewer: 是否获取更新的日记,ialObject*: ial对象 168 | * @returns 169 | */ 170 | export async function getNeighborDailyNoteDoc({sqlResult=null, docId=null, boxId=null, getNewer=true, ialObject=null}: {sqlResult?: any, docId?: string, getNewer?: boolean, ialObject?:any, boxId?: string}) { 171 | if (sqlResult == null && boxId == null) { 172 | sqlResult = await queryAPI(`SELECT * FROM blocks WHERE id = '${docId}'`); 173 | } else if (sqlResult == null && isValidStr(boxId) && isValidStr(docId)) { 174 | sqlResult = [{"id": docId, "box": boxId, "ial": JSON.stringify(ialObject)}]; 175 | } 176 | if (sqlResult == null || sqlResult.length == 0) { 177 | debugPush("未找到对应的block"); 178 | throw new Error("未找到对应的block" + docId); 179 | } 180 | if (!sqlResult[0].ial?.includes("custom-dailynote")) { 181 | return null; 182 | } 183 | // 我们应该根据情况获取,如果是按照月构建的dailynote,同一笔记上可能有多个标签 184 | let minCurrentDate = Number.MAX_SAFE_INTEGER.toString(); // 向上跳转用 185 | let maxCurrentDate = "0"; 186 | if (ialObject == null) { 187 | ialObject = await getblockAttr(sqlResult[0].id); 188 | } 189 | for (const key in ialObject) { 190 | if (key.startsWith("custom-dailynote-")) { 191 | if (parseInt(ialObject[key]) > parseInt(maxCurrentDate)) { 192 | maxCurrentDate = ialObject[key]; 193 | } 194 | if (parseInt(ialObject[key]) < parseInt(minCurrentDate)) { 195 | minCurrentDate = ialObject[key]; 196 | } 197 | } 198 | } 199 | if ((getNewer && maxCurrentDate == "0") && (!getNewer && minCurrentDate == Number.MAX_SAFE_INTEGER.toString())) { 200 | return null; 201 | } 202 | // 在这里我们假定id前截取到的8位数是dailynote的创建时间 203 | const response = await queryAPI(` 204 | SELECT b.content as name, b.id 205 | FROM attributes AS a 206 | JOIN blocks AS b ON a.root_id = b.id 207 | WHERE a.name LIKE 'custom-dailynote%' AND a.block_id = a.root_id 208 | AND b.box = '${sqlResult[0].box}' 209 | AND a.value ${getNewer ? ">" : "<"} '${getNewer ? maxCurrentDate : minCurrentDate}' 210 | ORDER BY 211 | a.value ${getNewer ? "ASC" : "DESC"} 212 | LIMIT 1`); 213 | debugPush("dailyNote结果", response); 214 | if (response && response.length > 0) { 215 | return response[0]; 216 | } else { 217 | debugPush("日记未定位到结果"); 218 | return null; 219 | } 220 | } 221 | 222 | // export function getNotebookSortMode(boxId: string) { 223 | // let sortType: string|number = window.document.querySelector(`.file-tree.sy__file ul[data-url='${boxId}']`)?.getAttribute("data-sortmode"); 224 | // if (!isValidStr(sortType)) { 225 | // sortType = window.siyuan.notebooks.filter((item) => item.id == boxId)[0]?.sortMode; 226 | // } 227 | // if (typeof sortType === "string") { 228 | // sortType = parseInt(sortType, 10); 229 | // } 230 | // if (sortType == DOC_SORT_TYPES.FOLLOW_DOC_TREE_ORI) { 231 | // sortType = window.siyuan.config?.fileTree?.sort; 232 | // } 233 | // return sortType; 234 | // } 235 | 236 | export function isSortAsc(sortMode: number) { 237 | return [DOC_SORT_TYPES.FILE_NAME_ASC, DOC_SORT_TYPES.NAME_NAT_ASC, DOC_SORT_TYPES.CREATED_TIME_ASC, 238 | DOC_SORT_TYPES.MODIFIED_TIME_ASC, DOC_SORT_TYPES.REF_COUNT_ASC, DOC_SORT_TYPES.DOC_SIZE_ASC, 239 | DOC_SORT_TYPES.SUB_DOC_COUNT_ASC 240 | ].includes(sortMode); 241 | } 242 | 243 | export function isSortByNameOrCreateTime(sortMode: number) { 244 | return [DOC_SORT_TYPES.FILE_NAME_ASC, DOC_SORT_TYPES.FILE_NAME_DESC, DOC_SORT_TYPES.NAME_NAT_ASC, 245 | DOC_SORT_TYPES.NAME_NAT_DESC, DOC_SORT_TYPES.CREATED_TIME_ASC, DOC_SORT_TYPES.CREATED_TIME_DESC 246 | ].includes(sortMode); 247 | } -------------------------------------------------------------------------------- /src/utils/settings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 设置、设置标签页定义 3 | * 请注意,设置项的初始化应该在语言文件加载后进行 4 | */ 5 | import { debugPush } from "@/logger"; 6 | import { isValidStr } from "./commonCheck"; 7 | import { lang } from "./lang"; 8 | 9 | interface IConfigProperty { 10 | key: string, 11 | type: IConfigPropertyType, // 设置项类型 12 | min?: number, // 设置项最小值 13 | max?: number, // 设置项最大值 14 | btndo?: () => void, // 按钮设置项的调用函数(callback) 15 | options?: Array, // 选项key数组,元素顺序决定排序顺序,请勿使用非法字符串 16 | optionSameAsSettingKey?: string, // 如果选项的描述文本和其他某个设置项相同,在此指定;请注意,仍需要指定options 17 | } 18 | 19 | export class ConfigProperty { 20 | key: string; 21 | type: IConfigPropertyType; 22 | min?: number; 23 | max?: number; 24 | btndo?: () => void; 25 | options?: Array; 26 | 27 | configName: string; 28 | description: string; 29 | tips: string; 30 | 31 | optionNames: Array; 32 | optionDesps: Array; 33 | 34 | constructor({key, type, min, max, btndo, options, optionSameAsSettingKey}: IConfigProperty){ 35 | this.key = key; 36 | this.type = type; 37 | this.min = min; 38 | this.max = max; 39 | this.btndo = btndo; 40 | this.options = options ?? new Array(); 41 | 42 | this.configName = lang(`setting_${key}_name`); 43 | this.description = lang(`setting_${key}_desp`); 44 | if (this.configName.startsWith("🧪")) { 45 | this.description = lang("setting_experimental") + this.description; 46 | } else if (this.configName.startsWith("✈")) { 47 | this.description = lang("setting_testing") + this.description; 48 | } else if (this.configName.startsWith("❌")) { 49 | this.description = lang("setting_deprecated") + this.description; 50 | } 51 | // this.tips = lang(`setting_${key}_tips`); 52 | 53 | this.optionNames = new Array(); 54 | this.optionDesps = new Array(); 55 | for(let optionKey of this.options){ 56 | this.optionNames.push(lang(`setting_${optionSameAsSettingKey ?? key}_option_${optionKey}`)); 57 | this.optionDesps.push(lang(`setting_${optionSameAsSettingKey ?? key}_option_${optionKey}_desp`)); 58 | } 59 | } 60 | 61 | } 62 | 63 | interface ITabProperty { 64 | key: string, 65 | props: Array|Record>, 66 | iconKey?: string 67 | } 68 | 69 | export class TabProperty { 70 | key: string; 71 | iconKey: string; 72 | props: {[name:string]:Array}; 73 | isColumn: boolean = false; 74 | columnNames: Array = new Array(); 75 | columnKeys: Array = new Array(); 76 | 77 | constructor({key, props, iconKey}: ITabProperty){ 78 | this.key = key; 79 | if (isValidStr(iconKey)) { 80 | this.iconKey = iconKey; 81 | } else { 82 | this.iconKey = "setting"; 83 | } 84 | if (!Array.isArray(props)) { 85 | this.isColumn = true; 86 | Object.keys(props).forEach((columnKey) => { 87 | this.columnNames.push(lang(`setting_column_${columnKey}_name`)); 88 | this.columnKeys.push(columnKey); 89 | }); 90 | this.props = props; 91 | } else { 92 | this.props = {"none": props}; 93 | this.columnNames.push(lang(`setting_column_none_name`)); 94 | this.columnKeys.push("none"); 95 | } 96 | } 97 | 98 | } 99 | 100 | 101 | /** 102 | * 设置标签页 103 | * @param tabDefinitions 设置标签页定义 104 | * @returns 105 | */ 106 | // export function loadDefinitionFromTabProperty(tabDefinitions: Array):Array { 107 | // let result: Array = []; 108 | // tabDefinitions.forEach((tabDefinition) => { 109 | // tabDefinition.props.forEach((property) => { 110 | // result.push(property); 111 | // }); 112 | // }); 113 | 114 | // return result; 115 | // } 116 | 117 | /** 118 | * 获得ConfigMap对象 119 | * @param tabDefinitions 120 | * @returns 121 | */ 122 | export function loadAllConfigPropertyFromTabProperty(tabDefinitions: Array):Record { 123 | let result: Record = {}; 124 | tabDefinitions.forEach((tabDefinition) => { 125 | Object.values(tabDefinition.props).forEach((properties) => { 126 | properties.forEach((property) => { 127 | result[property.key] = property; 128 | }); 129 | }); 130 | }); 131 | return result; 132 | } -------------------------------------------------------------------------------- /src/worker/commonProvider.ts: -------------------------------------------------------------------------------- 1 | import { debugPush, logPush, warnPush } from "@/logger"; 2 | import { getReadOnlyGSettings } from "@/manager/settingManager"; 3 | import { queryAPI, listDocsByPathT, DOC_SORT_TYPES, getDocInfo} from "@/syapi" 4 | import { parseDateString } from "@/utils/common"; 5 | import { isValidStr } from "@/utils/commonCheck"; 6 | export async function getBasicInfo(docId:string, docPath: string, notebookId: string): Promise { 7 | let result: IBasicInfo; 8 | result = { 9 | success: true, 10 | docBasicInfo: null, 11 | parentDocBasicInfo: null, 12 | allSiblingDocInfoList: null, // 性能 13 | userDemandSiblingDocInfoList: null, // 性能 14 | childDocInfoList: null, // x性能 15 | currentDocId: docId, 16 | currentDocAttrs: {}, 17 | subDocLimited: false, 18 | siblingDocLimited: false, 19 | }; 20 | result.docBasicInfo = await getSimpleDocInfo(docId, docPath, notebookId); 21 | const parentDocId = getParentDocIdFromPath(docPath); 22 | // 笔记本的时候就没有上层,也导致下面没有统计 23 | if (isValidStr(parentDocId)) { 24 | result.parentDocBasicInfo = await getSimpleDocInfo(parentDocId, getParentPath(docPath)); 25 | } 26 | // TODO: 这个失败怎么判断? 27 | // if (currentDocSqlResponse.length == 0) { 28 | // result["success"] = false; 29 | // return result; 30 | // } 31 | [result.subDocLimited, result.siblingDocLimited] = getLimitation(result.docBasicInfo, result.parentDocBasicInfo); 32 | result.currentDocAttrs = result.docBasicInfo.ial; 33 | // 是否包括数据库 34 | // 文档中块数判断(用于控制lcd) 35 | logPush("BasicProviderFinalR", result); 36 | return result; 37 | } 38 | 39 | async function getSimpleDocInfo(docId:string, docPath?:string, notebookId?: string):Promise { 40 | const result:ISimpleDocInfoResult = { 41 | id: docId, 42 | path: docPath??"", 43 | ial: {}, 44 | attrViews: {}, 45 | name: "", 46 | refCount: 0, 47 | refIDs: [], 48 | subFileCount: 0, 49 | createTime: null, 50 | updateTime: null, 51 | icon: null, 52 | box: notebookId 53 | // hPath: "", 54 | }; 55 | const docInfoResponse = await getDocInfo(docId); 56 | Object.assign(result, docInfoResponse); 57 | // 新旧api文档图标字段不一致 58 | result.icon = result.ial?.icon ?? result.icon; 59 | result.name = result.name + ".sy"; 60 | result.createTime = parseDateString(result.id.substring(0, 14)); 61 | result.updateTime = parseDateString(result.ial.updated ?? result.id.substring(0, 14)); 62 | return result; 63 | } 64 | 65 | 66 | /** 67 | * 获取文档相关信息:父文档、同级文档、子文档(按此顺序返回) 68 | * @returns [parentDoc, siblingDocs, childDocs, getSubFlag, getSiblingFlag] 69 | */ 70 | async function getDocumentRelations(docBasicInfo:ISimpleDocInfoResult) { 71 | const g_setting = getReadOnlyGSettings(); 72 | // TODO: 获取上层文档的文档数量 73 | 74 | // 获取子文档 75 | let reorderdChildDocs: Promise = Promise.resolve([]); 76 | let getSubFlag = !isTooMuchSubDoc(docBasicInfo.subFileCount); 77 | if (getSubFlag) { 78 | reorderdChildDocs = getAllChildDocuments(docBasicInfo.path, docBasicInfo.box, DOC_SORT_TYPES[g_setting.childOrder], g_setting.showHiddenDoc); 79 | } 80 | let parentDocId = getParentDocIdFromPath(docBasicInfo.path); 81 | let getSiblingFlag = true; 82 | if (parentDocId) { 83 | let parentDocInfo = await getDocInfo(parentDocId); 84 | getSiblingFlag = !isTooMuchSubDoc(parentDocInfo.subFileCount); 85 | } 86 | // 获取同级文档 87 | let siblingDocs = getSiblingFlag ? getAllSiblingDocuments(docBasicInfo.path, docBasicInfo.box) : Promise.resolve([]); 88 | // 获取显示用同级文档 89 | let userDemandSiblingDocs = getSiblingFlag ? getUserDemandSiblingDocuments(docBasicInfo.path, docBasicInfo.box, DOC_SORT_TYPES[g_setting.childOrder], g_setting.showHiddenDoc) : Promise.resolve([]); 90 | 91 | logPush("siblings", siblingDocs); 92 | const waitResult = await Promise.all([siblingDocs, reorderdChildDocs, userDemandSiblingDocs]); 93 | logPush("waitResult", waitResult); 94 | // 返回结果 95 | return [ waitResult[0], waitResult[1], waitResult[2], getSubFlag, getSiblingFlag]; 96 | } 97 | 98 | /** 99 | * 填充一个字段 100 | * @param basicInfo 基础信息 101 | * @param field 字段名称 102 | */ 103 | export async function fillOneDocRelationOfBasicInfo(basicInfo:IBasicInfo, field: "allSiblingDocInfoList"| "userDemandSiblingDocInfoList" | "childDocInfoList") { 104 | if (basicInfo[field] === null) { 105 | const g_setting = getReadOnlyGSettings(); 106 | let result = null; 107 | const docBasicInfo = basicInfo.docBasicInfo; 108 | switch (field) { 109 | case "allSiblingDocInfoList": { 110 | result = await getAllSiblingDocuments(docBasicInfo.path, docBasicInfo.box) 111 | break; 112 | } 113 | case "userDemandSiblingDocInfoList": { 114 | result = await getUserDemandSiblingDocuments(docBasicInfo.path, docBasicInfo.box, DOC_SORT_TYPES[g_setting.childOrder], g_setting.showHiddenDoc); 115 | break; 116 | } 117 | case "childDocInfoList": { 118 | result = await getAllChildDocuments(docBasicInfo.path, docBasicInfo.box, DOC_SORT_TYPES[g_setting.childOrder], g_setting.showHiddenDoc); 119 | break; 120 | } 121 | default: { 122 | throw new Error("不支持的字段类型"); 123 | } 124 | } 125 | basicInfo[field] = result; 126 | } 127 | } 128 | 129 | function getLimitation(docBasicInfo, parentDocInfo) { 130 | let limitSubFlag = isTooMuchSubDoc(docBasicInfo?.subFileCount); 131 | let limitSiblingFlag = isTooMuchSubDoc(parentDocInfo?.subFileCount); 132 | return [limitSubFlag, limitSiblingFlag]; 133 | } 134 | 135 | export function isTooMuchSubDoc(count: number) { 136 | if (count == null) { 137 | logPush("[性能]没有输入文档个数", count); 138 | return false; 139 | } 140 | const g_setting = getReadOnlyGSettings(); 141 | if (g_setting.performanceMode && count > 512) { 142 | logPush("[性能]性能模式限制", count); 143 | return true; 144 | } 145 | // const LIMIT = window["OG_FILE_PERFORM_LIMIT"] ?? 4096; 146 | // if (count > LIMIT) { 147 | // logPush("[性能]文档数量过多", count); 148 | // return true; 149 | // } 150 | return false; 151 | } 152 | 153 | 154 | export async function getParentDocument(sqlResult:SqlResult) { 155 | let splitText = sqlResult.path.split("/"); 156 | if (splitText.length <= 2) return null; 157 | let parentSqlResponse = await queryAPI(`SELECT * FROM blocks WHERE id = "${splitText[splitText.length - 2]}"`); 158 | if (parentSqlResponse.length == 0) { 159 | return null; 160 | } 161 | return parentSqlResponse[0]; 162 | } 163 | 164 | export async function getAllChildDocuments(docPath:string, notebookId: string, sortType?: number, showHidden?: boolean): Promise { 165 | let childDocs = await listDocsByPathT({path: docPath, notebook: notebookId, maxListCount: 0, sort: sortType, showHidden: showHidden}); 166 | return childDocs; 167 | } 168 | 169 | export async function getAllSiblingDocuments(currentDocPath: string, notebookId: string) { 170 | const parentDocPath = getParentPath(currentDocPath); 171 | let siblingDocs = await listDocsByPathT({path: parentDocPath, notebook: notebookId, maxListCount: 0, showHidden: true}); 172 | return siblingDocs; 173 | } 174 | 175 | export async function getUserDemandSiblingDocuments(currentDocPath: string, notebookId: string, sortType?: number, showHidden?: boolean) { 176 | const parentDocPath = getParentPath(currentDocPath); 177 | let siblingDocs = await listDocsByPathT({path: parentDocPath, notebook: notebookId, maxListCount: 0, showHidden: showHidden, sort: sortType}); 178 | return siblingDocs; 179 | } 180 | 181 | export async function getAllDescendantDocuments(currentDocPath: string, notebookId: string) { 182 | const path = currentDocPath.substring(0, currentDocPath.length - 3) + "/"; 183 | const sqlResult = await queryAPI(`SELECT * FROM blocks WHERE path like "%${path}%" AND box = "${notebookId}" AND type = 'd'`); 184 | return sqlResult; 185 | } 186 | 187 | /** 188 | * 从文档路径中提取父文档路径 189 | * @param docPath sy格式的文档路径 190 | * @returns sy格式的父文档路径 191 | */ 192 | export function getParentPath(docPath:string):string|undefined { 193 | if (!isValidStr(docPath)) throw Error("无效的文档路径" + docPath); 194 | const docPathItem = docPath.split("/"); 195 | if (docPathItem.length <= 2) return "/"; 196 | docPathItem.pop(); 197 | return docPathItem.join("/") + ".sy"; 198 | } 199 | 200 | export function getParentDocIdFromPath(docPath:string):string|undefined { 201 | if (!isValidStr(docPath)) throw Error("无效的文档路径" + docPath); 202 | const docPathItem = docPath.split("/"); 203 | if (docPathItem.length <= 2) return undefined; 204 | return docPathItem[docPathItem.length - 2]; 205 | } 206 | 207 | export async function getCurrentDocSqlResult(docId: string) { 208 | const sqlResult = await queryAPI(`SELECT * FROM blocks WHERE id = "${docId}"`); 209 | if (sqlResult.length == 0) { 210 | return null; 211 | } 212 | return sqlResult[0]; 213 | } -------------------------------------------------------------------------------- /src/worker/eventHandler.ts: -------------------------------------------------------------------------------- 1 | import { debugPush, errorPush, infoPush, isDebugMode, logPush, warnPush } from "@/logger"; 2 | import {type IProtyle, type IEventBusMap, showMessage} from "siyuan"; 3 | import * as siyuanAPIs from "siyuan"; 4 | import { getPluginInstance } from "@/utils/getInstance"; 5 | import { getBasicInfo } from "@/worker/commonProvider"; 6 | import ContentPrinter from "@/worker/contentPrinter"; 7 | import { getProtyleInfo } from "@/utils/onlyThisUtil" 8 | import ContentApplyer from "./contentApplyer"; 9 | import Mutex from "@/utils/mutex"; 10 | import { getReadOnlyGSettings } from "@/manager/settingManager"; 11 | import { sleep } from "@/utils/common"; 12 | import { CONSTANTS } from "@/constants"; 13 | import { getAllShowingDocId, getHPathById, isMobile } from "@/syapi"; 14 | import { isCurrentVersionLessThan } from "@/utils/commonCheck"; 15 | export default class EventHandler { 16 | private handlerBindList: Recordvoid> = { 17 | "loaded-protyle-static": this.loadedProtyleRetryEntry.bind(this), // mutex需要访问EventHandler的属性 18 | "switch-protyle": this.loadedProtyleRetryEntry.bind(this), 19 | "ws-main": this.wsMainHandler.bind(this), 20 | }; 21 | // 关联的设置项,如果设置项对应为true,则才执行绑定 22 | private relateGsettingKeyStr: Record = { 23 | "loaded-protyle-static": null, // mutex需要访问EventHandler的属性 24 | "switch-protyle": null, 25 | "ws-main": "immediatelyUpdate", 26 | }; 27 | 28 | private loadAndSwitchMutex: Mutex; 29 | private simpleMutex: number = 0; 30 | private docIdMutex: Record = {}; 31 | constructor() { 32 | this.loadAndSwitchMutex = new Mutex(); 33 | } 34 | 35 | bindHandler() { 36 | const plugin = getPluginInstance(); 37 | const g_setting = getReadOnlyGSettings(); 38 | // const g_setting = getReadOnlyGSettings(); 39 | for (let key in this.handlerBindList) { 40 | if (this.relateGsettingKeyStr[key] == null || g_setting[this.relateGsettingKeyStr[key]]) { 41 | plugin.eventBus.on(key, this.handlerBindList[key]); 42 | } 43 | } 44 | // 移动端高危操作,试图替换原有的goback,以使得插件响应此动作 45 | if (isMobile() && g_setting.mobileBackReplace && isCurrentVersionLessThan("3.1.25")) { 46 | const originGoBack = window.goBack; 47 | window["ogGoBackOri"] = originGoBack; 48 | if (originGoBack) { 49 | window.goBack = () => { 50 | if (window["ogGoBackOri"]) { 51 | const result = window["ogGoBackOri"](); 52 | plugin.eventBus.emit("mobile-goback", {detail: {protyle: window.siyuan.mobile.editor.protyle}}); 53 | const event = new CustomEvent("loaded-protyle-static", { 54 | detail: { protyle: window.siyuan.mobile.editor.protyle }, 55 | bubbles: true, 56 | cancelable: true 57 | }); 58 | this.loadedProtyleRetryEntry(event); 59 | return result; 60 | } 61 | } 62 | } 63 | } 64 | if (g_setting.mobileBackReplace && isCurrentVersionLessThan("3.1.25") && isDebugMode()) { 65 | warnPush("插件替换移动端返回功能仍在生效,如果思源版本大于3.1.25,这不应该发生!"); 66 | } 67 | if (siyuanAPIs.getAllEditor == null) { 68 | warnPush("不支持的思源版本,请关闭 及时更新 设置项! This version of SiYuan is not supported, please disable the 'immediatelyUpdate' setting!"); 69 | return; 70 | } 71 | const allEditor = siyuanAPIs.getAllEditor(); 72 | const ids = getAllShowingDocId(); 73 | if (ids != null && ids.length > 0) { 74 | for (let editor of allEditor) { 75 | if (ids.includes(editor.protyle.block.rootID)) { 76 | debugPush("由 首次加载触发"); 77 | const hello = new CustomEvent("loaded-protyle-static", { 78 | detail: { protyle: editor.protyle } 79 | }); 80 | this.loadedProtyleHandler(hello).catch(error=>{errorPush("Error in movedoc handler", error)}); 81 | } 82 | } 83 | } 84 | } 85 | 86 | unbindHandler() { 87 | const plugin = getPluginInstance(); 88 | for (let key in this.handlerBindList) { 89 | plugin.eventBus.off(key, this.handlerBindList[key]); 90 | } 91 | } 92 | 93 | async wsMainHandler(detail: CustomEvent){ 94 | const cmdType = ["moveDoc", "rename", "removeDoc"]; 95 | if (cmdType.indexOf(detail.detail.cmd) != -1) { 96 | try { 97 | debugPush("检查刷新中(由重命名、删除或移动触发)"); 98 | if (siyuanAPIs.getAllEditor == null) { 99 | warnPush("不支持的思源版本,请关闭 及时更新 设置项! This version of SiYuan is not supported, please disable the 'immediatelyUpdate' setting!"); 100 | return; 101 | } 102 | const allEditor = siyuanAPIs.getAllEditor(); 103 | const ids = getAllShowingDocId(); 104 | if (ids != null && ids.length > 0) { 105 | for (let editor of allEditor) { 106 | if (ids.includes(editor.protyle.block.rootID)) { 107 | debugPush("由重命名、删除或移动触发"); 108 | const hello = new CustomEvent("loaded-protyle-static", { 109 | detail: { protyle: editor.protyle } 110 | }); 111 | this.loadedProtyleHandler(hello).catch(error=>{errorPush("Error in movedoc handler", error)}); 112 | } 113 | } 114 | } 115 | }catch(err) { 116 | errorPush(err); 117 | } 118 | } 119 | } 120 | 121 | async loadedProtyleHandler(event: CustomEvent) { 122 | // 多个文档同时触发则串行执行,理论上是要判断文档id是否相同(相同的才可能会在同一个Element上操作);这里全部串行可能影响性能 123 | // 我也忘了为什么要绑定load-了(目前主要是其他载入情况使用,例如闪卡);只是打开文档的话,switch-protyle事件就够了 124 | // 下面主要是避免两个事件同时触发造成的反复更新 125 | const originBlockId = event?.detail?.protyle?.block?.id ?? "undefined"; 126 | if (this.docIdMutex[originBlockId] > 0) { 127 | const path = await getHPathById(event.detail.protyle.block.id); 128 | logPush("由于正在运行,部分刷新被停止", event.detail.protyle.block.id, path); 129 | return true; 130 | } else if (!this.docIdMutex[originBlockId]) { 131 | this.docIdMutex[originBlockId] = 0; 132 | } 133 | debugPush("mutex", originBlockId, this.docIdMutex[originBlockId]); 134 | this.docIdMutex[originBlockId]++; 135 | let doNotRetryFlag = true; 136 | if (isDebugMode()) { 137 | console.time(CONSTANTS.PLUGIN_NAME + " " + originBlockId); 138 | } 139 | try { 140 | await this.loadAndSwitchMutex.lock(); 141 | // 颜色状态码可以参考https://blog.csdn.net/weixin_44110772/article/details/105860997 142 | // x1b是十六进制,和文中的/033八进制没啥不同,同时应用加粗和Cryan就像下面这样;分隔 143 | logPush("\x1b[1;36m%s\x1b[0m", ">>>>>>>> mutex 新任务开始"); 144 | // 移动端由于闪卡面包屑,需要后面获取envInfo后处理; 145 | let protyle = event.detail.protyle; 146 | // 可能还需要套一个重试的壳 147 | // 另外,和swtich 共同存在时,需要防止并发 148 | // 获取当前文档id 149 | logPush("loadedProtyleHandler", protyle); 150 | logPush("currentDoc", JSON.stringify(protyle?.block)); 151 | const docId:string = protyle.block.rootID; 152 | // 区分工作环境 也就是区分个移动端、闪卡页面、桌面端(网页端通用);判断优先顺序闪卡页面>移动端>桌面端; 153 | // 疯了的话可能加入判断使用什么内容顺序(预设模板) 154 | if (protyle.element.classList.contains("fn__none")) { 155 | if (isDebugMode()) { 156 | showMessage(`触发更新的文档不可见, ${protyle.id}, ${docId}, ${protyle.element.children.length}——[syplugin-hierarchyNavigate]`); 157 | } 158 | debugPush(`当前文档不可见, ${protyle.id}, ${docId}, ${protyle.element.children.length}`); 159 | } 160 | const protyleEnvInfo:IProtyleEnvInfo = getProtyleInfo(protyle); 161 | logPush("protyleInfo", protyleEnvInfo); 162 | if (protyleEnvInfo.notTraditional && !protyleEnvInfo.flashCard && !protyleEnvInfo.mobile) { // 其他情况也显示层级导航:(弃用) && getReadOnlyGSettings().enableForOtherCircumstance == false 163 | debugPush("非常规情况,且设置不允许,跳过"); 164 | return true; 165 | } 166 | // 移动端且非闪卡,需要使用全局编辑器对象 167 | if (!protyleEnvInfo.flashCard && isMobile()) { 168 | protyle = window.siyuan.mobile.editor.protyle; 169 | protyleEnvInfo.originProtyle = protyle; 170 | } 171 | // if (isMobile()) { 172 | // showMessage(`移动端触发 ${docId} ${window.siyuan.mobile.editor.protyle.block.rootID}`); 173 | // } 174 | 175 | // 调用Provider获取必要信息 176 | const basicInfo = await getBasicInfo(docId, protyle.path, protyle.notebookId); 177 | logPush("basicInfo", basicInfo); 178 | if (!basicInfo.success) { 179 | logPush("获取文档信息失败"); 180 | return false; 181 | } 182 | // 区分生成内容;应该不会根据不同的配置使用不同的生成吧,那也太累了,这个部分可能需要使用contentPrinter的对象 183 | // 如果还需要根据不同的设备走不同的显示内容,就更麻烦了【这里不做区分,如果区分移动端,则使用移动端独有设置项文件mobile-setting.json/{uid}.json】 184 | const printer = new ContentPrinter(basicInfo, protyleEnvInfo); 185 | const printAreas = await printer.print(false); 186 | logPush("finalElement", printAreas); 187 | const applyer = new ContentApplyer(basicInfo, protyleEnvInfo, protyle.element); 188 | let elementPromiseList = []; 189 | if (printAreas) { 190 | // 还是需要回到这里setAndApply 191 | elementPromiseList.push(applyer.apply(printAreas)); 192 | } 193 | // 处理编辑器末尾的内容区 194 | if (!protyleEnvInfo.flashCard && !protyleEnvInfo.notTraditional) { 195 | const endPrintAreas = await printer.print(true); 196 | if (endPrintAreas) { 197 | // 还是需要回到这里setAndApply 198 | // const applyer = new ContentApplyer(basicInfo, protyleEnvInfo, protyle.element); 199 | elementPromiseList.push(applyer.applyToEnd(endPrintAreas)); 200 | } 201 | } 202 | // 设定绑定,处理宽度变化 203 | let elementList = await Promise.all(elementPromiseList); 204 | elementList.filter((e)=>e).forEach((e)=>applyer.weSetObserver(e)); 205 | } catch(error) { 206 | errorPush("ERROR", error); 207 | doNotRetryFlag = true; 208 | } finally { 209 | logPush("\x1b[1;36m%s\x1b[0m", "<<<<<<<< mutex 任务结束"); 210 | if (isDebugMode()) { 211 | console.timeEnd(CONSTANTS.PLUGIN_NAME + " " + originBlockId); 212 | } 213 | this.loadAndSwitchMutex.unlock(); 214 | this.docIdMutex[originBlockId]--; 215 | } 216 | return doNotRetryFlag; 217 | } 218 | 219 | async loadedProtyleRetryEntry(event: CustomEvent) { 220 | let doNotRetry = true; 221 | const g_setting = getReadOnlyGSettings(); 222 | let retryCount = 0; 223 | do { 224 | doNotRetry = await this.loadedProtyleHandler(event); 225 | retryCount++; 226 | if (doNotRetry == false) { 227 | logPush("重试过程中遇到问题"); 228 | await sleep(300); 229 | } 230 | } while(retryCount < 2 && !doNotRetry); 231 | if (!doNotRetry) { 232 | infoPush("多次重试仍然存在异常,请查看Log日志"); 233 | } 234 | } 235 | 236 | // async wsMainHandler(event: CustomEvent) { 237 | 238 | // debugPush(detail); 239 | // const cmdType = ["moveDoc", "rename", "removeDoc"]; 240 | // if (cmdType.indexOf(detail.detail.cmd) != -1) { 241 | // try { 242 | // debugPush("由 立即更新 触发"); 243 | // this.loadedProtyleRetryEntry(); 244 | // }catch(err) { 245 | // errorPush(err); 246 | // } 247 | // } 248 | // } 249 | } -------------------------------------------------------------------------------- /src/worker/pluginHelper.ts: -------------------------------------------------------------------------------- 1 | import { Dialog } from "siyuan"; 2 | import { Ref, ref } from "vue"; 3 | 4 | let showSwitchPanelDialog: Ref = ref(null); 5 | 6 | export function useShowSwitchPanel() { 7 | return showSwitchPanelDialog; 8 | } -------------------------------------------------------------------------------- /src/worker/setStyle.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultSettings, getReadOnlyGSettings } from "@/manager/settingManager"; 2 | import { CONSTANTS } from "@/constants"; 3 | import { logPush } from "@/logger"; 4 | import { isMobile } from "@/syapi"; 5 | import { lang } from "@/utils/lang"; 6 | 7 | export function setStyle() { 8 | removeStyle(); 9 | const g_setting = getReadOnlyGSettings(); 10 | logPush("set styleg_setting", g_setting); 11 | const g_setting_default = getDefaultSettings(); 12 | const head = document.getElementsByTagName('head')[0]; 13 | const style = document.createElement('style'); 14 | style.setAttribute("id", CONSTANTS.STYLE_ID); 15 | let linkWidthRestrict = g_setting.sameWidth == 0 ? "" : ` 16 | .og-hn-heading-docs-container span.docLinksWrapper { 17 | min-width: ${g_setting.sameWidth}em; 18 | }`; 19 | let noIndicatorStyle = g_setting.hideIndicator ? ` 20 | .og-hn-heading-docs-container .og-hierachy-navigate-doc-indicator { 21 | display:none; 22 | } 23 | `:""; 24 | 25 | let borderDisplayStyle = g_setting.areaBorder ? 26 | ` 27 | .og-hierachy-navigate-doc-container { 28 | border: 1px solid rgba(0, 0, 0, 0); 29 | } 30 | 31 | .og-hierachy-navigate-doc-container:hover { 32 | border: 1px solid var(--b3-theme-surface-lighter); 33 | } 34 | `: ""; 35 | 36 | let noneDisplayStyle = g_setting.noneAreaHide ? ` 37 | .${CONSTANTS.NONE_CLASS_NAME} { 38 | display: none; 39 | } 40 | ` : ""; 41 | let endDocAreaPaddingTop = g_setting.endDocAreaPaddingTop ? `/*文档结尾区域*/ 42 | .og-hn-heading-docs-container.og-hn-at-doc-end{ 43 | border-top: 2px dotted var(--b3-table-border-color); 44 | }` : ""; 45 | // 第二行后对齐链接文本,(向内缩进: #21) 46 | let alignStyle = ` 47 | .og-hn-container-multiline { 48 | text-indent: -2em; /*2.28略微多了*/ 49 | padding-left: 2em; 50 | overflow-x: hidden; 51 | /* #30 2.28em与100%导致宽度溢出 */ 52 | padding-right: 0em; 53 | width: auto; 54 | } 55 | .og-hn-container-multiline .og-hierachy-navigate-doc-indicator { 56 | 57 | } 58 | .og-hn-container-multiline .og-hn-emoji-and-name { 59 | text-indent: 0px; 60 | } 61 | 62 | .og-hn-container-multiline .og-hn-doc-none-word { 63 | text-indent: 0px; 64 | } 65 | 66 | `; 67 | 68 | let toTheTop = ` 69 | .${CONSTANTS.TO_THE_TOP_CLASS_NAME} { 70 | z-index: 9; 71 | position: fixed; 72 | top: 10px; 73 | left: 10px; 74 | background: var(--b3-toolbar-background); 75 | border: 1px solid var(--b3-toolbar-blur-background); 76 | } 77 | `; 78 | 79 | let calColumnCount = g_setting.sameWidthColumn; 80 | if (isMobile()) { 81 | calColumnCount = g_setting.sameWidthColumnMobile; 82 | } 83 | let docNameCenteringCSS = g_setting.docNameCentering ? "margin: 0 auto; /*居中显示*/": ""; 84 | const linkColumnStyle = calColumnCount > 0 ? 85 | ` 86 | .og-hierachy-navigate-doc-container.og-hierachy-navigate-children-doc-container span.docLinksWrapper, 87 | .og-hierachy-navigate-doc-container.og-hierachy-navigate-sibling-doc-container span.docLinksWrapper, 88 | .og-hierachy-navigate-doc-container.og-hierachy-navigate-onthisday-doc-container span.docLinksWrapper, 89 | .og-hierachy-navigate-doc-container.og-hierachy-navigate-next-doc-container span.docLinksWrapper, 90 | .og-hierachy-navigate-doc-container.og-hierachy-navigate-parent-doc-container span.docLinksWrapper, 91 | .og-hierachy-navigate-doc-container.og-hierachy-navigate-backlink-doc-container span.docLinksWrapper { 92 | width: calc( (100% - ${calColumnCount} * ${calColumnCount == 1 ? "0px" : "10px"}) / ${calColumnCount}); 93 | ${calColumnCount == 1 ? "margin-right: 0px;" : ""}/*仅一列时忽略margin-right*/ 94 | } 95 | `: ``; 96 | 97 | const defaultLinkStyle = ` 98 | .${CONSTANTS.CONTAINER_CLASS_NAME} span.docLinksWrapper{ 99 | background-color: var(--b3-protyle-code-background);/*var(--b3-protyle-inline-code-background); --b3-protyle-code-background --b3-theme-surface-light*/ 100 | color: var(--b3-protyle-inline-code-color); 101 | /*line-height: calc(${g_setting.fontSize}px + 0.2em);*/ /*此项导致连接上下可滚动*/ 102 | font-weight: 400; 103 | display: inline-flex; 104 | align-items: center; 105 | box-sizing: border-box; 106 | padding: 4px 6px; 107 | border-radius: ${(g_setting.fontSize + 2)}px; 108 | transition: var(--b3-transition); 109 | margin-bottom: 3px; 110 | text-overflow: ellipsis; 111 | white-space: nowrap; 112 | overflow: hidden; 113 | max-width: calc(100% - 10px); /*需要排除margin-right: 10px的影响*/ 114 | } 115 | .${CONSTANTS.CONTAINER_CLASS_NAME} span.docLinksWrapper.og-hn-docLinksWrapper-hl { 116 | background-color: color-mix(in srgb, var(--b3-protyle-code-background) 95%, var(--b3-theme-on-background)); 117 | border: 0.55px dashed color-mix(in srgb, var(--b3-protyle-code-background) 35%, var(--b3-theme-on-background)); 118 | } 119 | .${CONSTANTS.CONTAINER_CLASS_NAME} span.docLinksWrapper.og-none-click { 120 | color: var(--b3-theme-on-background); 121 | } 122 | 123 | 124 | .${CONSTANTS.CONTAINER_CLASS_NAME} span.og-hn-emoji-and-name { 125 | ${docNameCenteringCSS} /*居中显示*/ 126 | text-overflow: ellipsis; 127 | overflow-x: hidden; /* 修复文字下侧被截断的问题 */ 128 | } 129 | 130 | /* 语义调整 refLinks 的可点击,其他仅样式 https://github.com/OpaqueGlass/syplugin-hierarchyNavigate/issues/61 */ 131 | .og-hierachy-navigate-sibling-doc-container span.docLinksWrapper, 132 | .og-hierachy-navigate-children-doc-container span.docLinksWrapper, 133 | .og-hierachy-navigate-next-doc-container span.docLinksWrapper, 134 | .og-hierachy-navigate-backlink-doc-container span.docLinksWrapper { 135 | margin-right: 10px; 136 | } 137 | /* 这里,由于提示词单独占位,导致提示词-链接之间gap一样也有10px,或许可以考虑全都加入gap就不显得突兀了 */ 138 | .og-hierachy-navigate-sibling-doc-container, 139 | .og-hierachy-navigate-children-doc-container, 140 | .og-hierachy-navigate-next-doc-container, 141 | .og-hierachy-navigate-backlink-doc-container { 142 | /*display: flex; 143 | flex-wrap: wrap; 144 | gap: 10px; 145 | align-items: flex-start;*/ 146 | } 147 | `; 148 | 149 | const previewNext = ` 150 | .og-hierachy-navigate-next-preview-doc-container { 151 | max-height: 230px; 152 | overflow: scroll; 153 | } 154 | .og-hn-np-inner-flex { 155 | display: flex; 156 | justify-content: space-between; 157 | margin: 10px 0; 158 | gap: 20px; 159 | height: 100%; 160 | } 161 | 162 | .og-hn-np-nav-preview { 163 | flex: 1; 164 | min-width: 0; 165 | cursor: pointer; 166 | display: flex; 167 | } 168 | 169 | .og-hn-np-nav-preview-inner { 170 | border: 1px solid var(--b3-table-border-color); 171 | border-radius: 8px; 172 | padding: 15px; 173 | height: 100%; 174 | transition: all 0.3s ease; 175 | display: flex; 176 | flex-direction: column; 177 | width: 100%; 178 | min-height: 0; 179 | box-sizing: border-box; 180 | } 181 | 182 | .og-hn-np-nav-preview-inner:hover { 183 | border-color: #888; 184 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 185 | /*background-color: #00000011;*/ 186 | } 187 | 188 | .og-hn-np-prev .og-hn-np-nav-direction, 189 | .og-hn-np-prev .og-hn-np-nav-post-title, 190 | .og-hn-np-prev .og-hn-np-nav-excerpt { 191 | text-align: left; 192 | } 193 | 194 | .og-hn-np-next .og-hn-np-nav-direction, 195 | .og-hn-np-next .og-hn-np-nav-post-title{ 196 | text-align: right; 197 | } 198 | 199 | .og-hn-np-nav-direction { 200 | font-weight: bold; 201 | color: var(--custom-h2-color); 202 | margin-bottom: 5px; 203 | } 204 | 205 | .og-hn-np-nav-post-title { 206 | font-size: 1.1em; 207 | font-weight: bold; 208 | color: var(--custom-h2-color); 209 | margin-bottom: 10px; 210 | word-break: break-all; 211 | display: -webkit-box; 212 | -webkit-line-clamp: 2; 213 | -webkit-box-orient: vertical; 214 | overflow: hidden; 215 | text-overflow: ellipsis; 216 | } 217 | 218 | .og-hn-np-nav-excerpt { 219 | color: var(--b3-theme-on-surface); 220 | font-size: 1em; 221 | line-height: 1.5; 222 | overflow: hidden; 223 | text-overflow: ellipsis; 224 | display: -webkit-box; 225 | -webkit-line-clamp: 3; 226 | -webkit-box-orient: vertical; 227 | flex-grow: 1; 228 | } 229 | 230 | .og-hn-np-placeholder { 231 | background-color: #00000001; 232 | color: transparent; 233 | user-select: none; 234 | } 235 | 236 | .og-hn-np-placeholder .og-hn-np-nav-excerpt { 237 | background-color: #eee; 238 | color: transparent; 239 | border-radius: 4px; 240 | } 241 | 242 | .og-hn-np-placeholder .og-hn-np-nav-direction::after { 243 | content: "${lang("no_doc")}"; 244 | color: #999; 245 | display: block; 246 | } 247 | 248 | .og-hn-np-placeholder .og-hn-np-nav-post-title, 249 | .og-hn-np-placeholder .og-hn-np-nav-excerpt { 250 | visibility: hidden; 251 | } 252 | 253 | .og-hn-np-nav-preview.og-hn-np-non-clickable { 254 | cursor: not-allowed; 255 | } 256 | .og-hn-heading-docs-container.og-hn-at-doc-end { 257 | padding-bottom: 20px; 258 | } 259 | .og-hn-np-nav-preview-inner.og-hn-np-placeholder .og-hn-np-nav-direction { 260 | color: #999; 261 | } 262 | `; 263 | 264 | const previewBox = ` 265 | /* 主容器 */ 266 | .og-hn-pb-container { 267 | display: flex; 268 | flex-wrap: wrap; 269 | gap: 10px; 270 | overflow: scroll; 271 | } 272 | 273 | /* 单个文档方块 */ 274 | .og-hn-pb-doc-box { 275 | border: 1px solid var(--b3-table-border-color); 276 | flex: 1 0 250px; 277 | max-height: 200px; 278 | overflow: auto; 279 | border-radius: 10px; 280 | padding: 10px 15px 15px 8px; 281 | /*margin: 5px;*/ 282 | display: flex; 283 | flex-direction: column; 284 | } 285 | 286 | /* 文档标题 */ 287 | .og-hn-pb-title { 288 | margin: 10px 0; 289 | font-size: 1.1em; 290 | font-weight: bold; 291 | color: var(--custom-h2-color); 292 | cursor: pointer; 293 | } 294 | 295 | /* 文档内容预览 */ 296 | .og-hn-pb-content { 297 | margin: 0 0 0 7px; 298 | width: 95%; 299 | white-space: normal; 300 | overflow: hidden; 301 | font-size: 1em; 302 | } 303 | 304 | /* 子文档容器 */ 305 | .og-hn-pb-child-container { 306 | margin-left: 7px; 307 | width: 95%; 308 | } 309 | 310 | /* 子文档项 */ 311 | .og-hn-pb-child-item { 312 | margin: 5px 0; 313 | font-size: 0.9em; 314 | white-space: normal; 315 | } 316 | 317 | /* 子文档链接 */ 318 | .og-hn-pb-child-item { 319 | cursor: pointer; 320 | color: var(--b3-protyle-inline-link-color); 321 | } 322 | .og-hn-pb-child-item:hover { 323 | background-color: #00000010; 324 | } 325 | 326 | /* emoji图标 */ 327 | .og-hn-pb-emoji { 328 | margin-right: 5px; 329 | } 330 | 331 | /* 深色模式适配 */ 332 | .dark-mode .og-hn-pb-doc-box { 333 | background-color: #efefef15; 334 | color: #C9D1D9; 335 | } 336 | 337 | .dark-mode .og-hn-pb-doc-box:hover { 338 | background-color: #efefef25; 339 | } 340 | 341 | .dark-mode .og-hn-pb-content a { 342 | color: #7aa7d4 !important; 343 | } 344 | 345 | /* 悬停效果 */ 346 | .og-hn-pb-doc-box:hover { 347 | border-color: #888; 348 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 349 | } 350 | 351 | /* 任务列表样式调整 */ 352 | .og-hn-pb-content ul:has(> .protyle-task) { 353 | list-style-type: none; 354 | padding-left: 16px; 355 | } 356 | 357 | .og-hn-pb-content > ul:has(> .protyle-task) { 358 | padding-left: 0px; 359 | } 360 | 361 | .og-hn-pb-content .protyle-task input ~ p { 362 | display: inline-block; 363 | } 364 | 365 | /* 图片显示 */ 366 | .og-hn-pb-content .img img { 367 | max-width: 100%; 368 | display: inline-block; 369 | } 370 | .og-hn-pb-content .code-block { 371 | background-color: unset; 372 | } 373 | .og-hn-pb-content .code-block .hljs { 374 | overflow: hidden; 375 | } 376 | 377 | `; 378 | 379 | style.innerHTML = ` 380 | 381 | .og-hn-doc-none-word { 382 | background-color: #d23f3155;/*var(--b3-protyle-inline-code-background); --b3-protyle-code-background --b3-theme-surface-light*/ 383 | width: 2.5em; 384 | text-align: center; 385 | display: inline-grid !important; 386 | color: var(--b3-theme-on-background); 387 | line-height: ${g_setting.fontSize + 2}px; 388 | font-weight: 400; 389 | align-items: center; 390 | box-sizing: border-box; 391 | /* #30 调整padding左右,尽量避免换行导致右侧大量留白 */ 392 | padding: 4px 4px; 393 | border-radius: ${(g_setting.fontSize + 2)}px; 394 | transition: var(--b3-transition); 395 | margin-bottom: 3px; 396 | text-overflow: ellipsis; 397 | white-space: nowrap; 398 | overflow: hidden; 399 | } 400 | 401 | .og-hn-menu-emojitext, .og-hn-menu-emojipic { 402 | align-self: center; 403 | height: 14px; 404 | width: 14px; 405 | line-height: 14px; 406 | margin-right: 8px; 407 | flex-shrink: 0; 408 | } 409 | 410 | img.og-hn-menu-emojipic { 411 | width: 16px; 412 | height: 16px; 413 | } 414 | /* 语义调整 refLink的可点击,其他仅样式 https://github.com/OpaqueGlass/syplugin-hierarchyNavigate/issues/61 */ 415 | .og-hn-heading-docs-container span.docLinksWrapper.refLinks:hover { 416 | cursor: pointer; 417 | box-shadow: 0 0 2px var(--b3-list-hover); 418 | opacity: .86; 419 | /*background-color: var(--b3-toolbar-hover);*/ 420 | /*text-decoration: underline;*/ 421 | } 422 | 423 | .og-hn-heading-docs-container .trimDocName { 424 | overflow: hidden; 425 | text-overflow: ellipsis; 426 | } 427 | .og-hn-heading-docs-container { 428 | padding: 0px 6px; 429 | font-size: ${g_setting.fontSize}px; 430 | } 431 | 432 | ${linkWidthRestrict} 433 | 434 | ${noIndicatorStyle} 435 | 436 | ${noneDisplayStyle} 437 | 438 | ${g_setting.hideIndicator ? "" : alignStyle} 439 | 440 | ${linkColumnStyle} 441 | 442 | ${borderDisplayStyle} 443 | 444 | ${toTheTop} 445 | 446 | ${previewNext} 447 | 448 | ${previewBox} 449 | 450 | ${endDocAreaPaddingTop} 451 | 452 | /* 限制相邻文档区域 链接宽度 453 | .og-hierachy-navigate-doc-container.og-hierachy-navigate-next-doc-container span.docLinksWrapper { 454 | width: calc( (100% - 2em - 1 * 10px) / 4); 455 | }*/ 456 | 457 | .og-hierachy-navigate-doc-container { 458 | max-height: ${isMobile() ? "25vh" : g_setting.maxHeightLimit + "em"}; 459 | overflow-y: scroll; 460 | } 461 | 462 | .og-hierachy-navigate-doc-container.og-hn-not-fold { 463 | max-height: none; 464 | } 465 | 466 | .og-hierachy-navigate-doc-container + .og-hierachy-navigate-doc-container { 467 | padding-top: 6px; 468 | } 469 | 470 | .og-hn-create-at-wrapper, .og-hn-modify-at-wrapper, .og-hn-child-doc-count-wrapper, .og-hn-child-word-count-wrapper { 471 | margin-right: 8px; 472 | } 473 | 474 | .og-hn-create-at-indicator, .og-hn-modify-at-indicator, .og-hn-child-word-count-indicator, .og-hn-child-doc-count-wrapper { 475 | color: var(--b3-theme-on-surface); 476 | } 477 | 478 | .og-hn-create-at-content, .og-hn-modify-at-content, .og-hn-child-word-count-content, .og-hn-child-doc-count-content { 479 | font-weight: 600; 480 | color: var(--b3-theme-on-background); 481 | } 482 | 483 | .og-hn-notebook-wrapper { 484 | color: var(--b3-theme-on-background); 485 | } 486 | 487 | .og-hierachy-navigate-info-container { 488 | margin-bottom: 7px; 489 | } 490 | 491 | .${CONSTANTS.CONTAINER_CLASS_NAME} { 492 | text-align: left; 493 | } 494 | 495 | /* 面包屑箭头 */ 496 | .og-hn-heading-docs-container .og-fake-breadcrumb-arrow-span .${CONSTANTS.ARROW_CLASS_NAME} { 497 | height: 10px; 498 | width: 10px; 499 | color: var(--b3-theme-on-surface-light); 500 | margin: 0 4px; 501 | flex-shrink: 0; 502 | } 503 | 504 | /* 块没有>功能,屏蔽 */ 505 | .og-hn-heading-docs-container .og-fake-breadcrumb-arrow-span[data-type="FILE"], 506 | .og-hn-heading-docs-container .og-fake-breadcrumb-arrow-span[data-type="NOTEBOOK"] { 507 | display: inline-block; 508 | cursor: pointer; 509 | } 510 | 511 | .og-hn-parent-area-replace-with-breadcrumb .docLinksWrapper { 512 | margin: 0 auto; 513 | } 514 | 515 | .og-hn-widget-container { 516 | border-bottom: solid 1px var(--b3-border-color); 517 | 518 | } 519 | 520 | .og-hn-widget-container { 521 | padding: 0px 6px; 522 | } 523 | 524 | .og-hn-widget-container.og-hn-mobile { 525 | padding-top: 16px; 526 | padding-left: 24px; 527 | padding-right: 16px; 528 | } 529 | 530 | .og-hn-more-less { 531 | color: var(--b3-protyle-inline-link-color); 532 | cursor: pointer; 533 | } 534 | .og-hn-more-less:hover { 535 | text-decoration: underline; 536 | } 537 | 538 | ${g_setting.docLinkCSS == g_setting_default.docLinkCSS && g_setting.docLinkClass == g_setting_default.docLinkClass ? defaultLinkStyle:""} 539 | .${CONSTANTS.PARENT_CONTAINER_ID} {${styleEscape(g_setting.parentBoxCSS)}} 540 | 541 | .${CONSTANTS.CHILD_CONTAINER_ID} {${styleEscape(g_setting.childBoxCSS)}} 542 | 543 | .${CONSTANTS.SIBLING_CONTAINER_ID} {${styleEscape(g_setting.siblingBoxCSS)}} 544 | 545 | .${CONSTANTS.CONTAINER_CLASS_NAME} span.docLinksWrapper {${styleEscape(g_setting.docLinkCSS)}} 546 | 547 | /* 置顶后占位 */ 548 | .${CONSTANTS.PLACEHOLDER_FOR_POP_OUT_CLASS_NAME} { 549 | display: flex; 550 | justify-content: center; 551 | align-items: center; 552 | } 553 | /* 折叠隐藏 */ 554 | .og-hn-heading-docs-container .${CONSTANTS.IS_FOLDING_CLASS_NAME} { 555 | display: none !important; 556 | } 557 | `; 558 | head.appendChild(style); 559 | } 560 | 561 | function styleEscape(str) { 562 | if (!str) return ""; 563 | return str.replace(new RegExp("<[^<]*style[^>]*>", "g"), ""); 564 | } 565 | 566 | 567 | export function removeStyle() { 568 | document.getElementById(CONSTANTS.STYLE_ID)?.remove(); 569 | } 570 | 571 | export function setCouldHideStyle() { 572 | const style = document.createElement("style"); 573 | style.id = CONSTANTS.HIDE_COULD_FOLD_STYLE_ID; 574 | style.innerHTML = `.${CONSTANTS.COULD_FOLD_CLASS_NAME} {display: none !important;}`; 575 | document.head.appendChild(style); 576 | } 577 | 578 | export function removeCouldHideStyle() { 579 | document.getElementById(CONSTANTS.HIDE_COULD_FOLD_STYLE_ID)?.remove(); 580 | } -------------------------------------------------------------------------------- /src/worker/shortcutHandler.ts: -------------------------------------------------------------------------------- 1 | import { debugPush, isDebugMode, logPush } from "@/logger"; 2 | import { getblockAttr, getCurrentDocIdF, getNotebookSortModeF, isMobile, queryAPI } from "@/syapi"; 3 | import { generateUUID, getFocusedBlockId, replaceShortcutString } from "@/utils/common"; 4 | import { isValidStr } from "@/utils/commonCheck"; 5 | import { lang } from "@/utils/lang"; 6 | import { showMessage, Plugin } from "siyuan"; 7 | import { getAllChildDocuments, getUserDemandSiblingDocuments } from "./commonProvider"; 8 | import { getReadOnlyGSettings } from "@/manager/settingManager"; 9 | import { createApp } from "vue"; 10 | import switchPanel from "@/components/dialog/switchPanel.vue"; 11 | import * as siyuan from "siyuan"; 12 | import { useShowSwitchPanel } from "./pluginHelper"; 13 | import { getNeighborDailyNoteDoc, isSortAsc, isSortByNameOrCreateTime, openRefLinkByAPIWithConfig } from "@/utils/onlyThisUtil"; 14 | import { CONSTANTS } from "@/constants"; 15 | 16 | export function bindCommand(pluginInstance: Plugin) { 17 | pluginInstance.addCommand({ 18 | langKey: "go_up", 19 | hotkey: "⌥⌘←", 20 | callback: () => { 21 | goUpShortcutHandler(); 22 | }, 23 | }); 24 | 25 | pluginInstance.addCommand({ 26 | langKey: "go_down", 27 | hotkey: "⌥⌘→", 28 | callback: () => { 29 | goDownShortcutHandler(); 30 | } 31 | }); 32 | 33 | pluginInstance.addCommand({ 34 | langKey: "insert_lcd", 35 | hotkey: "", 36 | editorCallback: (protyle) => { 37 | addWidgetShortcutHandler(protyle); 38 | } 39 | }); 40 | 41 | pluginInstance.addCommand({ 42 | langKey: "go_to_previous_doc", 43 | hotkey: "⌥⌘↑", 44 | callback: () => { 45 | goToPreviousDocShortcutHandler(); 46 | } 47 | }); 48 | 49 | 50 | pluginInstance.addCommand({ 51 | langKey: "go_to_next_doc", 52 | hotkey: "⌥⌘↓", 53 | callback: () => { 54 | goToNextDocShortcutHandler(); 55 | }, 56 | }); 57 | 58 | pluginInstance.addCommand({ 59 | langKey: "show_switch_panel", 60 | hotkey: "⌥⌘E", 61 | callback: () => { 62 | showSwitchPanel(); 63 | }, 64 | }); 65 | 66 | // 图标的制作参见帮助文档 67 | pluginInstance.addIcons(` 68 | 69 | `); 70 | if (isDebugMode() || !isMobile()) { 71 | pluginInstance.addTopBar({ 72 | "icon": "iconOgHnBookUp", 73 | "title": lang("dialog_panel_switchDoc"), 74 | "position": "right", 75 | "callback": () => { 76 | showSwitchPanel(); 77 | } 78 | }); 79 | } 80 | 81 | // const topBarElement = this.addTopBar({ 82 | // icon: "iconTestStatistics", 83 | // title: this.i18n.addTopBarIcon, 84 | // position: "right", 85 | // callback: () => { 86 | // this.openStatisticTab(); 87 | // } 88 | // }); 89 | pluginInstance.addCommand({ 90 | langKey: "make_navigation_top", 91 | hotkey: "", 92 | callback: () => { 93 | turnNavigationToTop(); 94 | }, 95 | }); 96 | } 97 | 98 | 99 | async function showSwitchPanel() { 100 | const docId = getCurrentDocIdF(); 101 | if (!isValidStr(docId)) { 102 | debugPush("未能获取到当前文档id"); 103 | showMessage(lang("open_doc_first")); 104 | return; 105 | } 106 | let app = null; 107 | const uid = generateUUID(); 108 | const switchPanelDialogRef = useShowSwitchPanel(); 109 | if (switchPanelDialogRef.value) { 110 | switchPanelDialogRef.value.destroy(); 111 | switchPanelDialogRef.value = null; 112 | return; 113 | } 114 | // 获取文档id 115 | 116 | const switchPanelDialog = new siyuan.Dialog({ 117 | "title": lang("dialog_panel_plugin_name") + "--" + lang("dialog_panel_switchDoc"), 118 | "content": ` 119 |
120 | `, 121 | "width": isMobile() ? "80vw":"55vw", 122 | "height": isMobile() ? "75vh":"80vh", 123 | "destroyCallback": ()=>{app.unmount(); switchPanelDialogRef.value = null; debugPush("对话框销毁成功")}, 124 | }); 125 | switchPanelDialogRef.value = switchPanelDialog; 126 | app = createApp(switchPanel, {docId: docId, dialog: switchPanelDialog}); 127 | app.mount(`#og_plugintemplate_${uid}`); 128 | return; 129 | } 130 | 131 | 132 | 133 | 134 | async function goUpShortcutHandler() { 135 | const docId = getCurrentDocIdF(); 136 | const g_setting = getReadOnlyGSettings(); 137 | if (!isValidStr(docId)) { 138 | logPush("未能读取到打开文档的id"); 139 | showMessage(lang("open_doc_first")); 140 | return ; 141 | } 142 | // 通过正则判断IAL,匹配指定属性是否是禁止显示的文档 143 | let sqlResult = await queryAPI(`SELECT * FROM blocks WHERE id = "${docId}"`); 144 | let paths; 145 | if (sqlResult && sqlResult.length >= 1) { 146 | paths = sqlResult[0].path.split("/"); 147 | } else { 148 | return; 149 | } 150 | if (paths.length < 2) { 151 | return; 152 | } 153 | if (isValidStr(paths[paths.length - 2])) { 154 | let docId = paths[paths.length - 2]; 155 | docId = docId.replace(".sy", ""); 156 | openRefLinkByAPIWithConfig({paramDocId: docId, keyParam: { 157 | ctrlKey: false, 158 | shiftKey: false, 159 | altKey: false}, g_setting}); 160 | } else { 161 | showMessage(lang("is_top_document"), 2000) 162 | } 163 | } 164 | 165 | 166 | async function goDownShortcutHandler() { 167 | const docId = getCurrentDocIdF(); 168 | const g_setting = getReadOnlyGSettings(); 169 | let sqlResult = await queryAPI(`SELECT * FROM blocks WHERE id = "${docId}"`); 170 | if (sqlResult && sqlResult.length >= 1) { 171 | // TODO: 如果可以忽略超过范围的提示,再将这里的限制修改为3以下 172 | const childDocsList = await getAllChildDocuments(sqlResult[0].path, sqlResult[0].box); 173 | if (childDocsList && childDocsList.length >= 1) { 174 | const childDoc = childDocsList[0]; 175 | openRefLinkByAPIWithConfig({paramDocId: childDoc.id, keyParam: { 176 | ctrlKey: false, 177 | shiftKey: false, 178 | altKey: false}, g_setting}); 179 | } else { 180 | showMessage(lang("no_child_document"), 2000); 181 | } 182 | } else { 183 | showMessage(lang("canot_open_child_doc"), 2000); 184 | } 185 | } 186 | 187 | async function goToPreviousDocShortcutHandler() { 188 | const previousDoc = await getSiblingDocsForNeighborShortcut(false); 189 | const g_setting = getReadOnlyGSettings(); 190 | debugPush("previousDoc", previousDoc); 191 | if (previousDoc) { 192 | // 打开 193 | openRefLinkByAPIWithConfig({paramDocId: previousDoc.id, g_setting}); 194 | // openTab({ 195 | // app: getPluginInstance().app.appId, 196 | // doc: { 197 | // id: previousDoc.id, 198 | // } 199 | // }); 200 | } else { 201 | // 提示 202 | showMessage(lang("is_first_document"), 2000); 203 | } 204 | } 205 | 206 | async function goToNextDocShortcutHandler() { 207 | const nextDoc = await getSiblingDocsForNeighborShortcut(true); 208 | const g_setting = getReadOnlyGSettings(); 209 | debugPush("nextDoc", nextDoc); 210 | if (nextDoc) { 211 | openRefLinkByAPIWithConfig({paramDocId: nextDoc.id, g_setting}); 212 | // openTab({ 213 | // app: getPluginInstance().app.appId, 214 | // doc: { 215 | // id: nextDoc.id, 216 | // } 217 | // }); 218 | } else { 219 | // 提示 220 | showMessage(lang("is_last_document"), 2000); 221 | } 222 | } 223 | 224 | async function getSiblingDocsForNeighborShortcut(isNext) { 225 | let siblingDocs = null; 226 | let docId; 227 | docId = getCurrentDocIdF(); 228 | if (!isValidStr(docId)) { 229 | showMessage(lang("open_doc_first")); 230 | return ; 231 | } 232 | let sqlResult = await queryAPI(`SELECT * FROM blocks WHERE id = "${docId}"`); 233 | if (!sqlResult || sqlResult.length <= 0) { 234 | // debugPush(`第${retryCount}次获取文档信息失败,该文档可能是刚刚创建,休息一会儿后重新尝试`); 235 | // await sleep(200); 236 | // continue; 237 | debugPush("文档似乎是刚刚创建,无法获取上下文信息,停止处理"); 238 | return; 239 | } 240 | const g_setting = getReadOnlyGSettings(); 241 | // const parentSqlResult = await getParentDocument(sqlResult[0]); 242 | siblingDocs = await getUserDemandSiblingDocuments(sqlResult[0].path, sqlResult[0].box, undefined, true); 243 | 244 | // 处理sibling docs 245 | if (!sqlResult[0].ial?.includes("custom-dailynote") && (siblingDocs == null || siblingDocs.length == 1)) { 246 | debugPush("仅此一个文档,停止处理"); 247 | return null; 248 | } 249 | let iCurrentDoc = -1; 250 | for (let iSibling = 0; iSibling < siblingDocs.length; iSibling++) { 251 | if (siblingDocs[iSibling].id === docId) { 252 | iCurrentDoc = iSibling; 253 | break; 254 | } 255 | } 256 | if (iCurrentDoc >= 0) { 257 | // #78 启用日记独立排序时,日记先行 258 | if (sqlResult[0].ial?.includes("custom-dailynote") && g_setting.previousAndNextFollowDailynote) { 259 | return await getNeighborDailyNoteDoc({sqlResult: sqlResult, getNewer: isNext}); 260 | } 261 | if (iCurrentDoc > 0 && isNext == false) { 262 | return siblingDocs[iCurrentDoc - 1]; 263 | } 264 | if (iCurrentDoc + 1 < siblingDocs.length && isNext == true) { 265 | return siblingDocs[iCurrentDoc + 1]; 266 | } 267 | // #78 默认情况 文档排序方式 升降序补充缺失的上一篇或下一篇;文件名、自然排序、创建时间排序以外的排序方式,不补充; 268 | if (sqlResult[0].ial?.includes("custom-dailynote")) { 269 | const sortMode = getNotebookSortModeF(sqlResult[0].box); 270 | if (isSortByNameOrCreateTime(sortMode)) { 271 | if (isSortAsc(sortMode)) { 272 | return await getNeighborDailyNoteDoc({sqlResult: sqlResult, getNewer: isNext}); 273 | } else { 274 | // 若为降序,下一篇isNext为获取更早的日记,这里取反 275 | return await getNeighborDailyNoteDoc({sqlResult: sqlResult, getNewer: !isNext}); 276 | } 277 | } else { 278 | return null; 279 | } 280 | } 281 | 282 | return null; 283 | } 284 | return null; 285 | } 286 | 287 | async function addWidgetShortcutHandler(protyle:any) { 288 | const docId = getCurrentDocIdF(); 289 | if (docId == null) { 290 | logPush("未能读取到打开文档的id"); 291 | showMessage(lang("open_doc_first")); 292 | return ; 293 | } 294 | const focusedBlockId = getFocusedBlockId(); 295 | if (!isValidStr(focusedBlockId)) { 296 | return; 297 | } 298 | const WIDGET_HTML = ``; 299 | debugPush("shortCut,PROTYLE", protyle); 300 | protyle.getInstance()?.insert(WIDGET_HTML, true) 301 | } 302 | 303 | async function turnNavigationToTop() { 304 | if (removeToTheTop()) { 305 | return; 306 | } 307 | // 找到当前有效的,指定之 308 | const navigationArea = window.document.querySelector(".layout__wnd--active .protyle.fn__flex-1:not(.fn__none) .og-hn-heading-docs-container") as HTMLElement; 309 | if (!navigationArea) { 310 | return; 311 | } 312 | // 创建占位元素 313 | const navigationAreaElemRect = navigationArea.getBoundingClientRect(); 314 | const placeholder = document.createElement('div'); 315 | placeholder.classList.add(CONSTANTS.PLACEHOLDER_FOR_POP_OUT_CLASS_NAME); 316 | placeholder.style.width = `${navigationAreaElemRect.width}px`; 317 | placeholder.style.height = `${navigationAreaElemRect.height}px`; 318 | placeholder.innerHTML = lang("make_top_placeholder").replace("##", replaceShortcutString(window.siyuan.config.keymap.plugin["syplugin-hierarchyNavigate"]["make_navigation_top"].custom)); 319 | // placeholder.style.display = 'block'; 320 | // 添加和替换 321 | navigationArea.classList.add(CONSTANTS.TO_THE_TOP_CLASS_NAME); 322 | navigationArea.parentNode.insertBefore(placeholder, navigationArea); 323 | // 调整位置 324 | const protyleContentEle = window.document.querySelector(".layout__wnd--active .protyle.fn__flex-1:not(.fn__none) .protyle-content"); 325 | const rect = protyleContentEle.getBoundingClientRect(); 326 | const left = rect.left; 327 | const top = rect.top; 328 | navigationArea.style.left = `${left}px`; 329 | navigationArea.style.top = `${top}px`; 330 | // 添加监听,有点击事件则清除之 331 | // window.document.addEventListener("click", removeToTheTop); 332 | } 333 | 334 | export function removeToTheTop() { 335 | // window.document.removeEventListener("click", removeToTheTop); 336 | const navigationArea = window.document.querySelector(`.layout__wnd--active .protyle.fn__flex-1:not(.fn__none) .og-hn-heading-docs-container.${CONSTANTS.TO_THE_TOP_CLASS_NAME}`); 337 | if (navigationArea) { 338 | window.document.querySelectorAll(`.${CONSTANTS.PLACEHOLDER_FOR_POP_OUT_CLASS_NAME}`).forEach(elem=>elem.remove()); 339 | navigationArea.classList.remove(CONSTANTS.TO_THE_TOP_CLASS_NAME); 340 | navigationArea.style.left = ''; 341 | navigationArea.style.top = ''; 342 | return true; 343 | } 344 | return false; 345 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": [ 7 | "ES2021", //ES2020.String不支持.replaceAll 8 | "DOM", 9 | "DOM.Iterable" 10 | ], 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "Node", 14 | // "allowImportingTsExtensions": true, 15 | "allowSyntheticDefaultImports": true, 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "preserve", 20 | /* Linting */ 21 | "strict": false, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true, 25 | /* svelte 过去的设置*/ 26 | "allowJs": true, 27 | "checkJs": true, 28 | "types": [ 29 | "node", 30 | "vite/client", 31 | "vue", 32 | "sortablejs" 33 | ], 34 | // "baseUrl": "./src", 35 | "paths": { 36 | "@/*": ["./src/*"], 37 | "@/libs/*": ["./src/libs/*"], 38 | }, 39 | "typeRoots": ["./src/types"] 40 | }, 41 | "include": [ 42 | "tools/**/*.ts", 43 | "src/**/*.ts", 44 | "src/**/*.d.ts", 45 | "src/**/*.tsx", 46 | "src/**/*.vue" 47 | ], 48 | "references": [ 49 | { 50 | "path": "./tsconfig.node.json" 51 | } 52 | ], 53 | "root": "." 54 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": [ 10 | "vite.config.ts" 11 | ] 12 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path" 2 | import { defineConfig, loadEnv } from "vite" 3 | import minimist from "minimist" 4 | import { viteStaticCopy } from "vite-plugin-static-copy" 5 | import livereload from "rollup-plugin-livereload" 6 | import zipPack from "vite-plugin-zip-pack"; 7 | import fg from 'fast-glob'; 8 | import vue from '@vitejs/plugin-vue'; 9 | import fs from "fs"; 10 | 11 | const args = minimist(process.argv.slice(2)) 12 | const isWatch = args.watch || args.w || false; 13 | // 使用change-dir命令更改dev的目录 14 | const devDistDirInfo = "./notSync/devInfo.json"; 15 | const loadDirJsonContent = fs.existsSync(devDistDirInfo) 16 | ? JSON.parse(fs.readFileSync(devDistDirInfo, "utf-8")) 17 | : {}; 18 | const devDistDir = loadDirJsonContent["devDir"] ?? "./dev"; 19 | const distDir = isWatch ? devDistDir : "./dist" 20 | 21 | console.log("isWatch=>", isWatch) 22 | console.log("distDir=>", distDir) 23 | 24 | export default defineConfig({ 25 | resolve: { 26 | alias: { 27 | "@": resolve(__dirname, "src"), 28 | } 29 | }, 30 | 31 | plugins: [ 32 | vue(), 33 | viteStaticCopy({ 34 | targets: [ 35 | { 36 | src: "./README*.md", 37 | dest: "./", 38 | }, 39 | { 40 | src: "./icon.png", 41 | dest: "./", 42 | }, 43 | { 44 | src: "./preview.png", 45 | dest: "./", 46 | }, 47 | { 48 | src: "./plugin.json", 49 | dest: "./", 50 | }, 51 | { 52 | src: "./src/i18n/**", 53 | dest: "./i18n/", 54 | }, 55 | { 56 | src: "./LICENSE", 57 | dest: "./" 58 | }, 59 | { 60 | src: "./CHANGELOG.md", 61 | dest: "./" 62 | } 63 | ], 64 | }), 65 | ], 66 | 67 | // https://github.com/vitejs/vite/issues/1930 68 | // https://vitejs.dev/guide/env-and-mode.html#env-files 69 | // https://github.com/vitejs/vite/discussions/3058#discussioncomment-2115319 70 | // 在这里自定义变量 71 | define: { 72 | "process.env": process.env, 73 | "process.env.DEV_MODE": `"${isWatch}"`, 74 | __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, 75 | }, 76 | 77 | build: { 78 | // 输出路径 79 | outDir: distDir, 80 | emptyOutDir: false, 81 | 82 | // 构建后是否生成 source map 文件 83 | sourcemap: false, 84 | 85 | // 设置为 false 可以禁用最小化混淆 86 | // 或是用来指定是应用哪种混淆器 87 | // boolean | 'terser' | 'esbuild' 88 | // 不压缩,用于调试 89 | minify: !isWatch, 90 | 91 | lib: { 92 | // Could also be a dictionary or array of multiple entry points 93 | entry: resolve(__dirname, "src/index.ts"), 94 | // the proper extensions will be added 95 | fileName: "index", 96 | formats: ["cjs"], 97 | }, 98 | rollupOptions: { 99 | plugins: [ 100 | ...( 101 | isWatch ? [ 102 | livereload(devDistDir), 103 | { 104 | //监听静态资源文件 105 | name: 'watch-external', 106 | async buildStart() { 107 | const files = await fg([ 108 | 'src/i18n/*.json', 109 | './README*.md', 110 | './widget.json' 111 | ]); 112 | for (let file of files) { 113 | this.addWatchFile(file); 114 | } 115 | } 116 | } 117 | ] : [ 118 | zipPack({ 119 | inDir: './dist', 120 | outDir: './', 121 | outFileName: 'package.zip' 122 | }) 123 | ] 124 | ) 125 | ], 126 | 127 | // make sure to externalize deps that shouldn't be bundled 128 | // into your library 129 | external: ["siyuan", "process"], 130 | 131 | output: { 132 | entryFileNames: "[name].js", 133 | assetFileNames: (assetInfo) => { 134 | if (assetInfo.name === "style.css") { 135 | return "index.css" 136 | } 137 | return assetInfo.name 138 | }, 139 | }, 140 | }, 141 | } 142 | }) 143 | --------------------------------------------------------------------------------