├── .gitattributes ├── .github └── workflows │ └── stale.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── app ├── obsidian │ ├── .eslintrc.cjs │ ├── .release-it.cjs │ ├── esbuild-postcss.mjs │ ├── esbuild.config.mjs │ ├── manifest-beta.json │ ├── manifest.json │ ├── package.json │ ├── postcss.config.mjs │ ├── scripts │ │ ├── get-zotero-types.mjs │ │ ├── ob-bumper.mjs │ │ ├── ob.esbuild.mjs │ │ └── zip.mjs │ ├── src │ │ ├── api.ts │ │ ├── components │ │ │ ├── atch-suggest.ts │ │ │ ├── basic │ │ │ │ ├── context.tsx │ │ │ │ └── modal.ts │ │ │ ├── derived-file-view │ │ │ │ ├── index.d.ts │ │ │ │ └── index.js │ │ │ ├── item-details │ │ │ │ ├── getItemString.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── labelRenderer.tsx │ │ │ │ ├── shouldExpandNode.tsx │ │ │ │ └── valueRenderer.tsx │ │ │ ├── item-suggest │ │ │ │ ├── core.ts │ │ │ │ ├── editor.ts │ │ │ │ ├── index.ts │ │ │ │ └── popup.ts │ │ │ └── ui │ │ │ │ ├── collapsible.tsx │ │ │ │ └── tabs.tsx │ │ ├── dialog.less │ │ ├── index.css │ │ ├── install-guide │ │ │ ├── guide │ │ │ │ ├── atom.ts │ │ │ │ ├── auto.tsx │ │ │ │ ├── content.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── list-item.tsx │ │ │ │ ├── loading.tsx │ │ │ │ ├── manual.tsx │ │ │ │ └── utils.ts │ │ │ ├── index.tsx │ │ │ └── version.ts │ │ ├── log.ts │ │ ├── main.less │ │ ├── note-feature │ │ │ ├── annot-view │ │ │ │ ├── drag-insert.ts │ │ │ │ ├── more-options.ts │ │ │ │ ├── store.tsx │ │ │ │ ├── style.less │ │ │ │ └── view.tsx │ │ │ ├── citation-suggest │ │ │ │ ├── basic.ts │ │ │ │ ├── editor.ts │ │ │ │ ├── index.ts │ │ │ │ ├── popup.ts │ │ │ │ ├── settings.ts │ │ │ │ └── style.less │ │ │ ├── note-import │ │ │ │ └── index.ts │ │ │ ├── protocol │ │ │ │ └── service.ts │ │ │ ├── quick-switch │ │ │ │ ├── index.ts │ │ │ │ └── popup.ts │ │ │ ├── service.ts │ │ │ ├── template-preview │ │ │ │ ├── base.ts │ │ │ │ ├── details.tsx │ │ │ │ ├── open.ts │ │ │ │ └── preview.ts │ │ │ ├── topic-import │ │ │ │ ├── create-note.tsx │ │ │ │ ├── service.tsx │ │ │ │ ├── status.tsx │ │ │ │ └── utils.ts │ │ │ └── update-note.ts │ │ ├── platform.json │ │ ├── services │ │ │ ├── annot-block │ │ │ │ └── service.ts │ │ │ ├── citekey-click │ │ │ │ ├── onEditorClick.js │ │ │ │ └── service.ts │ │ │ ├── note-index │ │ │ │ ├── index.ts │ │ │ │ ├── service.ts │ │ │ │ ├── settings.ts │ │ │ │ └── utils.ts │ │ │ ├── note-parser │ │ │ │ ├── format.ts │ │ │ │ ├── parse │ │ │ │ │ └── color.ts │ │ │ │ └── service.ts │ │ │ ├── pdf-parser │ │ │ │ └── service.ts │ │ │ ├── server │ │ │ │ ├── service.ts │ │ │ │ └── settings.ts │ │ │ ├── template │ │ │ │ ├── defaults │ │ │ │ │ ├── ejs.d.ts │ │ │ │ │ ├── zt-annot.ejs │ │ │ │ │ ├── zt-annots.ejs │ │ │ │ │ ├── zt-cite.ejs │ │ │ │ │ ├── zt-cite2.ejs │ │ │ │ │ ├── zt-colored.ejs │ │ │ │ │ ├── zt-field.ejs │ │ │ │ │ └── zt-note.ejs │ │ │ │ ├── editor │ │ │ │ │ ├── bracket.ts │ │ │ │ │ ├── service.ts │ │ │ │ │ └── suggester.ts │ │ │ │ ├── eta │ │ │ │ │ ├── file-handling.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── patch.ts │ │ │ │ │ ├── preset.ts │ │ │ │ │ └── render.ts │ │ │ │ ├── frontmatter.ts │ │ │ │ ├── get-fm.ts │ │ │ │ ├── helper │ │ │ │ │ ├── annot.ts │ │ │ │ │ ├── attachment.ts │ │ │ │ │ ├── base.ts │ │ │ │ │ ├── collection.ts │ │ │ │ │ ├── colors.json │ │ │ │ │ ├── creator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── item.ts │ │ │ │ │ ├── tag.ts │ │ │ │ │ └── to-helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── object.group.d.ts │ │ │ │ ├── render.ts │ │ │ │ ├── settings.ts │ │ │ │ └── utils.ts │ │ │ └── zotero-db │ │ │ │ ├── api.ts │ │ │ │ ├── auto-refresh │ │ │ │ ├── service.ts │ │ │ │ └── settings.ts │ │ │ │ ├── connector │ │ │ │ ├── service.ts │ │ │ │ ├── service2.ts │ │ │ │ ├── settings.ts │ │ │ │ └── worker.ts │ │ │ │ ├── database.ts │ │ │ │ ├── img-import │ │ │ │ ├── service.ts │ │ │ │ └── settings.ts │ │ │ │ └── index.ts │ │ ├── setting-tab │ │ │ ├── common.tsx │ │ │ ├── components │ │ │ │ ├── Boolean.tsx │ │ │ │ ├── Setting.tsx │ │ │ │ ├── TextComfirm.tsx │ │ │ │ └── useExtraButton.tsx │ │ │ ├── connect │ │ │ │ ├── Background.tsx │ │ │ │ ├── Database.tsx │ │ │ │ ├── DatabasePath.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── useDatabaseStatus.tsx │ │ │ ├── general │ │ │ │ ├── ImageExcerpt.tsx │ │ │ │ ├── LibSelect.tsx │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ ├── misc │ │ │ │ ├── LogLevel.tsx │ │ │ │ └── index.tsx │ │ │ ├── patch-tab.tsx │ │ │ ├── suggester │ │ │ │ └── index.tsx │ │ │ ├── template │ │ │ │ ├── AutoTrim.tsx │ │ │ │ ├── EjectableTemplate.tsx │ │ │ │ ├── SimpleTemplateEdit.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── shared.ts │ │ │ └── update │ │ │ │ └── index.tsx │ │ ├── settings │ │ │ ├── base.ts │ │ │ ├── invault-path.ts │ │ │ └── service.ts │ │ ├── typings │ │ │ ├── electron-remote.d.ts │ │ │ ├── obsidian-ex.d.ts │ │ │ ├── svg.d.ts │ │ │ └── web-worker.d.ts │ │ ├── utils │ │ │ ├── click-notice.ts │ │ │ ├── create-initial.ts │ │ │ ├── icon.tsx │ │ │ ├── index.ts │ │ │ ├── merge.ts │ │ │ ├── once.ts │ │ │ ├── requests.ts │ │ │ └── union.ts │ │ ├── worker-iframe │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ │ ├── worker-web │ │ │ ├── annot-block │ │ │ │ ├── api.ts │ │ │ │ ├── main.ts │ │ │ │ ├── parse.ts │ │ │ │ └── stringify.ts │ │ │ └── tsconfig.json │ │ └── zt-main.ts │ ├── tailwind.config.cjs │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── versions.json └── zotero │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .release-it.js │ ├── chrome.manifest │ ├── package.json │ ├── prefs.json │ ├── public │ ├── content │ │ └── logo.svg │ └── prefs.xhtml │ ├── src │ ├── debounced.ts │ ├── main.ts │ ├── style.css │ ├── update-annots.ts │ └── zotero.d.ts │ ├── tsconfig.json │ └── update.json ├── commitlint-config-conventional.js ├── commitlint.config.js ├── common ├── autoinstallers │ └── rush-commitlint │ │ ├── package.json │ │ └── pnpm-lock.yaml ├── config │ ├── eslint │ │ ├── README.md │ │ ├── lint-staged.common.js │ │ ├── lint-staged.config.js │ │ ├── package.json │ │ ├── src │ │ │ ├── bases │ │ │ │ ├── graphql-schema.js │ │ │ │ ├── index.js │ │ │ │ ├── jest.js │ │ │ │ ├── prettier.js │ │ │ │ ├── react.js │ │ │ │ ├── regexp.js │ │ │ │ ├── storybook.js │ │ │ │ ├── tailwind.js │ │ │ │ └── typescript.js │ │ │ ├── helpers │ │ │ │ ├── getDefaultIgnorePatterns.js │ │ │ │ ├── getPrettierConfig.js │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ ├── patch │ │ │ │ └── modern-module-resolution.js │ │ │ └── prettier.base.config.js │ │ └── tsconfig.json │ └── rush │ │ ├── .npmrc │ │ ├── .npmrc-publish │ │ ├── .pnpmfile.cjs │ │ ├── artifactory.json │ │ ├── build-cache.json │ │ ├── command-line.json │ │ ├── common-versions.json │ │ ├── experiments.json │ │ ├── pnpm-config.json │ │ ├── pnpm-lock.yaml │ │ ├── repo-state.json │ │ ├── rush-plugins.json │ │ └── version-policies.json ├── git-hooks │ └── commit-msg ├── pnpm-patches │ └── flexsearch@0.7.31.patch └── scripts │ ├── install-run-rush-pnpm.js │ ├── install-run-rush.js │ ├── install-run-rushx.js │ └── install-run.js ├── giscus.json ├── lib ├── common │ ├── .eslintrc.cjs │ ├── package.json │ ├── src │ │ ├── binary.ts │ │ ├── block-id.ts │ │ ├── const.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── must-include.ts │ │ ├── worker.ts │ │ └── zotero-date.ts │ └── tsconfig.json ├── components │ ├── .eslintrc.cjs │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── app.css │ ├── src │ │ ├── components │ │ │ ├── annot-view │ │ │ │ ├── AnnotView │ │ │ │ │ ├── AnnotList.tsx │ │ │ │ │ ├── AttachmentSelector.tsx │ │ │ │ │ ├── CollapseButton.tsx │ │ │ │ │ ├── FollowButton.tsx │ │ │ │ │ ├── Header.tsx │ │ │ │ │ ├── RefreshButton.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Annotation │ │ │ │ │ ├── Comment.tsx │ │ │ │ │ ├── Content.tsx │ │ │ │ │ ├── Excerpt.tsx │ │ │ │ │ ├── Header.tsx │ │ │ │ │ ├── HeaderIcon.tsx │ │ │ │ │ ├── ImgExcerpt.tsx │ │ │ │ │ ├── MoreOptionsButton.tsx │ │ │ │ │ ├── PageLabel.tsx │ │ │ │ │ ├── Tags.tsx │ │ │ │ │ ├── hooks │ │ │ │ │ │ ├── useAnnotIcon.tsx │ │ │ │ │ │ ├── useAnnotRender.tsx │ │ │ │ │ │ └── useImgSrc.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── DetailsButton.tsx │ │ │ │ ├── context.tsx │ │ │ │ ├── index.ts │ │ │ │ └── store.tsx │ │ │ ├── icon │ │ │ │ ├── BtnIcon.tsx │ │ │ │ ├── Icon.tsx │ │ │ │ ├── IconButton.tsx │ │ │ │ ├── IconToggle.tsx │ │ │ │ ├── index.ts │ │ │ │ └── utils.tsx │ │ │ ├── index.tsx │ │ │ ├── note-fields │ │ │ │ ├── FieldName.tsx │ │ │ │ ├── FieldValue.tsx │ │ │ │ └── index.tsx │ │ │ ├── obsidian.ts │ │ │ ├── status │ │ │ │ ├── checkbox.tsx │ │ │ │ └── importing.tsx │ │ │ └── utils.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── mock │ │ │ ├── data.json │ │ │ ├── index.ts │ │ │ └── note-fields.tsx │ │ └── vite-env.d.ts │ ├── tailwind.config.cjs │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite-env.d.ts │ └── vite.config.ts ├── database │ ├── .eslintrc.cjs │ ├── package.json │ ├── src │ │ ├── api.ts │ │ ├── index.ts │ │ ├── item.ts │ │ ├── sql │ │ │ ├── annotations │ │ │ │ ├── base.ts │ │ │ │ ├── by-keys.ts │ │ │ │ └── by-parent.ts │ │ │ ├── attachments.ts │ │ │ ├── bibtex │ │ │ │ ├── base.ts │ │ │ │ ├── get-citekey.ts │ │ │ │ └── get-id.ts │ │ │ ├── collections │ │ │ │ ├── base.ts │ │ │ │ ├── full.ts │ │ │ │ └── part.ts │ │ │ ├── creator │ │ │ │ ├── base.ts │ │ │ │ ├── full.ts │ │ │ │ └── part.ts │ │ │ ├── item-fields │ │ │ │ ├── base.ts │ │ │ │ ├── full.ts │ │ │ │ └── part.ts │ │ │ ├── items │ │ │ │ ├── base.ts │ │ │ │ ├── full.ts │ │ │ │ └── part.ts │ │ │ ├── libraries │ │ │ │ └── full.ts │ │ │ ├── notes │ │ │ │ ├── base.ts │ │ │ │ ├── by-keys.ts │ │ │ │ └── by-parent.ts │ │ │ └── tags.ts │ │ └── utils │ │ │ ├── better-sqlite.d.ts │ │ │ ├── database.ts │ │ │ ├── getCacheImagePath.ts │ │ │ ├── index.ts │ │ │ ├── misc.ts │ │ │ ├── prepared.ts │ │ │ └── zotero-backlink.ts │ └── tsconfig.json ├── db-worker │ ├── .eslintrc.cjs │ ├── package.json │ ├── src │ │ ├── flexsearch-extra.d.ts │ │ ├── globals.ts │ │ ├── logger.ts │ │ ├── main.ts │ │ ├── modules │ │ │ ├── better-sqlite.d.ts │ │ │ ├── connections.ts │ │ │ ├── database.ts │ │ │ ├── item-builder.ts │ │ │ ├── item-fetcher.ts │ │ │ └── search-index.ts │ │ ├── utils │ │ │ ├── deferred.ts │ │ │ ├── log.ts │ │ │ └── nanoevent.ts │ │ └── web-worker.d.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── protocol │ ├── .eslintrc.cjs │ ├── package.json │ ├── src │ │ ├── bg.ts │ │ ├── index.ts │ │ ├── symbols.ts │ │ └── url.ts │ └── tsconfig.json ├── workerpool │ ├── .eslintrc.cjs │ ├── package.json │ └── src │ │ ├── base-tsconfig.json │ │ ├── common │ │ ├── interface.ts │ │ ├── tsconfig.json │ │ └── utils │ │ │ └── nanoevent.ts │ │ ├── web │ │ ├── handler.ts │ │ ├── handlers │ │ │ ├── iframe.ts │ │ │ └── web-worker.ts │ │ ├── index.ts │ │ ├── interface.ts │ │ ├── pool.ts │ │ └── tsconfig.json │ │ └── worker │ │ ├── index.ts │ │ └── tsconfig.json ├── zotero-helper │ ├── .eslintrc.cjs │ ├── README.md │ ├── package.json │ ├── public │ │ └── install.rdf.ejs │ ├── src │ │ ├── builder │ │ │ ├── build.ts │ │ │ ├── chrome.ts │ │ │ ├── global-patch.ts │ │ │ ├── manifest.ts │ │ │ ├── parse.ts │ │ │ ├── prefs.ts │ │ │ └── update.ts │ │ ├── const.js │ │ ├── index.ts │ │ ├── scripts │ │ │ ├── build.ts │ │ │ ├── dev.ts │ │ │ ├── pack.ts │ │ │ └── start.ts │ │ ├── utils.ts │ │ └── zotero │ │ │ ├── chrome.d.ts │ │ │ ├── events.ts │ │ │ ├── helper.ts │ │ │ ├── index.ts │ │ │ ├── manifest.d.ts │ │ │ ├── menu │ │ │ ├── menu-item.ts │ │ │ └── menu.ts │ │ │ ├── misc.ts │ │ │ ├── plugin-placeholder.d.ts │ │ │ ├── plugin.ts │ │ │ ├── polyfill │ │ │ ├── index.ts │ │ │ └── pref-type.ts │ │ │ ├── pref │ │ │ ├── base.ts │ │ │ ├── compat.ts │ │ │ └── index.ts │ │ │ ├── reader │ │ │ ├── event.ts │ │ │ ├── menu.ts │ │ │ └── utils.ts │ │ │ └── scaffold │ │ │ └── bootstrap.js │ ├── tsconfig.json │ └── tsconfig.zotero.json └── zotero-type │ ├── .eslintrc.cjs │ ├── package.json │ ├── scripts │ ├── db-type.mjs │ ├── field-type.mjs │ └── knex.mjs │ ├── src │ ├── db-types.ts │ ├── fields.extra.ts │ ├── fields.ts │ ├── index.ts │ ├── logger.ts │ ├── misc.ts │ ├── non-regular.ts │ └── regular.ts │ └── tsconfig.json ├── manifest-beta.json ├── manifest.json ├── obsidian-zotero.code-workspace ├── rush.json ├── utils └── esbuild-inline-worker │ ├── README.md │ ├── package.json │ ├── src │ ├── plugin.ts │ └── utils.ts │ └── tsconfig.json ├── versions.json └── zotero-def.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Don't allow people to merge changes to these generated files, because the result 2 | # may be invalid. You need to run "rush update" again. 3 | pnpm-lock.yaml merge=text 4 | shrinkwrap.yaml merge=binary 5 | npm-shrinkwrap.json merge=binary 6 | yarn.lock merge=binary 7 | 8 | # Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic 9 | # syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor 10 | # may also require a special configuration to allow comments in JSON. 11 | # 12 | # For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 13 | # 14 | *.json linguist-language=JSON-with-Comments 15 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | jobs: 6 | close-issues: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | steps: 12 | - uses: actions/stale@v5 13 | with: 14 | days-before-issue-stale: 14 15 | days-before-issue-close: 14 16 | stale-issue-label: "stale" 17 | stale-issue-message: "This issue is stale because it has been open for 14 days with no activity." 18 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." 19 | days-before-pr-stale: -1 20 | days-before-pr-close: -1 21 | repo-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/*/build 2 | 3 | app/zotero/dist 4 | app/zotero/start.yml 5 | docs/docs/reference/api 6 | 7 | dist 8 | tsconfig*.tsbuildinfo 9 | 10 | # Logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | 67 | # OS X temporary files 68 | .DS_Store 69 | 70 | # Rush temporary files 71 | common/deploy/ 72 | common/temp/ 73 | common/autoinstallers/*/.npmrc 74 | **/.rush/temp/ 75 | 76 | # Heft 77 | .heft 78 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/ophidian-lib-core"] 2 | path = lib/ophidian-lib-core 3 | url = https://github.com/aidenlx/ophidian-lib-core 4 | [submodule "docs"] 5 | path = docs 6 | url = https://github.com/aidenlx/obsidian-zotero-docs 7 | [submodule "lib/eta"] 8 | path = lib/eta 9 | url = https://github.com/aidenlx/eta 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2022-present AidenLx 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /app/obsidian/.release-it.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hooks: { 3 | // "before:init": ["npm run eslint"], 4 | "after:bump": [ 5 | "rush rebuild --verbose", 6 | "node ./scripts/zip.mjs", 7 | "git add ../../.", 8 | ], 9 | "after:release": 10 | "echo Successfully released obsidian plugin ${name} v${version} to ${repo.repository}.", 11 | }, 12 | git: { 13 | commitMessage: "chore: release obsidian plugin v${version}", 14 | tagName: "${version}", 15 | tagAnnotation: "Release Obsidian Plugin v${version}", 16 | addUntrackedFiles: true, 17 | }, 18 | plugins: { 19 | // "@release-it/conventional-changelog": { 20 | // preset: "angular", 21 | // infile: "CHANGELOG.md", 22 | // }, 23 | "./scripts/ob-bumper.mjs": { 24 | indent: 2, 25 | copyTo: "../..", 26 | }, 27 | }, 28 | npm: { 29 | publish: false, 30 | }, 31 | github: { 32 | release: true, 33 | assets: [ 34 | "build/main.js", 35 | "build/manifest.json", 36 | "build/styles.css", 37 | "build/zotlit.zip", 38 | ], 39 | proxy: process.env.HTTPS_PROXY, 40 | releaseName: "${version}", 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /app/obsidian/esbuild-postcss.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import postcss from "postcss"; 4 | import postcssConfig from "./postcss.config.mjs"; 5 | import { readFile } from "fs/promises"; 6 | 7 | /** 8 | * @returns {import("esbuild").Plugin} 9 | */ 10 | export default function PostcssPlugin() { 11 | return { 12 | name: "postcss", 13 | setup: async (build) => { 14 | build.onLoad({ filter: /\.css$/ }, async ({ path }) => { 15 | const processor = postcss(postcssConfig.plugins); 16 | const content = await readFile(path); 17 | const result = await processor.process(content, { from: path }); 18 | return { 19 | contents: result.toString(), 20 | loader: "css", 21 | }; 22 | }); 23 | }, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /app/obsidian/manifest-beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "zotlit", 3 | "name": "ZotLit", 4 | "version": "1.1.10", 5 | "minAppVersion": "1.6.7", 6 | "versions": { 7 | "better-sqlite3": "v11.9.1" 8 | }, 9 | "description": "Plugin to integrate with Zotero, create literature notes and insert citations from a Zotero library.", 10 | "author": "AidenLx", 11 | "authorUrl": "https://github.com/aidenlx", 12 | "isDesktopOnly": true 13 | } -------------------------------------------------------------------------------- /app/obsidian/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "zotlit", 3 | "name": "ZotLit", 4 | "version": "1.1.10", 5 | "minAppVersion": "1.6.7", 6 | "versions": { 7 | "better-sqlite3": "v11.9.1" 8 | }, 9 | "description": "Plugin to integrate with Zotero, create literature notes and insert citations from a Zotero library.", 10 | "author": "AidenLx", 11 | "authorUrl": "https://github.com/aidenlx", 12 | "isDesktopOnly": true 13 | } -------------------------------------------------------------------------------- /app/obsidian/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | import tailwindcss from "tailwindcss"; 2 | import autoprefixer from "autoprefixer"; 3 | import discard from "postcss-discard"; 4 | import prefixSelector from "postcss-prefix-selector"; 5 | 6 | /** @type {import("postcss").Plugin} */ 7 | const prefix = prefixSelector({ 8 | prefix: ".obzt", 9 | transform: (prefix, selector, prefixedSelector, _filePath, _rule) => { 10 | if (selector.includes(".theme-dark")) { 11 | return selector.replace(".theme-dark", `.theme-dark ${prefix}`); 12 | } else if (selector.includes(".obzt-")) { 13 | return selector; 14 | } else { 15 | return prefixedSelector; 16 | } 17 | }, 18 | }); 19 | 20 | export default { 21 | plugins: [ 22 | tailwindcss({ config: "./tailwind.config.cjs" }), 23 | autoprefixer({}), 24 | prefix, 25 | discard({ 26 | rule: ["html", "body"], 27 | }), 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /app/obsidian/scripts/ob.esbuild.mjs: -------------------------------------------------------------------------------- 1 | import { promises } from "fs"; 2 | import { dirname, join } from "path"; 3 | 4 | const { copyFile, rename, writeFile } = promises; 5 | 6 | /** 7 | * @param {{ hotreload?: boolean, beta?:boolean }} config 8 | * @returns {import("esbuild").Plugin} 9 | */ 10 | const obPlugin = (config = {}) => ({ 11 | name: "obsidian-plugin", 12 | setup: (build) => { 13 | const { hotreload = true, beta = false } = config; 14 | build.onEnd(async () => { 15 | const outDir = dirname(build.initialOptions.outfile ?? "main.js"); 16 | // fix default css output file name 17 | const { outfile } = build.initialOptions; 18 | try { 19 | await rename( 20 | outfile.replace(/\.js$/, ".css"), 21 | outfile.replace(/main\.js$/, "styles.css"), 22 | ); 23 | } catch (err) { 24 | if (err.code !== "ENOENT") throw err; 25 | } 26 | 27 | // copy manifest.json to build dir 28 | if (!beta) { 29 | await copyFile("manifest.json", join(outDir, "manifest.json")); 30 | } else { 31 | await copyFile("manifest-beta.json", join(outDir, "manifest.json")); 32 | } 33 | 34 | // create .hotreload if it doesn't exist 35 | if (hotreload) { 36 | try { 37 | await writeFile(join(outDir, ".hotreload"), "", { flag: "wx" }); 38 | } catch (err) { 39 | if (err.code !== "EEXIST") throw err; 40 | } 41 | } 42 | 43 | console.log("build finished"); 44 | }); 45 | }, 46 | }); 47 | export default obPlugin; 48 | -------------------------------------------------------------------------------- /app/obsidian/scripts/zip.mjs: -------------------------------------------------------------------------------- 1 | import { createReadStream, createWriteStream } from "fs"; 2 | import JSZip from "jszip"; 3 | import { join } from "path"; 4 | import { pipeline } from "stream/promises"; 5 | const assets = ["main.js", "styles.css", "manifest.json"]; 6 | const zip = new JSZip(); 7 | for (const filename of assets) { 8 | zip.file(filename, createReadStream(join("build", filename))); 9 | } 10 | await pipeline( 11 | zip.generateNodeStream({ type: "nodebuffer", streamFiles: true }), 12 | createWriteStream(join("build", "zotlit.zip")), 13 | ); 14 | console.log("zotlit.zip written."); 15 | -------------------------------------------------------------------------------- /app/obsidian/src/components/basic/context.tsx: -------------------------------------------------------------------------------- 1 | import type { ObsidianContextType } from "@obzt/components"; 2 | import { Component, MarkdownRenderer, setIcon } from "obsidian"; 3 | import type { RefCallback } from "react"; 4 | import { useRef } from "react"; 5 | 6 | export const context: ObsidianContextType = { 7 | sanitize: DOMPurify.sanitize.bind(DOMPurify), 8 | setIcon, 9 | renderMarkdown(content) { 10 | return ; 11 | }, 12 | }; 13 | 14 | function Markdown({ content }: { content: string }) { 15 | const componentRef = useRef(null); 16 | const ref: RefCallback = (node) => { 17 | if (node) { 18 | node.empty(); 19 | if (componentRef.current) { 20 | componentRef.current.unload(); 21 | } 22 | componentRef.current = new Component(); 23 | MarkdownRenderer.renderMarkdown(content, node, "", componentRef.current); 24 | } else { 25 | if (componentRef.current) { 26 | componentRef.current.unload(); 27 | componentRef.current = null; 28 | } 29 | } 30 | }; 31 | return
; 32 | } 33 | -------------------------------------------------------------------------------- /app/obsidian/src/components/derived-file-view/index.d.ts: -------------------------------------------------------------------------------- 1 | import { FileView } from "obsidian"; 2 | 3 | export declare class DerivedFileView extends FileView { 4 | abstract update(): void; 5 | } 6 | -------------------------------------------------------------------------------- /app/obsidian/src/components/item-details/shouldExpandNode.tsx: -------------------------------------------------------------------------------- 1 | import type { ShouldExpandNodeInitially } from "react-json-tree"; 2 | 3 | const neverExpand = new Set(["sortIndex"]), 4 | noExpandIfLarge = new Set(["creators", "tags"]), 5 | alwaysExpand = new Set(["position"]); 6 | export const shouldExpandNode: ShouldExpandNodeInitially = ( 7 | keyPath: readonly (string | number)[], 8 | data: unknown, 9 | level: number, 10 | ) => { 11 | const first = keyPath[0] as never; 12 | if ( 13 | neverExpand.has(first) || 14 | (noExpandIfLarge.has(first) && Array.isArray(data) && data.length > 6) 15 | ) 16 | return false; 17 | if ( 18 | alwaysExpand.has(first) || 19 | level < 1 || 20 | (level < 2 && Array.isArray(data) && data.length > 1) 21 | ) 22 | return true; 23 | return false; 24 | }; 25 | -------------------------------------------------------------------------------- /app/obsidian/src/components/item-suggest/editor.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Editor, 3 | EditorPosition, 4 | EditorSuggestContext, 5 | EditorSuggestTriggerInfo, 6 | } from "obsidian"; 7 | import { EditorSuggest } from "obsidian"; 8 | 9 | import type { SearchResult } from "@/services/zotero-db/database.js"; 10 | import type ZoteroPlugin from "@/zt-main.js"; 11 | import type { SuggesterBase } from "./core.js"; 12 | import { CLASS_ID, getSuggestions, renderSuggestion } from "./core.js"; 13 | 14 | export abstract class ZoteroItemEditorSuggest 15 | extends EditorSuggest 16 | implements SuggesterBase 17 | { 18 | constructor(public plugin: ZoteroPlugin) { 19 | super(plugin.app); 20 | this.suggestEl.addClass(CLASS_ID); 21 | } 22 | abstract onTrigger( 23 | cursor: EditorPosition, 24 | editor: Editor, 25 | ): EditorSuggestTriggerInfo | null; 26 | 27 | getSuggestions(context: EditorSuggestContext) { 28 | return getSuggestions(context.query, this.plugin); 29 | } 30 | renderSuggestion = renderSuggestion.bind(this); 31 | 32 | abstract selectSuggestion( 33 | suggestion: SearchResult, 34 | evt: MouseEvent | KeyboardEvent, 35 | ): void; 36 | } 37 | -------------------------------------------------------------------------------- /app/obsidian/src/components/item-suggest/index.ts: -------------------------------------------------------------------------------- 1 | export { ZoteroItemEditorSuggest } from "./editor.js"; 2 | export { ZoteroItemPopupSuggest } from "./popup.js"; 3 | -------------------------------------------------------------------------------- /app/obsidian/src/components/item-suggest/popup.ts: -------------------------------------------------------------------------------- 1 | import type { SearchResult } from "@/services/zotero-db/database.js"; 2 | import type ZoteroPlugin from "@/zt-main.js"; 3 | import { DebouncedSuggeestModal } from "../basic/modal.js"; 4 | import type { SuggesterBase } from "./core.js"; 5 | import { CLASS_ID, getSuggestions, renderSuggestion } from "./core.js"; 6 | 7 | export class ZoteroItemPopupSuggest 8 | extends DebouncedSuggeestModal 9 | implements SuggesterBase 10 | { 11 | constructor(public plugin: ZoteroPlugin) { 12 | super(plugin.app); 13 | this.modalEl.addClass(CLASS_ID); 14 | } 15 | 16 | // @ts-ignore 17 | getSuggestions(input: string) { 18 | return getSuggestions(input, this.plugin); 19 | } 20 | renderSuggestion = renderSuggestion.bind(this); 21 | 22 | onChooseSuggestion() { 23 | // handled with promise 24 | return; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/obsidian/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; 4 | 5 | const Collapsible = CollapsiblePrimitive.Root; 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 12 | -------------------------------------------------------------------------------- /app/obsidian/src/dialog.less: -------------------------------------------------------------------------------- 1 | .ophidian-dialog .dialog-text { margin-bottom: 0.75em; } 2 | 3 | // Highlight invalid input 4 | .ophidian-dialog.mod-confirmation input[type="text"] { 5 | &:invalid, &[aria-invalid='true'] { 6 | &, &:enabled:focus { 7 | border-color: var(--text-error); 8 | background-color: var(--background-modifier-error); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/obsidian/src/install-guide/guide/atom.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | import { getBinaryFullPath } from "../version"; 3 | import type { InstallGuideModal } from "."; 4 | 5 | export const modalAtom = atom(null as never); 6 | 7 | export const binaryNameAtom = atom((get) => { 8 | const { arch, platform, modules } = get(modalAtom).platform; 9 | const version = get(modalAtom).binaryVersion; 10 | return `better-sqlite3-${version}-electron-v${modules}-${platform}-${arch}.tar.gz`; 11 | }); 12 | 13 | export const binaryLinkAtom = atom( 14 | (get) => 15 | `https://github.com/aidenlx/better-sqlite3/releases/download/${ 16 | get(modalAtom).binaryVersion 17 | }/${get(binaryNameAtom)}` 18 | ); 19 | 20 | export const binaryLinkFastgitAtom = atom((get) => 21 | get(binaryLinkAtom).replace("github.com", "download.fastgit.org") 22 | ); 23 | 24 | export const binaryFullPathAtom = atom((get) => 25 | getBinaryFullPath(get(modalAtom).manifest) 26 | ); 27 | 28 | export const guideModeAtom = atom((get) => get(modalAtom).mode); 29 | 30 | export type GuideMode = "install" | "reset"; 31 | -------------------------------------------------------------------------------- /app/obsidian/src/install-guide/guide/index.tsx: -------------------------------------------------------------------------------- 1 | import { Provider } from "jotai"; 2 | import type { App, PluginManifest } from "obsidian"; 3 | import { Modal } from "obsidian"; 4 | import ReactDOM from "react-dom"; 5 | import { createInitialValues } from "@/utils/create-initial"; 6 | import type { PlatformDetails } from "../version"; 7 | import type { GuideMode } from "./atom"; 8 | import { modalAtom } from "./atom"; 9 | import { InstallGuide } from "./content"; 10 | 11 | export class InstallGuideModal extends Modal { 12 | constructor( 13 | public manifest: PluginManifest, 14 | public platform: PlatformDetails, 15 | public binaryVersion: string, 16 | public mode: GuideMode, 17 | public app: App, 18 | ) { 19 | super(app); 20 | this.titleEl.setText("Setup ZotLit"); 21 | this.modalEl.addClass("mod-zt-install-guide"); 22 | } 23 | 24 | // root = createRoot(this.contentEl); 25 | onOpen() { 26 | const init = createInitialValues(); 27 | init.set(modalAtom, this); 28 | ReactDOM.render( 29 | 30 | 31 | , 32 | this.contentEl, 33 | ); 34 | } 35 | onClose() { 36 | ReactDOM.unmountComponentAtNode(this.contentEl); 37 | } 38 | 39 | async reloadPlugin() { 40 | await this.app.plugins.disablePlugin(this.manifest.id); 41 | this.close(); 42 | await this.app.plugins.enablePlugin(this.manifest.id); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/obsidian/src/install-guide/guide/list-item.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import React from "react"; 3 | 4 | export const ListItem = ({ 5 | name, 6 | desc, 7 | button, 8 | onClick, 9 | icon, 10 | className, 11 | ...rest 12 | }: { 13 | onClick?: () => void; 14 | button?: string; 15 | name?: string | null; 16 | desc?: string; 17 | icon?: JSX.Element; 18 | } & React.HtmlHTMLAttributes) => { 19 | return ( 20 |
21 | {icon &&
{icon}
} 22 |
23 |
{name}
24 |
{desc}
25 |
26 |
27 | {button && ( 28 | 31 | )} 32 |
33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /app/obsidian/src/main.less: -------------------------------------------------------------------------------- 1 | .modal.mod-settings .vertical-tab-content.obzt { 2 | padding: var(--size-4-2); 3 | padding-right: var(--size-4-3); 4 | } 5 | 6 | .obzt { 7 | .setting-item:last-child { 8 | padding-bottom: 0; 9 | border-bottom: none; 10 | } 11 | } 12 | .workspace-leaf-content[data-type="zotero-template-preview"] pre { 13 | user-select: text; 14 | } 15 | -------------------------------------------------------------------------------- /app/obsidian/src/note-feature/annot-view/style.less: -------------------------------------------------------------------------------- 1 | .workspace-leaf-content[data-type="zotero-annotation-view"] .view-content { 2 | padding: 0; 3 | display: flex; 4 | flex-direction: column; 5 | height: 100%; 6 | 7 | .select-flashing { 8 | background-color: var(--text-highlight-bg); 9 | mix-blend-mode: var(--highlight-mix-blend-mode); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/obsidian/src/note-feature/citation-suggest/index.ts: -------------------------------------------------------------------------------- 1 | import "./style.less"; 2 | export { CitationEditorSuggest } from "./editor"; 3 | export { insertCitationTo, chooseLiterature } from "./popup"; 4 | -------------------------------------------------------------------------------- /app/obsidian/src/note-feature/citation-suggest/popup.ts: -------------------------------------------------------------------------------- 1 | import type { Editor, TFile } from "obsidian"; 2 | import { openModal } from "@/components/basic/modal"; 3 | import { ZoteroItemPopupSuggest } from "@/components/item-suggest"; 4 | import type ZoteroPlugin from "@/zt-main"; 5 | import { insertCitation, isShift } from "./basic"; 6 | 7 | const instructions = [ 8 | { command: "↑↓", purpose: "to navigate" }, 9 | { command: "↵", purpose: "to insert Markdown citation" }, 10 | { command: "shift ↵", purpose: "to insert secondary Markdown citation" }, 11 | { command: "esc", purpose: "to dismiss" }, 12 | ]; 13 | 14 | class CitationPopupSuggest extends ZoteroItemPopupSuggest { 15 | constructor(public plugin: ZoteroPlugin) { 16 | super(plugin); 17 | this.setInstructions(instructions); 18 | } 19 | } 20 | 21 | export async function insertCitationTo( 22 | editor: Editor, 23 | file: TFile | null, 24 | plugin: ZoteroPlugin, 25 | ) { 26 | const result = await chooseLiterature(plugin); 27 | if (!result) return false; 28 | const cursor = editor.getCursor(); 29 | await insertCitation( 30 | { item: result.value.item, alt: isShift(result.evt) }, 31 | { 32 | start: cursor, 33 | end: cursor, 34 | editor, 35 | file, 36 | }, 37 | plugin.templateRenderer, 38 | ); 39 | return true; 40 | } 41 | 42 | export async function chooseLiterature(plugin: ZoteroPlugin) { 43 | const result = await openModal(new CitationPopupSuggest(plugin)); 44 | return result; 45 | } 46 | -------------------------------------------------------------------------------- /app/obsidian/src/note-feature/citation-suggest/settings.ts: -------------------------------------------------------------------------------- 1 | export interface SettingsSuggester { 2 | citationEditorSuggester: boolean; 3 | showCitekeyInSuggester: boolean; 4 | } 5 | 6 | export const defaultSettingsSuggester: SettingsSuggester = { 7 | citationEditorSuggester: true, 8 | showCitekeyInSuggester: false, 9 | }; 10 | -------------------------------------------------------------------------------- /app/obsidian/src/note-feature/quick-switch/index.ts: -------------------------------------------------------------------------------- 1 | export { openOrCreateNote } from "./popup"; 2 | -------------------------------------------------------------------------------- /app/obsidian/src/note-feature/quick-switch/popup.ts: -------------------------------------------------------------------------------- 1 | import { Keymap } from "obsidian"; 2 | import { openModal } from "@/components/basic/modal"; 3 | import { ZoteroItemPopupSuggest } from "@/components/item-suggest/popup.js"; 4 | import type ZoteroPlugin from "@/zt-main.js"; 5 | 6 | const instructions = [ 7 | { command: "↑↓", purpose: "to navigate" }, 8 | { command: "↵", purpose: "to open/create literature note" }, 9 | // { command: "shift ↵", purpose: "to insert secondary Markdown citation" }, 10 | { command: "esc", purpose: "to dismiss" }, 11 | ]; 12 | 13 | class NoteQuickSwitch extends ZoteroItemPopupSuggest { 14 | constructor(public plugin: ZoteroPlugin) { 15 | super(plugin); 16 | this.setInstructions(instructions); 17 | } 18 | } 19 | 20 | export async function openOrCreateNote(plugin: ZoteroPlugin): Promise { 21 | const result = await openModal(new NoteQuickSwitch(plugin)); 22 | if (!result) return false; 23 | const { 24 | value: { item }, 25 | evt, 26 | } = result; 27 | if (await plugin.noteFeatures.openNote(item, true)) return true; 28 | const notePath = await plugin.noteFeatures.createNoteForDocItemFull(item); 29 | await plugin.app.workspace.openLinkText( 30 | notePath, 31 | "", 32 | Keymap.isModEvent(evt), 33 | { active: true }, 34 | ); 35 | return true; 36 | } 37 | -------------------------------------------------------------------------------- /app/obsidian/src/note-feature/topic-import/create-note.tsx: -------------------------------------------------------------------------------- 1 | import type { IDLibID } from "@obzt/database"; 2 | import { Notice } from "obsidian"; 3 | import log from "@/log"; 4 | import type ZoteroPlugin from "@/zt-main"; 5 | 6 | export async function createNote( 7 | ids: IDLibID[], 8 | { currTopic, plugin }: { currTopic: string; plugin: ZoteroPlugin }, 9 | ) { 10 | const items = (await plugin.databaseAPI.getItems(ids, true)).flatMap( 11 | (item, index) => { 12 | if (item === null) { 13 | log.warn("item not found", ids[index]); 14 | return []; 15 | } 16 | return [[item, index] as const]; 17 | }, 18 | ); 19 | const tags = await plugin.databaseAPI.getTags(ids); 20 | 21 | for (const [item, index] of items) { 22 | const attachments = await plugin.databaseAPI.getAttachments(...ids[index]); 23 | const extra = { 24 | docItem: item, 25 | tags, 26 | attachment: null, 27 | allAttachments: attachments, 28 | annotations: [], 29 | notes: await plugin.databaseAPI 30 | .getNotes(item.itemID, item.libraryID) 31 | .then((notes) => plugin.noteParser.normalizeNotes(notes)), 32 | }; 33 | await plugin.noteFeatures.createNoteForDocItem(item, { 34 | note: (template, ctx) => 35 | template.renderNote(extra, ctx, { tags: [currTopic] }), 36 | filename: (template, ctx) => template.renderFilename(extra, ctx), 37 | }); 38 | new Notice(`Created note for ${item.title}`, 1e3); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/obsidian/src/note-feature/topic-import/utils.ts: -------------------------------------------------------------------------------- 1 | import type { CheckedState } from "@obzt/components"; 2 | import { createStore as create } from "@obzt/components"; 3 | 4 | export const topicPrefix = "#zt-topic/", 5 | toDisplayName = (topic: string) => topic.substring(topicPrefix.length); 6 | 7 | export const selectDisabled = (v: IStore) => v.topics.length === 0; 8 | 9 | export interface IStore { 10 | topics: string[]; 11 | activeTopic: number; 12 | watching: boolean; 13 | setWatching(val: CheckedState): void; 14 | setActiveTopic(index: number): void; 15 | emptyTopics(): void; 16 | setTopics(topics: string[]): void; 17 | } 18 | 19 | export const createStore = () => 20 | create((set) => ({ 21 | topics: [], 22 | activeTopic: -1, 23 | watching: false, 24 | setWatching: (val: CheckedState) => { 25 | set((state) => 26 | state.topics 27 | ? { 28 | ...state, 29 | watching: val === true ? val : false, 30 | } 31 | : state, 32 | ); 33 | }, 34 | setActiveTopic: (index: number) => { 35 | set((state) => ({ ...state, activeTopic: index })); 36 | }, 37 | setTopics: (topics) => { 38 | set((state) => ({ 39 | ...state, 40 | topics, 41 | activeTopic: topics.indexOf(state.topics[state.activeTopic]), 42 | })); 43 | }, 44 | emptyTopics: () => 45 | set((state) => ({ ...state, topics: [], activeTopic: -1 })), 46 | })); 47 | -------------------------------------------------------------------------------- /app/obsidian/src/platform.json: -------------------------------------------------------------------------------- 1 | { 2 | "123": { 3 | "darwin": [ 4 | "arm64", 5 | "x64" 6 | ], 7 | "linux": [ 8 | "x64", 9 | "arm64" 10 | ], 11 | "win32": [ 12 | "ia32", 13 | "x64", 14 | "arm64" 15 | ] 16 | }, 17 | "125": { 18 | "darwin": [ 19 | "arm64", 20 | "x64" 21 | ], 22 | "linux": [ 23 | "x64", 24 | "arm64" 25 | ], 26 | "win32": [ 27 | "ia32", 28 | "x64", 29 | "arm64" 30 | ] 31 | }, 32 | "128": { 33 | "darwin": [ 34 | "arm64", 35 | "x64" 36 | ], 37 | "linux": [ 38 | "x64", 39 | "arm64" 40 | ], 41 | "win32": [ 42 | "ia32", 43 | "x64", 44 | "arm64" 45 | ] 46 | }, 47 | "130": { 48 | "darwin": [ 49 | "arm64", 50 | "x64" 51 | ], 52 | "linux": [ 53 | "x64", 54 | "arm64" 55 | ], 56 | "win32": [ 57 | "ia32", 58 | "x64", 59 | "arm64" 60 | ] 61 | }, 62 | "132": { 63 | "darwin": [ 64 | "arm64", 65 | "x64" 66 | ], 67 | "linux": [ 68 | "x64", 69 | "arm64" 70 | ], 71 | "win32": [ 72 | "ia32", 73 | "x64", 74 | "arm64" 75 | ] 76 | }, 77 | "133": { 78 | "darwin": [ 79 | "arm64", 80 | "x64" 81 | ], 82 | "linux": [ 83 | "x64", 84 | "arm64" 85 | ], 86 | "win32": [ 87 | "ia32", 88 | "x64", 89 | "arm64" 90 | ] 91 | } 92 | } -------------------------------------------------------------------------------- /app/obsidian/src/services/note-index/index.ts: -------------------------------------------------------------------------------- 1 | export { getItemKeyGroupID } from "@obzt/common"; 2 | 3 | export { getItemKeyOf, isLiteratureNote } from "./utils"; 4 | -------------------------------------------------------------------------------- /app/obsidian/src/services/note-index/settings.ts: -------------------------------------------------------------------------------- 1 | export interface SettingsNoteIndex { 2 | literatureNoteFolder: string; 3 | } 4 | 5 | export const defaultSettingsNoteIndex: SettingsNoteIndex = { 6 | literatureNoteFolder: "LiteratureNotes", 7 | }; 8 | -------------------------------------------------------------------------------- /app/obsidian/src/services/note-parser/format.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { scope } from "arktype"; 3 | 4 | // uri like http://zotero.org/users/6775115/items/C689D5Q2 5 | const itemURI = /^https?:\/\/zotero\.org\/.*\/items\/([A-Z\d]+)$/; 6 | export function keyFromItemURI(uri: string) { 7 | const match = uri.match(itemURI); 8 | if (!match) return null; 9 | return match[1]; 10 | } 11 | 12 | export const { DataCitation, DataAnnotation } = scope({ 13 | CitationItem: { 14 | uris: [itemURI], 15 | }, 16 | DataCitation: { 17 | citationItems: "CitationItem[]", 18 | "properties?": "any", 19 | "locator?": "string", 20 | }, 21 | DataAnnotation: { 22 | attachmentURI: itemURI, 23 | annotationKey: "string", 24 | citationItem: "CitationItem", 25 | }, 26 | }).compile(); 27 | -------------------------------------------------------------------------------- /app/obsidian/src/services/server/settings.ts: -------------------------------------------------------------------------------- 1 | export interface SettingsServer { 2 | enableServer: boolean; 3 | serverPort: number; 4 | serverHostname: string; 5 | } 6 | 7 | export const defaultSettingsServer: SettingsServer = { 8 | enableServer: false, 9 | serverPort: 9091, 10 | serverHostname: "127.0.0.1", 11 | }; 12 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/defaults/ejs.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.ejs" { 2 | const text: string; 3 | export default text; 4 | } 5 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/defaults/zt-annot.ejs: -------------------------------------------------------------------------------- 1 | [!note] Page <%= it.pageLabel %> 2 | 3 | <%= it.imgEmbed %><%= it.text %> 4 | <% if (it.comment) { %> 5 | --- 6 | <%= it.comment %> 7 | <% } %> -------------------------------------------------------------------------------- /app/obsidian/src/services/template/defaults/zt-annots.ejs: -------------------------------------------------------------------------------- 1 | <% for (const annotation of it) { %> 2 | <%~ include("annotation", annotation) %> 3 | <% } %> -------------------------------------------------------------------------------- /app/obsidian/src/services/template/defaults/zt-cite.ejs: -------------------------------------------------------------------------------- 1 | [<%= it.map(lit => `@${lit.citekey}`).join("; ") %>] -------------------------------------------------------------------------------- /app/obsidian/src/services/template/defaults/zt-cite2.ejs: -------------------------------------------------------------------------------- 1 | <%= it.map(lit => `@${lit.citekey}`).join("; ") %> -------------------------------------------------------------------------------- /app/obsidian/src/services/template/defaults/zt-colored.ejs: -------------------------------------------------------------------------------- 1 | <%= it.content %> -------------------------------------------------------------------------------- /app/obsidian/src/services/template/defaults/zt-field.ejs: -------------------------------------------------------------------------------- 1 | title: "<%= it.title %>" 2 | citekey: "<%= it.citekey %>" -------------------------------------------------------------------------------- /app/obsidian/src/services/template/defaults/zt-note.ejs: -------------------------------------------------------------------------------- 1 | # <%= it.title %> 2 | 3 | [Zotero](<%= it.backlink %>) <%= it.fileLink %> 4 | <%~ include("annots", it.annotations) %> -------------------------------------------------------------------------------- /app/obsidian/src/services/template/editor/bracket.ts: -------------------------------------------------------------------------------- 1 | import type { CloseBracketConfig } from "@codemirror/autocomplete"; 2 | import { EditorState, Prec } from "@codemirror/state"; 3 | import type { Vault } from "obsidian"; 4 | import { editorInfoField } from "obsidian"; 5 | import { isEtaFile } from "../utils"; 6 | 7 | export const bracketExtension = (vault: Vault) => 8 | Prec.highest( 9 | EditorState.languageData.of((state) => { 10 | const brackets = []; 11 | // default behavior 12 | const pb = vault.getConfig("autoPairBrackets"), 13 | pm = vault.getConfig("autoPairMarkdown"); 14 | pb && brackets.push("(", "[", "{", "'", '"'); 15 | pm && brackets.push("*", "_", "`", "```"); 16 | // custom match '<' & '%' on eta files 17 | const fileinfo = state.field(editorInfoField); 18 | if (fileinfo?.file && isEtaFile(fileinfo?.file)) { 19 | brackets.push("<", "%"); 20 | } 21 | const closeBrackets: CloseBracketConfig = { 22 | brackets, 23 | }; 24 | return [{ closeBrackets }]; 25 | }), 26 | ); 27 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/editor/service.ts: -------------------------------------------------------------------------------- 1 | import type { Extension } from "@codemirror/state"; 2 | import { Service, calc, effect } from "@ophidian/core"; 3 | import { SettingsService, skip } from "@/settings/base"; 4 | import ZoteroPlugin from "@/zt-main"; 5 | import { bracketExtension } from "./bracket"; 6 | import { EtaSuggest } from "./suggester"; 7 | 8 | export class TemplateEditorHelper extends Service { 9 | /** null if not registered */ 10 | #editorExtensions: Extension[] | null = null; 11 | 12 | plugin = this.use(ZoteroPlugin); 13 | 14 | settings = this.use(SettingsService); 15 | 16 | #registerEtaEditorHelper() { 17 | this.plugin.registerEditorSuggest(new EtaSuggest(this.plugin.app)); 18 | } 19 | 20 | @calc 21 | get etaBracketPairing() { 22 | return this.settings.current?.autoPairEta; 23 | } 24 | #setEtaBracketPairing(enable: boolean) { 25 | const loadedBefore = this.#editorExtensions !== null; 26 | if (this.#editorExtensions === null) { 27 | this.#editorExtensions = []; 28 | this.plugin.registerEditorExtension(this.#editorExtensions); 29 | } else { 30 | this.#editorExtensions.length = 0; 31 | } 32 | if (enable) { 33 | this.#editorExtensions.push(bracketExtension(this.plugin.app.vault)); 34 | } 35 | if (loadedBefore) { 36 | this.plugin.app.workspace.updateOptions(); 37 | } 38 | } 39 | 40 | onload(): void { 41 | this.#registerEtaEditorHelper(); 42 | this.register( 43 | effect( 44 | skip( 45 | () => this.#setEtaBracketPairing(this.etaBracketPairing), 46 | () => this.etaBracketPairing, 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/frontmatter.ts: -------------------------------------------------------------------------------- 1 | export const ZOTERO_KEY_FIELDNAME = "zotero-key"; 2 | export const ZOTERO_ATCHS_FIELDNAME = "zt-attachments"; 3 | export const ZOTERO_ATCH_KEYS_FIELDNAME = "zotero-atchs"; 4 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/helper/attachment.ts: -------------------------------------------------------------------------------- 1 | import type { AttachmentInfo } from "@obzt/database"; 2 | import { getAttachmentPath } from "../utils"; 3 | import type { Context } from "./base"; 4 | import { isProxied, Proxied, zoteroDataDir } from "./base"; 5 | 6 | export type AttachmentHelper = Readonly< 7 | AttachmentInfo & { 8 | fullname: string; 9 | } 10 | >; 11 | 12 | export function withAttachmentHelper( 13 | data: T, 14 | ctx: Context, 15 | ) { 16 | if (!data || isProxied(data)) return data; 17 | 18 | return new Proxy( 19 | { 20 | get filePath(): string { 21 | return getAttachmentPath(zoteroDataDir(ctx), data); 22 | }, 23 | }, 24 | { 25 | get(target, p, receiver) { 26 | if (p === Proxied) return true; 27 | return ( 28 | Reflect.get(data, p, receiver) ?? Reflect.get(target, p, receiver) 29 | ); 30 | }, 31 | ownKeys(target) { 32 | return [...Reflect.ownKeys(data), ...Reflect.ownKeys(target)]; 33 | }, 34 | getOwnPropertyDescriptor(target, prop) { 35 | if (Object.prototype.hasOwnProperty.call(data, prop)) { 36 | return Reflect.getOwnPropertyDescriptor(data, prop); 37 | } 38 | return Reflect.getOwnPropertyDescriptor(target, prop); 39 | }, 40 | }, 41 | ) as unknown as AttachmentHelper; 42 | } 43 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/helper/base.ts: -------------------------------------------------------------------------------- 1 | import type ZoteroPlugin from "@/zt-main"; 2 | 3 | export interface Context { 4 | sourcePath?: string | null; 5 | plugin: ZoteroPlugin; 6 | /** enable merging cross-page annotation */ 7 | merge?: boolean; 8 | } 9 | 10 | export const zoteroDataDir = (ctx: Context) => 11 | ctx.plugin.settings.current?.zoteroDataDir; 12 | 13 | // eslint-disable-next-line @typescript-eslint/naming-convention 14 | export const Proxied = Symbol("proxied"); 15 | export const isProxied = (obj: any): boolean => !!(obj as any)[Proxied]; 16 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/helper/collection.ts: -------------------------------------------------------------------------------- 1 | import type { Collection } from "@obzt/database"; 2 | 3 | export type CollectionHelper = Readonly; 4 | 5 | class CollectionPath extends Array { 6 | toString(): string { 7 | return this.join(" > "); 8 | } 9 | } 10 | 11 | export const withCollectionHelper = ({ path: _path, ...data }: Collection) => { 12 | const proxy = { 13 | path: CollectionPath.from(_path), 14 | toString() { 15 | return data.name; 16 | }, 17 | }; 18 | return new Proxy(proxy, { 19 | get(target, p, receiver) { 20 | // proxy properties should override properties of data 21 | return Reflect.get(target, p, receiver) ?? Reflect.get(data, p, receiver); 22 | }, 23 | ownKeys(target) { 24 | return [ 25 | ...Reflect.ownKeys(data), 26 | ...Reflect.ownKeys(target).filter( 27 | (v) => !(v === "toJSON" || v === "toString"), 28 | ), 29 | ]; 30 | }, 31 | getOwnPropertyDescriptor(target, prop) { 32 | if (Object.prototype.hasOwnProperty.call(data, prop)) { 33 | return Reflect.getOwnPropertyDescriptor(data, prop); 34 | } 35 | return Reflect.getOwnPropertyDescriptor(target, prop); 36 | }, 37 | }) as unknown as CollectionHelper; 38 | }; 39 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/helper/colors.json: -------------------------------------------------------------------------------- 1 | { 2 | "#FF6666": "red", 3 | "#FF8C19": "orange", 4 | "#F19837": "orange", 5 | "#FFD400": "yellow", 6 | "#999999": "gray", 7 | "#AAAAAA": "gray", 8 | "#5FB236": "green", 9 | "#009980": "cyan", 10 | "#2EA8E5": "blue", 11 | "#576DD9": "navy", 12 | "#A28AE5": "purple", 13 | "#A6507B": "brown", 14 | "#E56EEE": "magenta" 15 | } 16 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/helper/creator.ts: -------------------------------------------------------------------------------- 1 | import type { ItemCreator } from "@obzt/database"; 2 | import { getCreatorName } from "@obzt/database"; 3 | 4 | type Creator = Omit; 5 | 6 | export type CreatorHelper = Readonly< 7 | Creator & { 8 | fullname: string; 9 | } 10 | >; 11 | 12 | export const withCreatorHelper = (data: Creator) => { 13 | const proxy = { 14 | get fullname(): string { 15 | return getCreatorName(data) ?? ""; 16 | }, 17 | toString() { 18 | return this.fullname; 19 | }, 20 | toJSON() { 21 | return this.fullname; 22 | }, 23 | }; 24 | return new Proxy(proxy, { 25 | get(target, p, receiver) { 26 | // proxy properties should override properties of data 27 | return Reflect.get(target, p, receiver) ?? Reflect.get(data, p, receiver); 28 | }, 29 | ownKeys(target) { 30 | return [ 31 | ...Reflect.ownKeys(data), 32 | ...Reflect.ownKeys(target).filter( 33 | (v) => !(v === "toJSON" || v === "toString"), 34 | ), 35 | ]; 36 | }, 37 | getOwnPropertyDescriptor(target, prop) { 38 | if (Object.prototype.hasOwnProperty.call(data, prop)) { 39 | return Reflect.getOwnPropertyDescriptor(data, prop); 40 | } 41 | return Reflect.getOwnPropertyDescriptor(target, prop); 42 | }, 43 | }) as unknown as CreatorHelper; 44 | }; 45 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/helper/index.ts: -------------------------------------------------------------------------------- 1 | export type { AnnotationExtra, AnnotHelper } from "./annot"; 2 | export { withAnnotHelper } from "./annot"; 3 | export type { CreatorHelper } from "./creator"; 4 | export { withCreatorHelper } from "./creator"; 5 | export type { CollectionHelper } from "./collection"; 6 | export { withCollectionHelper } from "./collection"; 7 | export type { RegularItemInfoExtra, DocItemHelper } from "./item"; 8 | export { withDocItemHelper } from "./item"; 9 | export type { Context } from "./base"; 10 | export type { HelperExtra } from "./to-helper"; 11 | export { toHelper } from "./to-helper"; 12 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/helper/tag.ts: -------------------------------------------------------------------------------- 1 | import type { TagInfo } from "@obzt/database"; 2 | // import type { Context } from "./base"; 3 | import { Proxied, isProxied } from "./base"; 4 | 5 | export type TagHelper = Readonly; 6 | 7 | export function withTagHelper( 8 | data: T, 9 | // ctx: Context, 10 | ) { 11 | if (!data || isProxied(data)) return data; 12 | return new Proxy( 13 | { 14 | toString() { 15 | return data.name; 16 | }, 17 | toJSON() { 18 | return data.name; 19 | }, 20 | }, 21 | { 22 | get(target, p, receiver) { 23 | if (p === Proxied) return true; 24 | return ( 25 | Reflect.get(target, p, receiver) ?? Reflect.get(data, p, receiver) 26 | ); 27 | }, 28 | ownKeys(target) { 29 | return [ 30 | ...Reflect.ownKeys(data), 31 | ...Reflect.ownKeys(target).filter( 32 | (v) => !(v === "toJSON" || v === "toString"), 33 | ), 34 | ]; 35 | }, 36 | getOwnPropertyDescriptor(target, prop) { 37 | if (Object.prototype.hasOwnProperty.call(data, prop)) { 38 | return Reflect.getOwnPropertyDescriptor(data, prop); 39 | } 40 | return Reflect.getOwnPropertyDescriptor(target, prop); 41 | }, 42 | }, 43 | ) as unknown as TagHelper; 44 | } 45 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/index.ts: -------------------------------------------------------------------------------- 1 | export { Template as TemplateRenderer } from "./render"; 2 | export { TemplateEditorHelper } from "./editor/service"; 3 | export { toHelper } from "./helper"; 4 | export { ZOTERO_KEY_FIELDNAME } from "./frontmatter"; 5 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/object.group.d.ts: -------------------------------------------------------------------------------- 1 | type ObjectGroupByFunction = { 2 | ( 3 | obj: T, 4 | predicate: (value: T, index: number, array: T[]) => string, 5 | ): Record; 6 | }; 7 | type MapGroupByFunction = { 8 | (obj: T, predicate: (value: T, index: number, array: T[]) => string): Map< 9 | string, 10 | T[] 11 | >; 12 | }; 13 | declare module "core-js-pure/full/object/group-by" { 14 | const shim: ObjectGroupByFunction; 15 | export default shim; 16 | } 17 | 18 | declare module "core-js-pure/full/map/group-by" { 19 | const shim: MapGroupByFunction; 20 | export default shim; 21 | } 22 | -------------------------------------------------------------------------------- /app/obsidian/src/services/template/settings.ts: -------------------------------------------------------------------------------- 1 | import type { trimConfig } from "eta-prf"; 2 | import { Template, type TplType } from "./eta/preset"; 3 | 4 | export interface SettingsTemplate { 5 | template: { folder: string; templates: Record }; 6 | updateAnnotBlock: boolean; 7 | updateOverwrite: boolean; 8 | autoPairEta: boolean; 9 | autoTrim: [trimConfig, trimConfig]; 10 | } 11 | 12 | export const defaultSettingsTemplate: SettingsTemplate = { 13 | template: { folder: "ZtTemplates", templates: Template.Embeded }, 14 | updateAnnotBlock: false, 15 | updateOverwrite: false, 16 | autoPairEta: false, 17 | autoTrim: [false, false], 18 | }; 19 | -------------------------------------------------------------------------------- /app/obsidian/src/services/zotero-db/api.ts: -------------------------------------------------------------------------------- 1 | export type { DbWorkerAPIWorkpool as DbWorkerAPI } from "@obzt/database/api"; 2 | -------------------------------------------------------------------------------- /app/obsidian/src/services/zotero-db/auto-refresh/settings.ts: -------------------------------------------------------------------------------- 1 | export interface SettingsWatcher { 2 | autoRefresh: boolean; 3 | } 4 | 5 | export const defaultSettingsWatcher: SettingsWatcher = { 6 | autoRefresh: true, 7 | }; 8 | -------------------------------------------------------------------------------- /app/obsidian/src/services/zotero-db/connector/service2.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { Server } from "http"; 3 | import { Service, calc } from "@ophidian/core"; 4 | import { App } from "obsidian"; 5 | import { SettingsService } from "@/settings/base"; 6 | import ZoteroPlugin from "@/zt-main"; 7 | import { DatabaseWorkerPool } from "./worker"; 8 | 9 | export const enum DatabaseStatus { 10 | NotInitialized, 11 | Pending, 12 | Ready, 13 | } 14 | 15 | export default class Database extends Service { 16 | settings = this.use(SettingsService); 17 | app = this.use(App); 18 | plugin = this.use(ZoteroPlugin); 19 | server = this.use(Server); 20 | #instance = new DatabaseWorkerPool({ 21 | minWorkers: 1, 22 | maxWorkers: 1, 23 | }); 24 | get api() { 25 | return this.#instance.proxy; 26 | } 27 | 28 | @calc get zoteroDataDir(): string { 29 | return this.settings.current?.zoteroDataDir; 30 | } 31 | 32 | #status = DatabaseStatus.NotInitialized; 33 | } 34 | -------------------------------------------------------------------------------- /app/obsidian/src/services/zotero-db/connector/settings.ts: -------------------------------------------------------------------------------- 1 | import { homedir } from "os"; 2 | import { join } from "path"; 3 | 4 | export interface SettingsDatabase { 5 | zoteroDataDir: string; 6 | citationLibrary: number; 7 | } 8 | 9 | export const getDefaultSettingsDatabase = (): SettingsDatabase => ({ 10 | zoteroDataDir: join(homedir(), "Zotero"), 11 | citationLibrary: 1, 12 | }); 13 | -------------------------------------------------------------------------------- /app/obsidian/src/services/zotero-db/connector/worker.ts: -------------------------------------------------------------------------------- 1 | import { fromScriptText } from "@aidenlx/esbuild-plugin-inline-worker/utils"; 2 | import { WebWorkerHandler, WorkerPool } from "@aidenlx/workerpool"; 3 | import dbWorker from "worker:@obzt/db-worker"; 4 | import type { DbWorkerAPI } from "../api"; 5 | 6 | class DatabaseWorker extends WebWorkerHandler { 7 | initWebWorker(): Worker { 8 | return fromScriptText(dbWorker, { 9 | name: "zotlit database worker", 10 | }); 11 | } 12 | } 13 | export class DatabaseWorkerPool extends WorkerPool { 14 | workerCtor() { 15 | return new DatabaseWorker(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/obsidian/src/services/zotero-db/img-import/settings.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from "obsidian"; 2 | 3 | export interface SettingsImgImporter { 4 | imgExcerptImport: false | "symlink" | "copy"; 5 | imgExcerptPath: string; 6 | } 7 | 8 | export const defaultSettingsImgImporter: SettingsImgImporter = { 9 | imgExcerptImport: Platform.isWin ? "copy" : "symlink", 10 | imgExcerptPath: "ZtImgExcerpt", 11 | }; 12 | -------------------------------------------------------------------------------- /app/obsidian/src/services/zotero-db/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DatabaseWatcher } from "./auto-refresh/service"; 2 | export { default as DatabaseWorker } from "./connector/service"; 3 | export { ZoteroDatabase } from "./database"; 4 | export { ImgCacheImporter } from "./img-import/service"; 5 | -------------------------------------------------------------------------------- /app/obsidian/src/setting-tab/components/useExtraButton.tsx: -------------------------------------------------------------------------------- 1 | import { useMemoizedFn } from "ahooks"; 2 | import { ExtraButtonComponent } from "obsidian"; 3 | import type { RefCallback } from "react"; 4 | import { useCallback, useEffect, useRef } from "react"; 5 | 6 | export default function useExtraButton( 7 | onClick: () => void, 8 | { 9 | icon, 10 | desc, 11 | disable, 12 | }: Partial<{ icon: string; desc: string; disable: boolean }>, 13 | ) { 14 | const onClickImmu = useMemoizedFn(onClick); 15 | const compRef = useRef(null); 16 | useEffect(() => { 17 | compRef.current?.setIcon(icon ?? ""); 18 | }, [icon]); 19 | useEffect(() => { 20 | compRef.current?.setTooltip(desc ?? ""); 21 | }, [desc]); 22 | useEffect(() => { 23 | compRef.current?.setDisabled(disable ?? false); 24 | }, [disable]); 25 | return useCallback>( 26 | (node) => { 27 | if (!node) { 28 | compRef.current?.extraSettingsEl.remove(); 29 | compRef.current = null; 30 | } else { 31 | const comp = new ExtraButtonComponent(node); 32 | comp.onClick(onClickImmu); 33 | compRef.current = comp; 34 | } 35 | }, 36 | // eslint-disable-next-line react-hooks/exhaustive-deps 37 | [], 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/obsidian/src/setting-tab/connect/DatabasePath.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@obzt/components/utils"; 2 | import type { PropsWithChildren } from "react"; 3 | import type { DatabaseStatus } from "./useDatabaseStatus"; 4 | 5 | export function DatabasePathWithTitle({ 6 | children: name, 7 | path, 8 | state, 9 | }: PropsWithChildren<{ 10 | path: string; 11 | state: DatabaseStatus; 12 | }>) { 13 | return ( 14 |
15 | {name}: {state === "failed" && "(Failed to load)"} 16 | 17 |
18 | ); 19 | } 20 | export function DatabasePath({ 21 | path, 22 | state, 23 | }: { 24 | path: string; 25 | state: DatabaseStatus; 26 | }) { 27 | return ( 28 | 36 | {path} 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/obsidian/src/setting-tab/connect/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { SettingTabCtx } from "../common"; 3 | import BooleanSetting from "../components/Boolean"; 4 | import { BackgroundConnectSetting } from "./Background"; 5 | import DatabaseSetting from "./Database"; 6 | 7 | export default function Connect() { 8 | return ( 9 | <> 10 | 11 | s.autoRefresh} 14 | set={(v, s) => ({ ...s, autoRefresh: v })} 15 | /> 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/obsidian/src/setting-tab/connect/useDatabaseStatus.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { SettingTabCtx, useRefreshAsync } from "../common"; 3 | 4 | export function useDatabaseStatus(target: "zotero" | "bbt") { 5 | const { database } = useContext(SettingTabCtx); 6 | const [promise, refresh] = useRefreshAsync( 7 | () => 8 | database.api 9 | .getLoadStatus() 10 | .then((s) => (target === "zotero" ? s.main : s.bbt)), 11 | [target], 12 | ); 13 | 14 | let state: DatabaseStatus; 15 | if (promise.loading) { 16 | state = "disabled"; 17 | } else if (promise.error) { 18 | state = "failed"; 19 | } else { 20 | state = promise.result ? "success" : "failed"; 21 | } 22 | return [state, refresh] as const; 23 | } 24 | export type DatabaseStatus = "success" | "failed" | "disabled"; 25 | -------------------------------------------------------------------------------- /app/obsidian/src/setting-tab/general/LibSelect.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { useIconRef } from "@/utils/icon"; 3 | import { SettingTabCtx, useRefreshAsync } from "../common"; 4 | import Setting, { useSetting } from "../components/Setting"; 5 | 6 | export default function CitationLibrarySelect() { 7 | const { database } = useContext(SettingTabCtx); 8 | 9 | const [value, setValue] = useSetting( 10 | (s) => s.citationLibrary, 11 | (v, prev) => ({ ...prev, citationLibrary: v }), 12 | ); 13 | 14 | const [data, refresh] = useRefreshAsync(() => database.api.getLibs(), []); 15 | 16 | const libs = data.result ?? [ 17 | { groupID: null, libraryID: 1, name: "My Library" }, 18 | ]; 19 | 20 | const [refreshIconRef] = useIconRef("switch"); 21 | return ( 22 | 23 | 38 |