├── .env.example ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── feature_request.yml │ └── plugin_submit.yml ├── dependabot.yml ├── renovate.json └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .prettierignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── toolkit.code-snippets ├── LICENSE ├── README.md ├── addon ├── _locales │ ├── en_US │ │ └── messages.json │ ├── nl_NL │ │ └── messages.json │ └── zh_CN │ │ └── messages.json ├── bootstrap.js ├── content │ ├── ArtalkLite.css │ ├── ArtalkLite.js │ ├── InitArtalk.js │ ├── addonDetail.xhtml │ ├── addons.xhtml │ └── icons │ │ └── favicon.svg ├── locale │ ├── en-US │ │ ├── addon.ftl │ │ ├── addonDetail.ftl │ │ └── addonTable.ftl │ ├── nl-NL │ │ ├── addon.ftl │ │ ├── addonDetail.ftl │ │ └── addonTable.ftl │ └── zh-CN │ │ ├── addon.ftl │ │ ├── addonDetail.ftl │ │ └── addonTable.ftl └── manifest.json ├── doc ├── README-CN.md └── screenshot.jpg ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── src ├── addon.ts ├── hooks.ts ├── index.ts ├── modules │ ├── addonDetail.ts │ ├── addonInfo.ts │ ├── addonListenerManager.ts │ ├── addonTable.ts │ ├── crypto.ts │ ├── guide.ts │ └── registerScheme.ts └── utils │ ├── configuration.ts │ ├── locale.ts │ ├── prefs.ts │ ├── utils.ts │ ├── window.ts │ └── ztoolkit.ts ├── tsconfig.json ├── typings ├── global.d.ts ├── i10n.d.ts └── prefs.d.ts ├── update.json └── zotero-plugin.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | # Usage: 2 | # Copy this file as `.env` and fill in the variables below as instructed. 3 | 4 | # If you are developing more than one plugin, you can store the bin path and 5 | # profile path in the system environment variables, which can be omitted here. 6 | 7 | # The path of the Zotero binary file. 8 | # The path delimiter should be escaped as `\\` for win32. 9 | # The path is `*/Zotero.app/Contents/MacOS/zotero` for MacOS. 10 | ZOTERO_PLUGIN_ZOTERO_BIN_PATH = /path/to/zotero.exe 11 | 12 | # The path of the profile used for development. 13 | # Start the profile manager by `/path/to/zotero.exe -p` to create a profile for development. 14 | # @see https://www.zotero.org/support/kb/profile_directory 15 | ZOTERO_PLUGIN_PROFILE_PATH = /path/to/profile 16 | 17 | # The directory where the database is located. 18 | # If this field is kept empty, Zotero will start with the default data. 19 | # @see https://www.zotero.org/support/zotero_data 20 | ZOTERO_PLUGIN_DATA_DIR = 21 | 22 | # Custom commands to kill Zotero processes. 23 | # Commands for different platforms are already built into zotero-plugin, 24 | # if the built-in commands are not suitable for your needs, please modify this variable. 25 | # ZOTERO_PLUGIN_KILL_COMMAND = 26 | 27 | # GitHub Token 28 | # For scaffold auto create release and upload assets. 29 | # Fill in this variable if you are publishing locally instead of CI. 30 | # GITHUB_TOKEN = -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug / issue 3 | title: "[Bug] " 4 | labels: 5 | - bug 6 | assignees: syt2 7 | body: 8 | - type: checkboxes 9 | id: check-search 10 | attributes: 11 | label: Is there an existing issue for this? 12 | description: Please search to see if an issue already exists for the bug you encountered. 13 | options: 14 | - label: I have searched the existing issues 15 | required: true 16 | 17 | - type: checkboxes 18 | id: check-version 19 | attributes: 20 | label: Are you using the latest Zotero and the latest plugin? 21 | description: Only bug reports that can be reproduced on the latest Zotero and plugin will be considered. 22 | options: 23 | - label: I have confirmed I'm using the latest Zotero and the latest plugin 24 | required: true 25 | 26 | - type: textarea 27 | attributes: 28 | label: Environment 29 | description: | 30 | examples: 31 | - **OS**: Windows/macOS/Linux 32 | - **Zotero Version**: 7.0.0(-beta.xx) 33 | - **Plugin Version**: 1.0.0 34 | value: | 35 | - OS: 36 | - Zotero Version: 37 | - Plugin Version: 38 | validations: 39 | required: true 40 | 41 | - type: textarea 42 | id: description 43 | attributes: 44 | label: Describe the bug 45 | description: | 46 | A clear and concise description of what the bug is. 47 | If applicable, add screenshots to help explain your problem. 48 | validations: 49 | required: true 50 | 51 | - type: textarea 52 | id: debug-output 53 | attributes: 54 | label: Debug Output 55 | description: | 56 | Steps to get debug output: 57 | 1. Disable all other plugins, exit Zotero, and restart Zotero 58 | 2. menu -> `Help` -> `Debug Output` -> `View Output` 59 | 3. Do steps to reproduce the bug 60 | 4. In the debug output window, press `Ctrl/Cmd + S` 61 | 5. Upload the debug output here 62 | validations: 63 | required: false 64 | 65 | - type: textarea 66 | id: additional-context 67 | attributes: 68 | label: Anything else? 69 | description: | 70 | Links? References? Anything that will give us more context about the issue you are encountering! 71 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 72 | validations: 73 | required: false 74 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: "[Feature] " 4 | labels: 5 | - enhancement 6 | assignees: syt2 7 | body: 8 | - type: checkboxes 9 | id: check-search 10 | attributes: 11 | label: Is there an existing issue for this? 12 | description: Please search to see if an issue already exists for this feature request. 13 | options: 14 | - label: I have searched the existing issues 15 | required: true 16 | 17 | - type: textarea 18 | id: description 19 | attributes: 20 | label: Describe the feature request 21 | value: | 22 | **Is your feature request related to a problem? Please describe.** 23 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 24 | 25 | **Why do you need this feature?** 26 | A clear and concise description of why you need this feature. 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | id: solution 32 | attributes: 33 | label: Describe the solution you'd like 34 | value: | 35 | **The solution you'd like** 36 | A clear and concise description of what you want to happen. 37 | 38 | **Alternatives you've considered** 39 | A clear and concise description of any alternative solutions or features you've considered. 40 | validations: 41 | required: false 42 | 43 | - type: textarea 44 | id: additional-context 45 | attributes: 46 | label: Anything else? 47 | description: | 48 | Links? References? Anything that will give us more context about the issue you are encountering! 49 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 50 | validations: 51 | required: false 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/plugin_submit.yml: -------------------------------------------------------------------------------- 1 | name: Plugin submit 2 | description: Submit a new plugin for market 3 | title: "[Plugin] " 4 | labels: 5 | - new plugin 6 | assignees: syt2 7 | body: 8 | - type: checkboxes 9 | id: check-search 10 | attributes: 11 | label: Is the plugin you submit already in the repository? 12 | description: Please search to see if the plugin you want to submit is already in the repository. in [zotero-chinese/zotero-plugins](https://github.com/zotero-chinese/zotero-plugins) and [syt2/zotero-addons-scraper](https://github.com/syt2/zotero-addons-scraper) 13 | options: 14 | - label: I have searched the existing plugins 15 | required: true 16 | 17 | - type: input 18 | id: repository-url 19 | attributes: 20 | label: Zotero Plugin Repository URL 21 | description: Please provide the URL of the Zotero plugin repository. 22 | placeholder: https://github.com/username/zotero-plugin-repository 23 | validations: 24 | required: true -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | groups: 13 | all-non-major: 14 | update-types: 15 | - "minor" 16 | - "patch" 17 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":semanticPrefixChore", 6 | ":prHourlyLimitNone", 7 | ":prConcurrentLimitNone", 8 | ":enableVulnerabilityAlerts", 9 | ":dependencyDashboard", 10 | "group:allNonMajor", 11 | "schedule:weekly" 12 | ], 13 | "labels": ["dependencies"], 14 | "packageRules": [ 15 | { 16 | "matchPackageNames": [ 17 | "zotero-plugin-toolkit", 18 | "zotero-types", 19 | "zotero-plugin-scaffold" 20 | ], 21 | "schedule": ["at any time"], 22 | "automerge": true 23 | } 24 | ], 25 | "git-submodules": { 26 | "enabled": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GitHub_TOKEN }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 20 26 | cache: npm 27 | 28 | - name: Install deps 29 | run: | 30 | npm install 31 | 32 | # - name: Run Lint 33 | # run: | 34 | # npm run lint:check 35 | 36 | build: 37 | runs-on: ubuntu-latest 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GitHub_TOKEN }} 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | with: 44 | fetch-depth: 0 45 | 46 | - name: Setup Node.js 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version: 20 50 | cache: npm 51 | 52 | - name: Install deps 53 | run: | 54 | npm install 55 | 56 | - name: Run Build 57 | run: | 58 | npm run build 59 | 60 | - name: Upload build result 61 | uses: actions/upload-artifact@v4 62 | with: 63 | name: build-result 64 | path: | 65 | build 66 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - V** 7 | 8 | permissions: 9 | contents: write 10 | issues: write 11 | pull-requests: write 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GitHub_TOKEN }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 20 28 | 29 | - name: Install deps 30 | run: npm install 31 | 32 | - name: Build 33 | run: | 34 | npm run build 35 | 36 | - name: Release to GitHub 37 | run: | 38 | npm run release 39 | sleep 1s 40 | 41 | # - name: Notify release 42 | # uses: apexskier/github-release-commenter@v1 43 | # continue-on-error: true 44 | # with: 45 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | # comment-template: | 47 | # :rocket: _This ticket has been resolved in {release_tag}. See {release_link} for release notes._ 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dot files 2 | .DS_Store 3 | 4 | # Node.js 5 | node_modules 6 | builds 7 | pnpm-lock.yaml 8 | yarn.lock 9 | 10 | # TSC 11 | tsconfig.tsbuildinfo 12 | 13 | # Scaffold 14 | .env 15 | .scaffold 16 | build 17 | logs -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build 3 | logs 4 | node_modules 5 | package-lock.json 6 | yarn.lock 7 | pnpm-lock.yaml 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "macabeus.vscode-fluent" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Start", 11 | "runtimeExecutable": "npm", 12 | "runtimeArgs": ["run", "start"] 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Build", 18 | "runtimeExecutable": "npm", 19 | "runtimeArgs": ["run", "build"] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnType": false, 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit" 6 | }, 7 | "typescript.tsdk": "node_modules/typescript/lib" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/toolkit.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "appendElement - full": { 3 | "scope": "javascript,typescript", 4 | "prefix": "appendElement", 5 | "body": [ 6 | "appendElement({", 7 | "\ttag: '${1:div}',", 8 | "\tid: '${2:id}',", 9 | "\tnamespace: '${3:html}',", 10 | "\tclassList: ['${4:class}'],", 11 | "\tstyles: {${5:style}: '$6'},", 12 | "\tproperties: {},", 13 | "\tattributes: {},", 14 | "\t[{ '${7:onload}', (e: Event) => $8, ${9:false} }],", 15 | "\tcheckExistanceParent: ${10:HTMLElement},", 16 | "\tignoreIfExists: ${11:true},", 17 | "\tskipIfExists: ${12:true},", 18 | "\tremoveIfExists: ${13:true},", 19 | "\tcustomCheck: (doc: Document, options: ElementOptions) => ${14:true},", 20 | "\tchildren: [$15]", 21 | "}, ${16:container});" 22 | ] 23 | }, 24 | "appendElement - minimum": { 25 | "scope": "javascript,typescript", 26 | "prefix": "appendElement", 27 | "body": "appendElement({ tag: '$1' }, $2);" 28 | }, 29 | "register Notifier": { 30 | "scope": "javascript,typescript", 31 | "prefix": "registerObserver", 32 | "body": [ 33 | "registerObserver({", 34 | "\t notify: (", 35 | "\t\tevent: _ZoteroTypes.Notifier.Event,", 36 | "\t\ttype: _ZoteroTypes.Notifier.Type,", 37 | "\t\tids: string[],", 38 | "\t\textraData: _ZoteroTypes.anyObj", 39 | "\t) => {", 40 | "\t\t$0", 41 | "\t}", 42 | "});" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Add-on Market for Zotero 2 | 3 | [![zotero target version](https://img.shields.io/badge/Zotero-7-green?style=flat-square&logo=zotero&logoColor=CC2936)](https://www.zotero.org) 4 | [![Release](https://img.shields.io/github/v/release/syt2/zotero-addons?style=flat-square&logo=github&color=red)](https://github.com/syt2/zotero-addons/releases/latest) 5 | ![Downloads@z7](https://img.shields.io/github/downloads/syt2/zotero-addons/latest/total?style=flat-square&logo=github&label=downloads@z7) 6 | [![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template) 7 | [![Using Zotero Chinese Plugins](https://img.shields.io/badge/Using-Zotero%20Chinese%20Plugins-blue?style=flat-square&logo=github)](https://github.com/zotero-chinese/zotero-plugins) 8 | [![Using Zotero Scraper](https://img.shields.io/badge/Using-Zotero%20Addons%20Scraper-blue?style=flat-square&logo=github)](https://github.com/syt2/zotero-addons-scraper) 9 | [![Using Artalk](https://img.shields.io/badge/Using-Artalk-blue?style=flat-square&logo=github)](https://github.com/ArtalkJS/Artalk) 10 | 11 | 12 | [English](README.md) | [简体中文](doc/README-CN.md) 13 | 14 | ## Introduction 15 | 16 | This is a Zotero plugin designed for browsing, installing, and reviewing plugins within [Zotero](https://www.zotero.org). 17 | 18 | 19 | ## Install 20 | 21 | 1. Download the [latest release xpi file](https://github.com/syt2/zotero-addons/releases/latest/download/zotero-addons.xpi) 22 | 23 | 2. Install in Zotero `(Tools) -> (Add-ons)` 24 | 25 | ## Usage 26 | 27 | After install this add-on in Zotero, click in Toolbar, or click `Add-on market` in `Tools` menu. 28 | 29 | 30 | ## Add-on Data Source 31 | 32 | ### [zotero-chinese/zotero-plugins](https://github.com/zotero-chinese/zotero-plugins) 33 | 34 | The main data source for add-ons comes from **[zotero-chinese/zotero-plugins](https://github.com/zotero-chinese/zotero-plugins)**. 35 | 36 | Switch the source to `(zotero-chinese)` in Add-on Market to use this source. 37 | 38 | > If you have new add-ons to add, submit it to [zotero-chinese/zotero-plugins](https://github.com/zotero-chinese/zotero-plugins). 39 | 40 | ### [syt2/zotero-addons-scraper](https://github.com/syt2/zotero-addons-scraper) 41 | 42 | Switch the source to `(addon-scraper)` in Add-on Market to use this source. 43 | 44 | ### Custom Source 45 | 46 | You can also use other custom data sources, as long as the data source format is consistent with the format in the [zotero-chinese/zotero-plugins](https://github.com/zotero-chinese/zotero-plugins). 47 | 48 | ## Star History 49 | 50 | 51 | 52 | 53 | 54 | Star History Chart 55 | 56 | 57 | -------------------------------------------------------------------------------- /addon/_locales/en_US/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "description": "name", 4 | "message": "Add-on Market for Zotero" 5 | }, 6 | "description": { 7 | "description": "description", 8 | "message": "Browsing, installing, and reviewing plugins within Zotero" 9 | } 10 | } -------------------------------------------------------------------------------- /addon/_locales/nl_NL/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "description": "name", 4 | "message": "Add-on Markt voor Zotero" 5 | }, 6 | "description": { 7 | "description": "description", 8 | "message": "Bladeren, installeren en beoordelen van plugins binnen Zotero" 9 | } 10 | } -------------------------------------------------------------------------------- /addon/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "description": "name", 4 | "message": "Zotero 插件市场" 5 | }, 6 | "description": { 7 | "description": "description", 8 | "message": "在Zotero内浏览、安装和评论插件" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /addon/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | /** 4 | * Most of this code is from Zotero team's official Make It Red example[1] 5 | * or the Zotero 7 documentation[2]. 6 | * [1] https://github.com/zotero/make-it-red 7 | * [2] https://www.zotero.org/support/dev/zotero_7_for_developers 8 | */ 9 | 10 | var chromeHandle; 11 | 12 | function install(data, reason) {} 13 | 14 | async function startup({ id, version, resourceURI, rootURI }, reason) { 15 | await Zotero.initializationPromise; 16 | 17 | // String 'rootURI' introduced in Zotero 7 18 | if (!rootURI) { 19 | rootURI = resourceURI.spec; 20 | } 21 | 22 | var aomStartup = Components.classes[ 23 | "@mozilla.org/addons/addon-manager-startup;1" 24 | ].getService(Components.interfaces.amIAddonManagerStartup); 25 | var manifestURI = Services.io.newURI(rootURI + "manifest.json"); 26 | chromeHandle = aomStartup.registerChrome(manifestURI, [ 27 | ["content", "__addonRef__", rootURI + "content/"], 28 | ]); 29 | 30 | /** 31 | * Global variables for plugin code. 32 | * The `_globalThis` is the global root variable of the plugin sandbox environment 33 | * and all child variables assigned to it is globally accessible. 34 | * See `src/index.ts` for details. 35 | */ 36 | const ctx = { 37 | rootURI, 38 | }; 39 | ctx._globalThis = ctx; 40 | 41 | Services.scriptloader.loadSubScript( 42 | `${rootURI}/content/scripts/__addonRef__.js`, 43 | ctx, 44 | ); 45 | Zotero.__addonInstance__.hooks.onStartup(); 46 | } 47 | 48 | async function onMainWindowLoad({ window }, reason) { 49 | Zotero.__addonInstance__?.hooks.onMainWindowLoad(window); 50 | } 51 | 52 | async function onMainWindowUnload({ window }, reason) { 53 | Zotero.__addonInstance__?.hooks.onMainWindowUnload(window); 54 | } 55 | 56 | function shutdown({ id, version, resourceURI, rootURI }, reason) { 57 | if (reason === APP_SHUTDOWN) { 58 | return; 59 | } 60 | 61 | if (typeof Zotero === "undefined") { 62 | Zotero = Components.classes["@zotero.org/Zotero;1"].getService( 63 | Components.interfaces.nsISupports, 64 | ).wrappedJSObject; 65 | } 66 | Zotero.__addonInstance__?.hooks.onShutdown(); 67 | 68 | Cc["@mozilla.org/intl/stringbundle;1"] 69 | .getService(Components.interfaces.nsIStringBundleService) 70 | .flushBundles(); 71 | 72 | Cu.unload(`${rootURI}/content/scripts/__addonRef__.js`); 73 | 74 | if (chromeHandle) { 75 | chromeHandle.destruct(); 76 | chromeHandle = null; 77 | } 78 | } 79 | 80 | function uninstall(data, reason) {} 81 | -------------------------------------------------------------------------------- /addon/content/ArtalkLite.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";.artalk,.atk-layer-wrap{--at-color-font: #2a2e2e;--at-color-deep: #2a2e2e;--at-color-sub: #757575;--at-color-grey: #747474;--at-color-meta: #697182;--at-color-border: #eceff2;--at-color-light: #4285f4;--at-color-bg: #fff;--at-color-bg-transl: rgba(255, 255, 255, .94);--at-color-bg-grey: #f3f4f5;--at-color-bg-grey-transl: rgba(244, 244, 244, .75);--at-color-bg-light: rgba(29, 161, 242, .1);--at-color-main: #0083ff;--at-color-red: #ff5652;--at-color-pink: #fa5a57;--at-color-yellow: #ff7c37;--at-color-green: #4caf50;--at-color-gradient: linear-gradient(180deg, transparent, rgba(255, 255, 255))}.artalk.atk-dark-mode,.atk-layer-wrap.atk-dark-mode{--at-color-font: #fff;--at-color-deep: #e7e7e7;--at-color-sub: #e7e7e7;--at-color-grey: #fff;--at-color-meta: #fff;--at-color-border: #2d3235;--at-color-light: #687a86;--at-color-bg: #1e2224;--at-color-bg-transl: rgba(30, 34, 36, .95);--at-color-bg-grey: #46494e;--at-color-bg-grey-transl: rgba(8, 8, 8, .95);--at-color-bg-light: rgba(29, 161, 242, .1);--at-color-main: #0083ff;--at-color-red: #ff5652;--at-color-pink: #fa5a57;--at-color-yellow: #ff7c37;--at-color-green: #4caf50;--at-color-gradient: linear-gradient(180deg, transparent, rgba(30, 34, 36, 1))}.atk-comment-wrap{overflow:hidden;position:relative;border-bottom:1px solid transparent}.atk-comment-wrap.atk-flash-once{animation:atkFlashOnce 1s ease-in-out 0s}@keyframes atkFlashOnce{0%{background:#0083ff33}to{background:transparent}}.atk-comment-wrap.atk-unread:before{content:" ";position:absolute;left:0;top:10%;width:3px;height:80%;background:var(--at-color-main)}.atk-comment-wrap.atk-openable{cursor:pointer}.atk-comment-wrap.atk-openable:hover{background:var(--at-color-bg-grey)}.atk-comment-wrap.atk-openable .atk-height-limit:after{background:transparent!important}.atk-comment-wrap:last-child{border-bottom:none}.atk-comment{display:block;padding:10px}.atk-comment>.atk-avatar{display:block;padding:2px 0;float:left}.atk-comment>.atk-avatar img{width:50px;height:50px;border-radius:3px}.atk-comment>.atk-main{display:block;margin-left:63px}.atk-comment>.atk-main>.atk-header{line-height:1.5;font-size:13px;margin-bottom:.5em;overflow:hidden;position:relative;display:flex;flex-wrap:wrap;align-items:center}.atk-comment>.atk-main>.atk-header .atk-item{display:flex;align-items:center;margin-top:2px;margin-bottom:2px;color:var(--at-color-meta)}.atk-comment>.atk-main>.atk-header .atk-item:not(:last-child){margin-right:6px}.atk-comment>.atk-main>.atk-header .atk-item.atk-nick,.atk-comment>.atk-main>.atk-header .atk-item.atk-nick a{font-size:14px;color:var(--at-color-main);text-decoration:none}.atk-comment>.atk-main>.atk-header .atk-item.atk-reply-at{margin-left:2px}.atk-comment>.atk-main>.atk-header .atk-item.atk-reply-at>.atk-arrow:before{content:"";vertical-align:middle;transform:rotate(90deg);border-bottom:4px solid var(--at-color-grey);border-left:3px solid transparent;border-right:3px solid transparent;display:inline-block;margin-top:-1px}.atk-comment>.atk-main>.atk-header .atk-item.atk-reply-at>.atk-nick{color:var(--at-color-main);cursor:pointer;margin-left:6px}.atk-comment>.atk-main>.atk-header .badge,.atk-comment>.atk-main>.atk-header .atk-ua,.atk-comment>.atk-main>.atk-header .atk-pinned-badge,.atk-comment>.atk-main>.atk-header .atk-region-badge,.atk-comment>.atk-main>.atk-header .atk-badge{display:inline-block;color:var(--at-color-meta);background:var(--at-color-bg-grey);padding:0 6px;line-height:17px;border-radius:3px}.atk-comment>.atk-main>.atk-header .badge:not(:last-child),.atk-comment>.atk-main>.atk-header .atk-ua:not(:last-child),.atk-comment>.atk-main>.atk-header .atk-pinned-badge:not(:last-child),.atk-comment>.atk-main>.atk-header .atk-region-badge:not(:last-child),.atk-comment>.atk-main>.atk-header .atk-badge:not(:last-child){margin-right:6px}.atk-comment>.atk-main>.atk-header .atk-badge-wrap>*:last-child{margin-right:6px}.atk-comment>.atk-main>.atk-header .atk-badge{color:#fff}.atk-comment>.atk-main>.atk-header .atk-pinned-badge{color:#fff;background:#f44336}@media only screen and (max-width: 768px){.atk-comment>.atk-main>.atk-header .atk-ua-wrap{display:block}}.atk-comment>.atk-main>.atk-body{display:block;overflow:hidden;position:relative}.atk-comment>.atk-main>.atk-body img{max-width:100%}.atk-comment>.atk-main>.atk-body>.atk-content{word-break:break-word}.atk-comment>.atk-main>.atk-body>.atk-content.atk-type-collapsed{border:3px solid var(--at-color-bg-grey);border-bottom:0;padding:5px 10px;border-radius:6px 6px 0 0;margin-bottom:-5px}.atk-comment>.atk-main>.atk-body>.atk-content>*:first-child{margin-top:0}.atk-comment>.atk-main>.atk-body>.atk-content>*:last-child{margin-bottom:0}.atk-comment>.atk-main>.atk-body>.atk-content .atk-height-limit-btn{bottom:5px}.atk-comment>.atk-main>.atk-body>.atk-pending{color:var(--at-color-meta);margin:3px 0;font-size:13px;padding:10px 18px;display:block;background:var(--at-color-bg-grey);border-left:4px solid #f44336}.atk-comment>.atk-main>.atk-body>.atk-reply-to{padding:5px 15px;border-left:3px solid var(--at-color-border);margin-bottom:10px;position:relative;margin-top:10px}.atk-comment>.atk-main>.atk-body>.atk-reply-to .atk-meta{font-size:15px}.atk-comment>.atk-main>.atk-body>.atk-reply-to .atk-meta .atk-nick{color:var(--at-color-main)}.atk-comment>.atk-main>.atk-body>.atk-reply-to .atk-content{margin-top:5px}.atk-comment>.atk-main>.atk-body>.atk-collapsed{margin:3px 0;font-size:13px;padding:10px 18px;display:block;background:var(--at-color-bg-grey);border-radius:6px}.atk-comment>.atk-main>.atk-body>.atk-collapsed .atk-text{color:var(--at-color-meta)}.atk-comment>.atk-main>.atk-body>.atk-collapsed .atk-show-btn{color:var(--at-color-main);cursor:pointer;-webkit-user-select:none;user-select:none;margin-left:3px}.atk-comment>.atk-main>.atk-body>.atk-collapsed .atk-show-btn:hover{color:var(--at-color-main)}.atk-comment>.atk-main>.atk-footer{margin-top:5px}.atk-comment>.atk-main>.atk-footer .atk-actions{display:flex;flex-direction:row;align-items:center;flex-wrap:wrap}.atk-comment>.atk-main>.atk-footer .atk-actions>span:not(:last-child):not(.atk-hide){margin-right:16px}.atk-comment .atk-height-limit:after{position:absolute;z-index:1;display:block;overflow:hidden;width:100%;content:" ";bottom:0;height:80px;background:var(--at-color-gradient)}.atk-comment .atk-height-limit-btn{z-index:2;position:absolute;left:50%;bottom:10px;transform:translate(-50%);cursor:pointer;border:1px solid var(--at-color-border);border-radius:6px;background:var(--at-color-bg);padding:1px 20px;font-size:15px;color:var(--at-color-meta);-webkit-user-select:none;user-select:none}.atk-comment .atk-height-limit-btn:hover{background:var(--at-color-bg-grey)}.atk-comment .atk-height-limit .atk-height-limit .atk-height-limit-btn{display:none}.atk-comment .atk-height-limit-scroll{margin-top:10px;overflow-y:auto;background:linear-gradient(var(--at-color-bg) 1px,transparent 1px calc(100% - 1px)) center top,linear-gradient(transparent calc(100% - 1px),var(--at-color-bg) calc(100% - 1px) 1px) center bottom,linear-gradient(var(--at-color-border) 1px,transparent 1px calc(100% - 1px)) center top,linear-gradient(transparent calc(100% - 1px),var(--at-color-border) calc(100% - 1px) 1px) center bottom;background-repeat:no-repeat;background-color:transparent;background-size:100% 1px,100% 1px,100% 1px,100% 1px;background-attachment:local,local,scroll,scroll}.atk-comment-children>.atk-comment-wrap{margin-top:10px;border-left:1px dashed transparent;border-bottom-color:transparent}.atk-comment-children>.atk-comment-wrap:not(:last-child){margin-bottom:10px}.atk-comment-children>.atk-comment-wrap>.atk-comment{padding:0}.atk-comment-children>.atk-comment-wrap>.atk-comment>.atk-avatar img{width:36px;height:36px}.atk-comment-children>.atk-comment-wrap>.atk-comment>.atk-main{margin-left:47px}.artalk>.atk-list{position:relative}.artalk>.atk-list>.atk-list-header{display:flex;flex-direction:row;padding:10px 17px}.artalk>.atk-list>.atk-list-header .atk-text{display:inline-block}.artalk>.atk-list>.atk-list-header .atk-dropdown-wrap{position:relative;cursor:pointer}.artalk>.atk-list>.atk-list-header .atk-dropdown-wrap .atk-arrow-down-icon{cursor:pointer;vertical-align:middle;border-top:5px solid var(--at-color-grey);border-left:3px solid transparent;border-right:3px solid transparent;margin-top:-1px;margin-left:.8rem;display:inline-block}.artalk>.atk-list>.atk-list-header .atk-dropdown-wrap:hover .atk-dropdown{display:block}.artalk>.atk-list>.atk-list-header .atk-dropdown-wrap .atk-dropdown{z-index:3;display:none;height:auto!important;max-height:calc(100vh - 2.7rem);overflow-y:auto;position:absolute;top:100%;right:0;width:100%;background-color:var(--at-color-bg);padding:.6rem 0;border:1px solid var(--at-color-border);text-align:center;border-radius:6px;white-space:nowrap;margin:0;list-style:none}.artalk>.atk-list>.atk-list-header .atk-dropdown-wrap .atk-dropdown-item span,.artalk>.atk-list>.atk-list-header .atk-dropdown-wrap .atk-dropdown-item a{display:block;line-height:2rem;position:relative;border-bottom:none;font-weight:400;padding:0 1.5rem}.artalk>.atk-list>.atk-list-header .atk-dropdown-wrap .atk-dropdown-item span:hover,.artalk>.atk-list>.atk-list-header .atk-dropdown-wrap .atk-dropdown-item a:hover{color:var(--at-color-main)}.artalk>.atk-list>.atk-list-header .atk-dropdown-wrap .atk-dropdown-item.active span,.artalk>.atk-list>.atk-list-header .atk-dropdown-wrap .atk-dropdown-item a{color:var(--at-color-main)}.artalk>.atk-list>.atk-list-header .atk-comment-count{font-size:15px}.artalk>.atk-list>.atk-list-header .atk-comment-count .atk-comment-count-num{font-size:22px;margin-right:4px}.artalk>.atk-list>.atk-list-header .atk-right-action{display:flex;flex:1;flex-direction:row;align-items:center;justify-content:flex-end}.artalk>.atk-list>.atk-list-header .atk-right-action>span{font-size:14px;color:var(--at-color-meta);cursor:pointer;-webkit-user-select:none;user-select:none;position:relative}.artalk>.atk-list>.atk-list-header .atk-right-action>span.atk-on,.artalk>.atk-list>.atk-list-header .atk-right-action>span.atk-on *{color:var(--at-color-main)}.artalk>.atk-list>.atk-list-header .atk-right-action>span:not(:last-child):not(.atk-hide){margin-right:10px;padding-right:10px}.artalk>.atk-list>.atk-list-header .atk-right-action>span .atk-unread-badge{position:absolute;top:-5px;left:-6px;color:#fff;background:var(--at-color-pink);text-align:center;min-width:16px;height:16px;padding:0 3px;border-radius:8px;line-height:16px;font-size:12px}.artalk>.atk-list>.atk-list-body{min-height:150px}.artalk>.atk-list>.atk-list-footer{text-align:right}@media only screen and (max-width: 768px){.artalk>.atk-list>.atk-list-footer{float:initial;text-align:center}}.artalk>.atk-list>.atk-list-footer .atk-copyright{display:block;font-size:12px;color:var(--at-color-meta);padding-right:15px}.artalk>.atk-list>.atk-list-footer .atk-copyright a{color:var(--at-color-main);text-decoration:none}.atk-list-no-comment{height:150px;display:flex;font-size:19px;justify-content:center;align-items:center;word-break:break-word;text-align:center}.atk-list-read-more{border-top:1px dashed var(--at-color-border);margin-top:28px;padding-bottom:25px}@media only screen and (max-width: 768px){.atk-list-read-more{padding-bottom:10px}}.atk-list-read-more.atk-err .atk-text{color:var(--at-color-red)!important}.atk-list-read-more .atk-list-read-more-inner{cursor:pointer;-webkit-user-select:none;user-select:none;padding:0 15px;font-size:14px;border-radius:6px;border:1px solid transparent;display:flex;height:30px;flex-direction:row;place-content:center;align-items:center;width:120px;margin:-15px auto 0;background:var(--at-color-bg);border-color:var(--at-color-border)}.atk-list-read-more .atk-list-read-more-inner>.atk-loading-icon{height:15px;width:15px}.atk-list-read-more .atk-list-read-more-inner>.atk-text{color:var(--at-color-meta)}.atk-list-read-more .atk-list-read-more-inner:hover{background:var(--at-color-bg-grey)}.atk-pagination{display:flex;flex-direction:row;justify-content:center;padding:10px 0;position:relative}.atk-pagination>.atk-btn,.atk-pagination>.atk-input{height:30px;border:1px solid var(--at-color-border);border-radius:3px;padding:0 5px;text-align:center;background:var(--at-color-bg)}.atk-pagination>.atk-btn{-webkit-user-select:none;user-select:none;width:60px;cursor:pointer;display:flex;justify-content:center;align-items:center}.atk-pagination>.atk-btn:hover{background:var(--at-color-bg-grey)}.atk-pagination>.atk-btn.atk-disabled{color:var(--at-color-sub)}.atk-pagination>.atk-btn.atk-disabled:hover{cursor:default;background:initial}.atk-pagination>.atk-input{background:transparent;color:var(--at-color-font);font-size:18px;width:60px;outline:none}.atk-pagination>.atk-input:focus{border-color:var(--at-color-main)}.atk-pagination>*:not(:last-child){margin-right:10px}.atk-main-editor{position:relative;overflow:hidden;background:var(--at-color-bg);border:1px solid var(--at-color-border);border-radius:6px;margin-bottom:10px}@media only screen and (max-width: 768px){.atk-main-editor{margin-bottom:7px}}.atk-main-editor.editor-traveling{margin-top:5px;margin-bottom:10px}.atk-main-editor>.atk-header{display:flex;flex-direction:row;padding:10px 14px 0}.atk-main-editor>.atk-header input{flex:1;width:100%;font-size:14px;background:transparent;border:2px solid transparent;border-radius:3px;padding:6px 5px;resize:none;outline:none}.atk-main-editor>.atk-header input:not(:last-child){margin-right:2px}.atk-main-editor>.atk-textarea-wrap{position:relative}.atk-main-editor>.atk-textarea-wrap>.atk-textarea{display:block;overflow:hidden;color:var(--at-color-font);font-size:15px;background-color:var(--at-color-bg);border:2px solid transparent;border-radius:3px;width:100%;min-height:120px;margin-top:2px;padding:10px 20px;resize:none;word-wrap:break-word;outline:none}.atk-main-editor>.atk-textarea-wrap>.atk-comment-closed{pointer-events:none;color:var(--at-color-meta);font-size:12px;background-color:var(--at-color-bg);border-top:1px solid var(--at-color-border);padding:5px 15px;margin-top:10px}.atk-main-editor>.atk-plug-panel-wrap{position:relative;height:180px;width:100%;overflow:hidden;border-top:1px solid var(--at-color-border);animation:.3s both atkFadeIn;transition:.2s height ease-in-out}.atk-main-editor>.atk-bottom{display:flex;flex-direction:row;row-gap:5px;justify-content:space-between;padding:5px;flex-wrap:wrap}.atk-main-editor>.atk-bottom>.atk-item{display:flex;flex-direction:row;align-items:center}.atk-main-editor>.atk-bottom>.atk-bottom-left>.atk-state-wrap{margin-right:5px}.atk-main-editor>.atk-bottom>.atk-bottom-left>.atk-plug-btn-wrap{display:flex;flex-direction:row;align-items:center;flex-wrap:wrap;row-gap:5px}.atk-main-editor>.atk-bottom .atk-plug-btn{display:flex;justify-content:center;place-items:center;padding:0 10px;line-height:30px;height:30px;cursor:pointer;color:var(--at-color-grey);font-size:14px;-webkit-user-select:none;user-select:none;border-radius:3px;word-break:keep-all}.atk-main-editor>.atk-bottom .atk-plug-btn:not(:last-child){margin-right:5px}.atk-main-editor>.atk-bottom .atk-plug-btn:hover{background:var(--at-color-bg-grey)}.atk-main-editor>.atk-bottom .atk-plug-btn.active{color:var(--at-color-main)}.atk-main-editor>.atk-bottom .atk-plug-btn.active svg.markdown path{fill:var(--at-color-main)}.atk-main-editor>.atk-bottom .atk-plug-btn i{display:flex;justify-content:center;place-items:center;color:var(--at-color-grey)}.atk-main-editor>.atk-bottom .atk-plug-btn i:not(:first-child){margin-left:4px}.atk-main-editor>.atk-bottom .atk-state-btn{z-index:2;height:30px;padding:0 0 0 10px;font-size:14px;position:relative;display:flex;flex-direction:row;justify-content:center;place-items:center;background:var(--at-color-bg-grey-transl);cursor:pointer;overflow:hidden;border-radius:3px}.atk-main-editor>.atk-bottom .atk-state-btn:hover .atk-cancel{background:#0000000a}@media only screen and (max-width: 768px){.atk-main-editor>.atk-bottom .atk-state-btn{padding:0}.atk-main-editor>.atk-bottom .atk-state-btn .atk-text-wrap{display:none}}.atk-main-editor>.atk-bottom .atk-state-btn .atk-text-wrap{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 8px 0 2px;max-width:8em}.atk-main-editor>.atk-bottom .atk-state-btn .atk-cancel{display:flex;justify-content:center;place-items:center;padding:0 10px;font-weight:700;height:100%;background:#00000005}.atk-main-editor>.atk-bottom .atk-send-btn{background:var(--at-color-main);color:#fff;font-size:14px;border:none;margin:0;height:30px;min-width:7.3em;cursor:pointer;transition:opacity .3s ease-in-out;outline:none;border-radius:3px}@media only screen and (max-width: 768px){.atk-main-editor>.atk-bottom .atk-send-btn{min-width:6em}}.atk-main-editor>.atk-bottom .atk-send-btn:active{opacity:.9}.atk-main-editor>.atk-notify-wrap{z-index:3;position:absolute;right:-2px;bottom:40px;width:225px;opacity:.83}.atk-sidebar-layer{position:fixed;z-index:99999;top:0;right:0;width:430px;height:100%;background:var(--at-color-bg);transition:transform .45s cubic-bezier(.23,1,.32,1) 0ms;transform:translate(430px)}@media only screen and (max-width: 430px){.atk-sidebar-layer{width:100%}}.atk-sidebar-layer .atk-sidebar-inner{position:relative;height:100%}.atk-sidebar-layer .atk-sidebar-header{position:absolute;top:0;right:0;display:flex;flex-direction:row;align-items:center;z-index:99999}.atk-sidebar-layer .atk-sidebar-header .atk-sidebar-close{display:flex;flex-direction:column;width:60px;height:60px;align-items:center;place-content:center;cursor:pointer;-webkit-user-select:none;user-select:none;margin-left:10px;font-size:22px}.atk-sidebar-layer .atk-sidebar-header .atk-sidebar-close:hover :after{background-color:#e81123e6}.atk-sidebar-layer .atk-sidebar-iframe-wrap{height:100%;position:relative}.atk-sidebar-layer .atk-sidebar-iframe-wrap iframe{border:0;width:100%;height:100%}.atk-sidebar-layer .atk-sidebar-iframe-wrap .atk-err-alert{z-index:9999;top:50%;left:50%;transform:translate(-50%,-50%);position:absolute;background:var(--at-color-bg);padding:40px 30px;width:80%;text-align:center;border-radius:4px}.atk-sidebar-layer .atk-sidebar-iframe-wrap .atk-err-alert .atk-title{font-size:1.4em;margin-bottom:20px;color:var(--at-color-font)}.atk-sidebar-layer .atk-sidebar-iframe-wrap .atk-err-alert .atk-text{color:var(--at-color-font)}.atk-sidebar-layer .atk-sidebar-iframe-wrap .atk-err-alert .atk-text span{cursor:pointer;color:var(--at-color-main)}.artalk{position:relative;width:100%;min-height:200px}.artalk,.atk-layer-wrap{color:var(--at-color-font);word-wrap:break-word;word-break:break-word}.artalk *,.atk-layer-wrap *{box-sizing:border-box}.artalk input,.artalk textarea,.artalk button,.artalk optgroup,.artalk select,.atk-layer-wrap input,.atk-layer-wrap textarea,.atk-layer-wrap button,.atk-layer-wrap optgroup,.atk-layer-wrap select{font-family:inherit;color:inherit;font-size:inherit}.artalk code,.atk-layer-wrap code{font-family:source code pro,Consolas,Monaco,Menlo,sans-serif;margin:0 .05em;padding:0 .4em;display:inline-block;vertical-align:middle;font-size:.9em;background-color:var(--at-color-bg-grey);color:var(--at-color-font);border-radius:2px}.artalk pre,.atk-layer-wrap pre{margin:10px 0 0;padding:0;line-height:0}.artalk pre code,.atk-layer-wrap pre code{line-height:1.6em;display:block;padding:10px 15px;white-space:pre-wrap!important;background-color:var(--at-color-bg-grey);color:var(--at-color-font);margin:0}.artalk pre code *,.atk-layer-wrap pre code *{font-family:source code pro,Consolas,Monaco,Menlo,sans-serif}.artalk a,.atk-layer-wrap a{color:var(--at-color-main);text-decoration:none}.artalk blockquote,.atk-layer-wrap blockquote{position:static;margin:10px 0;padding:10px 20px;background:var(--at-color-bg-grey);border-left:4px solid #687a86;color:var(--at-color-font)}.artalk p:first-child,.atk-layer-wrap p:first-child{margin-top:0}.artalk p:last-child,.atk-layer-wrap p:last-child{margin-bottom:0}.artalk img,.atk-layer-wrap img{max-width:100%}.artalk table,.atk-layer-wrap table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:1.5em;font-size:.96em}.artalk td,.artalk th,.atk-layer-wrap td,.atk-layer-wrap th{text-align:left;padding:4px 8px 4px 10px;border:1px solid var(--at-color-border)}.artalk td,.atk-layer-wrap td{vertical-align:top}.artalk tr:nth-child(2n),.atk-layer-wrap tr:nth-child(2n){background-color:var(--at-color-bg-grey)}.artalk ul,.atk-layer-wrap ul{list-style:disc}.artalk ol,.atk-layer-wrap ol{list-style:decimal}.artalk li+li,.atk-layer-wrap li+li{margin-top:8px}.artalk li>ol,.artalk li>ul,.atk-layer-wrap li>ol,.atk-layer-wrap li>ul{margin:8px 0 0}.atk-hide{display:none!important}.atk-full-layer,.atk-layer-dialog-wrap,.atk-error-layer,.atk-loading{width:100%;height:100%;position:absolute;top:0;left:0;background:var(--at-color-bg);z-index:4;align-items:center;justify-content:center;flex-flow:column;display:flex}.atk-loading-spinner{position:relative;width:50px;height:50px}.atk-loading-spinner svg{animation:atkRotate 2s linear infinite;transform-origin:center center;width:100%;height:100%;position:absolute;top:0;left:0}.atk-loading-spinner svg circle{stroke-dasharray:1,200;stroke-dashoffset:0;animation:atkDash 1.5s ease-in-out infinite,atkColor 6s ease-in-out infinite;stroke-linecap:round}@keyframes atkRotate{to{transform:rotate(360deg)}}@keyframes atkDash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:89,200;stroke-dashoffset:-35px}to{stroke-dasharray:89,200;stroke-dashoffset:-124px}}@keyframes atkColor{0%,to{stroke:#ff5652}40%{stroke:#2196f3}66%{stroke:#32c787}80%,90%{stroke:#ffc107}}@keyframes atkLoadingIconRotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.atk-loading-icon{width:18px;height:18px;box-sizing:border-box;border:solid 1px transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;animation:atkLoadingIconRotate .4s linear infinite}.atk-fade-in{animation:atkFadeIn both .3s}.atk-fade-out{animation:atkFadeOut both .2s}.atk-rotate{animation:atkRotate 2s linear infinite}@keyframes atkFadeIn{0%{opacity:0}to{opacity:1}}@keyframes atkFadeOut{to{opacity:0}}@keyframes atkRotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.atk-icon:after{display:block;content:"";width:1em;height:1em;background-color:var(--at-color-deep);background-size:contain;background-repeat:no-repeat;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center;-webkit-mask-size:contain;mask-size:contain}.atk-icon-sync:after{-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.99133 4.87635C2.22512 7.64257 2.22512 12.1275 4.99133 14.8937C6.04677 15.9491 7.3524 16.6019 8.71732 16.8519' stroke='%234E5969' stroke-width='2'/%3E%3Cpath d='M14.4179 15.4815L15.0072 14.8922C17.7734 12.126 17.7734 7.64107 15.0072 4.87486C13.9518 3.81942 12.6461 3.16668 11.2812 2.91664' stroke='%234E5969' stroke-width='2'/%3E%3Cpath d='M6.17106 4.99252L5.58181 4.40327L4.99255 3.81401H6.17106V4.99252Z' fill='%23C4C4C4' stroke='%234E5969' stroke-width='2'/%3E%3Cpath d='M13.8299 15.0084L14.4192 15.5976L15.0084 16.1869H13.8299V15.0084Z' fill='%23C4C4C4' stroke='%234E5969' stroke-width='2'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.99133 4.87635C2.22512 7.64257 2.22512 12.1275 4.99133 14.8937C6.04677 15.9491 7.3524 16.6019 8.71732 16.8519' stroke='%234E5969' stroke-width='2'/%3E%3Cpath d='M14.4179 15.4815L15.0072 14.8922C17.7734 12.126 17.7734 7.64107 15.0072 4.87486C13.9518 3.81942 12.6461 3.16668 11.2812 2.91664' stroke='%234E5969' stroke-width='2'/%3E%3Cpath d='M6.17106 4.99252L5.58181 4.40327L4.99255 3.81401H6.17106V4.99252Z' fill='%23C4C4C4' stroke='%234E5969' stroke-width='2'/%3E%3Cpath d='M13.8299 15.0084L14.4192 15.5976L15.0084 16.1869H13.8299V15.0084Z' fill='%23C4C4C4' stroke='%234E5969' stroke-width='2'/%3E%3C/svg%3E")}.atk-icon-del:after{background-color:var(--at-color-red)!important;-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='22' height='22' viewBox='0 0 22 22' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2.29167 5.04166L4.81251 5.04166M4.81251 5.04166L4.81251 18.3333C4.81251 18.5865 5.01771 18.7917 5.27084 18.7917L16.7292 18.7917C16.9823 18.7917 17.1875 18.5865 17.1875 18.3333V5.04166M4.81251 5.04166L7.33334 5.04166M17.1875 5.04166L19.7083 5.04166M17.1875 5.04166L14.6667 5.04166M7.33334 5.04166V3.20833L14.6667 3.20833V5.04166M7.33334 5.04166L14.6667 5.04166' stroke='%23D06565' stroke-width='2'/%3E%3Cpath d='M9.16667 8.25V15.125M12.8333 8.25V15.125' stroke='%23D06565' stroke-width='2'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='22' height='22' viewBox='0 0 22 22' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2.29167 5.04166L4.81251 5.04166M4.81251 5.04166L4.81251 18.3333C4.81251 18.5865 5.01771 18.7917 5.27084 18.7917L16.7292 18.7917C16.9823 18.7917 17.1875 18.5865 17.1875 18.3333V5.04166M4.81251 5.04166L7.33334 5.04166M17.1875 5.04166L19.7083 5.04166M17.1875 5.04166L14.6667 5.04166M7.33334 5.04166V3.20833L14.6667 3.20833V5.04166M7.33334 5.04166L14.6667 5.04166' stroke='%23D06565' stroke-width='2'/%3E%3Cpath d='M9.16667 8.25V15.125M12.8333 8.25V15.125' stroke='%23D06565' stroke-width='2'/%3E%3C/svg%3E")}.atk-icon-edit:after{-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='16' height='17' viewBox='0 0 16 17' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.70618 4.08515L12.6081 7.06376M1.00041 13.021L11.7376 2L14.6392 4.97861L3.90274 16H1L1.00041 13.021Z' stroke='%234E5969' stroke-width='1.5'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='16' height='17' viewBox='0 0 16 17' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.70618 4.08515L12.6081 7.06376M1.00041 13.021L11.7376 2L14.6392 4.97861L3.90274 16H1L1.00041 13.021Z' stroke='%234E5969' stroke-width='1.5'/%3E%3C/svg%3E")}.atk-icon-yes:after,.atk-icon-check:after{-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='25' height='25' viewBox='0 0 25 25' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M21.7071 5.75533L9.92197 17.5404L3.29285 10.9113' stroke='%234E5969'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='25' height='25' viewBox='0 0 25 25' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M21.7071 5.75533L9.92197 17.5404L3.29285 10.9113' stroke='%234E5969'/%3E%3C/svg%3E")}.atk-icon-plus:after{-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2.08331 10H17.9166' stroke='%234E5969' stroke-width='2'/%3E%3Cpath d='M10 2.08334L10 17.9167' stroke='%234E5969' stroke-width='2'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2.08331 10H17.9166' stroke='%234E5969' stroke-width='2'/%3E%3Cpath d='M10 2.08334L10 17.9167' stroke='%234E5969' stroke-width='2'/%3E%3C/svg%3E")}.atk-icon-close-slim:after{-webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 0 25 25' fill='none'%3E%3Cpath d='M19.8657 5.13431L12.5 12.5L5.13431 19.8657' stroke='%234E5969'/%3E%3Cpath d='M5.13431 5.13432L12.5 12.5L19.8657 19.8657' stroke='%234E5969'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 0 25 25' fill='none'%3E%3Cpath d='M19.8657 5.13431L12.5 12.5L5.13431 19.8657' stroke='%234E5969'/%3E%3Cpath d='M5.13431 5.13432L12.5 12.5L19.8657 19.8657' stroke='%234E5969'/%3E%3C/svg%3E")}.atk-icon-arrow-left:after{-webkit-mask-image:url('data:image/svg+xml,');mask-image:url('data:image/svg+xml,')}.atk-icon-no:after,.atk-icon-close:after{-webkit-mask-image:url('data:image/svg+xml,');mask-image:url('data:image/svg+xml,')}.atk-icon-verified{height:1.4em;width:1.4em;background-repeat:no-repeat;background-position:center;background-size:contain;display:block;background-image:url('data:image/svg+xml,')}.atk-error-layer{background-color:var(--at-color-bg-transl)}.atk-error-layer .atk-error-title{color:var(--at-color-red)}.atk-error-layer .atk-warn-title{color:var(--at-color-yellow)}.atk-error-layer .atk-error-title,.atk-error-layer .atk-warn-title{display:inline-block;padding:0 15px;margin-bottom:20px;font-size:20px;letter-spacing:-.5px}.atk-error-layer .atk-error-text{text-align:center;padding:0 20px}.atk-error-layer .atk-error-text *{color:var(--at-color-deep)}.atk-error-layer .atk-error-text a{color:var(--at-color-meta)}.atk-version-check-notice{background:var(--at-color-bg-grey);border-radius:6px;padding:10px 20px;margin-bottom:10px;font-size:14px}.atk-version-check-notice .atk-info{color:var(--at-color-meta)}.atk-version-check-notice .atk-ignore-btn{cursor:pointer;float:right}.atk-version-check-notice .atk-ignore-btn:hover{opacity:.8}.atk-layer-dialog-wrap{background-color:var(--at-color-bg-transl)}.atk-layer-dialog-wrap>.atk-layer-dialog{width:25%}.atk-layer-dialog-wrap>.atk-layer-dialog>.atk-layer-dialog-content .atk-captcha-img{cursor:pointer;width:170px;height:auto;margin-right:10px;padding-right:10px;border-right:1px solid var(--at-color-border);vertical-align:bottom}.atk-layer-dialog-wrap>.atk-layer-dialog>.atk-layer-dialog-content input{width:100%;line-height:34px;background-color:var(--at-color-bg);border:1px solid var(--at-color-border);border-radius:3px;outline:none;padding:0 6px;display:block;margin-top:10px;margin-bottom:5px;text-align:center}.atk-layer-dialog-wrap>.atk-layer-dialog>.atk-layer-dialog-actions{display:flex;flex-direction:row}.atk-layer-dialog-wrap>.atk-layer-dialog>.atk-layer-dialog-actions button{flex:1;display:block;cursor:pointer;border:1px solid var(--at-color-main);background:transparent;color:var(--at-color-main);border-radius:3px;padding:0 15px;line-height:30px;outline:none}.atk-layer-dialog-wrap>.atk-layer-dialog>.atk-layer-dialog-actions button:active{color:#fff;background:var(--at-color-main)}.atk-layer-dialog-wrap>.atk-layer-dialog>.atk-layer-dialog-actions button:not(:last-child){margin-right:5px}.atk-layer-dialog-wrap>.atk-layer-dialog>.atk-layer-dialog-actions button.error{color:#fff;background:#ff5652;border-color:#ff5652}.atk-layer-dialog-wrap>.atk-layer-dialog .atk-checker-iframe-wrap{position:fixed;z-index:999998;left:0;top:0;height:100vh;width:100vw}.atk-layer-dialog-wrap>.atk-layer-dialog .atk-checker-iframe-wrap>iframe{width:100%;height:100%;border:0}.atk-layer-dialog-wrap>.atk-layer-dialog .atk-checker-iframe-wrap .atk-close-btn{z-index:999999;position:fixed;top:20px;right:20px;display:flex;flex-direction:column;width:50px;height:50px;align-items:center;place-content:center;cursor:pointer;-webkit-user-select:none;user-select:none;margin-left:10px}.atk-layer-dialog-wrap>.atk-layer-dialog .atk-checker-iframe-wrap .atk-close-btn:hover .atk-icon-close:after{background-color:#e81123e6}@media only screen and (max-width: 768px){.atk-layer-dialog-wrap>.atk-layer-dialog{width:90%!important}}.atk-notify{display:block;overflow:hidden;background-color:#2c2c2c;color:#fff;border-radius:3px;cursor:pointer;font-size:14px;padding:5px 15px}.atk-notify:not(:last-child){margin-bottom:3px}.atk-notify .atk-notify-content{color:#fff}.atk-layer-wrap .atk-layer-mask{position:fixed;top:0;left:0;width:100%;height:100%;z-index:99998;background:#0000004d}.atk-layer-wrap .atk-layer-item{position:fixed;z-index:99999;top:0;right:0;width:100%;height:100%}.atk-common-action-btn{color:var(--at-color-meta);font-size:13px;line-height:25px;display:inline-flex;cursor:pointer;-webkit-user-select:none;user-select:none}.atk-common-action-btn.atk-error,.atk-common-action-btn.atk-error:hover{color:var(--at-color-red)}.atk-common-action-btn:hover{color:var(--at-color-deep)}.atk-common-action-btn.atk-btn-confirm,.atk-common-action-btn.atk-btn-warn{color:var(--at-color-yellow)!important}.atk-common-action-btn.atk-btn-error{color:var(--at-color-red)!important}.atk-common-action-btn.atk-btn-success{color:var(--at-color-green)!important}img[atk-emoticon]{max-height:60px;display:initial}.atk-slim-scrollbar::-webkit-scrollbar,.atk-editor-plug-emoticons>.atk-grp-wrap::-webkit-scrollbar{width:4px;height:4px;background:transparent}.atk-slim-scrollbar::-webkit-scrollbar-thumb,.atk-editor-plug-emoticons>.atk-grp-wrap::-webkit-scrollbar-thumb,.atk-slim-scrollbar::-webkit-scrollbar-thumb:window-inactive{background:#5656564d}.atk-slim-scrollbar::-webkit-scrollbar-thumb:vertical:hover,.atk-editor-plug-emoticons>.atk-grp-wrap::-webkit-scrollbar-thumb:vertical:hover{background:#414a52c4}.atk-slim-scrollbar::-webkit-scrollbar-thumb:vertical:active,.atk-editor-plug-emoticons>.atk-grp-wrap::-webkit-scrollbar-thumb:vertical:active{background:#292f35c4}.atk-editor-plug-emoticons{height:100%;width:100%}.atk-editor-plug-emoticons>.atk-grp-wrap{overflow-y:scroll;overflow-x:hidden;height:100%;width:100%}.atk-editor-plug-emoticons>.atk-grp-wrap>.atk-grp{display:flex;flex-wrap:wrap;flex-direction:row;padding:5px 10px 35px}.atk-editor-plug-emoticons>.atk-grp-wrap>.atk-grp[data-type=image]>.atk-item{height:63px;width:63px}.atk-editor-plug-emoticons>.atk-grp-wrap>.atk-grp>.atk-item{display:flex;align-items:center;justify-content:center;padding:5px;cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:3px;font-size:15px;min-width:35px;margin:2px}.atk-editor-plug-emoticons>.atk-grp-wrap>.atk-grp>.atk-item>img{max-height:100%;width:auto}.atk-editor-plug-emoticons>.atk-grp-wrap>.atk-grp>.atk-item:hover{background:var(--at-color-bg-grey)}.atk-editor-plug-emoticons>.atk-grp-switcher{position:absolute;bottom:0;left:0;width:100%;background:var(--at-color-bg-transl);height:30px;border-top:1px solid var(--at-color-border);border-bottom:1px solid var(--at-color-border)}.atk-editor-plug-emoticons>.atk-grp-switcher>span{-webkit-user-select:none;user-select:none;padding:0 10px;line-height:30px;float:left;display:block;cursor:pointer;font-size:14px}.atk-editor-plug-emoticons>.atk-grp-switcher>span:hover,.atk-editor-plug-emoticons>.atk-grp-switcher>span.active{background:var(--at-color-bg-grey)}.atk-slim-scrollbar::-webkit-scrollbar{width:4px;height:4px;background:transparent}.atk-slim-scrollbar::-webkit-scrollbar-thumb,.atk-slim-scrollbar::-webkit-scrollbar-thumb:window-inactive{background:#5656564d}.atk-slim-scrollbar::-webkit-scrollbar-thumb:vertical:hover{background:#414a52c4}.atk-slim-scrollbar::-webkit-scrollbar-thumb:vertical:active{background:#292f35c4}.atk-editor-plug-preview{overflow-y:scroll;overflow-x:hidden;height:100%;width:100%;padding:10px 15px} 2 | -------------------------------------------------------------------------------- /addon/content/InitArtalk.js: -------------------------------------------------------------------------------- 1 | /* global Services, document, console, Artalk */ 2 | 3 | function initArtalk(window, document) { 4 | const addonInfo = window.arguments[0].addonInfo; 5 | const downloadSourceAction = window.arguments[0].downloadSourceAction; 6 | const openInViewAction = window.arguments[0].openInViewAction; 7 | const site = window.arguments[0].site; 8 | const zotero = window.arguments[0].zotero; 9 | const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); 10 | const getString = window.arguments[0].getString; 11 | const needLogin = window.arguments[0].needLogin; 12 | 13 | let hasLogin = false; 14 | try { 15 | const artalkUser = Services.prefs.getStringPref('ArtalkUser', '{}'); 16 | const user = JSON.parse(artalkUser); 17 | if (!user.email || !user.name) { 18 | const name = zotero.Users.getCurrentUsername() || zotero.Prefs.get('sync.server.username') || ""; 19 | const email = Services.prefs.getStringPref('extensions.zotero.sync.server.username', '') || ""; 20 | // const email = Services.prefs.getStringPref('extensions.zotero.sync.server.username', '') || `${zotero.Users.getCurrentUserID()}@zotero.org` || ""; 21 | if (!user.email) { 22 | user.email = email; 23 | } 24 | if (!user.name) { 25 | user.name = name; 26 | } 27 | Services.prefs.setStringPref('ArtalkUser', JSON.stringify(user)); 28 | } 29 | hasLogin = !!user.email 30 | if (hasLogin) hasLogin = !!user.name 31 | } catch (e) { 32 | console.error(e); 33 | } 34 | 35 | const artalk = Artalk.init({ 36 | el: '#artalk-placeholder', 37 | pageKey: `/${addonInfo.repo}/post`, 38 | pageTitle: addonInfo.name, 39 | server: 'https://artalk.zotero.store', 40 | site: site, 41 | darkMode: prefersDarkScheme.matches, 42 | getItem: (key) => Services.prefs.getStringPref(`${key}`, ''), 43 | setItem: (key, value) => Services.prefs.setStringPref(`${key}`, value), 44 | downloadSource: (url) => downloadSourceAction(url), 45 | openInView: (url) => openInViewAction(url), 46 | beforeSubmit: (editor, next) => { 47 | if (!artalk.getConf().pluginURLs || hasLogin || !needLogin) { 48 | next(); 49 | return; 50 | } 51 | const artalkUser = Services.prefs.getStringPref('ArtalkUser', '{}'); 52 | const user = JSON.parse(artalkUser); 53 | if (!user.email || !user.name) { 54 | const addonInfoString = encodeURIComponent(JSON.stringify(addonInfo)); 55 | const url = `https://plugin.zotero.store?callbackZoteroUserConfig=1&addonInfo=` + addonInfoString; 56 | const pluginAuthWindow = openInViewAction(url); 57 | zotero.Promise.delay(1000).then(() => { 58 | pluginAuthWindow.onunload = () => { 59 | window.arguments[0].reload(); 60 | } 61 | }); 62 | return; 63 | } 64 | next(); 65 | } 66 | }); 67 | artalk.on('mounted', () => { // not working in init, so we need to call it again 68 | artalk?.update({ 69 | locale: Services.prefs.getStringPref(`intl.accept_languages`, '[en-US]').split(',')[0], 70 | // sendBtn: (needLogin ? 1 : 0) + (!!artalk.getConf().pluginURLs ? 1 : 0) + (!hasLogin ? 1 : 0) === 3 ? getString('send-button-status-login') : '', 71 | }) 72 | 73 | document.addEventListener('click', function (e) { 74 | const anchor = e.target.closest('a[target="_blank"]') 75 | if (!anchor) { return } 76 | if (!anchor.hasAttribute('href')) { return } 77 | const href = anchor.getAttribute('href') 78 | if (href.startsWith('javascript:')) { return } 79 | if (href === window.location.href) { return } 80 | if (!href.trim()) { return } 81 | e.preventDefault(); 82 | e.stopImmediatePropagation(); 83 | openInViewAction(anchor.href); 84 | }, true); 85 | document.addEventListener('click', function (e) { 86 | const placeholderAttrName = 'name'; 87 | const anchor = e.target.closest(`a[${placeholderAttrName}^="zotero://"]`); 88 | if (!anchor) { return } 89 | const scheme = anchor.getAttribute(placeholderAttrName) 90 | e.preventDefault(); 91 | e.stopImmediatePropagation(); 92 | 93 | const link = document.createElement('a'); 94 | link.href = scheme; 95 | link.target = '_self'; 96 | link.click(); 97 | }, true); 98 | }) 99 | window.addEventListener('unload', () => { 100 | artalk.destroy(); 101 | }); 102 | 103 | const updateTheme = (e) => { 104 | const newTheme = e.matches ? 'dark' : 'light' 105 | artalk.setDarkMode(newTheme === 'dark') 106 | } 107 | prefersDarkScheme.addEventListener('change', updateTheme) 108 | 109 | return artalk; 110 | } -------------------------------------------------------------------------------- /addon/content/addonDetail.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 32 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 |
78 | 87 | 94 |
95 |
101 | 110 | 117 |
118 |
124 | 131 | 138 | 145 | 152 | 159 |
160 | 167 | 173 |
174 | 175 | 186 | 187 |
188 |
189 |
190 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /addon/content/addons.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 31 | 102 | 103 | 104 | 105 | 106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /addon/content/icons/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /addon/locale/en-US/addon.ftl: -------------------------------------------------------------------------------- 1 | addon-name = Add-on Market for Zotero 2 | menuitem-addons = Add-on Market 3 | name = Name 4 | description = Description 5 | stars = ⭐️ 6 | state = Install status 7 | unknown = Unknown 8 | menu-name = Name 9 | menu-desc = Description 10 | menu-star = ⭐️ 11 | menu-author = Author 12 | menu-install-state = Local status 13 | menu-remote-update-time = Release time 14 | menu-remote-version = Latest version 15 | menu-local-version = Local version 16 | menu-download-latest-count = Downloads@latest 17 | menu-download-all-count = Downloads@all 18 | install-failed = Install { $name } failed 19 | install-succeed = Install { $name } succeed 20 | download-failed = Download { $name } failed 21 | uninstall-succeed = Uninstall { $name } succeed 22 | uninstall-failed = Uninstall { $name } failed 23 | uninstall-confirm-title = Uninstall add-on? 24 | install-failed-uncompatible = This Add-on is not adapted to current version of Zotero. 25 | uninstall-confirm-message = Are you sure you want to uninstall { $name }? 26 | uninstall-confirm-confirm = Uninstall 27 | update-all-uncompatible-title = Update uncompatible add-ons? 28 | update-all-uncompatible-message = Found uncompatible add-ons, do you want to update these uncompatible add-ons? 29 | update-all-uncompatible-confirm = Update 30 | installing = Installing { $name } 31 | downloading = Downloading { $name } 32 | downloading-source = source { $name } 33 | update-succeed = Update succeed 34 | state-unknown = ⚠️ Unknown 35 | state-uncompatible = ❌ Uncompatible 36 | state-installed = ✅ Installed 37 | state-notInstalled = 🚧 Not installed 38 | state-pendingUninstall = 🚮 Uninstalled 39 | state-outdate = ⬆️ Upgradable 40 | state-disabled = 🚫 Disabled 41 | scheme-config-success = Config source succeed 42 | scheme-install-confirm-title = Install unknown add-on? 43 | scheme-install-confirm-message = Are you sure you want to install add-on from unknown source? 44 | scheme-install-confirm-confirm = Install 45 | 46 | source-custom = Custom 47 | source-auto = Auto 48 | source-zotero-chinese-github = GitHub (zotero-chinese) 49 | source-zotero-chinese-gitee = Gitee (zotero-chinese) 50 | source-zotero-chinese-jsdelivr = jsDelivr (zotero-chinese) 51 | source-zotero-chinese-ghproxy = gh-proxy (zotero-chinese) 52 | source-zotero-scraper-github = GitHub (addon-scraper) 53 | source-zotero-scraper-gitee = Gitee (addon-scraper) 54 | source-zotero-scraper-ghproxy = gh-proxy (addon-scraper) 55 | source-zotero-scraper-jsdelivr = jsDelivr (addon-scraper) 56 | 57 | menu-install = Install 58 | menu-update = Update 59 | menu-reinstall = Reinstall 60 | menu-install-and-update = Install & Update 61 | menu-uninstall = Uninstall 62 | menu-remove = Remove 63 | menu-uninstall-undo = Restore 64 | menu-homepage = Homepage 65 | menu-refresh = Refresh add-on list 66 | menu-systemAddon = Manage installed add-ons 67 | menu-updateAllIfNeed = Update upgradable add-ons 68 | menu-enable = Enable 69 | menu-disable = Disable 70 | menu-items-count = items 71 | menu-open-xpi-location = Show in File Manager 72 | 73 | source-github = GitHub 74 | source-gitee = Gitee 75 | source-jsdelivr = jsDelivr 76 | source-ghproxy = gh-proxy 77 | source-kgithub = kGitHub 78 | source-others = Unknown 79 | 80 | release-uncompatible-description = This version is suitable for ({ $minVersion })-({ $maxVersion }), it may not be compatible with your current version of Zotero ({ $currentVersion }). 81 | 82 | guide-guide-done = Done 83 | guide-open-addons-table-title = Open add-on market 84 | guide-open-addons-table-message = Click to enter the Add-on market, directly search and install add-ons within Zotero. 85 | guide-addons-table-switch-source-title = Switch source 86 | guide-addons-table-switch-source-message = Switch Add-on data source if page is blank, encounters an error, or fails to retrieve add-ons. 87 | 88 | send-button-status-login = login 89 | -------------------------------------------------------------------------------- /addon/locale/en-US/addonDetail.ftl: -------------------------------------------------------------------------------- 1 | install = 2 | .label = Install 3 | update = 4 | .label = Update 5 | reinstall = 6 | .label = Reinstall 7 | uninstall = 8 | .label = Uninstall 9 | remove = 10 | .label = Remove 11 | uninstallUndo = 12 | .label = Restore 13 | enable = 14 | .label = Enable 15 | disable = 16 | .label = Disable -------------------------------------------------------------------------------- /addon/locale/en-US/addonTable.ftl: -------------------------------------------------------------------------------- 1 | title = Add-on Market 2 | refresh = 3 | .label = Refresh 4 | selectSource = Select Source: 5 | customSource = Custom Source: 6 | autoUpdate = 7 | .label = Automatic update add-ons 8 | hideToolbarEntrance = 9 | .label = Hide toolbar entrance 10 | search-field = 11 | .placeholder = Search... 12 | -------------------------------------------------------------------------------- /addon/locale/nl-NL/addon.ftl: -------------------------------------------------------------------------------- 1 | addon-name = Add-on Markt voor Zotero 2 | menuitem-addons = Add-on Markt 3 | name = Naam 4 | description = Beschrijving 5 | stars = ⭐️ 6 | state = Installatiestatus 7 | unknown = Onbekend 8 | menu-name = Naam 9 | menu-desc = Beschrijving 10 | menu-star = ⭐️ 11 | menu-author = Auteur 12 | menu-install-state = Lokale status 13 | menu-remote-update-time = Uitgavetijd 14 | menu-remote-version = Recente versie 15 | menu-local-version = Lokale versie 16 | menu-download-latest-count = Downloads@recente 17 | menu-download-all-count = Downloads@alle 18 | install-failed = Installatie { $name } mislukt 19 | install-succeed = Installatie { $name } geslaagd 20 | download-failed = Downloaden { $name } mislukt 21 | uninstall-succeed = Deïnstallatie { $name } geslaagd 22 | uninstall-failed = Deïnstallatie { $name } mislukt 23 | install-failed-uncompatible = Deze Add-on is niet aangepast aan de huidige versie van Zotero. 24 | uninstall-confirm-title = Add-on deïnstalleren? 25 | uninstall-confirm-message = Weet je zeker dat je { $name } wilt deïnstalleren? 26 | uninstall-confirm-confirm = Deïnstalleren 27 | update-all-uncompatible-title = Niet-ondersteunde add-ons bijwerken? 28 | update-all-uncompatible-message = Niet-ondersteunde add-ons gevonden, wil je deze niet-ondersteunde add-ons bijwerken? 29 | update-all-uncompatible-confirm = Bijwerken 30 | installing = Bezig met { $name } installeren 31 | downloading = Bezig met { $name } downloaden 32 | downloading-source = bron { $name } 33 | update-succeed = Bijwerken gelukt 34 | state-unknown = ⚠️ Onbekend 35 | state-uncompatible = ❌ Niet-ondersteund 36 | state-installed = ✅ Geïnstalleerd 37 | state-notInstalled = 🚧 Niet geïnstalleerd 38 | state-pendingUninstall = 🚮 Gedeïnstalleerd 39 | state-outdate = ⬆️ Bijwerkbaar 40 | state-disabled = 🚫 Uitgeschakeld 41 | scheme-config-success = Configuratiebron geslaagd 42 | scheme-install-confirm-title = Onbekende add-on installeren? 43 | scheme-install-confirm-message = Weet je zeker dat je de add-on van onbekende bron wilt installeren? 44 | scheme-install-confirm-confirm = Installeren 45 | 46 | source-custom = Aangepast 47 | source-auto = Auto 48 | source-zotero-chinese-github = GitHub (zotero-chinese) 49 | source-zotero-chinese-gitee = Gitee (zotero-chinese) 50 | source-zotero-chinese-jsdelivr = jsDelivr (zotero-chinese) 51 | source-zotero-chinese-ghproxy = gh-proxy (zotero-chinese) 52 | source-zotero-scraper-github = GitHub (addon-scraper) 53 | source-zotero-scraper-gitee = Gitee (addon-scraper) 54 | source-zotero-scraper-ghproxy = gh-proxy (addon-scraper) 55 | source-zotero-scraper-jsdelivr = jsDelivr (addon-scraper) 56 | 57 | menu-install = Installeren 58 | menu-update = Bijwerken 59 | menu-reinstall = Herinstalleren 60 | menu-install-and-update = Installeren & bijwerken 61 | menu-uninstall = Deïnstalleren 62 | menu-remove = Verwijderen 63 | menu-uninstall-undo = Herstellen 64 | menu-homepage = Startpagina 65 | menu-refresh = Add-on lijst vernieuwen 66 | menu-systemAddon = Geïnstalleerde add-ons beheren 67 | menu-updateAllIfNeed = Bijwerkbare add-ons bijwerken 68 | menu-enable = Inschakelen 69 | menu-disable = Uitschakelen 70 | menu-items-count = items 71 | menu-open-xpi-location = Weergeven in Bestandsbeheer 72 | 73 | source-github = GitHub 74 | source-gitee = Gitee 75 | source-jsdelivr = jsDelivr 76 | source-ghproxy = gh-proxy 77 | source-kgithub = kGitHub 78 | source-others = Onbekend 79 | 80 | release-uncompatible-description = Deze versie is geschikt voor ({ $minVersion })-({ $maxVersion }), het is mogelijk niet compatibel met uw huidige Zotero-versie ({ $currentVersion }). 81 | 82 | guide-guide-done = Gereed 83 | guide-open-addons-table-title = Open de add-on markt 84 | guide-open-addons-table-message = Klik om de add-on markt te openen, zoek en installeer add-ons direct binnen Zotero. 85 | guide-addons-table-switch-source-title = Wissel bron 86 | guide-addons-table-switch-source-message = Wissel van add-on gegevensbron als de pagina leeg is, een fout tegenkomt of geen add-ons kan ophalen. 87 | 88 | send-button-status-login = Inloggen 89 | -------------------------------------------------------------------------------- /addon/locale/nl-NL/addonDetail.ftl: -------------------------------------------------------------------------------- 1 | install = 2 | .label = Installeren 3 | update = 4 | .label = Bijwerken 5 | reinstall = 6 | .label = Herinstalleren 7 | uninstall = 8 | .label = Deïnstalleren 9 | remove = 10 | .label = Verwijderen 11 | uninstallUndo = 12 | .label = Herstellen 13 | enable = 14 | .label = Inschakelen 15 | disable = 16 | .label = Uitschakelen 17 | -------------------------------------------------------------------------------- /addon/locale/nl-NL/addonTable.ftl: -------------------------------------------------------------------------------- 1 | title = Add-on Markt 2 | refresh = 3 | .label = Verversen 4 | selectSource = Selecteer Bron: 5 | customSource = Aangepaste Bron: 6 | autoUpdate = 7 | .label = Automatische add-ons bijwerken 8 | hideToolbarEntrance = 9 | .label = Werkbalkingang verbergen 10 | search-field = 11 | .placeholder = Zoeken 12 | -------------------------------------------------------------------------------- /addon/locale/zh-CN/addon.ftl: -------------------------------------------------------------------------------- 1 | addon-name = Zotero插件市场 2 | menuitem-addons = 插件市场 3 | name = 名称 4 | description = 简介 5 | stars = ⭐️ 6 | state = 安装状态 7 | unknown = 未知 8 | menu-name = 名称 9 | menu-desc = 简介 10 | menu-star = ⭐️ 11 | menu-author = 作者 12 | menu-install-state = 本地安装状态 13 | menu-remote-update-time = 发布时间 14 | menu-remote-version = 最新版本 15 | menu-local-version = 本地版本 16 | menu-download-latest-count = 最新版下载量 17 | menu-download-all-count = 总下载量 18 | install-failed = 安装 { $name } 失败 19 | install-succeed = 安装 { $name } 成功 20 | download-failed = 下载 { $name } 失败 21 | uninstall-succeed = 卸载 { $name } 成功 22 | uninstall-failed = 卸载 { $name } 失败 23 | install-failed-uncompatible = 插件未适配当前Zotero版本 24 | uninstall-confirm-title = 卸载插件? 25 | uninstall-confirm-message = 确定卸载 { $name }? 26 | uninstall-confirm-confirm = 卸载 27 | update-all-uncompatible-title = 更新未适配插件? 28 | update-all-uncompatible-message = 发现未适配插件,需要更新这些未适配的插件吗? 29 | update-all-uncompatible-confirm = 更新 30 | installing = 正在安装 { $name } 31 | downloading = 正在下载 { $name } 32 | downloading-source = 来源 { $name } 33 | update-succeed = 更新成功 34 | state-unknown = ⚠️ 未知 35 | state-uncompatible = ❌ 未适配 36 | state-installed = ✅ 已安装 37 | state-notInstalled = 🚧 未安装 38 | state-pendingUninstall = 🚮 已卸载 39 | state-outdate = ⬆️ 可升级 40 | state-disabled = 🚫 已禁用 41 | scheme-config-success = 配置插件源成功 42 | scheme-install-confirm-title = 安装未知来源的插件? 43 | scheme-install-confirm-message = 确定安装未知来源的插件? 44 | scheme-install-confirm-confirm = 安装 45 | 46 | source-custom = 自定义 47 | source-auto = 自动 48 | source-zotero-chinese-github = GitHub (zotero中文社区) 49 | source-zotero-chinese-gitee = Gitee (zotero中文社区) 50 | source-zotero-chinese-jsdelivr = jsDelivr (zotero中文社区) 51 | source-zotero-chinese-ghproxy = gh-proxy (zotero中文社区) 52 | source-zotero-scraper-github = GitHub (插件爬虫) 53 | source-zotero-scraper-gitee = Gitee (插件爬虫) 54 | source-zotero-scraper-ghproxy = gh-proxy (插件爬虫) 55 | source-zotero-scraper-jsdelivr = jsDelivr (插件爬虫) 56 | 57 | menu-install = 安装 58 | menu-update = 更新 59 | menu-reinstall = 重新安装 60 | menu-install-and-update = 安装 & 更新 61 | menu-uninstall = 卸载 62 | menu-remove = 移除 63 | menu-uninstall-undo = 恢复 64 | menu-homepage = 主页 65 | menu-refresh = 刷新插件列表 66 | menu-systemAddon = 管理本地插件 67 | menu-updateAllIfNeed = 一键更新可升级插件 68 | menu-enable = 启用 69 | menu-disable = 禁用 70 | menu-items-count = 项 71 | menu-open-xpi-location = 在文件管理器中打开 72 | 73 | source-github = GitHub 74 | source-gitee = Gitee 75 | source-jsdelivr = jsDelivr 76 | source-ghproxy = gh-proxy 77 | source-kgithub = kGitHub 78 | source-others = 未知 79 | 80 | release-uncompatible-description = 此版本适用于({ $minVersion })-({ $maxVersion }), 可能不适配您当前的Zotero版本({ $currentVersion })。 81 | 82 | guide-guide-done = 完成 83 | guide-open-addons-table-title = 打开插件市场 84 | guide-open-addons-table-message = 点击进入插件市场, 直接在 Zotero 内检索和安装插件 85 | guide-addons-table-switch-source-title = 切换插件源 86 | guide-addons-table-switch-source-message = 如果插件页面空白、出现异常、获取不到插件,可以尝试在此切换插件源 87 | 88 | send-button-status-login = 登录 89 | -------------------------------------------------------------------------------- /addon/locale/zh-CN/addonDetail.ftl: -------------------------------------------------------------------------------- 1 | install = 2 | .label = 安装 3 | update = 4 | .label = 更新 5 | reinstall = 6 | .label = 重新安装 7 | uninstall = 8 | .label = 卸载 9 | remove = 10 | .label = 移除 11 | uninstallUndo = 12 | .label = 恢复 13 | enable = 14 | .label = 启用 15 | disable = 16 | .label = 禁用 -------------------------------------------------------------------------------- /addon/locale/zh-CN/addonTable.ftl: -------------------------------------------------------------------------------- 1 | title = 插件市场 2 | refresh = 3 | .label = 刷新 4 | selectSource = 选择源: 5 | customSource = 自定义源: 6 | autoUpdate = 7 | .label = 自动更新插件 8 | hideToolbarEntrance = 9 | .label = 隐藏工具栏入口 10 | search-field = 11 | .placeholder = 关键字搜索 12 | -------------------------------------------------------------------------------- /addon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "default_locale": "en_US", 4 | "name": "__MSG_name__", 5 | "version": "__buildVersion__", 6 | "description": "__MSG_description__", 7 | "homepage_url": "__homepage__", 8 | "author": "__author__", 9 | "icons": { 10 | "48": "content/icons/favicon.svg", 11 | "96": "content/icons/favicon.svg" 12 | }, 13 | "applications": { 14 | "zotero": { 15 | "id": "__addonID__", 16 | "update_url": "__updateURL__", 17 | "strict_min_version": "6.999", 18 | "strict_max_version": "7.*" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /doc/README-CN.md: -------------------------------------------------------------------------------- 1 | # Zotero 插件市场 2 | 3 | [![zotero target version](https://img.shields.io/badge/Zotero-7-green?style=flat-square&logo=zotero&logoColor=CC2936)](https://www.zotero.org) 4 | [![Release](https://img.shields.io/github/v/release/syt2/zotero-addons?style=flat-square&logo=github&color=red)](https://github.com/syt2/zotero-addons/releases/latest) 5 | ![Downloads@z7](https://img.shields.io/github/downloads/syt2/zotero-addons/latest/total?style=flat-square&logo=github&label=downloads@z7) 6 | [![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template) 7 | [![Using Zotero Chinese Plugins](https://img.shields.io/badge/Using-Zotero%20Chinese%20Plugins-blue?style=flat-square&logo=github)](https://github.com/zotero-chinese/zotero-plugins) 8 | [![Using Zotero Scraper](https://img.shields.io/badge/Using-Zotero%20Addons%20Scraper-blue?style=flat-square&logo=github)](https://github.com/syt2/zotero-addons-scraper) 9 | [![Using Artalk](https://img.shields.io/badge/Using-Artalk-blue?style=flat-square&logo=github)](https://github.com/ArtalkJS/Artalk) 10 | 11 | [English](../README.md) | [简体中文](README-CN.md) 12 | 13 | ## 简介 14 | 15 | 这是一个用于在 [Zotero](https://www.zotero.org) 内浏览、安装和评论插件的 [Zotero](https://www.zotero.org) 插件 16 | 17 | ## 安装 18 | 19 | 1. 下载[最新版xpi安装包](https://github.com/syt2/zotero-addons/releases/latest/download/zotero-addons.xpi) 20 | 21 | 2. 在 Zotero 内安装 `(工具) -> (附加组件)` 22 | 23 | ## 使用方法 24 | 25 | 安装完成后,点击工具栏的 按钮,或者在`工具`菜单内点击`插件市场` 26 | 27 | 28 | 29 | ## 插件数据源 30 | 31 | 对于国内用户,若遇到插件页面空白、加载不出插件的情况,请尝试切换不同的数据源 32 | 插件提供**自动源**选项,将会自动选择可连接的源 33 | 34 | ### [zotero-chinese/zotero-plugins](https://github.com/zotero-chinese/zotero-plugins) 35 | 36 | 插件主数据源来自Zotero中文社区 **[zotero-chinese/zotero-plugins](https://github.com/zotero-chinese/zotero-plugins)**. 37 | 38 | 在插件市场界面选择 `(zotero中文社区)` 即可使用该数据源 39 | 40 | > 若你有新的插件想要添加到插件源内,请提交至 [zotero-chinese/zotero-plugins](https://github.com/zotero-chinese/zotero-plugins). 41 | 42 | ### [syt2/zotero-addons-scraper](https://github.com/syt2/zotero-addons-scraper) 43 | 44 | 在插件市场界面选择 `(插件爬虫)` 即可使用该数据源 45 | 46 | ### 自定义源 47 | 48 | 任何符合 [zotero-chinese/zotero-plugins](https://github.com/zotero-chinese/zotero-plugins) 格式的数据源都可用作本插件的数据源,你可以在插件内选择`自定义源`并提供数据源URL即可 49 | 50 | ## Star 历史 51 | 52 | 53 | 54 | 55 | 56 | Star History Chart 57 | 58 | 59 | -------------------------------------------------------------------------------- /doc/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syt2/zotero-addons/7ded12ef4bb8ccd7ff6db3bf86047ed79b074849/doc/screenshot.jpg -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check Let TS check this config file 2 | 3 | import eslint from "@eslint/js"; 4 | import tseslint from "typescript-eslint"; 5 | 6 | export default tseslint.config( 7 | { 8 | ignores: ["build/**", ".scaffold/**", "node_modules/**", "scripts/"], 9 | }, 10 | { 11 | extends: [eslint.configs.recommended, ...tseslint.configs.recommended], 12 | rules: { 13 | "no-restricted-globals": [ 14 | "error", 15 | { message: "Use `Zotero.getMainWindow()` instead.", name: "window" }, 16 | { 17 | message: "Use `Zotero.getMainWindow().document` instead.", 18 | name: "document", 19 | }, 20 | { 21 | message: "Use `Zotero.getActiveZoteroPane()` instead.", 22 | name: "ZoteroPane", 23 | }, 24 | "Zotero_Tabs", 25 | ], 26 | 27 | "@typescript-eslint/ban-ts-comment": [ 28 | "warn", 29 | { 30 | "ts-expect-error": "allow-with-description", 31 | "ts-ignore": "allow-with-description", 32 | "ts-nocheck": "allow-with-description", 33 | "ts-check": "allow-with-description", 34 | }, 35 | ], 36 | "@typescript-eslint/no-unused-vars": "off", 37 | "@typescript-eslint/no-explicit-any": [ 38 | "off", 39 | { 40 | ignoreRestArgs: true, 41 | }, 42 | ], 43 | "@typescript-eslint/no-non-null-assertion": "off", 44 | }, 45 | }, 46 | ); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zotero-addons", 3 | "type": "module", 4 | "version": "2.1.1", 5 | "config": { 6 | "addonName": "Add-on Market for Zotero", 7 | "addonID": "zoteroAddons@ytshen.com", 8 | "addonRef": "zoteroaddons", 9 | "addonInstance": "ZoteroAddons", 10 | "prefsPrefix": "extensions.zotero.zoteroaddons" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/syt2/zotero-addons.git" 15 | }, 16 | "author": "ytshen", 17 | "bugs": { 18 | "url": "https://github.com/syt2/zotero-addons/issues" 19 | }, 20 | "homepage": "https://github.com/syt2/zotero-addons#readme", 21 | "license": "AGPL-3.0-or-later", 22 | "scripts": { 23 | "start": "zotero-plugin serve", 24 | "build": "zotero-plugin build && tsc --noEmit", 25 | "lint:check": "prettier --check . && eslint .", 26 | "lint:fix": "prettier --write . && eslint . --fix", 27 | "release": "zotero-plugin release", 28 | "test": "echo \"Error: no test specified\" && exit 1", 29 | "update-deps": "npm update --save" 30 | }, 31 | "dependencies": { 32 | "fuse.js": "^7.1.0", 33 | "zotero-plugin-toolkit": "^5.0.0-1" 34 | }, 35 | "devDependencies": { 36 | "@eslint/js": "^9.27.0", 37 | "@types/node": "^22.15.19", 38 | "eslint": "^9.27.0", 39 | "prettier": "^3.5.3", 40 | "typescript": "^5.8.3", 41 | "typescript-eslint": "^8.32.1", 42 | "zotero-plugin-scaffold": "^0.6.0", 43 | "zotero-types": "^4.0.0-beta.10" 44 | }, 45 | "prettier": { 46 | "printWidth": 80, 47 | "tabWidth": 2, 48 | "endOfLine": "lf", 49 | "overrides": [ 50 | { 51 | "files": [ 52 | "*.xhtml" 53 | ], 54 | "options": { 55 | "htmlWhitespaceSensitivity": "css" 56 | } 57 | } 58 | ] 59 | } 60 | } -------------------------------------------------------------------------------- /src/addon.ts: -------------------------------------------------------------------------------- 1 | import { config } from "../package.json"; 2 | import hooks from "./hooks"; 3 | import actions from "./modules/registerScheme"; 4 | import { createZToolkit } from "./utils/ztoolkit"; 5 | 6 | class Addon { 7 | public data: { 8 | alive: boolean; 9 | config: typeof config; 10 | // Env type, see build.js 11 | env: "development" | "production"; 12 | ztoolkit: ZToolkit; 13 | locale?: { 14 | current: any; 15 | }; 16 | }; 17 | // Lifecycle hooks 18 | public hooks: typeof hooks; 19 | // APIs 20 | public api: object; 21 | 22 | public actions: typeof actions; 23 | 24 | constructor() { 25 | this.data = { 26 | alive: true, 27 | config, 28 | env: __env__, 29 | ztoolkit: createZToolkit(), 30 | }; 31 | this.hooks = hooks; 32 | this.api = {}; 33 | this.actions = actions; 34 | } 35 | } 36 | 37 | export default Addon; 38 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { config } from "../package.json"; 2 | import { getString, initLocale } from "./utils/locale"; 3 | import { createZToolkit } from "./utils/ztoolkit"; 4 | import { AddonTable } from "./modules/addonTable"; 5 | import { AddonInfoManager } from "./modules/addonInfo"; 6 | import { currentSource } from "./utils/configuration"; 7 | import { AddonInfoDetail } from "./modules/addonDetail"; 8 | import { AddonListenerManager } from "./modules/addonListenerManager"; 9 | import { getPref } from "./utils/prefs"; 10 | import { registerConfigScheme } from "./modules/registerScheme"; 11 | import { Guide } from "./modules/guide"; 12 | 13 | async function onStartup() { 14 | await Promise.all([ 15 | Zotero.initializationPromise, 16 | Zotero.unlockPromise, 17 | Zotero.uiReadyPromise, 18 | ]); 19 | 20 | initLocale(); 21 | 22 | registerConfigScheme(); 23 | Guide.initPrefs(); 24 | 25 | await Promise.all( 26 | Zotero.getMainWindows().map((win) => onMainWindowLoad(win)), 27 | ); 28 | 29 | (async () => { 30 | if (currentSource().id === "source-auto") { 31 | // if selected auto source, switch to a connectable source automatically at launching 32 | await AddonInfoManager.autoSwitchAvaliableApi(); 33 | } else { 34 | // fetch addonInfo from specific source 35 | await AddonInfoManager.shared.fetchAddonInfos(true); 36 | } 37 | 38 | // refresh table if AddonTable already displayed, so specific `force` to false 39 | AddonTable.refresh(false); 40 | 41 | if (getPref("autoUpdate")) { 42 | // update automatically if need 43 | AddonTable.updateExistAddons(); 44 | } 45 | })(); 46 | 47 | AddonListenerManager.addListener(); 48 | } 49 | 50 | async function onMainWindowLoad(win: _ZoteroTypes.MainWindow): Promise { 51 | // Create ztoolkit for every window 52 | addon.data.ztoolkit = createZToolkit(); 53 | AddonTable.registerInToolbar(); 54 | AddonTable.registerInMenuTool(); 55 | 56 | Guide.showGuideInMainWindowIfNeed(win); 57 | // @ts-ignore This is a moz feature 58 | // win.MozXULElement.insertFTLIfNeeded( 59 | // `${addon.data.config.addonRef}-mainWindow.ftl`, 60 | // ); 61 | } 62 | 63 | async function onMainWindowUnload(win: Window): Promise { 64 | ztoolkit.unregisterAll(); 65 | } 66 | 67 | function onShutdown(): void { 68 | ztoolkit.unregisterAll(); 69 | AddonTable.close(); 70 | AddonInfoDetail.close(); 71 | AddonListenerManager.removeListener(); 72 | AddonTable.unregisterAll(); 73 | // Remove addon object 74 | addon.data.alive = false; 75 | // @ts-ignore - Plugin instance is not typed 76 | delete Zotero[addon.data.config.addonInstance]; 77 | } 78 | 79 | export default { 80 | onStartup, 81 | onShutdown, 82 | onMainWindowLoad, 83 | onMainWindowUnload, 84 | }; 85 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { BasicTool } from "zotero-plugin-toolkit"; 2 | import Addon from "./addon"; 3 | import { config } from "../package.json"; 4 | 5 | const basicTool = new BasicTool(); 6 | 7 | // @ts-ignore - Plugin instance is not typed 8 | if (!basicTool.getGlobal("Zotero")[config.addonInstance]) { 9 | _globalThis.addon = new Addon(); 10 | defineGlobal("ztoolkit", () => { 11 | return _globalThis.addon.data.ztoolkit; 12 | }); 13 | // @ts-ignore - Plugin instance is not typed 14 | Zotero[config.addonInstance] = addon; 15 | } 16 | 17 | function defineGlobal(name: Parameters[0]): void; 18 | function defineGlobal(name: string, getter: () => any): void; 19 | function defineGlobal(name: string, getter?: () => any) { 20 | Object.defineProperty(_globalThis, name, { 21 | get() { 22 | return getter ? getter() : basicTool.getGlobal(name); 23 | }, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/addonDetail.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AddonInfo, 3 | addonReleaseInfo, 4 | addonReleaseTime, 5 | relatedAddons, 6 | xpiDownloadUrls, 7 | } from "./addonInfo"; 8 | import { getString } from "../utils/locale"; 9 | import { installAddonFrom, undoUninstall, uninstall } from "../utils/utils"; 10 | import { config } from "../../package.json"; 11 | import { isWindowAlive } from "../utils/window"; 12 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 13 | // @ts-ignore 14 | const { XPIDatabase } = ChromeUtils.import( 15 | "resource://gre/modules/addons/XPIDatabase.jsm", 16 | ); 17 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 18 | // @ts-ignore 19 | const { AddonManager } = ChromeUtils.import( 20 | "resource://gre/modules/AddonManager.jsm", 21 | ); 22 | 23 | export class AddonInfoDetail { 24 | private static window: Window | null; 25 | private static addonInfo?: AddonInfo; 26 | 27 | /** 28 | * Close detail window 29 | */ 30 | static async close() { 31 | this.window?.close(); 32 | } 33 | 34 | /** 35 | * Show detail window for specific AddonInfo 36 | * @param addonInfo AddonInfo specified 37 | */ 38 | static async showDetailWindow(addonInfo: AddonInfo) { 39 | this.window?.close(); 40 | this.addonInfo = addonInfo; 41 | const windowArgs = { 42 | _initPromise: Zotero.Promise.defer(), 43 | addonInfo: addonInfo, 44 | site: __env__ === 'development' ? 'Zotero Plugin Market for testing' : 'Zotero Plugin Market', 45 | downloadSourceAction: async (url: string) => { 46 | const response = await Zotero.HTTP.request("GET", url); 47 | return btoa(response.response); 48 | }, 49 | openInViewAction: (url: string) => Zotero.openInViewer(url), 50 | }; 51 | const win = Zotero.getMainWindow().openDialog( 52 | `chrome://${config.addonRef}/content/addonDetail.xhtml`, 53 | `${config.addonRef}-addonDetail`, 54 | `chrome,centerscreen,resizable,status,dialog=no,width=800,height=640`, 55 | windowArgs, 56 | ); 57 | await windowArgs._initPromise.promise; 58 | this.window = win; 59 | // @ts-ignore ignore keyboardevent type check 60 | win?.addEventListener("keypress", (e: KeyboardEvent) => { 61 | if ( 62 | ((Zotero.isMac && e.metaKey && !e.ctrlKey) || 63 | (!Zotero.isMac && e.ctrlKey)) && 64 | !e.altKey && 65 | e.key === "w" 66 | ) { 67 | this.close(); 68 | } 69 | }); 70 | 71 | this.installButton.addEventListener("click", async (e) => { 72 | if (this.installButton.disabled) { 73 | return; 74 | } 75 | this.installButton.disabled = true; 76 | await this.installAddon(addonInfo); 77 | this.installButton.disabled = false; 78 | }); 79 | this.updateButton.addEventListener("click", async (e) => { 80 | if (this.updateButton.disabled) { 81 | return; 82 | } 83 | this.updateButton.disabled = true; 84 | await this.installAddon(addonInfo); 85 | this.updateButton.disabled = false; 86 | }); 87 | this.reinstallButton.addEventListener("click", async (e) => { 88 | if (this.reinstallButton.disabled) { 89 | return; 90 | } 91 | this.reinstallButton.disabled = true; 92 | await this.installAddon(addonInfo); 93 | this.reinstallButton.disabled = false; 94 | }); 95 | this.uninstallButton.addEventListener("click", async (e) => { 96 | if (this.uninstallButton.disabled) { 97 | return; 98 | } 99 | this.uninstallButton.disabled = true; 100 | await uninstall(await this.localAddon(), { popConfirmDialog: true }); 101 | this.uninstallButton.disabled = false; 102 | }); 103 | this.removeButton.addEventListener("click", async (e) => { 104 | if (this.removeButton.disabled) { 105 | return; 106 | } 107 | this.removeButton.disabled = true; 108 | await uninstall(await this.localAddon()); 109 | this.removeButton.disabled = false; 110 | }); 111 | this.uninstallUndoButton.addEventListener("click", async (e) => { 112 | if (this.uninstallUndoButton.disabled) { 113 | return; 114 | } 115 | this.uninstallUndoButton.disabled = true; 116 | await undoUninstall(await this.localAddon()); 117 | this.uninstallUndoButton.disabled = false; 118 | }); 119 | this.enableButton.addEventListener("click", async (e) => { 120 | if (this.enableButton.disabled) { 121 | return; 122 | } 123 | this.enableButton.disabled = true; 124 | await (await this.localAddon()).enable(); 125 | this.enableButton.disabled = false; 126 | }); 127 | this.disableButton.addEventListener("click", async (e) => { 128 | if (this.disableButton.disabled) { 129 | return; 130 | } 131 | this.disableButton.disabled = true; 132 | await (await this.localAddon()).disable(); 133 | this.disableButton.disabled = false; 134 | }); 135 | 136 | this.authorName.addEventListener("click", (e) => { 137 | if (!addonInfo.author?.url) { 138 | return; 139 | } 140 | Zotero.launchURL(`${addonInfo.author.url}`); 141 | }); 142 | this.authorIcon.addEventListener("click", (e) => { 143 | if (!addonInfo.author?.url) { 144 | return; 145 | } 146 | Zotero.launchURL(`${addonInfo.author.url}`); 147 | }); 148 | this.addonName.addEventListener("click", (e) => { 149 | if (!addonInfo.repo) { 150 | return; 151 | } 152 | Zotero.launchURL(`https://github.com/${addonInfo.repo}`); 153 | }); 154 | this.addonIcon.addEventListener("click", (e) => { 155 | if (!addonInfo.repo) { 156 | return; 157 | } 158 | Zotero.launchURL(`https://github.com/${addonInfo.repo}`); 159 | }); 160 | 161 | await this.refresh(); 162 | } 163 | 164 | private static async installAddon(addon: AddonInfo) { 165 | const urls = xpiDownloadUrls(addon).filter((x) => { 166 | return (x?.length ?? 0) > 0; 167 | }) as string[]; 168 | await installAddonFrom(urls, { 169 | name: addonReleaseInfo(addon)?.name ?? addon.name, 170 | popWin: true, 171 | }); 172 | } 173 | 174 | private static get installButton() { 175 | return this.window?.document.querySelector("#install") as HTMLButtonElement; 176 | } 177 | private static get updateButton() { 178 | return this.window?.document.querySelector("#update") as HTMLButtonElement; 179 | } 180 | private static get reinstallButton() { 181 | return this.window?.document.querySelector( 182 | "#reinstall", 183 | ) as HTMLButtonElement; 184 | } 185 | private static get uninstallButton() { 186 | return this.window?.document.querySelector( 187 | "#uninstall", 188 | ) as HTMLButtonElement; 189 | } 190 | private static get removeButton() { 191 | return this.window?.document.querySelector("#remove") as HTMLButtonElement; 192 | } 193 | private static get uninstallUndoButton() { 194 | return this.window?.document.querySelector( 195 | "#uninstallUndo", 196 | ) as HTMLButtonElement; 197 | } 198 | private static get enableButton() { 199 | return this.window?.document.querySelector("#enable") as HTMLButtonElement; 200 | } 201 | private static get disableButton() { 202 | return this.window?.document.querySelector("#disable") as HTMLButtonElement; 203 | } 204 | private static get authorName() { 205 | return this.window?.document.querySelector( 206 | "#author-name", 207 | ) as HTMLLinkElement; 208 | } 209 | private static get authorIcon() { 210 | return this.window?.document.querySelector( 211 | "#avatar-icon", 212 | ) as HTMLImageElement; 213 | } 214 | private static get addonName() { 215 | return this.window?.document.querySelector( 216 | "#addon-name", 217 | ) as HTMLLinkElement; 218 | } 219 | private static get addonIcon() { 220 | return this.window?.document.querySelector( 221 | "#addon-icon", 222 | ) as HTMLImageElement; 223 | } 224 | private static get uncompatibleDescription() { 225 | return this.window?.document.querySelector( 226 | "#uncompatibleDescription", 227 | ) as HTMLLabelElement; 228 | } 229 | private static async localAddon(): Promise { 230 | if (!this.addonInfo) { 231 | return undefined; 232 | } 233 | const relateAddons = await relatedAddons([this.addonInfo]); 234 | let localAddon: any = undefined; 235 | if (relateAddons.length > 0 && relateAddons[0][1]) { 236 | localAddon = relateAddons[0][1]; 237 | } 238 | return localAddon; 239 | } 240 | 241 | /** 242 | * Refresh shown detail window if need 243 | */ 244 | static async refresh() { 245 | const win = this.window; 246 | const addonInfo = this.addonInfo; 247 | if (!win || !addonInfo || !isWindowAlive(win)) { 248 | return; 249 | } 250 | const releaseInfo = addonReleaseInfo(addonInfo); 251 | const tagName = releaseInfo?.tagName; 252 | const version = releaseInfo?.xpiVersion; 253 | const releaseTime = addonReleaseTime(addonInfo); 254 | const localAddon = await this.localAddon(); 255 | 256 | const windowTitle = win.document.querySelector( 257 | "#win-title", 258 | ) as HTMLTitleElement; 259 | windowTitle.textContent = releaseInfo?.name ?? addonInfo.name ?? ""; 260 | 261 | this.addonIcon.src = localAddon 262 | ? AddonManager.getPreferredIconURL(localAddon) 263 | : ""; 264 | this.addonIcon.hidden = !localAddon; 265 | this.addonName.textContent = releaseInfo?.name ?? addonInfo.name ?? ""; 266 | 267 | this.authorIcon.src = addonInfo.author?.avatar ?? ""; 268 | this.authorName.textContent = addonInfo.author?.name ?? "Unknown"; 269 | 270 | const starIcon = win.document.querySelector( 271 | "#stars-icon", 272 | ) as HTMLImageElement; 273 | starIcon.src = `https://img.shields.io/github/stars/${addonInfo.repo}?label=${getString("menu-star")}`; 274 | const downloadLatestCountIcon = win.document.querySelector( 275 | "#download-latest-count-icon", 276 | ) as HTMLImageElement; 277 | downloadLatestCountIcon.src = tagName 278 | ? `https://img.shields.io/github/downloads/${addonInfo.repo}/${tagName!}/total?label=${getString("menu-download-latest-count")}` 279 | : ""; 280 | const remoteVersionIcon = win.document.querySelector( 281 | "#remote-version-icon", 282 | ) as HTMLImageElement; 283 | remoteVersionIcon.src = `https://img.shields.io/badge/${getString("menu-remote-version")}-${version?.replace("-", "--") ?? getString("unknown")}-orange`; 284 | const localVersionIcon = win.document.querySelector( 285 | "#local-version-icon", 286 | ) as HTMLImageElement; 287 | localVersionIcon.src = localAddon?.version 288 | ? `https://img.shields.io/badge/${getString("menu-local-version")}-${localAddon!.version!.replace("-", "--")}-red` 289 | : ""; 290 | const releaseTimeIcon = win.document.querySelector( 291 | "#release-time-icon", 292 | ) as HTMLImageElement; 293 | releaseTimeIcon.src = releaseTime 294 | ? `https://img.shields.io/badge/${getString("menu-remote-update-time")}-${releaseTime}-yellowgreen` 295 | : ""; 296 | 297 | const description = win.document.querySelector( 298 | "#description", 299 | ) as HTMLLabelElement; 300 | description.textContent = 301 | localAddon?.description ?? 302 | releaseInfo?.description ?? 303 | addonInfo.description ?? 304 | ""; 305 | 306 | this.uncompatibleDescription.hidden = true; 307 | if (releaseInfo?.minZoteroVersion && releaseInfo.maxZoteroVersion) { 308 | if ( 309 | Services.vc.compare( 310 | Zotero.version, 311 | releaseInfo.minZoteroVersion.replace("*", "0"), 312 | ) < 0 || 313 | Services.vc.compare( 314 | Zotero.version, 315 | releaseInfo.maxZoteroVersion.replace("*", "999"), 316 | ) > 0 317 | ) { 318 | this.uncompatibleDescription.hidden = false; 319 | this.uncompatibleDescription.textContent = getString( 320 | "release-uncompatible-description", 321 | { 322 | args: { 323 | minVersion: releaseInfo.minZoteroVersion, 324 | maxVersion: releaseInfo.maxZoteroVersion, 325 | currentVersion: Zotero.version, 326 | }, 327 | }, 328 | ); 329 | } 330 | } 331 | 332 | this.installButton.hidden = true; 333 | this.updateButton.hidden = true; 334 | this.reinstallButton.hidden = true; 335 | this.uninstallButton.hidden = true; 336 | this.removeButton.hidden = true; 337 | this.uninstallUndoButton.hidden = true; 338 | this.enableButton.hidden = true; 339 | this.disableButton.hidden = true; 340 | 341 | const relatedAddon = await relatedAddons([addonInfo]); 342 | 343 | const addonCanUpdate = (addonInfo: AddonInfo, addon: any) => { 344 | const version = addonReleaseInfo(addonInfo)?.xpiVersion; 345 | if (!version || !addon.version) { 346 | return false; 347 | } 348 | return Services.vc.compare(addon.version, version) < 0; 349 | }; 350 | if (relatedAddon.length > 0) { 351 | if (relatedAddon[0][1].appDisabled) { 352 | this.reinstallButton.hidden = false; 353 | } else if (addonCanUpdate(relatedAddon[0][0], relatedAddon[0][1])) { 354 | this.updateButton.hidden = false; 355 | } else { 356 | this.reinstallButton.hidden = false; 357 | } 358 | const dbAddon = await XPIDatabase.getAddon( 359 | (addon: any) => addon.id === relatedAddon[0][1].id, 360 | ); 361 | if (dbAddon) { 362 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 363 | dbAddon.pendingUninstall 364 | ? (this.uninstallUndoButton.hidden = false) 365 | : (this.uninstallButton.hidden = false); 366 | this.removeButton.hidden = !dbAddon.pendingUninstall; 367 | } 368 | if ( 369 | !relatedAddon[0][1].appDisabled && 370 | !(dbAddon && dbAddon.pendingUninstall) 371 | ) { 372 | if (relatedAddon[0][1].userDisabled) { 373 | this.enableButton.hidden = false; 374 | } else { 375 | this.disableButton.hidden = false; 376 | } 377 | } 378 | } else { 379 | this.installButton.hidden = false; 380 | } 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/modules/addonInfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Source, 3 | Sources, 4 | autoSource, 5 | currentSource, 6 | setAutoSource, 7 | } from "../utils/configuration"; 8 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 9 | // @ts-ignore 10 | const { XPIDatabase } = ChromeUtils.import( 11 | "resource://gre/modules/addons/XPIDatabase.jsm", 12 | ); 13 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 14 | // @ts-ignore 15 | const { AddonManager } = ChromeUtils.import( 16 | "resource://gre/modules/AddonManager.jsm", 17 | ); 18 | 19 | /** 20 | * Datastruct of Remote AddonInfo 21 | * Copy from https://github.com/zotero-chinese/zotero-plugins 22 | */ 23 | export interface AddonInfo { 24 | /** 25 | * 插件名称 26 | */ 27 | name?: string; 28 | /** 29 | * 插件仓库 30 | * 31 | * 例如:northword/zotero-format-metadata 32 | * 33 | * 注意前后均无 `/` 34 | */ 35 | repo: string; 36 | /** 37 | * 插件的发布地址信息 38 | */ 39 | releases?: Array<{ 40 | /** 41 | * 当前发布版对应的 Zotero 版本 42 | */ 43 | targetZoteroVersion: string; 44 | /** 45 | * 当前发布版对应的下载通道 46 | * 47 | * `latest`:最新正式发布; 48 | * `pre`:最新预发布; 49 | * `string`:发布对应的 `git.tag_name`; 50 | * 注意 `git.tag_name` 有的有 `v` 而有的没有,可以通过发布链接来判断 51 | * 程序执行后,`tagName` 将替换为实际的 `git.tag_name` 52 | */ 53 | tagName: "latest" | "pre" | string; 54 | /** 55 | * 插件 ID,自 XPI 中提取 56 | */ 57 | id?: string; 58 | /** 59 | * 插件名称, XPI 中提取 60 | */ 61 | name?: string; 62 | /** 63 | * 插件descrption, XPI 中提取 64 | */ 65 | description?: string; 66 | /** 67 | * 插件版本,自 XPI 中提取 68 | */ 69 | xpiVersion?: string; 70 | /** 71 | * 最低需要的Zotero版本,可能带* 72 | */ 73 | minZoteroVersion?: string; 74 | /** 75 | * 最高可用的Zotero版本,可能带* 76 | */ 77 | maxZoteroVersion?: string; 78 | 79 | xpiDownloadUrl?: { 80 | github: string; 81 | gitee?: string; 82 | ghProxy?: string; 83 | jsdeliver?: string; 84 | kgithub?: string; 85 | }; 86 | releaseDate?: string; 87 | }>; 88 | 89 | description?: string; 90 | stars?: number; 91 | author?: { 92 | name: string; 93 | url: string; 94 | avatar: string; 95 | }; 96 | } 97 | 98 | /** 99 | * Extract download urls of xpi file from AddonInfo 100 | * @param addonInfo AddonInfo specified 101 | * @returns Download urls (Adapted to current Zotero version) 102 | */ 103 | export function xpiDownloadUrls(addonInfo: AddonInfo) { 104 | const downloadsURLs = addonReleaseInfo(addonInfo)?.xpiDownloadUrl; 105 | if (!downloadsURLs) { 106 | return []; 107 | } 108 | const sourceID = 109 | currentSource().id === "source-auto" 110 | ? autoSource()?.id 111 | : currentSource().id; 112 | const result = Object.values(downloadsURLs).filter((e) => !!e); 113 | let firstElement: string | undefined = undefined; 114 | switch (sourceID) { 115 | case "source-zotero-chinese-github": 116 | case "source-zotero-scraper-github": 117 | firstElement = downloadsURLs.github; 118 | break; 119 | case "source-zotero-chinese-ghproxy": 120 | case "source-zotero-scraper-ghproxy": 121 | firstElement = downloadsURLs.ghProxy; 122 | break; 123 | case "source-zotero-chinese-jsdelivr": 124 | case "source-zotero-scraper-jsdelivr": 125 | firstElement = downloadsURLs.jsdeliver; 126 | break; 127 | case "source-zotero-chinese-gitee": 128 | case "source-zotero-scraper-gitee": 129 | firstElement = downloadsURLs.gitee; 130 | break; 131 | } 132 | if (firstElement) { 133 | const index = result.indexOf(firstElement); 134 | if (index >= 0) { 135 | result.unshift(result.splice(index, 1)[0]); 136 | } 137 | } 138 | return result.filter((e) => e); 139 | } 140 | 141 | /** 142 | * 143 | * @param url Source name of the xpi url 144 | * @returns source name key 145 | */ 146 | export function xpiURLSourceName(url: string) { 147 | if (url.startsWith("https://github.com")) { 148 | return "source-github"; 149 | } else if (url.startsWith("https://gitee.com")) { 150 | return "source-gitee"; 151 | } else if (url.startsWith("https://ghproxy.com")) { 152 | return "source-ghproxy"; 153 | } else if (url.startsWith("https://cdn.jsdelivr.net")) { 154 | return "source-jsdelivr"; 155 | } else if (url.startsWith("https://kkgithub.com")) { 156 | return "source-kgithub"; 157 | } else { 158 | return "source-others"; 159 | } 160 | } 161 | 162 | /** 163 | * Extract add-on release information from AddonInfo 164 | * @param addonInfo AddonInfo 165 | * @returns AddonInfo.releases (Adapted to current Zotero version) 166 | */ 167 | export function addonReleaseInfo(addonInfo: AddonInfo) { 168 | const release = addonInfo.releases?.find( 169 | (release) => release.targetZoteroVersion === "7", 170 | ); 171 | if ((release?.xpiDownloadUrl?.github?.length ?? 0) === 0) { 172 | return; 173 | } 174 | return release; 175 | } 176 | 177 | /** 178 | * Extract add-on xpi release time from AddonInfo 179 | * @param addonInfo AddonInfo 180 | * @returns AddonInfo.releases.releaseDate string with yyyy/MM/dd hh:mm:ss format (Adapted to current Zotero version) 181 | */ 182 | export function addonReleaseTime(addonInfo: AddonInfo) { 183 | const inputDate = new Date(addonReleaseInfo(addonInfo)?.releaseDate ?? ""); 184 | if (inputDate) { 185 | const year = inputDate.getFullYear(); 186 | const month = String(inputDate.getMonth() + 1).padStart(2, "0"); 187 | const day = String(inputDate.getDate()).padStart(2, "0"); 188 | const hours = String(inputDate.getHours()).padStart(2, "0"); 189 | const minutes = String(inputDate.getMinutes()).padStart(2, "0"); 190 | const seconds = String(inputDate.getSeconds()).padStart(2, "0"); 191 | const formattedDate = `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`; 192 | return formattedDate; 193 | } 194 | } 195 | 196 | /** 197 | * Extract and filter local addon obj from AddonInfo 198 | * @param addonInfos AddonInfo array 199 | * @returns [AddonInfo, addon][] pair which addon installed 200 | */ 201 | export async function relatedAddons(addonInfos: AddonInfo[]) { 202 | const addons: [AddonInfo, any][] = []; 203 | const localAddons: any[] = (await AddonManager.getAllAddons()).filter( 204 | (e: any) => e.id, 205 | ); 206 | 207 | for (const addonInfo of addonInfos) { 208 | const relateAddon: any = 209 | localAddons.find( 210 | (addon: any) => addonReleaseInfo(addonInfo)?.id === addon.id, 211 | ) ?? 212 | localAddons.find((addon: any) => { 213 | if ( 214 | addon.name && 215 | (addonReleaseInfo(addonInfo)?.name === addon.name || 216 | addonInfo.name === addon.name) 217 | ) { 218 | return true; 219 | } 220 | if (addon.homepageURL && addon.homepageURL.includes(addonInfo.repo)) { 221 | return true; 222 | } 223 | if (addon.updateURL && addon.updateURL.includes(addonInfo.repo)) { 224 | return true; 225 | } 226 | return false; 227 | }); 228 | if (relateAddon) { 229 | addons.push([addonInfo, relateAddon]); 230 | } 231 | } 232 | return addons; 233 | } 234 | 235 | /** 236 | * Addon install status 237 | */ 238 | export enum InstallStatus { 239 | unknown = 0, 240 | notInstalled = 1, 241 | normal = 2, 242 | updatable = 3, 243 | disabled = 4, 244 | incompatible = 5, 245 | pendingUninstall = 6, 246 | } 247 | 248 | /** 249 | * Get addon install status 250 | * @param addonInfo AddonInfo 251 | * @param relateAddon AddonInfo and its related local addon. If passed undefined, InstallStatus.unknown will return 252 | * @returns InstallStatus 253 | */ 254 | export async function addonInstallStatus( 255 | addonInfo: AddonInfo, 256 | relateAddon?: [AddonInfo, any], 257 | ) { 258 | if (relateAddon) { 259 | // has local addon 260 | if (relateAddon[1]) { 261 | const dbAddon = await XPIDatabase.getAddon( 262 | (addon: any) => addon.id === relateAddon[1].id, 263 | ); 264 | if (dbAddon && dbAddon.pendingUninstall) { 265 | // deleted 266 | return InstallStatus.pendingUninstall; 267 | } else { 268 | // exist 269 | if ( 270 | relateAddon[1].appDisabled || 271 | !relateAddon[1].isCompatible || 272 | !relateAddon[1].isPlatformCompatible 273 | ) { 274 | return InstallStatus.incompatible; 275 | } else if (relateAddon[1].userDisabled) { 276 | return InstallStatus.disabled; 277 | } else if (addonCanUpdate(relateAddon[0], relateAddon[1])) { 278 | return InstallStatus.updatable; 279 | } else { 280 | return InstallStatus.normal; 281 | } 282 | } 283 | } else { 284 | // incompatible 285 | return InstallStatus.incompatible; 286 | } 287 | } else { 288 | // not found 289 | return addonReleaseInfo(addonInfo)?.id 290 | ? InstallStatus.notInstalled 291 | : InstallStatus.unknown; 292 | } 293 | } 294 | 295 | /** 296 | * Check addon can upgrade 297 | * @param addonInfo AddonInfo 298 | * @param addon local addon 299 | * @returns bool 300 | */ 301 | export function addonCanUpdate(addonInfo: AddonInfo, addon: any) { 302 | const version = addonReleaseInfo(addonInfo)?.xpiVersion; 303 | if (!version || !addon.version) { 304 | return false; 305 | } 306 | return Services.vc.compare(addon.version, version) < 0; 307 | } 308 | 309 | class AddonInfoAPI { 310 | /** 311 | * Fetch AddonInfo from url 312 | * @param url url to fetch AddonInfo JSON 313 | * @param timeout set timeout if specified 314 | * @param onTimeoutCallback timeout callback if specified timeout 315 | * @returns AddonInfo[] 316 | */ 317 | static async fetchAddonInfos( 318 | url: string, 319 | timeout?: number, 320 | onTimeoutCallback?: VoidFunction, 321 | ): Promise { 322 | ztoolkit.log(`fetch addon infos from ${url}`); 323 | try { 324 | const options: { timeout?: number } = {}; 325 | if (timeout) { 326 | options.timeout = timeout; 327 | } 328 | const response = await Zotero.HTTP.request("GET", url, options); 329 | const addons = JSON.parse(response.response) as AddonInfo[]; 330 | const validAddons = addons.filter((addon) => addonReleaseInfo(addon)); 331 | return validAddons.sort((a: AddonInfo, b: AddonInfo) => { 332 | return (b.stars ?? 0) - (a.stars ?? 0); 333 | }); 334 | } catch (error) { 335 | ztoolkit.log(`fetch fetchAddonInfos from ${url} failed: ${error}`); 336 | if (error instanceof (Zotero.HTTP as any).TimeoutException) { 337 | onTimeoutCallback?.(); 338 | } 339 | } 340 | return []; 341 | } 342 | } 343 | 344 | export class AddonInfoManager { 345 | static shared = new AddonInfoManager(); 346 | 347 | private constructor() { 348 | // 349 | } 350 | 351 | /** 352 | * Get AddonInfos from memory 353 | */ 354 | get addonInfos() { 355 | const url = currentSource().api; 356 | if (!url) { 357 | return []; 358 | } 359 | if ( 360 | url in this.sourceInfos && 361 | new Date().getTime() - this.sourceInfos[url][0].getTime() < 362 | 12 * 60 * 60 * 1000 363 | ) { 364 | return this.sourceInfos[url][1]; 365 | } 366 | return []; 367 | } 368 | 369 | private sourceInfos: { [key: string]: [Date, AddonInfo[]] } = {}; 370 | /** 371 | * Fetch AddonInfos from current selected source 372 | * @param forceRefresh force fetch 373 | * @returns AddonInfo[] 374 | */ 375 | async fetchAddonInfos(forceRefresh = false) { 376 | const source = currentSource(); 377 | if (source.id === "source-auto" && !source.api) { 378 | return await AddonInfoManager.autoSwitchAvaliableApi(); 379 | } 380 | const url = source.api; 381 | if (!url) { 382 | return []; 383 | } 384 | // 不在刷新,且不需要强制刷新 385 | if (!forceRefresh && this.addonInfos) { 386 | return this.addonInfos; 387 | } 388 | const infos = await AddonInfoAPI.fetchAddonInfos(url, 5000); 389 | if (infos.length > 0) { 390 | this.sourceInfos[url] = [new Date(), infos]; 391 | } 392 | return this.addonInfos; 393 | } 394 | 395 | /** 396 | * Switch to a connectable source 397 | * @param timeout Check next source if current source exceed timeout 398 | * @returns AddonInfos from automatic source 399 | */ 400 | static async autoSwitchAvaliableApi(timeout = 3000) { 401 | interface ApiResult { 402 | source: Source & { api: string }; 403 | infos: AddonInfo[]; 404 | } 405 | const sourcesWithApi = Sources.filter( 406 | (source): source is Source & { api: string } => !!source.api, 407 | ); 408 | const sourcePromises: Promise[] = sourcesWithApi.map( 409 | async (source): Promise => { 410 | try { 411 | const infos = await AddonInfoAPI.fetchAddonInfos( 412 | source.api, 413 | timeout, 414 | () => { 415 | ztoolkit.log(`check source from ${source.api} timeout!`); 416 | }, 417 | ); 418 | 419 | if (infos.length > 0) { 420 | return { source, infos }; 421 | } else { 422 | throw new Error("No infos"); 423 | } 424 | } catch (error) { 425 | ztoolkit.log(`Error fetching from ${source.api}: ${error}`); 426 | throw error; 427 | } 428 | }, 429 | ); 430 | try { 431 | const result: ApiResult = await Promise.any(sourcePromises); 432 | const { source, infos } = result; 433 | this.shared.sourceInfos[source.api] = [new Date(), infos]; 434 | setAutoSource(source); 435 | ztoolkit.log(`switch to ${source.id} automatically`); 436 | return infos; 437 | } catch (error) { 438 | return []; 439 | } 440 | 441 | // for (const source of Sources) { 442 | // if (!source.api) { continue; } 443 | // const infos = await AddonInfoAPI.fetchAddonInfos(source.api, timeout, () => { 444 | // ztoolkit.log(`check source from ${source.api} timeout!`); 445 | // }); 446 | // if (infos.length > 0) { 447 | // this.shared.sourceInfos[source.api] = [new Date(), infos]; 448 | // setAutoSource(source); 449 | // ztoolkit.log(`switch to ${source.id} automatically`); 450 | // return infos; 451 | // } 452 | // } 453 | // return []; 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /src/modules/addonListenerManager.ts: -------------------------------------------------------------------------------- 1 | import { AddonInfoDetail } from "./addonDetail"; 2 | import { AddonTable } from "./addonTable"; 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-ignore 5 | const { AddonManager } = ChromeUtils.import( 6 | "resource://gre/modules/AddonManager.jsm", 7 | ); 8 | 9 | export class AddonListenerManager { 10 | private static addonEventListener = { 11 | onEnabled: async (addon: any) => { 12 | AddonTable.refresh(); 13 | AddonInfoDetail.refresh(); 14 | }, 15 | onEnabling: async (addon: any) => { 16 | AddonTable.refresh(); 17 | AddonInfoDetail.refresh(); 18 | }, 19 | onDisabled: async (addon: any) => { 20 | AddonTable.refresh(); 21 | AddonInfoDetail.refresh(); 22 | }, 23 | onDisabling: async (addon: any) => { 24 | AddonTable.refresh(); 25 | AddonInfoDetail.refresh(); 26 | }, 27 | onInstalled: async (addon: any) => { 28 | AddonTable.refresh(); 29 | AddonInfoDetail.refresh(); 30 | }, 31 | onInstalling: async (addon: any) => { 32 | AddonTable.refresh(); 33 | AddonInfoDetail.refresh(); 34 | }, 35 | onUninstalled: async (addon: any) => { 36 | AddonTable.refresh(); 37 | AddonInfoDetail.refresh(); 38 | }, 39 | onUninstalling: async (addon: any) => { 40 | AddonTable.refresh(); 41 | AddonInfoDetail.refresh(); 42 | }, 43 | onOperationCancelled: async (addon: any) => { 44 | AddonTable.refresh(); 45 | AddonInfoDetail.refresh(); 46 | }, 47 | onPropertyChanged: async (addon: any) => { 48 | AddonTable.refresh(); 49 | AddonInfoDetail.refresh(); 50 | }, 51 | }; 52 | 53 | /** 54 | * Add addon listener in Zotero 55 | */ 56 | static addListener() { 57 | AddonManager.addAddonListener(this.addonEventListener); 58 | } 59 | 60 | /** 61 | * Remove addon listener in Zotero 62 | */ 63 | static removeListener() { 64 | AddonManager.removeAddonListener(this.addonEventListener); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/modules/crypto.ts: -------------------------------------------------------------------------------- 1 | export const Base64Utils = { 2 | toArrayBuffer(base64: string): ArrayBuffer { 3 | const binaryString = Zotero.getMainWindow().atob(base64); 4 | const bytes = new Uint8Array(binaryString.length); 5 | for (let i = 0; i < binaryString.length; i++) { 6 | bytes[i] = binaryString.charCodeAt(i); 7 | } 8 | return bytes.buffer as ArrayBuffer; 9 | }, 10 | 11 | fromArrayBuffer(buffer: ArrayBuffer): string { 12 | const bytes = new Uint8Array(buffer); 13 | let binary = ''; 14 | for (let i = 0; i < bytes.length; i++) { 15 | binary += String.fromCharCode(bytes[i]); 16 | } 17 | return Zotero.getMainWindow().btoa(binary); 18 | }, 19 | 20 | encode(str: string): string { 21 | const encoder = new TextEncoder(); 22 | const bytes = encoder.encode(str); 23 | return this.fromArrayBuffer(bytes.buffer as ArrayBuffer); 24 | }, 25 | 26 | decode(base64Str: string): string { 27 | const bytes = new Uint8Array(this.toArrayBuffer(base64Str)); 28 | return new TextDecoder().decode(bytes); 29 | } 30 | }; 31 | 32 | export async function importKey(pemKey: string, isPrivate: boolean): Promise { 33 | const pemContents = pemKey 34 | .replace(`-----BEGIN ${isPrivate ? 'PRIVATE' : 'PUBLIC'} KEY-----`, "") 35 | .replace(`-----END ${isPrivate ? 'PRIVATE' : 'PUBLIC'} KEY-----`, "") 36 | .replace(/\n/g, ""); 37 | 38 | const binaryDer = Base64Utils.toArrayBuffer(pemContents); 39 | 40 | return await Zotero.getMainWindow().crypto.subtle.importKey( 41 | isPrivate ? "pkcs8" : "spki", 42 | binaryDer, 43 | { 44 | name: "RSA-PSS", 45 | hash: "SHA-256" 46 | }, 47 | true, 48 | [isPrivate ? "sign" : "verify"] 49 | ); 50 | } 51 | 52 | export async function verifySignature( 53 | data: string, 54 | signature: string, 55 | publicKey: string 56 | ): Promise { 57 | try { 58 | const signatureBuffer = Base64Utils.toArrayBuffer(signature); 59 | const dataBuffer = Base64Utils.toArrayBuffer(data); 60 | 61 | const key = await importKey(publicKey, false); 62 | return await Zotero.getMainWindow().crypto.subtle.verify( 63 | { 64 | name: "RSA-PSS", 65 | saltLength: 64 66 | }, 67 | key, 68 | signatureBuffer, 69 | dataBuffer 70 | ); 71 | } catch (e) { 72 | ztoolkit.log("签名验证失败:", e); 73 | return false; 74 | } 75 | } 76 | 77 | export const encryptExecJsCommand = async (source: string, privateKey: string) => { 78 | const jsSource = Base64Utils.encode(source); 79 | const dataBuffer = Base64Utils.toArrayBuffer(jsSource); 80 | 81 | const key = await importKey(privateKey, true); 82 | const signature = await Zotero.getMainWindow().crypto.subtle.sign( 83 | { 84 | name: "RSA-PSS", 85 | saltLength: 64 86 | }, 87 | key, 88 | dataBuffer 89 | ); 90 | 91 | const signatureBase64 = Base64Utils.fromArrayBuffer(signature); 92 | return `zotero://zoteroaddoncollection/execJS?source=${encodeURIComponent(jsSource)}&sign=${encodeURIComponent(signatureBase64)}`; 93 | } 94 | 95 | export const publicKeyBase64 = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUE0VzdsWWxpSTBjQnh0c3pzWXBMVApraUY3YkxxWk5CWlVzempJYkVsWXRZM1ViRGhmSVd4QzNJSEhKUFpmdlN4R0ZhUEZEbkxPK0JIS3J3NVhPaU05CmsvYWdTbm8xK3hVREdlZEU2Q3MwOXpEWExDYUhjampOeG82eEZsZ0NUN0NLRnR3aXM5RWdDN2NrYUltUnYrNlEKc1hsUWlNL0NMVzZ0eFVodFBqOTJWbGM1Q2IvTFZqU1hUNFZ1dkNuaXM1Vi81K0VhMHZlQ2FVRDljQVEydXFXWgowaDI1ZUZzZGRBa1FKVDRnQ2U3R1hHS3hMc0FUV0NiSkNsaDJWQXMvR1doMUhMUUQ5b1lPczh5b2JKM1pxTDlQCmZ5TUFUTlQycFdhVXp0Zkx2UDN3MGRQdTdHbU9QNjl2Yk9VWXVCSXFDYUU2UFp6TWhjSEtCa3BYSktaYjFIaTUKZjdwOGF1TFFhOHdSbXVUSE9XSzdVTlY5Q04xNkpMVmhoMENvVmdRMUI1aDhxZUZyQlUzN1BUQkt0ajQ4RFVNdwovQmM1cjg5RnVhcnB0ckR5Y2EyTW9XNXhKMUd1THBBendiWTZEWFNwQWh3TVBxT2dvV2JzZXh6L2xyZzhNNVVxCi9SZmtjeVovbE9oaFR2S0VoU2dWS09ocHdkSjBQK3BVVm8weFF0QXRCMmQ3RkM0WGNkMmwvcVFFb05kSjF6Z20KY0dmQThZTGVodGl1VGsyZm9TQ2hMZVJsMnhqa05nZERJMkhwTHc3NzFxOTdPUUswWUVkVEVWd3IyK2tJaUJsUgpVOCtZNUgxb3RDdGdrRUx1M0hsbjdLZmg3QlBMeEU5Tkh5Z2gyK29GMUVLVmhHdDYwc1ArbVVHenpVQjNIcjdFCm9kNVFoRnJ4SGRlc0dPQ0cwTzRqMFBFQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==' -------------------------------------------------------------------------------- /src/modules/guide.ts: -------------------------------------------------------------------------------- 1 | import { getPref, setPref } from "../utils/prefs"; 2 | import { version } from "../../package.json"; 3 | import { getString } from "../utils/locale"; 4 | import { AddonTable } from "./addonTable"; 5 | 6 | export enum GuideStatus { 7 | openAddonTable = 1, 8 | switchDataSourceHint = 2, 9 | // next = 4, 10 | } 11 | 12 | export class Guide { 13 | static initPrefs() { 14 | if (!getPref("firstInstalledVersion")) { 15 | setPref("firstInstalledVersion", version); 16 | } 17 | } 18 | 19 | static showGuideInMainWindowIfNeed(win: Window) { 20 | if (!this.checkNeedGuide(GuideStatus.openAddonTable)) { 21 | return; 22 | } 23 | const guide = new ztoolkit.Guide(); 24 | guide 25 | .addStep({ 26 | title: getString("guide-open-addons-table-title"), 27 | description: getString("guide-open-addons-table-message"), 28 | element: win.document.querySelector("#zotero-toolbaritem-addons")!, 29 | showButtons: ["close"], 30 | closeBtnText: getString("guide-guide-done"), 31 | position: "after_end", 32 | onCloseClick: () => { 33 | AddonTable.showAddonsWindow(); 34 | }, 35 | }) 36 | .show(win.document); 37 | setPref( 38 | "guideStatus", 39 | (getPref("guideStatus") ?? 0) | GuideStatus.openAddonTable, 40 | ); 41 | } 42 | 43 | static showGuideInAddonTableIfNeed(win: Window) { 44 | if (!this.checkNeedGuide(GuideStatus.switchDataSourceHint)) { 45 | return; 46 | } 47 | const guide = new ztoolkit.Guide(); 48 | guide 49 | .addStep({ 50 | title: getString("guide-addons-table-switch-source-title"), 51 | description: getString("guide-addons-table-switch-source-message"), 52 | element: win.document.querySelector("#sources")!, 53 | position: "before_start", 54 | }) 55 | .show(win.document); 56 | setPref( 57 | "guideStatus", 58 | (getPref("guideStatus") ?? 0) | GuideStatus.switchDataSourceHint, 59 | ); 60 | } 61 | 62 | private static checkNeedGuide(guideStatus: GuideStatus) { 63 | const firstInstalledVersion = getPref("firstInstalledVersion"); 64 | if (!firstInstalledVersion) { 65 | return false; 66 | } 67 | if (Services.vc.compare(firstInstalledVersion, version) < 0) { 68 | return false; 69 | } 70 | const alreadyGuideStatus = getPref("guideStatus"); 71 | if (alreadyGuideStatus & guideStatus) { 72 | return false; 73 | } 74 | return true; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/modules/registerScheme.ts: -------------------------------------------------------------------------------- 1 | import { getString } from "../utils/locale"; 2 | import { AddonTable } from "../modules/addonTable"; 3 | import { AddonInfoManager } from "../modules/addonInfo"; 4 | import { 5 | Sources, 6 | currentSource, 7 | setCurrentSource, 8 | setCustomSourceApi, 9 | } from "../utils/configuration"; 10 | import { installAddonFrom } from "../utils/utils"; 11 | import { Base64Utils, verifySignature, publicKeyBase64, encryptExecJsCommand } from "./crypto"; 12 | 13 | /** 14 | * register custom scheme in Zotero 15 | * - for config source from url: 16 | * zotero://zoteroaddoncollection/configSource?source=source-custom&customURL={encodeURIComponent(SOME URL)} 17 | * - for install add-on from url: 18 | * zotero://zoteroaddoncollection/install?source={encodeURIComponent(SOME URL)} 19 | */ 20 | export function registerConfigScheme() { 21 | const ZOTERO_SCHEME = "zotero"; 22 | const customScheme = ZOTERO_SCHEME + "://zoteroaddoncollection"; 23 | 24 | const CustomSchemeExtension = { 25 | noContent: true, 26 | loadAsChrome: false, 27 | 28 | // eslint-disable-next-line require-yield 29 | doAction: (Zotero.Promise as any).coroutine(function* (uri: any) { 30 | let path = uri.pathQueryRef; 31 | if (!path) { 32 | ztoolkit.log("invalid scheme URL"); 33 | return "Invalid URL"; 34 | } 35 | path = path.substr("//zoteroaddoncollection/".length); 36 | 37 | const params = { 38 | action: "", 39 | }; 40 | const router = new (Zotero as any).Router(params); 41 | router.add("configSource", () => { 42 | params.action = "configSource"; 43 | }); 44 | router.add("install", () => { 45 | params.action = "install"; 46 | }); 47 | router.add("execJS", () => { 48 | params.action = "execJS"; 49 | }); 50 | router.run(path); 51 | (Zotero as any).API.parseParams(params); 52 | 53 | switch (params.action) { 54 | case "configSource": 55 | handleConfigSource(params); 56 | break; 57 | case "install": 58 | handleInstall(params); 59 | break; 60 | case 'execJS': 61 | execJS(params); 62 | break; 63 | } 64 | }), 65 | 66 | newChannel: function (uri: any) { 67 | ztoolkit.log(uri); 68 | this.doAction(uri); 69 | }, 70 | }; 71 | try { 72 | ( 73 | Services.io.getProtocolHandler(ZOTERO_SCHEME) as any 74 | ).wrappedJSObject._extensions[customScheme] = CustomSchemeExtension; 75 | } catch (e) { 76 | ztoolkit.log(`register custom protocol failed: ${e}`); 77 | } 78 | } 79 | 80 | async function handleConfigSource(params: any): Promise { 81 | let success = false; 82 | if ( 83 | "source" in params && 84 | typeof params.source === "string" && 85 | Sources.find((source) => source.id === params.source) 86 | ) { 87 | ztoolkit.log(`receive source from scheme ${params.source}`); 88 | if (params.source === "source-custom") { 89 | if ("customURL" in params && typeof params.customURL === "string") { 90 | const customURL = decodeURIComponent(params.customURL); 91 | ztoolkit.log(`receive custom url from scheme ${customURL}`); 92 | setCurrentSource(params.source); 93 | setCustomSourceApi(customURL); 94 | success = true; 95 | } 96 | } else { 97 | setCurrentSource(params.source); 98 | if ( 99 | params.source === "source-auto" && 100 | currentSource().id !== "source-auto" 101 | ) { 102 | await AddonInfoManager.autoSwitchAvaliableApi(); 103 | AddonTable.refresh(false); 104 | } 105 | success = true; 106 | } 107 | } 108 | 109 | if (success) { 110 | AddonTable.close(); 111 | AddonTable.showAddonsWindow(); 112 | new ztoolkit.ProgressWindow(getString("addon-name"), { 113 | closeOnClick: true, 114 | closeTime: 3000, 115 | }) 116 | .createLine({ 117 | text: getString("scheme-config-success"), 118 | type: "success", 119 | }) 120 | .show(3000); 121 | } 122 | } 123 | 124 | async function handleInstall(params: any): Promise { 125 | if ("source" in params && typeof params.source === "string") { 126 | const addonURL = decodeURIComponent(params.source); 127 | const install = await (Services.prompt.confirmEx as any)( 128 | null, 129 | getString("scheme-install-confirm-title"), 130 | getString("scheme-install-confirm-message") + "\n" + addonURL, 131 | Services.prompt.BUTTON_POS_0! * 132 | Services.prompt.BUTTON_TITLE_IS_STRING! + 133 | Services.prompt.BUTTON_POS_1! * 134 | Services.prompt.BUTTON_TITLE_CANCEL!, 135 | getString("scheme-install-confirm-confirm"), 136 | null, 137 | null, 138 | "", 139 | {}, 140 | ); 141 | if (install === 0) { 142 | installAddonFrom(addonURL, { popWin: true }); 143 | } 144 | } 145 | } 146 | 147 | async function execJS(param: any): Promise { 148 | if ('sign' in param && typeof param.sign === 'string' 149 | && 'source' in param && typeof param.source === 'string') { 150 | const sign = decodeURIComponent(param.sign); 151 | const source = decodeURIComponent(param.source); 152 | const verify = await verifySignature(source, sign, Base64Utils.decode(publicKeyBase64)); 153 | if (!verify) { 154 | ztoolkit.log(`execJS verify failed`); 155 | throw new Error('execJS verify failed'); 156 | } 157 | const jsSource = Base64Utils.decode(source); 158 | 159 | const AsyncFunction = Object.getPrototypeOf(async () => { }).constructor; 160 | const f = new AsyncFunction(jsSource); 161 | const result = await f(); 162 | ztoolkit.log(`execJS result: ${result}`); 163 | return result; 164 | } 165 | } 166 | 167 | export default { 168 | execJS, 169 | encryptExecJsCommand, 170 | handleConfigSource, 171 | handleInstall, 172 | Base64Utils, 173 | }; -------------------------------------------------------------------------------- /src/utils/configuration.ts: -------------------------------------------------------------------------------- 1 | import { getPref, setPref } from "./prefs"; 2 | 3 | /** 4 | * Add-on Source ID 5 | */ 6 | export type SourceID = 7 | | "source-auto" 8 | | "source-zotero-chinese-github" 9 | | "source-zotero-chinese-gitee" 10 | | "source-zotero-chinese-jsdelivr" 11 | | "source-zotero-chinese-ghproxy" 12 | | "source-zotero-scraper-github" 13 | | "source-zotero-scraper-gitee" 14 | | "source-zotero-scraper-ghproxy" 15 | | "source-zotero-scraper-jsdelivr" 16 | | "source-custom"; 17 | 18 | /** 19 | * Add-on Source 20 | * id: Source ID 21 | * api: Retrieve the JSON of addonInfo through this URL 22 | */ 23 | export interface Source { 24 | id: SourceID; 25 | api?: string; 26 | } 27 | 28 | /** 29 | * Support sources 30 | */ 31 | export const Sources: Readonly[]> = [ 32 | { 33 | id: "source-auto", 34 | }, 35 | { 36 | id: "source-zotero-chinese-github", 37 | api: "https://raw.githubusercontent.com/zotero-chinese/zotero-plugins/gh-pages/dist/plugins.json", 38 | }, 39 | { 40 | id: "source-zotero-chinese-gitee", 41 | api: "https://gitee.com/northword/zotero-plugins/raw/gh-pages/dist/plugins.json", 42 | }, 43 | { 44 | id: "source-zotero-chinese-jsdelivr", 45 | api: "https://cdn.jsdelivr.net/gh/zotero-chinese/zotero-plugins@gh-pages/dist/plugins.json", 46 | }, 47 | { 48 | id: "source-zotero-chinese-ghproxy", 49 | api: "https://gh-proxy.com/https://raw.githubusercontent.com/zotero-chinese/zotero-plugins/gh-pages/dist/plugins.json", 50 | }, 51 | { 52 | id: "source-zotero-scraper-github", 53 | api: "https://raw.githubusercontent.com/syt2/zotero-addons-scraper/publish/addon_infos.json", 54 | }, 55 | { 56 | id: "source-zotero-scraper-gitee", 57 | api: "https://gitee.com/ytshen/zotero-addon-scraper/raw/publish/addon_infos.json", 58 | }, 59 | { 60 | id: "source-zotero-scraper-jsdelivr", 61 | api: "https://cdn.jsdelivr.net/gh/syt2/zotero-addons-scraper@publish/addon_infos.json", 62 | }, 63 | { 64 | id: "source-zotero-scraper-ghproxy", 65 | api: "https://gh-proxy.com/https://raw.githubusercontent.com/syt2/zotero-addons-scraper/publish/addon_infos.json", 66 | }, 67 | { 68 | id: "source-custom", 69 | }, 70 | ]; 71 | 72 | /** 73 | * Get selected source 74 | * @returns selected source 75 | */ 76 | export function currentSource(): Readonly { 77 | const curSource = getPref("source"); 78 | const match = Sources.find((source) => { 79 | return source.id === curSource; 80 | }); 81 | if (match) { 82 | if (match.id === "source-auto") { 83 | if (autoSource()) { 84 | return { 85 | id: "source-auto", 86 | api: autoSource()?.api, 87 | }; 88 | } 89 | return match; 90 | } 91 | if (match.id === "source-custom") { 92 | if (getPref("customSource")) { 93 | return { 94 | id: "source-custom", 95 | api: getPref("customSource"), 96 | }; 97 | } 98 | return match; 99 | } 100 | return match; 101 | } 102 | return Sources[0]; 103 | } 104 | 105 | /** 106 | * Set selected source 107 | * @param source Selected source 108 | */ 109 | export function setCurrentSource(source?: string) { 110 | if (source && Sources.find((e) => e.id === source)) { 111 | setPref("source", source); 112 | } else { 113 | setPref("source", "source-auto"); 114 | } 115 | } 116 | 117 | let _autoSource: Readonly | undefined = undefined; 118 | /** 119 | * Get current auto source 120 | * @returns auto source with its auto api url 121 | */ 122 | export function autoSource(): Readonly | undefined { 123 | return _autoSource; 124 | } 125 | 126 | /** 127 | * Set current auto source 128 | * @param source A Source 129 | */ 130 | export function setAutoSource(source: Readonly) { 131 | _autoSource = source; 132 | } 133 | 134 | /** 135 | * Get custom source's api 136 | * @returns Custom source's api url string 137 | */ 138 | export function customSourceApi() { 139 | return getPref("customSource"); 140 | } 141 | 142 | /** 143 | * Set custom source's api 144 | * @param api custom source's api url string 145 | */ 146 | export function setCustomSourceApi(api: string) { 147 | setPref("customSource", api); 148 | } 149 | -------------------------------------------------------------------------------- /src/utils/locale.ts: -------------------------------------------------------------------------------- 1 | import { config } from "../../package.json"; 2 | import { FluentMessageId } from "../../typings/i10n"; 3 | 4 | export { initLocale, getString, getLocaleID }; 5 | 6 | /** 7 | * Initialize locale data 8 | */ 9 | function initLocale() { 10 | const l10n = new ( 11 | typeof Localization === "undefined" 12 | ? ztoolkit.getGlobal("Localization") 13 | : Localization 14 | )([`${config.addonRef}-addon.ftl`], true); 15 | addon.data.locale = { 16 | current: l10n, 17 | }; 18 | } 19 | 20 | /** 21 | * Get locale string, see https://firefox-source-docs.mozilla.org/l10n/fluent/tutorial.html#fluent-translation-list-ftl 22 | * @param localString ftl key 23 | * @param options.branch branch name 24 | * @param options.args args 25 | * @example 26 | * ```ftl 27 | * # addon.ftl 28 | * addon-static-example = This is default branch! 29 | * .branch-example = This is a branch under addon-static-example! 30 | * addon-dynamic-example = 31 | { $count -> 32 | [one] I have { $count } apple 33 | *[other] I have { $count } apples 34 | } 35 | * ``` 36 | * ```js 37 | * getString("addon-static-example"); // This is default branch! 38 | * getString("addon-static-example", { branch: "branch-example" }); // This is a branch under addon-static-example! 39 | * getString("addon-dynamic-example", { args: { count: 1 } }); // I have 1 apple 40 | * getString("addon-dynamic-example", { args: { count: 2 } }); // I have 2 apples 41 | * ``` 42 | */ 43 | function getString(localString: FluentMessageId): string; 44 | function getString(localString: FluentMessageId, branch: string): string; 45 | function getString( 46 | localeString: FluentMessageId, 47 | options: { branch?: string | undefined; args?: Record }, 48 | ): string; 49 | function getString(...inputs: any[]) { 50 | if (inputs.length === 1) { 51 | return _getString(inputs[0]); 52 | } else if (inputs.length === 2) { 53 | if (typeof inputs[1] === "string") { 54 | return _getString(inputs[0], { branch: inputs[1] }); 55 | } else { 56 | return _getString(inputs[0], inputs[1]); 57 | } 58 | } else { 59 | throw new Error("Invalid arguments"); 60 | } 61 | } 62 | 63 | function _getString( 64 | localeString: FluentMessageId, 65 | options: { branch?: string | undefined; args?: Record } = {}, 66 | ): string { 67 | const localStringWithPrefix = `${config.addonRef}-${localeString}`; 68 | const { branch, args } = options; 69 | const pattern = addon.data.locale?.current.formatMessagesSync([ 70 | { id: localStringWithPrefix, args }, 71 | ])[0]; 72 | if (!pattern) { 73 | return localStringWithPrefix; 74 | } 75 | if (branch && pattern.attributes) { 76 | for (const attr of pattern.attributes) { 77 | if (attr.name === branch) { 78 | return attr.value; 79 | } 80 | } 81 | return pattern.attributes[branch] || localStringWithPrefix; 82 | } else { 83 | return pattern.value || localStringWithPrefix; 84 | } 85 | } 86 | 87 | function getLocaleID(id: FluentMessageId) { 88 | return `${config.addonRef}-${id}`; 89 | } 90 | -------------------------------------------------------------------------------- /src/utils/prefs.ts: -------------------------------------------------------------------------------- 1 | import { config } from "../../package.json"; 2 | 3 | type PluginPrefsMap = _ZoteroTypes.Prefs["PluginPrefsMap"]; 4 | 5 | const PREFS_PREFIX = config.prefsPrefix; 6 | 7 | /** 8 | * Get preference value. 9 | * Wrapper of `Zotero.Prefs.get`. 10 | * @param key 11 | */ 12 | export function getPref(key: K) { 13 | return Zotero.Prefs.get(`${PREFS_PREFIX}.${key}`, true) as PluginPrefsMap[K]; 14 | } 15 | 16 | /** 17 | * Set preference value. 18 | * Wrapper of `Zotero.Prefs.set`. 19 | * @param key 20 | * @param value 21 | */ 22 | export function setPref( 23 | key: K, 24 | value: PluginPrefsMap[K], 25 | ) { 26 | return Zotero.Prefs.set(`${PREFS_PREFIX}.${key}`, value, true); 27 | } 28 | 29 | /** 30 | * Clear preference value. 31 | * Wrapper of `Zotero.Prefs.clear`. 32 | * @param key 33 | */ 34 | export function clearPref(key: string) { 35 | return Zotero.Prefs.clear(`${PREFS_PREFIX}.${key}`, true); 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { ProgressWindowHelper } from "zotero-plugin-toolkit"; 2 | import { config } from "../../package.json"; 3 | import { getString } from "../utils/locale"; 4 | import { xpiURLSourceName } from "../modules/addonInfo"; 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | // @ts-ignore 7 | const { AddonManager } = ChromeUtils.import( 8 | "resource://gre/modules/AddonManager.jsm", 9 | ); 10 | 11 | /** 12 | * Undo uninstall add-on 13 | * @param addon The add-on to undo uninstall 14 | */ 15 | export async function undoUninstall(addon: any) { 16 | try { 17 | addon.cancelUninstall(); 18 | } catch (error) { 19 | ztoolkit.log(`undo ${addon.name} failed: ${error}`); 20 | } 21 | } 22 | 23 | /** 24 | * Uninstall an add-on 25 | * @param addon The add-on to uninstall 26 | * @param options Additional options 27 | * @param options.popConfirmDialog Present a comfirm dialog berfore install 28 | * @param options.canRestore Enable undo uninstall after uninstall 29 | */ 30 | export async function uninstall( 31 | addon: any, 32 | options?: { popConfirmDialog?: boolean; canRestore?: boolean }, 33 | ) { 34 | if (options?.popConfirmDialog) { 35 | const confirm = await (Services as any).prompt.confirmEx( 36 | null, 37 | getString("uninstall-confirm-title"), 38 | getString("uninstall-confirm-message", { 39 | args: { name: addon.name ?? "Unknown" }, 40 | }), 41 | Services.prompt.BUTTON_POS_0! * 42 | Services.prompt.BUTTON_TITLE_IS_STRING! + 43 | Services.prompt.BUTTON_POS_1! * 44 | Services.prompt.BUTTON_TITLE_CANCEL!, 45 | getString("uninstall-confirm-confirm"), 46 | null, 47 | null, 48 | "", 49 | {}, 50 | ); 51 | if (confirm !== 0) { 52 | return; 53 | } 54 | } 55 | 56 | const popWin = new ztoolkit.ProgressWindow(getString("addon-name"), { 57 | closeOnClick: true, 58 | closeTime: 3000, 59 | }); 60 | try { 61 | await addon.uninstall(options?.canRestore); 62 | popWin.createLine({ 63 | text: getString("uninstall-succeed", { 64 | args: { name: addon.name ?? "Unknown" }, 65 | }), 66 | type: `success`, 67 | progress: 0, 68 | }); 69 | } catch (error) { 70 | ztoolkit.log(`uninstall ${addon.name} failed: ${error}`); 71 | popWin 72 | .createLine({ 73 | text: getString("uninstall-failed", { 74 | args: { name: addon.name ?? "Unknown" }, 75 | }), 76 | type: `fail`, 77 | progress: 0, 78 | }) 79 | .addDescription(`${error}`.slice(0, 45)); 80 | } 81 | popWin.show(3000); 82 | } 83 | 84 | /** 85 | * install add-on from url 86 | * @param url A url string or url string array with this add-on 87 | * @param options Additional options 88 | * @param options.name The add-on name 89 | * @param options.popWin Present a progress window during downloading and installing 90 | * @param options.startIndex Specify which URL to use for recursion, usually not necessary to specify when calling 91 | */ 92 | export async function installAddonFrom( 93 | url: string | string[], 94 | options?: { 95 | name?: string; 96 | popWin?: boolean; 97 | startIndex?: number; 98 | }, 99 | ) { 100 | if (!Array.isArray(url)) { 101 | url = [url]; 102 | } 103 | const startIndex = options?.startIndex ?? 0; 104 | if (startIndex >= url.length || startIndex < 0) { 105 | return; 106 | } 107 | const xpiUrl = url[startIndex]; 108 | const xpiName = options?.name ?? extractFileNameFromUrl(xpiUrl) ?? "Unknown"; 109 | let sourceName = xpiURLSourceName(xpiUrl); 110 | if (sourceName === "source-others") { 111 | sourceName = `${getString(sourceName)} ${startIndex + 1}`; 112 | } else { 113 | // @ts-ignore ignore getString type check 114 | sourceName = getString(sourceName); 115 | } 116 | const source = getString("downloading-source", { 117 | args: { name: sourceName }, 118 | }); 119 | 120 | let popWin: ProgressWindowHelper | undefined = undefined; 121 | if (options?.popWin) { 122 | popWin = new ztoolkit.ProgressWindow(getString("addon-name"), { 123 | closeOnClick: true, 124 | closeTime: -1, 125 | }) 126 | .createLine({ 127 | text: getString("downloading", { 128 | args: { name: xpiName + ` (${source})` }, 129 | }), 130 | type: "default", 131 | progress: 0, 132 | }) 133 | .show(-1); 134 | } 135 | 136 | // reference in gecko 137 | // > getInstallForURL: 138 | // > https://github.com/mozilla/gecko-dev/blob/f170bc26fdcfda53a270dc4f257202e62f4b781f/toolkit/mozapps/extensions/internal/XPIInstall.jsm#L4418 139 | // > DownloadAddonInstall: 140 | // > https://github.com/mozilla/gecko-dev/blob/f170bc26fdcfda53a270dc4f257202e62f4b781f/toolkit/mozapps/extensions/internal/XPIInstall.jsm#L2225 141 | // > installAddonFromURL example: 142 | // > https://github.com/mozilla/gecko-dev/blob/fc757816ed9d8f8552dbcb96c1f89f8108f37b2a/browser/components/enterprisepolicies/Policies.sys.mjs#L2618 143 | // > AddonManager states and error types: 144 | // > `https://github.com/mozilla/gecko-dev/blob/fc757816ed9d8f8552dbcb96c1f89f8108f37b2a/toolkit/mozapps/extensions/AddonManager.sys.mjs#L3993` 145 | const actualInstall = async () => { 146 | try { 147 | const install = await AddonManager.getInstallForURL(xpiUrl, { 148 | telemetryInfo: { source: config.addonID }, 149 | }); 150 | return await new Promise((resolve) => { 151 | const listener = { 152 | onDownloadStarted: (install: any) => { 153 | if (!popWin) { 154 | return; 155 | } 156 | // 下载进度条 157 | (async () => { 158 | while (install.state === AddonManager.STATE_DOWNLOADING) { 159 | await Zotero.Promise.delay(200); 160 | if (install.maxProgress > 0 && install.progress > 0) { 161 | popWin.changeLine({ 162 | progress: (100 * install.progress) / install.maxProgress, 163 | }); 164 | } 165 | } 166 | })(); 167 | }, 168 | onDownloadEnded: (install: any) => { 169 | // Install failed, error will be reported elsewhere. 170 | if (!install.addon) { 171 | return; 172 | } 173 | 174 | if (install.addon.appDisabled) { 175 | ztoolkit.log(`Incompatible add-on from ${xpiUrl}`); 176 | install.removeListener(listener); 177 | install.cancel(); 178 | popWin?.changeLine({ 179 | text: `${getString("install-failed", { args: { name: xpiName } })} [${getString("install-failed-uncompatible")}]`, 180 | type: "fail", 181 | progress: 0, 182 | }); 183 | resolve(false); 184 | return; 185 | } 186 | popWin?.changeLine({ 187 | text: getString("installing", { args: { name: xpiName } }), 188 | type: "default", 189 | progress: 0, 190 | }); 191 | }, 192 | onDownloadFailed: () => { 193 | ztoolkit.log( 194 | `download from ${xpiUrl} failed ${AddonManager.errorToString(install.error)}`, 195 | ); 196 | install.removeListener(listener); 197 | popWin 198 | ?.changeLine({ 199 | text: getString("download-failed", { 200 | args: { name: xpiName + ` (${source})` }, 201 | }), 202 | type: "fail", 203 | progress: 0, 204 | }) 205 | .addDescription( 206 | AddonManager.errorToString(install.error).slice(0, 45), 207 | ); 208 | resolve(true); 209 | }, 210 | onInstallFailed: () => { 211 | ztoolkit.log( 212 | `install failed ${AddonManager.errorToString(install.error)} from ${xpiUrl}`, 213 | ); 214 | install.removeListener(listener); 215 | popWin 216 | ?.changeLine({ 217 | text: `${getString("install-failed", { args: { name: xpiName } })} [${AddonManager.errorToString(install.error)}]`, 218 | type: "fail", 219 | progress: 0, 220 | }) 221 | .addDescription( 222 | AddonManager.errorToString(install.error).slice(0, 45), 223 | ); 224 | resolve(true); 225 | }, 226 | onInstallEnded: (install: any, addon: any) => { 227 | install.removeListener(listener); 228 | ztoolkit.log(`install success`); 229 | popWin?.changeLine({ 230 | text: getString("install-succeed", { args: { name: xpiName } }), 231 | type: "success", 232 | progress: 0, 233 | }); 234 | resolve(false); 235 | }, 236 | }; 237 | install.addListener(listener); 238 | // install.install(); 239 | install.install(); 240 | }); 241 | } catch (e) { 242 | ztoolkit.log(`install from ${xpiUrl} failed: ${e}`); 243 | popWin 244 | ?.changeLine({ 245 | text: getString("install-failed", { args: { name: xpiName } }), 246 | type: "fail", 247 | progress: 0, 248 | }) 249 | .addDescription(`${e}`.slice(0, 45)); 250 | return true; 251 | } 252 | }; 253 | 254 | const doNextUrlInstall = await actualInstall(); 255 | popWin?.startCloseTimer(2000); 256 | if (doNextUrlInstall && Array.isArray(url) && url.length > 1) { 257 | options = options ?? {}; 258 | options.startIndex = startIndex + 1; 259 | return await installAddonFrom(url, options); 260 | } 261 | } 262 | 263 | /** 264 | * extract file name from url 265 | * @param url url 266 | * @returns filename 267 | */ 268 | export function extractFileNameFromUrl(url: string) { 269 | try { 270 | return new URL(url).pathname.split("/").pop(); 271 | } catch { 272 | // 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/utils/window.ts: -------------------------------------------------------------------------------- 1 | export { isWindowAlive }; 2 | 3 | /** 4 | * Check if the window is alive. 5 | * Useful to prevent opening duplicate windows. 6 | * @param win 7 | */ 8 | function isWindowAlive(win?: Window) { 9 | return win && !Components.utils.isDeadWrapper(win) && !win.closed; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/ztoolkit.ts: -------------------------------------------------------------------------------- 1 | import { config } from "../../package.json"; 2 | 3 | export { createZToolkit }; 4 | 5 | function createZToolkit() { 6 | // const _ztoolkit = new ZoteroToolkit(); 7 | /** 8 | * Alternatively, import toolkit modules you use to minify the plugin size. 9 | * You can add the modules under the `MyToolkit` class below and uncomment the following line. 10 | */ 11 | const _ztoolkit = new MyToolkit(); 12 | initZToolkit(_ztoolkit); 13 | return _ztoolkit; 14 | } 15 | 16 | function initZToolkit(_ztoolkit: ReturnType) { 17 | const env = __env__; 18 | _ztoolkit.basicOptions.log.prefix = `[${config.addonName}]`; 19 | _ztoolkit.basicOptions.log.disableConsole = env === "production"; 20 | _ztoolkit.UI.basicOptions.ui.enableElementJSONLog = __env__ === "development"; 21 | _ztoolkit.UI.basicOptions.ui.enableElementDOMLog = __env__ === "development"; 22 | // Getting basicOptions.debug will load global modules like the debug bridge. 23 | // since we want to deprecate it, should avoid using it unless necessary. 24 | // _ztoolkit.basicOptions.debug.disableDebugBridgePassword = 25 | // __env__ === "development"; 26 | _ztoolkit.basicOptions.api.pluginID = config.addonID; 27 | _ztoolkit.ProgressWindow.setIconURI( 28 | "default", 29 | `chrome://${config.addonRef}/content/icons/favicon.svg`, 30 | ); 31 | } 32 | 33 | import { 34 | VirtualizedTableHelper, 35 | ProgressWindowHelper, 36 | MenuManager, 37 | GuideHelper, 38 | UITool, 39 | BasicTool, 40 | unregister, 41 | } from "zotero-plugin-toolkit"; 42 | 43 | class MyToolkit extends BasicTool { 44 | UI: UITool; 45 | VirtualizedTable: typeof VirtualizedTableHelper; 46 | ProgressWindow: typeof ProgressWindowHelper; 47 | Menu: MenuManager; 48 | Guide: typeof GuideHelper; 49 | 50 | constructor() { 51 | super(); 52 | this.UI = new UITool(this); 53 | this.VirtualizedTable = VirtualizedTableHelper; 54 | this.ProgressWindow = ProgressWindowHelper; 55 | this.Menu = new MenuManager(this); 56 | this.Guide = GuideHelper; 57 | } 58 | 59 | unregisterAll() { 60 | unregister(this); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "zotero-types/entries/sandbox/", 3 | "include": ["src", "typings"], 4 | "exclude": ["build", "addon"] 5 | } 6 | -------------------------------------------------------------------------------- /typings/global.d.ts: -------------------------------------------------------------------------------- 1 | declare const _globalThis: { 2 | [key: string]: any; 3 | Zotero: _ZoteroTypes.Zotero; 4 | ztoolkit: ZToolkit; 5 | addon: typeof addon; 6 | }; 7 | 8 | declare type ZToolkit = ReturnType< 9 | typeof import("../src/utils/ztoolkit").createZToolkit 10 | >; 11 | 12 | declare const ztoolkit: ZToolkit; 13 | 14 | declare const rootURI: string; 15 | 16 | declare const addon: import("../src/addon").default; 17 | 18 | declare const __env__: "production" | "development"; 19 | -------------------------------------------------------------------------------- /typings/i10n.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by zotero-plugin-scaffold 2 | /* prettier-ignore */ 3 | /* eslint-disable */ 4 | // @ts-nocheck 5 | export type FluentMessageId = 6 | | 'addon-name' 7 | | 'autoUpdate' 8 | | 'customSource' 9 | | 'description' 10 | | 'disable' 11 | | 'download-failed' 12 | | 'downloading' 13 | | 'downloading-source' 14 | | 'enable' 15 | | 'guide-addons-table-switch-source-message' 16 | | 'guide-addons-table-switch-source-title' 17 | | 'guide-guide-done' 18 | | 'guide-open-addons-table-message' 19 | | 'guide-open-addons-table-title' 20 | | 'hideToolbarEntrance' 21 | | 'install' 22 | | 'install-failed' 23 | | 'install-failed-uncompatible' 24 | | 'install-succeed' 25 | | 'installing' 26 | | 'menu-author' 27 | | 'menu-desc' 28 | | 'menu-disable' 29 | | 'menu-download-all-count' 30 | | 'menu-download-latest-count' 31 | | 'menu-enable' 32 | | 'menu-homepage' 33 | | 'menu-install' 34 | | 'menu-install-and-update' 35 | | 'menu-install-state' 36 | | 'menu-items-count' 37 | | 'menu-local-version' 38 | | 'menu-name' 39 | | 'menu-open-xpi-location' 40 | | 'menu-refresh' 41 | | 'menu-reinstall' 42 | | 'menu-remote-update-time' 43 | | 'menu-remote-version' 44 | | 'menu-remove' 45 | | 'menu-star' 46 | | 'menu-systemAddon' 47 | | 'menu-uninstall' 48 | | 'menu-uninstall-undo' 49 | | 'menu-update' 50 | | 'menu-updateAllIfNeed' 51 | | 'menuitem-addons' 52 | | 'name' 53 | | 'refresh' 54 | | 'reinstall' 55 | | 'release-uncompatible-description' 56 | | 'remove' 57 | | 'scheme-config-success' 58 | | 'scheme-install-confirm-confirm' 59 | | 'scheme-install-confirm-message' 60 | | 'scheme-install-confirm-title' 61 | | 'search-field' 62 | | 'selectSource' 63 | | 'send-button-status-login' 64 | | 'source-auto' 65 | | 'source-custom' 66 | | 'source-ghproxy' 67 | | 'source-gitee' 68 | | 'source-github' 69 | | 'source-jsdelivr' 70 | | 'source-kgithub' 71 | | 'source-others' 72 | | 'source-zotero-chinese-ghproxy' 73 | | 'source-zotero-chinese-gitee' 74 | | 'source-zotero-chinese-github' 75 | | 'source-zotero-chinese-jsdelivr' 76 | | 'source-zotero-scraper-ghproxy' 77 | | 'source-zotero-scraper-gitee' 78 | | 'source-zotero-scraper-github' 79 | | 'source-zotero-scraper-jsdelivr' 80 | | 'stars' 81 | | 'state' 82 | | 'state-disabled' 83 | | 'state-installed' 84 | | 'state-notInstalled' 85 | | 'state-outdate' 86 | | 'state-pendingUninstall' 87 | | 'state-uncompatible' 88 | | 'state-unknown' 89 | | 'title' 90 | | 'uninstall' 91 | | 'uninstall-confirm-confirm' 92 | | 'uninstall-confirm-message' 93 | | 'uninstall-confirm-title' 94 | | 'uninstall-failed' 95 | | 'uninstall-succeed' 96 | | 'uninstallUndo' 97 | | 'unknown' 98 | | 'update' 99 | | 'update-all-uncompatible-confirm' 100 | | 'update-all-uncompatible-message' 101 | | 'update-all-uncompatible-title' 102 | | 'update-succeed'; 103 | -------------------------------------------------------------------------------- /typings/prefs.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by zotero-plugin-scaffold 2 | /* prettier-ignore */ 3 | /* eslint-disable */ 4 | // @ts-nocheck 5 | 6 | // prettier-ignore 7 | declare namespace _ZoteroTypes { 8 | interface Prefs { 9 | PluginPrefsMap: { 10 | 'autoUpdate': boolean; 11 | 'source': string; 12 | 'customSource': string; 13 | 'hideToolbarEntrance': boolean; 14 | 'guideStatus': number; 15 | 'firstInstalledVersion': string; 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /update.json: -------------------------------------------------------------------------------- 1 | { 2 | "addons": { 3 | "zoteroAddons@ytshen.com": { 4 | "updates": [ 5 | { 6 | "version": "0.6.7", 7 | "update_link": "https://github.com/syt2/zotero-addons/releases/download/0.6.0-6/zotero-addons.xpi", 8 | "applications": { 9 | "gecko": { 10 | "strict_min_version": "60.0" 11 | } 12 | } 13 | }, 14 | { 15 | "version": "1.7.2", 16 | "update_link": "https://github.com/syt2/zotero-addons/releases/download/V1.7.2/zotero-addons.xpi", 17 | "applications": { 18 | "zotero": { 19 | "strict_min_version": "6.999" 20 | } 21 | } 22 | } 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /zotero-plugin.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "zotero-plugin-scaffold"; 2 | import pkg from "./package.json"; 3 | 4 | export default defineConfig({ 5 | source: ["src", "addon"], 6 | dist: ".scaffold/build", 7 | name: "__MSG_name__", 8 | xpiName: pkg.name, 9 | id: pkg.config.addonID, 10 | namespace: pkg.config.addonRef, 11 | updateURL: `https://github.com/{{owner}}/{{repo}}/releases/download/release/${ 12 | pkg.version.includes("-") ? "update-beta.json" : "update.json" 13 | }`, 14 | xpiDownloadLink: 15 | "https://github.com/{{owner}}/{{repo}}/releases/download/V{{version}}/{{xpiName}}.xpi", 16 | server: { 17 | asProxy: true, 18 | }, 19 | 20 | build: { 21 | assets: ["addon/**/*.*"], 22 | define: { 23 | ...pkg.config, 24 | author: pkg.author, 25 | homepage: pkg.homepage, 26 | buildVersion: pkg.version, 27 | buildTime: "{{buildTime}}", 28 | }, 29 | // prefs: { 30 | // prefix: pkg.config.prefsPrefix, 31 | // }, 32 | esbuildOptions: [ 33 | { 34 | entryPoints: ["src/index.ts"], 35 | define: { 36 | __env__: `"${process.env.NODE_ENV}"`, 37 | }, 38 | bundle: true, 39 | target: "firefox115", 40 | outfile: `.scaffold/build/addon/content/scripts/${pkg.config.addonRef}.js`, 41 | }, 42 | ], 43 | }, 44 | release: { 45 | bumpp: { 46 | commit: "chore(publish): release V%s", 47 | tag: "V%s", 48 | }, 49 | }, 50 | // If you need to see a more detailed log, uncomment the following line: 51 | // logLevel: "trace", 52 | }); 53 | --------------------------------------------------------------------------------