Compare commits
304 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed129f2a45 | ||
|
|
2a9125aa99 | ||
|
|
5d4dde170a | ||
|
|
2d10915c7d | ||
|
|
18fac41190 | ||
|
|
f653f8a11a | ||
|
|
28cf7c86de | ||
|
|
6edfda0388 | ||
|
|
6056c4208f | ||
|
|
5cc74c4986 | ||
|
|
a26f5fd0a9 | ||
|
|
eec0d47a96 | ||
|
|
df847e59be | ||
|
|
50956fefff | ||
|
|
f93d22d29b | ||
|
|
a9d13a20c6 | ||
|
|
3c9695fa71 | ||
|
|
735da8a43a | ||
|
|
b56669ae8f | ||
|
|
66c0460513 | ||
|
|
f52eb9f100 | ||
|
|
0075dc8a43 | ||
|
|
cd14a26c11 | ||
|
|
fdd4d20a95 | ||
|
|
2cc3ac7622 | ||
|
|
0663e5b0b1 | ||
|
|
7738410680 | ||
|
|
d1f6367825 | ||
|
|
2839b3ac53 | ||
|
|
7f6b7c54b2 | ||
|
|
fc8a4ac9d9 | ||
|
|
f5f2fd3297 | ||
|
|
be0b82f23d | ||
|
|
6f3a6d112e | ||
|
|
3d9f8a8a0d | ||
|
|
032aa74f6e | ||
|
|
57028c7ba8 | ||
|
|
4eb662ea2f | ||
|
|
56c427b26a | ||
|
|
133dde03c7 | ||
|
|
3c8f1b3db3 | ||
|
|
fb9109d87d | ||
|
|
e2885ec563 | ||
|
|
ade49628ff | ||
|
|
1208ad54a9 | ||
|
|
a56e31fa0d | ||
|
|
12e7cb9641 | ||
|
|
77e43bf378 | ||
|
|
57319f498f | ||
|
|
df7bde4757 | ||
|
|
1364288293 | ||
|
|
1572a57f22 | ||
|
|
ddb2bc6898 | ||
|
|
23ffc5fdf8 | ||
|
|
25990fd552 | ||
|
|
10e57afadb | ||
|
|
7f436b320f | ||
|
|
296ce35e16 | ||
|
|
6d4e9628a5 | ||
|
|
0d67969ad8 | ||
|
|
dad1df7b52 | ||
|
|
6ebb421185 | ||
|
|
861d6685a0 | ||
|
|
67498280a0 | ||
|
|
309447ca68 | ||
|
|
e9b9e7ef46 | ||
|
|
f1d5275e6f | ||
|
|
24c229f736 | ||
|
|
7fd2de3554 | ||
|
|
eed95d402e | ||
|
|
033bafa5bc | ||
|
|
03fadd7b25 | ||
|
|
526a18cc37 | ||
|
|
e1088f36da | ||
|
|
51afe19524 | ||
|
|
005a0160aa | ||
|
|
9f5fc12560 | ||
|
|
ba57111764 | ||
|
|
b69feab780 | ||
|
|
ebddeeb3ef | ||
|
|
1e12c84ba6 | ||
|
|
b48953f405 | ||
|
|
a5517736fa | ||
|
|
3c780febc9 | ||
|
|
4f5f6f8e12 | ||
|
|
96245a0923 | ||
|
|
9f91c764c0 | ||
|
|
11134e365e | ||
|
|
efd56c94e1 | ||
|
|
9da839374c | ||
|
|
9fd98494f7 | ||
|
|
9495059eb9 | ||
|
|
8a36eb2ab6 | ||
|
|
3738657b9a | ||
|
|
229411ae61 | ||
|
|
18476d9624 | ||
|
|
79e87f050b | ||
|
|
10d9879252 | ||
|
|
0ff56d5303 | ||
|
|
0971bd0f44 | ||
|
|
458a6a5f84 | ||
|
|
3adad053b1 | ||
|
|
a2d2d3a36c | ||
|
|
acb6cd79d1 | ||
|
|
d45b72111d | ||
|
|
c2808745c8 | ||
|
|
724a2eb2e6 | ||
|
|
7c8ab3b5dc | ||
|
|
0ceff9728a | ||
|
|
0713c8212b | ||
|
|
932d491d9e | ||
|
|
66c0b12f3f | ||
|
|
01d80eb730 | ||
|
|
cb9efcfaae | ||
|
|
bd8c6c78d6 | ||
|
|
110b806eca | ||
|
|
f7b7483a8a | ||
|
|
6184e7a9c1 | ||
|
|
e28af8724d | ||
|
|
b08fd4642b | ||
|
|
3e6b744307 | ||
|
|
4d7f61ee0b | ||
|
|
164c8a01be | ||
|
|
ab845ddb35 | ||
|
|
9e2553661d | ||
|
|
a30f0c7c64 | ||
|
|
138c6316ac | ||
|
|
eeb1ace01d | ||
|
|
55368dea50 | ||
|
|
d274334398 | ||
|
|
5cf4e57228 | ||
|
|
ac15ec5035 | ||
|
|
6576618634 | ||
|
|
9c824d6faa | ||
|
|
fd3fafe8fc | ||
|
|
88c2f9e686 | ||
|
|
6f6d3e463a | ||
|
|
0b74eab10d | ||
|
|
d2d8922f66 | ||
|
|
4d983132c2 | ||
|
|
f22a071042 | ||
|
|
1121dc5d6c | ||
|
|
29a318e828 | ||
|
|
74c3a03952 | ||
|
|
a2697d1dfb | ||
|
|
389e7590e1 | ||
|
|
9850e5e676 | ||
|
|
35afb45200 | ||
|
|
4d3bc65b94 | ||
|
|
dcef4b554f | ||
|
|
a254ff8bc3 | ||
|
|
4d0b0a457d | ||
|
|
a4741696e6 | ||
|
|
25081bf450 | ||
|
|
8540d1b4cf | ||
|
|
2dac131b12 | ||
|
|
8680035d8d | ||
|
|
b84489ff6b | ||
|
|
b2b490b191 | ||
|
|
f7592ebf38 | ||
|
|
27c68b1167 | ||
|
|
3ba1f9d40e | ||
|
|
c4fc2c44e7 | ||
|
|
671315e9ad | ||
|
|
2c3f6d1d7f | ||
|
|
d9680008d5 | ||
|
|
f7d095bade | ||
|
|
55d15eb796 | ||
|
|
4a92c2795e | ||
|
|
b592a35bd5 | ||
|
|
49fe7c5ec4 | ||
|
|
1c43ef671a | ||
|
|
98a1c7770b | ||
|
|
3bd5b1137d | ||
|
|
c48e085b8d | ||
|
|
dccda31611 | ||
|
|
17fa2b83e7 | ||
|
|
15f1a04259 | ||
|
|
7f733416ee | ||
|
|
fe224ba50d | ||
|
|
ab0157dbe1 | ||
|
|
fb98d85c5c | ||
|
|
6bfe8c28d2 | ||
|
|
5852ecfc9f | ||
|
|
67f2dcb7fc | ||
|
|
11668e9c15 | ||
|
|
0e8f5b34d0 | ||
|
|
414bf81fd1 | ||
|
|
bb4c31b54c | ||
|
|
5abb83c518 | ||
|
|
0afe7772c5 | ||
|
|
f625f326eb | ||
|
|
b0010ee8c7 | ||
|
|
833a7ee8ec | ||
|
|
6467a12089 | ||
|
|
2ff86f92db | ||
|
|
8be6b0ba49 | ||
|
|
792263726d | ||
|
|
c4777e35a9 | ||
|
|
9ed41e33aa | ||
|
|
080736cd0f | ||
|
|
47fdd995d5 | ||
|
|
296f03944a | ||
|
|
e2112995ad | ||
|
|
7ee1619b8f | ||
|
|
576c08b5cf | ||
|
|
7b9e139cb8 | ||
|
|
c1ad9c5ee7 | ||
|
|
aa51abaeb7 | ||
|
|
91d295dfe6 | ||
|
|
ceb4d602cc | ||
|
|
fac772b7b8 | ||
|
|
4f0e936f09 | ||
|
|
7a39b8223c | ||
|
|
bbbd2509a8 | ||
|
|
29b57965e2 | ||
|
|
20c2a1f7de | ||
|
|
d66a00e50f | ||
|
|
623c3830f5 | ||
|
|
18e77880da | ||
|
|
e6aa4f1b6b | ||
|
|
ffbaf8cd3d | ||
|
|
a0b33a2211 | ||
|
|
0f33389dae | ||
|
|
1ad2832a6b | ||
|
|
c80f3ed457 | ||
|
|
8025496b44 | ||
|
|
4524527867 | ||
|
|
72acc1b2fe | ||
|
|
ce636bdfd3 | ||
|
|
baede0c9c1 | ||
|
|
33ccc61267 | ||
|
|
4a06cd0535 | ||
|
|
d48a628693 | ||
|
|
05ad059c04 | ||
|
|
33c38d3595 | ||
|
|
3ea8520d60 | ||
|
|
16c3952f80 | ||
|
|
a8dcf5e310 | ||
|
|
3c5dd0089c | ||
|
|
81c43ccef5 | ||
|
|
b8c644acc5 | ||
|
|
8cc7ace414 | ||
|
|
08d72a4e2b | ||
|
|
c0f7694480 | ||
|
|
5a1800254d | ||
|
|
a95515fc47 | ||
|
|
d42abcbfcc | ||
|
|
f87cdfa1ef | ||
|
|
6a986305a4 | ||
|
|
5d3cde376d | ||
|
|
398bf89a23 | ||
|
|
25befec9c6 | ||
|
|
329e6dab88 | ||
|
|
84beaab5fd | ||
|
|
aa9763e9a9 | ||
|
|
626cff548b | ||
|
|
4469cbc82d | ||
|
|
6426ae5198 | ||
|
|
4f2e81e661 | ||
|
|
acf0e98cf2 | ||
|
|
a4873cdb99 | ||
|
|
b0b15adc2b | ||
|
|
a9a05e28cc | ||
|
|
ae91587fb4 | ||
|
|
51badae189 | ||
|
|
0bd4b5987e | ||
|
|
717548a470 | ||
|
|
eb36b9a7f6 | ||
|
|
29b64b5800 | ||
|
|
bb737ec090 | ||
|
|
206961fa9a | ||
|
|
fc5661fd14 | ||
|
|
4ff9c79263 | ||
|
|
3d1cf9ded3 | ||
|
|
7e993e4c55 | ||
|
|
a1007c83a0 | ||
|
|
af6dc3bab0 | ||
|
|
8c372fe0e3 | ||
|
|
c55548da5a | ||
|
|
7e92f919f6 | ||
|
|
d4af09bf1f | ||
|
|
2307380f0d | ||
|
|
177fa37eca | ||
|
|
2d19e5501b | ||
|
|
49bd75d3e4 | ||
|
|
2e74972f76 | ||
|
|
36bb38f40e | ||
|
|
99965519f6 | ||
|
|
f8a6b7a1ed | ||
|
|
67fb288be5 | ||
|
|
969a5274c6 | ||
|
|
c3db612775 | ||
|
|
7f4a99d998 | ||
|
|
7bcbd3e025 | ||
|
|
600bda7bd4 | ||
|
|
86969a010b | ||
|
|
fd4294e395 | ||
|
|
cd71fa9bce | ||
|
|
dcf82a360f | ||
|
|
5b99dfe8ba | ||
|
|
5adb1444d8 | ||
|
|
e1bb4d8787 | ||
|
|
d7210d930a |
|
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":semanticPrefixChore",
|
||||
":prHourlyLimitNone",
|
||||
":prConcurrentLimitNone",
|
||||
":enableVulnerabilityAlerts",
|
||||
":dependencyDashboard",
|
||||
"schedule:weekends"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": ["zotero-plugin-toolkit", "zotero-types"],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"git-submodules": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- V**
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GitHub_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install deps
|
||||
run: npm install
|
||||
|
||||
- name: Release to GitHub
|
||||
run: |
|
||||
npm run release -- --no-increment --no-git --github.release --ci --VV
|
||||
sleep 1s
|
||||
|
||||
- name: Notify release
|
||||
uses: apexskier/github-release-commenter@v1
|
||||
continue-on-error: true
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-template: |
|
||||
:rocket: _This ticket has been resolved in {release_tag}. See {release_link} for release notes._
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
**/builds
|
||||
build
|
||||
logs
|
||||
node_modules
|
||||
package-lock.json
|
||||
zotero-cmd.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
zotero-cmd.json
|
||||
.DS_Store
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
build
|
||||
logs
|
||||
node_modules
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
# zotero-cmd.json
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"npm": {
|
||||
"publish": false
|
||||
},
|
||||
"github": {
|
||||
"release": true,
|
||||
"assets": ["builds/*.xpi"]
|
||||
},
|
||||
"hooks": {
|
||||
"after:bump": "npm run build",
|
||||
"after:release": "echo Successfully released ${name} v${version} to ${repo.repository}."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"macabeus.vscode-fluent"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,15 +1,22 @@
|
|||
{
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 悬停以查看现有属性的描述。
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Restart",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "restart"],
|
||||
}
|
||||
]
|
||||
}
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 悬停以查看现有属性的描述。
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Start",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "start"]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Build",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "build"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"editor.formatOnType": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"appendElement - full": {
|
||||
"scope": "javascript,typescript",
|
||||
"prefix": "appendElement",
|
||||
"body": [
|
||||
"appendElement({",
|
||||
"\ttag: '${1:div}',",
|
||||
"\tid: '${2:id}',",
|
||||
"\tnamespace: '${3:html}',",
|
||||
"\tclassList: ['${4:class}'],",
|
||||
"\tstyles: {${5:style}: '$6'},",
|
||||
"\tproperties: {},",
|
||||
"\tattributes: {},",
|
||||
"\t[{ '${7:onload}', (e: Event) => $8, ${9:false} }],",
|
||||
"\tcheckExistanceParent: ${10:HTMLElement},",
|
||||
"\tignoreIfExists: ${11:true},",
|
||||
"\tskipIfExists: ${12:true},",
|
||||
"\tremoveIfExists: ${13:true},",
|
||||
"\tcustomCheck: (doc: Document, options: ElementOptions) => ${14:true},",
|
||||
"\tchildren: [$15]",
|
||||
"}, ${16:container});",
|
||||
],
|
||||
},
|
||||
"appendElement - minimum": {
|
||||
"scope": "javascript,typescript",
|
||||
"prefix": "appendElement",
|
||||
"body": "appendElement({ tag: '$1' }, $2);",
|
||||
},
|
||||
"register Notifier": {
|
||||
"scope": "javascript,typescript",
|
||||
"prefix": "registerObserver",
|
||||
"body": [
|
||||
"registerObserver({",
|
||||
"\t notify: (",
|
||||
"\t\tevent: _ZoteroTypes.Notifier.Event,",
|
||||
"\t\ttype: _ZoteroTypes.Notifier.Type,",
|
||||
"\t\tids: string[],",
|
||||
"\t\textraData: _ZoteroTypes.anyObj",
|
||||
"\t) => {",
|
||||
"\t\t$0",
|
||||
"\t}",
|
||||
"});",
|
||||
],
|
||||
},
|
||||
}
|
||||
185
README.md
185
README.md
|
|
@ -1,180 +1,17 @@
|
|||
# Zotero Addon Template
|
||||
# Zotero TL;DR
|
||||
|
||||
This is an addon/plugin template for [Zotero](https://www.zotero.org/).
|
||||
[](https://www.zotero.org)
|
||||
[](https://github.com/windingwind/zotero-plugin-template)
|
||||
|
||||
[Documentation](https://zotero.yuque.com/books/share/8d230829-6004-4934-b4c6-685a7001bfa0/vec88d)(Chinese, provides English translation)
|
||||
This is an add-on for [Zotero 7+](https://www.zotero.org) that automatically fetch TL;DR (Too Long; Didn't Read) from [Sematic scholar](https://www.semanticscholar.org) for items.
|
||||
|
||||
> 👍You are currently in `bootstrap` extension mode. To use `overlay` mode, plsase switch to `overlay` branch in git.
|
||||
## Install
|
||||
|
||||
> ⚠️`overlay` mode will no longer be supported in the coming Zotero 7. Please use the `bootstrap` extension mode instead. See discussion here: https://groups.google.com/g/zotero-dev/c/TT_rcLVpQwg
|
||||
1. Download the [latest release](https://github.com/syt2/zotero-tldr/releases/latest/download/zotero-tldr.xpi) xpi file.
|
||||
2. Install in Zotero (Tools -> Add-ons)
|
||||
|
||||
## Features
|
||||
## Usage
|
||||
|
||||
- TypeScript support;
|
||||
- Build addon settings and versions automatically;
|
||||
- Build and reload code in Zotero automatically;
|
||||
- Release to GitHub automatically(using [release-it](https://github.com/release-it/release-it));
|
||||
- Extensive skeleton;
|
||||
- Some sample code of UI and lifecycle.
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
- Fork this repo;
|
||||
- Git clone the forked repo;
|
||||
- Enter the repo folder;
|
||||
- Modify the settings in `./package.json`, including:
|
||||
|
||||
```
|
||||
author,
|
||||
description,
|
||||
homepage,
|
||||
releasepage,
|
||||
updaterdf,
|
||||
addonName,
|
||||
addonID,
|
||||
addonRef
|
||||
```
|
||||
|
||||
> Be careful to set the addonID and addonRef to avoid confliction.
|
||||
|
||||
- Run `npm install` to setup the plugin and install dependencies. If you don't have NodeJS installed, please download it [here](https://nodejs.org/en/);
|
||||
- Run `npm run build` to build the plugin. The xpi for installation and the built code is under builds folder.
|
||||
|
||||
### Directory Structure
|
||||
|
||||
This section shows the directory structure of a template.
|
||||
|
||||
- All `.js/.ts` code files are in `./src`;
|
||||
- Addon config files: `./addon/chrome.manifest`, `./addon/install.rdf`;
|
||||
- UI files: `./addon/chrome/content/*.xul`. The `overlay.xul` also defines the main entrance;
|
||||
- Locale files: `./addon/chrome/locale/*.dtd`;
|
||||
- Resource files: `./addon/chrome/skin/default/__addonRef__/*.dtd`;
|
||||
- Preferences file: `./addon/chrome/defaults/preferences/defaults.js`;
|
||||
> Don't break the lines in the `defaults.js`
|
||||
|
||||
```shell
|
||||
│ .gitignore
|
||||
│ .release-it.json # release-it conf
|
||||
| tsconfig.json # https://code.visualstudio.com/docs/languages/jsconfig#
|
||||
│ build.js # esbuild
|
||||
│ LICENSE
|
||||
│ package.json # npm conf
|
||||
│ README.md # readme
|
||||
│ update.rdf # addon update
|
||||
│
|
||||
├─.github # github conf
|
||||
│
|
||||
├─addon # addon dir
|
||||
│ │ chrome.manifest #addon conf
|
||||
│ │ install.rdf # addon install conf
|
||||
│ │ bootstrap.js # addon load/unload script, like a main.c
|
||||
│ │
|
||||
│ └─chrome
|
||||
│ ├─content # UI
|
||||
│ │ │ preferences.xul
|
||||
│ │ │
|
||||
│ │ └─scripts
|
||||
│ ├─locale # locale
|
||||
│ │ ├─en-US
|
||||
│ │ │ overlay.dtd
|
||||
│ │ │
|
||||
│ │ └─zh-CN
|
||||
│ │ overlay.dtd
|
||||
│ │
|
||||
│ └─skin # style
|
||||
│ └─default
|
||||
│ └─addonname
|
||||
│ favicon.png
|
||||
│ favicon@0.5x.png
|
||||
│
|
||||
├─builds # build dir
|
||||
│ └─.xpi
|
||||
│
|
||||
└─src # source code
|
||||
│ index.ts # main entry
|
||||
│ module.ts # module class
|
||||
│ addon.ts # base class
|
||||
│ events.ts # events class
|
||||
│ views.ts # UI class
|
||||
└─ prefs.ts # preferences class
|
||||
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```shell
|
||||
# A release-it command: version increase, npm run build, git push, and GitHub release
|
||||
# You need to set the environment variable GITHUB_TOKEN https://github.com/settings/tokens
|
||||
# release-it: https://github.com/release-it/release-it
|
||||
npm run release
|
||||
```
|
||||
|
||||
Alternatively, build it directly using build.js: `npm run build`
|
||||
|
||||
### Build Steps
|
||||
|
||||
1. Clean `./builds`
|
||||
2. Copy `./addon` to `./builds`
|
||||
3. Esbuild to `./builds/addon/chrome/content/scripts`
|
||||
4. Replace `__buildVersion__` and `__buildTime__` in `./builds/addon`
|
||||
5. Zip the `./builds/addon` to `./builds/*.xpi`
|
||||
|
||||
### Debug
|
||||
|
||||
1. Copy zotero command line config file. Modify the commands.
|
||||
|
||||
```sh
|
||||
cp zotero-cmd-default.json zotero-cmd.json
|
||||
```
|
||||
|
||||
2. Setup addon development environment following this [link](https://www.zotero.org/support/dev/client_coding/plugin_development#setting_up_a_plugin_development_environment).
|
||||
|
||||
3. Build addon and restart Zotero with this npm command.
|
||||
|
||||
4. Launch Firefox 60
|
||||
5. In Firefox, go to devtools, go to settings, click "enable remote debugging" and the one next to it that's also about debugging(or press `shift+F8`).
|
||||
6. In Zotero, go to setting, advanced, config editor, look up "debugging" and click on "allow remote debugging"
|
||||
7. In Firefox, click the hamburger menu in the top right -> web developer -> Connect...
|
||||
8. Enter localhost:6100
|
||||
9. Connect
|
||||
10. Click "Inspect Main Process"
|
||||
|
||||
```sh
|
||||
npm run restart
|
||||
```
|
||||
|
||||
You can also debug code in these ways:
|
||||
|
||||
- Test code segments in Tools->Developer->Run Javascript;
|
||||
- Debug output with `Zotero.debug()`. Find the outputs in Help->Debug Output Logging->View Output;
|
||||
- UI debug. Zotero is built on the Firefox XUL framework. Debug XUL UI with software like [XUL Explorer](https://udn.realityripple.com/docs/Archive/Mozilla/XUL_Explorer).
|
||||
> XUL Documents:
|
||||
> https://www.xul.fr/tutorial/
|
||||
> http://www.xulplanet.com/
|
||||
|
||||
### Development
|
||||
|
||||
**Search for a Zotero API**
|
||||
Zotero docs are outdated or incomplete. Searching the source code of Zotero is unavoidable.
|
||||
Clone https://github.com/zotero/zotero and search the keyword globally. You can search the UI text in `.xul`/`.dtd` files, and then search the keys of the text value in `.js`/`.xul` files.
|
||||
|
||||
> ⭐The [zotero-types](https://github.com/windingwind/zotero-types) provides most frequently used Zotero APIs. It's included in this template by default.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Use this code under AGPL. No warranties are provided. Keep the laws of your locality in mind!
|
||||
|
||||
If you want to change the license, please contact me at wyzlshx@foxmail.com
|
||||
|
||||
Part of the code of this repo refers to other open-source projects within the allowed scope.
|
||||
|
||||
- zotero-better-bibtex(`d.ts`)
|
||||
|
||||
## Zotero Addons Build with the Template
|
||||
|
||||
- [zotero-better-notes](https://github.com/windingwind/zotero-better-notes): Everything about note management. All in Zotero.
|
||||
- [zotero-pdf-preview](https://github.com/windingwind/zotero-pdf-preview): PDF Preview for Zotero.
|
||||
- [zotero-pdf-translate](https://github.com/windingwind/zotero-pdf-translate): PDF Translation for Zotero 6.
|
||||
- [zotero-tag](https://github.com/windingwind/zotero-tag): Automatically tag items/Batch tagging
|
||||
|
||||
- [zotero-theme](https://github.com/iShareStuff/ZoteroTheme): Customize Zotero theme
|
||||
There are no configuration steps required.
|
||||
The add-on will automatically fetch the TL;DR information for all items.
|
||||
You can view the TLDR information in details on the right side.
|
||||
|
|
|
|||
|
|
@ -1,120 +1,78 @@
|
|||
/* Copyright 2012 Will Shanks.
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
if (typeof Zotero == "undefined") {
|
||||
var Zotero;
|
||||
}
|
||||
/**
|
||||
* Most of this code is from Zotero team's official Make It Red example[1]
|
||||
* or the Zotero 7 documentation[2].
|
||||
* [1] https://github.com/zotero/make-it-red
|
||||
* [2] https://www.zotero.org/support/dev/zotero_7_for_developers
|
||||
*/
|
||||
|
||||
var chromeHandle;
|
||||
|
||||
// In Zotero 6, bootstrap methods are called before Zotero is initialized, and using include.js
|
||||
// to get the Zotero XPCOM service would risk breaking Zotero startup. Instead, wait for the main
|
||||
// Zotero window to open and get the Zotero object from there.
|
||||
//
|
||||
// In Zotero 7, bootstrap methods are not called until Zotero is initialized, and the 'Zotero' is
|
||||
// automatically made available.
|
||||
async function waitForZotero() {
|
||||
if (typeof Zotero != "undefined") {
|
||||
await Zotero.initializationPromise;
|
||||
}
|
||||
|
||||
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
var windows = Services.wm.getEnumerator("navigator:browser");
|
||||
var found = false;
|
||||
while (windows.hasMoreElements()) {
|
||||
let win = windows.getNext();
|
||||
if (win.Zotero) {
|
||||
Zotero = win.Zotero;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
await new Promise((resolve) => {
|
||||
var listener = {
|
||||
onOpenWindow: function (aWindow) {
|
||||
// Wait for the window to finish loading
|
||||
let domWindow = aWindow
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
|
||||
domWindow.addEventListener(
|
||||
"load",
|
||||
function () {
|
||||
domWindow.removeEventListener("load", arguments.callee, false);
|
||||
if (domWindow.Zotero) {
|
||||
Services.wm.removeListener(listener);
|
||||
Zotero = domWindow.Zotero;
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
},
|
||||
};
|
||||
Services.wm.addListener(listener);
|
||||
});
|
||||
}
|
||||
await Zotero.initializationPromise;
|
||||
}
|
||||
|
||||
function install(data, reason) {}
|
||||
|
||||
async function startup({ id, version, resourceURI, rootURI }, reason) {
|
||||
await waitForZotero();
|
||||
await Zotero.initializationPromise;
|
||||
|
||||
// String 'rootURI' introduced in Zotero 7
|
||||
if (!rootURI) {
|
||||
rootURI = resourceURI.spec;
|
||||
}
|
||||
|
||||
const ctx = { Zotero, rootURI };
|
||||
var aomStartup = Components.classes[
|
||||
"@mozilla.org/addons/addon-manager-startup;1"
|
||||
].getService(Components.interfaces.amIAddonManagerStartup);
|
||||
var manifestURI = Services.io.newURI(rootURI + "manifest.json");
|
||||
chromeHandle = aomStartup.registerChrome(manifestURI, [
|
||||
["content", "__addonRef__", rootURI + "chrome/content/"],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Global variables for plugin code.
|
||||
* The `_globalThis` is the global root variable of the plugin sandbox environment
|
||||
* and all child variables assigned to it is globally accessible.
|
||||
* See `src/index.ts` for details.
|
||||
*/
|
||||
const ctx = {
|
||||
rootURI,
|
||||
};
|
||||
ctx._globalThis = ctx;
|
||||
|
||||
Services.scriptloader.loadSubScript(
|
||||
`${rootURI}/chrome/content/scripts/index.js`,
|
||||
ctx
|
||||
`${rootURI}/chrome/content/scripts/__addonRef__.js`,
|
||||
ctx,
|
||||
);
|
||||
Zotero.__addonInstance__.hooks.onStartup();
|
||||
}
|
||||
|
||||
if (Zotero.platformMajorVersion >= 102) {
|
||||
var aomStartup = Components.classes[
|
||||
"@mozilla.org/addons/addon-manager-startup;1"
|
||||
].getService(Components.interfaces.amIAddonManagerStartup);
|
||||
var manifestURI = Services.io.newURI(rootURI + "manifest.json");
|
||||
chromeHandle = aomStartup.registerChrome(manifestURI, [
|
||||
["content", "__addonRef__", rootURI + "chrome/content/"],
|
||||
["locale", "__addonRef__", "en-US", rootURI + "chrome/locale/en-US/"],
|
||||
["locale", "__addonRef__", "zh-CN", rootURI + "chrome/locale/zh-CN/"],
|
||||
]);
|
||||
async function onMainWindowLoad({ window }, reason) {
|
||||
Zotero.__addonInstance__?.hooks.onMainWindowLoad(window);
|
||||
}
|
||||
|
||||
// Zotero.PreferencePanes.register({
|
||||
// pluginID: "__addonID__",
|
||||
// src: rootURI + "chrome/content/preferences.xhtml",
|
||||
// extraDTD: ["chrome://__addonRef__/locale/overlay.dtd"],
|
||||
// defaultXUL: true,
|
||||
// });
|
||||
}
|
||||
async function onMainWindowUnload({ window }, reason) {
|
||||
Zotero.__addonInstance__?.hooks.onMainWindowUnload(window);
|
||||
}
|
||||
|
||||
function shutdown({ id, version, resourceURI, rootURI }, reason) {
|
||||
if (reason === APP_SHUTDOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof Zotero === "undefined") {
|
||||
Zotero = Components.classes["@zotero.org/Zotero;1"].getService(
|
||||
Components.interfaces.nsISupports
|
||||
Components.interfaces.nsISupports,
|
||||
).wrappedJSObject;
|
||||
}
|
||||
Zotero.AddonTemplate.events.onUnInit(Zotero);
|
||||
Zotero.__addonInstance__?.hooks.onShutdown();
|
||||
|
||||
Cc["@mozilla.org/intl/stringbundle;1"]
|
||||
.getService(Components.interfaces.nsIStringBundleService)
|
||||
.flushBundles();
|
||||
|
||||
Cu.unload(`${rootURI}/chrome/content/scripts/index.js`);
|
||||
Cu.unload(`${rootURI}/chrome/content/scripts/__addonRef__.js`);
|
||||
|
||||
chromeHandle.destruct();
|
||||
chromeHandle = null;
|
||||
if (chromeHandle) {
|
||||
chromeHandle.destruct();
|
||||
chromeHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
function uninstall(data, reason) {}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
content __addonRef__ chrome/content/
|
||||
locale __addonRef__ en-US chrome/locale/en-US/
|
||||
locale __addonRef__ zh-CN chrome/locale/zh-CN/
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 677 B After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 836 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -1,21 +0,0 @@
|
|||
<vbox
|
||||
id="zotero-prefpane-__addonRef__"
|
||||
onload="Zotero.AddonTemplate.prefs.initPreferences(window)"
|
||||
>
|
||||
<groupbox>
|
||||
<label><html:h2>Addon Template Example</html:h2></label>
|
||||
<checkbox
|
||||
id="zotero-prefpane-__addonRef__-enable"
|
||||
label="&zotero.__addonRef__.pref.enable.label;"
|
||||
/>
|
||||
<hbox>
|
||||
<html:input
|
||||
type="text"
|
||||
id="zotero-prefpane-__addonRef__-input"
|
||||
></html:input>
|
||||
<html:label for="zotero-prefpane-__addonRef__-input"
|
||||
>&zotero.__addonRef__.pref.input.label;</html:label
|
||||
>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
</vbox>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<!ENTITY zotero.__addonRef__.itemmenu.test.label "addon template">
|
||||
<!ENTITY zotero.__addonRef__.pref.enable.label "Enable">
|
||||
<!ENTITY zotero.__addonRef__.pref.input.label "Input">
|
||||
|
||||
<!ENTITY zotero.__addonRef__.help.version.label "__addonName__ VERSION __buildVersion__">
|
||||
<!ENTITY zotero.__addonRef__.help.releasetime.label "Build __buildTime__">
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<!ENTITY zotero.__addonRef__.itemmenu.test.label "插件模板">
|
||||
<!ENTITY zotero.__addonRef__.pref.enable.label "开启">
|
||||
<!ENTITY zotero.__addonRef__.pref.input.label "输入">
|
||||
|
||||
<!ENTITY zotero.__addonRef__.help.version.label "__addonName__ 版本 __buildVersion__">
|
||||
<!ENTITY zotero.__addonRef__.help.releasetime.label "Build __buildTime__">
|
||||
|
|
@ -1 +0,0 @@
|
|||
pref("extensions.zotero.__addonRef__.enable", true);
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<RDF:RDF
|
||||
xmlns:em="http://www.mozilla.org/2004/em-rdf#"
|
||||
xmlns:NC="http://home.netscape.com/NC-rdf#"
|
||||
xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<RDF:Description
|
||||
RDF:about="urn:mozilla:install-manifest"
|
||||
em:id="__addonID__"
|
||||
em:name="__addonName__"
|
||||
em:version="__buildVersion__"
|
||||
em:type="2"
|
||||
em:creator="__author__"
|
||||
em:description="__description__"
|
||||
em:homepageURL="__homepage__"
|
||||
em:iconURL="chrome://__addonRef__/content/icons/favicon.png"
|
||||
em:optionsURL="chrome://__addonRef__/content/preferences.xul"
|
||||
em:updateURL="__updaterdf__"
|
||||
em:multiprocessCompatible="true"
|
||||
em:bootstrap="true">>
|
||||
<em:type>2</em:type>
|
||||
<em:targetApplication RDF:resource="rdf:#$x61SL3"/>
|
||||
<em:targetApplication>
|
||||
<Description>
|
||||
<em:id>zotero@chnm.gmu.edu</em:id>
|
||||
<em:minVersion>5.0</em:minVersion>
|
||||
<em:maxVersion>*</em:maxVersion>
|
||||
</Description>
|
||||
</em:targetApplication>
|
||||
<em:targetApplication>
|
||||
<Description>
|
||||
<em:id>juris-m@juris-m.github.io</em:id>
|
||||
<em:minVersion>5.0</em:minVersion>
|
||||
<em:maxVersion>*</em:maxVersion>
|
||||
</Description>
|
||||
</em:targetApplication>
|
||||
</RDF:Description>
|
||||
</RDF:RDF>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
menuitem-updatetldrlabel = update TLDR
|
||||
menucollection-updatetldrlabel = update TLDR
|
||||
itembox-tldrlabel = TLDR
|
||||
tldr-unrelated = TLDR Unrelated in Semantic scholar
|
||||
tldr-itemnotfound = Item Not Found in Semantic scholar
|
||||
popWindow-succeed = Succeed
|
||||
popWindow-failed = Failed
|
||||
popWindow-waiting = Waiting
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
itemPaneSection-header =
|
||||
.label = TLDR
|
||||
itemPaneSection-sidenav =
|
||||
.tooltiptext = TLDR
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
menuitem-updatetldrlabel = 更新TLDR
|
||||
menucollection-updatetldrlabel = 批量更新TLDR
|
||||
itembox-tldrlabel = TLDR
|
||||
tldr-unrelated = 未关联TLDR
|
||||
tldr-itemnotfound = 未搜索到此条目
|
||||
popWindow-succeed = 成功
|
||||
popWindow-failed = 失败
|
||||
popWindow-waiting = 等待
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
itemPaneSection-header =
|
||||
.label = TLDR
|
||||
itemPaneSection-sidenav =
|
||||
.tooltiptext = TLDR
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
"name": "__addonName__",
|
||||
"version": "__buildVersion__",
|
||||
"description": "__description__",
|
||||
"homepage_url": "__homepage__",
|
||||
"author": "__author__",
|
||||
"icons": {
|
||||
"48": "chrome/content/icons/favicon@0.5x.png",
|
||||
|
|
@ -11,7 +12,7 @@
|
|||
"applications": {
|
||||
"zotero": {
|
||||
"id": "__addonID__",
|
||||
"update_url": "__updaterdf__",
|
||||
"update_url": "__updateURL__",
|
||||
"strict_min_version": "6.999",
|
||||
"strict_max_version": "7.0.*"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
/* eslint-disable no-undef */
|
||||
181
build.js
181
build.js
|
|
@ -1,181 +0,0 @@
|
|||
const esbuild = require("esbuild");
|
||||
const compressing = require("compressing");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const process = require("process");
|
||||
const replace = require("replace-in-file");
|
||||
const {
|
||||
name,
|
||||
author,
|
||||
description,
|
||||
homepage,
|
||||
releasepage,
|
||||
updaterdf,
|
||||
addonName,
|
||||
addonID,
|
||||
addonRef,
|
||||
version,
|
||||
} = require("./package.json");
|
||||
|
||||
function copyFileSync(source, target) {
|
||||
var targetFile = target;
|
||||
|
||||
// If target is a directory, a new file with the same name will be created
|
||||
if (fs.existsSync(target)) {
|
||||
if (fs.lstatSync(target).isDirectory()) {
|
||||
targetFile = path.join(target, path.basename(source));
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(targetFile, fs.readFileSync(source));
|
||||
}
|
||||
|
||||
function copyFolderRecursiveSync(source, target) {
|
||||
var files = [];
|
||||
|
||||
// Check if folder needs to be created or integrated
|
||||
var targetFolder = path.join(target, path.basename(source));
|
||||
if (!fs.existsSync(targetFolder)) {
|
||||
fs.mkdirSync(targetFolder);
|
||||
}
|
||||
|
||||
// Copy
|
||||
if (fs.lstatSync(source).isDirectory()) {
|
||||
files = fs.readdirSync(source);
|
||||
files.forEach(function (file) {
|
||||
var curSource = path.join(source, file);
|
||||
if (fs.lstatSync(curSource).isDirectory()) {
|
||||
copyFolderRecursiveSync(curSource, targetFolder);
|
||||
} else {
|
||||
copyFileSync(curSource, targetFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clearFolder(target) {
|
||||
if (fs.existsSync(target)) {
|
||||
fs.rmSync(target, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(target, { recursive: true });
|
||||
}
|
||||
|
||||
function dateFormat(fmt, date) {
|
||||
let ret;
|
||||
const opt = {
|
||||
"Y+": date.getFullYear().toString(),
|
||||
"m+": (date.getMonth() + 1).toString(),
|
||||
"d+": date.getDate().toString(),
|
||||
"H+": date.getHours().toString(),
|
||||
"M+": date.getMinutes().toString(),
|
||||
"S+": date.getSeconds().toString(),
|
||||
};
|
||||
for (let k in opt) {
|
||||
ret = new RegExp("(" + k + ")").exec(fmt);
|
||||
if (ret) {
|
||||
fmt = fmt.replace(
|
||||
ret[1],
|
||||
ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, "0")
|
||||
);
|
||||
}
|
||||
}
|
||||
return fmt;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const t = new Date();
|
||||
const buildTime = dateFormat("YYYY-mm-dd HH:MM:SS", t);
|
||||
const buildDir = "builds";
|
||||
|
||||
console.log(
|
||||
`[Build] BUILD_DIR=${buildDir}, VERSION=${version}, BUILD_TIME=${buildTime}`
|
||||
);
|
||||
|
||||
clearFolder(buildDir);
|
||||
|
||||
copyFolderRecursiveSync("addon", buildDir);
|
||||
|
||||
copyFileSync("update-template.json", "update.json");
|
||||
copyFileSync("update-template.rdf", "update.rdf");
|
||||
|
||||
await esbuild
|
||||
.build({
|
||||
entryPoints: ["src/index.ts"],
|
||||
bundle: true,
|
||||
// Entry should be the same as addon/chrome/content/overlay.xul
|
||||
outfile: path.join(buildDir, "addon/chrome/content/scripts/index.js"),
|
||||
// minify: true,
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
|
||||
console.log("[Build] Run esbuild OK");
|
||||
|
||||
const optionsAddon = {
|
||||
files: [
|
||||
path.join(buildDir, "**/*.rdf"),
|
||||
path.join(buildDir, "**/*.dtd"),
|
||||
path.join(buildDir, "**/*.xul"),
|
||||
path.join(buildDir, "**/*.xhtml"),
|
||||
path.join(buildDir, "**/*.json"),
|
||||
path.join(buildDir, "addon/defaults", "**/*.js"),
|
||||
path.join(buildDir, "addon/chrome.manifest"),
|
||||
path.join(buildDir, "addon/manifest.json"),
|
||||
path.join(buildDir, "addon/bootstrap.js"),
|
||||
"update.json",
|
||||
"update.rdf",
|
||||
],
|
||||
from: [
|
||||
/__author__/g,
|
||||
/__description__/g,
|
||||
/__homepage__/g,
|
||||
/__releasepage__/g,
|
||||
/__updaterdf__/g,
|
||||
/__addonName__/g,
|
||||
/__addonID__/g,
|
||||
/__addonRef__/g,
|
||||
/__buildVersion__/g,
|
||||
/__buildTime__/g,
|
||||
],
|
||||
to: [
|
||||
author,
|
||||
description,
|
||||
homepage,
|
||||
releasepage,
|
||||
updaterdf,
|
||||
addonName,
|
||||
addonID,
|
||||
addonRef,
|
||||
version,
|
||||
buildTime,
|
||||
],
|
||||
countMatches: true,
|
||||
};
|
||||
|
||||
_ = replace.sync(optionsAddon);
|
||||
console.log(
|
||||
"[Build] Run replace in ",
|
||||
_.filter((f) => f.hasChanged).map(
|
||||
(f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`
|
||||
)
|
||||
);
|
||||
|
||||
console.log("[Build] Replace OK");
|
||||
|
||||
console.log("[Build] Addon prepare OK");
|
||||
|
||||
compressing.zip.compressDir(
|
||||
path.join(buildDir, "addon"),
|
||||
path.join(buildDir, `${name}.xpi`),
|
||||
{
|
||||
ignoreBase: true,
|
||||
}
|
||||
);
|
||||
|
||||
console.log("[Build] Addon pack OK");
|
||||
console.log(
|
||||
`[Build] Finished in ${(new Date().getTime() - t.getTime()) / 1000} s.`
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,420 @@
|
|||
# Zotero Plugin Template
|
||||
|
||||
[](https://www.zotero.org)
|
||||
[](https://github.com/windingwind/zotero-plugin-template)
|
||||
|
||||
这是 [Zotero](https://www.zotero.org/) 的插件模板.
|
||||
|
||||
[English](../README.md) | [简体中文](./README-zhCN.md)
|
||||
|
||||
📖 [插件开发文档](https://zotero.yuque.com/books/share/8d230829-6004-4934-b4c6-685a7001bfa0/vec88d) (中文版,已过时)
|
||||
|
||||
[📖 Zotero 7 插件开发文档](https://www.zotero.org/support/dev/zotero_7_for_developers)
|
||||
|
||||
🛠️ [Zotero 插件工具包](https://github.com/windingwind/zotero-plugin-toolkit) | [API 文档](https://github.com/windingwind/zotero-plugin-toolkit/blob/master/docs/zotero-plugin-toolkit.md)
|
||||
|
||||
ℹ️ [Zotero 类型定义](https://github.com/windingwind/zotero-types)
|
||||
|
||||
📜 [Zotero 源代码](https://github.com/zotero/zotero)
|
||||
|
||||
📌 [Zotero 插件模板](https://github.com/windingwind/zotero-plugin-template) (即本仓库)
|
||||
|
||||
> [!tip]
|
||||
> 👁 Watch 本仓库,以及时收到修复或更新的通知.
|
||||
|
||||
## 使用此模板构建的插件
|
||||
|
||||
[](https://github.com/windingwind/zotero-better-notes)
|
||||
[](https://github.com/windingwind/zotero-pdf-preview)
|
||||
[](https://github.com/windingwind/zotero-pdf-translate)
|
||||
[](https://github.com/windingwind/zotero-tag)
|
||||
[](https://github.com/iShareStuff/ZoteroTheme)
|
||||
[](https://github.com/MuiseDestiny/zotero-reference)
|
||||
[](https://github.com/MuiseDestiny/zotero-citation)
|
||||
[](https://github.com/MuiseDestiny/ZoteroStyle)
|
||||
[](https://github.com/volatile-static/Chartero)
|
||||
[](https://github.com/l0o0/tara)
|
||||
[](https://github.com/redleafnew/delitemwithatt)
|
||||
[](https://github.com/redleafnew/zotero-updateifsE)
|
||||
[](https://github.com/northword/zotero-format-metadata)
|
||||
[](https://github.com/inciteful-xyz/inciteful-zotero-plugin)
|
||||
[](https://github.com/MuiseDestiny/zotero-gpt)
|
||||
[](https://github.com/zoushucai/zotero-journalabbr)
|
||||
[](https://github.com/MuiseDestiny/zotero-figure)
|
||||
[](https://github.com/l0o0/jasminum)
|
||||
[](https://github.com/lifan0127/ai-research-assistant)
|
||||
|
||||
[](https://github.com/daeh/zotero-markdb-connect)
|
||||
|
||||
如果你正在使用此库,我建议你将这个标志 ([](https://github.com/windingwind/zotero-plugin-template)) 放在 README 文件中:
|
||||
|
||||
```md
|
||||
[](https://github.com/windingwind/zotero-plugin-template)
|
||||
```
|
||||
|
||||
## Features 特性
|
||||
|
||||
- 事件驱动、函数式编程的可扩展框架;
|
||||
- 简单易用,开箱即用;
|
||||
- ⭐[新特性!]自动热重载!每当修改源码时,都会自动编译并重新加载插件;[详情请跳转→](#自动热重载)
|
||||
- `src/modules/examples.ts` 中有丰富的示例,涵盖了插件中常用的大部分API (使用的插件工具包 zotero-plugin-toolkit,仓库地址 https://github.com/windingwind/zotero-plugin-toolkit);
|
||||
- TypeScript 支持:
|
||||
- 为使用 JavaScript 编写的Zotero源码提供全面的类型定义支持 (使用类型定义包 zotero-types,仓库地址 https://github.com/windingwind/zotero-types);
|
||||
- 全局变量和环境设置;
|
||||
- 插件开发/构建/发布工作流:
|
||||
- 自动生成/更新插件id和版本、更新配置和设置环境变量 (`development`/`production`);
|
||||
- 自动在 Zotero 中构建和重新加载代码;
|
||||
- 自动发布到GitHub (使用[release-it](https://github.com/release-it/release-it));
|
||||
- 集成Prettier和ES Lint;
|
||||
|
||||
> [!warning]
|
||||
> Zotero本地化已升级(`dtd` 已弃用,我们将不再使用 `.properties`). 主分支将只支持 Zotero 7.0.0-beta.12 或更高版本. 如果需要支持 Zotero 6,你可能需要同时使用`dtd`、`properties` 和`ftl`. 请参考此库的 `zotero6-bootstrap` 分支.
|
||||
|
||||
## Examples 示例
|
||||
|
||||
此库提供了 [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit) 中API的示例.
|
||||
|
||||
在 `src/examples.ts` 中搜索`@example` 查看示例. 这些示例在 `src/hooks.ts` 中调用演示.
|
||||
|
||||
### 基本示例(Basic Examples)
|
||||
|
||||
- registerNotifier
|
||||
- registerPrefs, unregisterPrefs
|
||||
|
||||
### 快捷键示例(Shortcut Keys Examples)
|
||||
|
||||
- registerShortcuts
|
||||
- exampleShortcutLargerCallback
|
||||
- exampleShortcutSmallerCallback
|
||||
- exampleShortcutConflictionCallback
|
||||
|
||||
### UI示例(UI Examples)
|
||||
|
||||

|
||||
|
||||
- registerStyleSheet(the official make-it-red example)
|
||||
- registerRightClickMenuItem
|
||||
- registerRightClickMenuPopup
|
||||
- registerWindowMenuWithSeprator
|
||||
- registerExtraColumn
|
||||
- registerExtraColumnWithCustomCell
|
||||
- registerCustomItemBoxRow
|
||||
- registerLibraryTabPanel
|
||||
- registerReaderTabPanel
|
||||
|
||||
### 首选项面板示例(Preference Pane Examples)
|
||||
|
||||

|
||||
|
||||
- Preferences bindings
|
||||
- UI Events
|
||||
- Table
|
||||
- Locale
|
||||
|
||||
详情参见 [`src/modules/preferenceScript.ts`](./src/modules/preferenceScript.ts)
|
||||
|
||||
### 帮助示例(HelperExamples)
|
||||
|
||||

|
||||
|
||||
- dialogExample
|
||||
- clipboardExample
|
||||
- filePickerExample
|
||||
- progressWindowExample
|
||||
- vtableExample(See Preference Pane Examples)
|
||||
|
||||
### 指令行示例(PromptExamples)
|
||||
|
||||
Obsidian风格的指令输入模块,它通过接受文本来运行插件,并在弹出窗口中显示可选项.
|
||||
|
||||
使用 `Shift+P` 激活.
|
||||
|
||||

|
||||
|
||||
- registerAlertPromptExample
|
||||
|
||||
## Quick Start Guide 快速入门指南
|
||||
|
||||
### 0 前置要求(Requirement)
|
||||
|
||||
1. 安装测试版 Zotero:https://www.zotero.org/support/beta_builds
|
||||
2. 安装 Node.js(https://nodejs.org/en/)和 Git(https://git-scm.com/)
|
||||
|
||||
> [!note]
|
||||
> 本指南假定你已经对 Zotero 插件的基本结构和工作原理有初步的了解. 如果你还不了解,请先参考官方文档(https://www.zotero.org/support/dev/zotero_7_for_developers)和官方插件样例 Make It Red(仓库地址 https://github.com/zotero/make-it-red).
|
||||
|
||||
### 1 创建你的仓库(Create Your Repo)
|
||||
|
||||
1. 点击 `Use this template`;
|
||||
2. 使用 `git clone` 克隆上一步生成的仓库;
|
||||
<details >
|
||||
<summary>💡 从 GitHub Codespace 开始</summary>
|
||||
|
||||
_GitHub CodeSpace_ 使你可以直接开始开发而无需在本地下载代码/IDE/依赖.
|
||||
|
||||
重复下列步骤,仅需三十秒即可开始构建你的第一个插件!
|
||||
|
||||
- 去 [homepage](https://github.com/windingwind/zotero-plugin-template)顶部,点击绿色按钮`Use this template`,点击 `Open in codespace`, 你需要登录你的GitHub账号.
|
||||
- 等待 codespace 加载.
|
||||
|
||||
</details>
|
||||
|
||||
3. 进入项目文件夹;
|
||||
|
||||
### 2 配置模板和开发环境(Config Template Settings and Enviroment)
|
||||
|
||||
1. 修改 `./package.json` 中的设置,包括:
|
||||
|
||||
```json5
|
||||
{
|
||||
version: "", // to 0.0.0
|
||||
author: "",
|
||||
description: "",
|
||||
homepage: "",
|
||||
config: {
|
||||
addonName: "", // name to be displayed in the plugin manager
|
||||
addonID: "", // ID to avoid conflict. IMPORTANT!
|
||||
addonRef: "", // e.g. Element ID prefix
|
||||
addonInstance: "", // the plugin's root instance: Zotero.${addonInstance}
|
||||
prefsPrefix: "extensions.zotero.${addonRef}", // the prefix of prefs
|
||||
releasePage: "", // URL to releases
|
||||
updateJSON: "", // URL to update.json
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
> [!warning]
|
||||
> 注意设置 addonID 和 addonRef 以避免冲突.
|
||||
|
||||
如果你需要在GitHub以外的地方托管你的 XPI 包,请删除 `releasePage` 并添加 `updateLink`,并将值设置为你的 XPI 下载地址.
|
||||
|
||||
2. 复制 Zotero 启动配置,填入 Zotero 可执行文件路径和 profile 路径.
|
||||
|
||||
> (可选项) 此操作仅需执行一次: 使用 `/path/to/zotero -p` 启动 Zotero,创建一个新的配置文件并用作开发配置文件.
|
||||
> 将配置文件的路径 `profilePath` 放入 `zotero-cmd.json` 中,以指定要使用的配置文件.
|
||||
|
||||
```sh
|
||||
cp ./scripts/zotero-cmd-template.json ./scripts/zotero-cmd.json
|
||||
vim ./scripts/zotero-cmd.json
|
||||
```
|
||||
|
||||
3. 运行 `npm install` 以安装相关依赖
|
||||
|
||||
> 如果你使用 `pnpm` 作为包管理器,你需要添加 `public-hoist-pattern[]=*@types/bluebird*` 到`.npmrc`, 详情请查看 zotero-types(https://github.com/windingwind/zotero-types?tab=readme-ov-file#usage)的文档.
|
||||
|
||||
### 3 开始开发(Coding)
|
||||
|
||||
使用 `npm start` 启动开发服务器,它将:
|
||||
|
||||
- 在开发模式下预构建插件
|
||||
- 启动 Zotero ,并让其从 `build/` 中加载插件
|
||||
- 打开开发者工具(devtool)
|
||||
- 监听 `src/**` 和 `addon/**`.
|
||||
- 如果 `src/**` 修改了,运行 esbuild 并且重新加载
|
||||
- 如果 `addon/**` 修改了,(在开发模式下)重新构建插件并且重新加载
|
||||
|
||||
#### 自动热重载
|
||||
|
||||
厌倦了无休止的重启吗?忘掉它,拥抱热加载!
|
||||
|
||||
1. 运行 `npm start`.
|
||||
2. 编码. (是的,就这么简单)
|
||||
|
||||
当检测到 `src` 或 `addon` 中的文件修改时,插件将自动编译并重新加载.
|
||||
|
||||
<details style="text-indent: 2em">
|
||||
<summary>💡 将此功能添加到现有插件的步骤</summary>
|
||||
|
||||
1. 复制 `scripts/**.mjs`
|
||||
2. 复制 `server` 、`build` 和 `stop` 命令到 `package.json`
|
||||
3. 运行 `npm install --save-dev chokidar`
|
||||
4. 结束.
|
||||
|
||||
</details>
|
||||
|
||||
#### 在 Zotero 中 Debug
|
||||
|
||||
你还可以:
|
||||
|
||||
- 在 Tools->Developer->Run Javascript 中测试代码片段;
|
||||
|
||||
- 使用 `Zotero.debug()` 调试输出. 在 Help->Debug Output Logging->View Output 查看输出;
|
||||
|
||||
- 调试 UI. Zotero 建立在 Firefox XUL 框架之上. 使用 [XUL Explorer](https://udn.realityripple.com/docs/Archive/Mozilla/XUL_Explorer) 等软件调试 XUL UI.
|
||||
|
||||
> XUL 文档: <http://www.devdoc.net/web/developer.mozilla.org/en-US/docs/XUL.html>
|
||||
|
||||
### 4 构建(Build)
|
||||
|
||||
运行 `npm run build` 在生产模式下构建插件,构建的结果位于 `build/` 目录中.
|
||||
|
||||
`scripts/build.mjs` 的运行步骤:
|
||||
|
||||
- 创建/清空 `build/`
|
||||
- 复制 `addon/**` 到 `build/addon/**`
|
||||
- 替换占位符:使用 `replace-in-file` 去替换在 `package.json` 中定义的关键字和配置 (`xhtml`、`.flt` 等)
|
||||
- 准备本地化文件以避免冲突,查看官方文档了解更多(https://www.zotero.org/support/dev/zotero_7_for_developers#avoiding_localization_conflicts)
|
||||
- 重命名`**/*.flt` 为 `**/${addonRef}-*.flt`
|
||||
- 在每个消息前加上 `addonRef-`
|
||||
- 使用 Esbuild 来将 `.ts` 源码构建为 `.js`,从 `src/index.ts` 构建到`./build/addon/chrome/content/scripts`
|
||||
- (仅在生产模式下工作) 压缩 `./build/addon` 目录为 `./build/*.xpi`
|
||||
- (仅在生产模式下工作) 准备 `update.json` 或 `update-beta.json`
|
||||
|
||||
> [!note]
|
||||
>
|
||||
> **Dev & prod 两者有什么区别?**
|
||||
>
|
||||
> - 此环境变量存储在 `Zotero.${addonInstance}.data.env` 中,控制台输出在生产模式下被禁用.
|
||||
> - 你可以根据此变量决定用户无法查看/使用的内容.
|
||||
> - 在生产模式下,构建脚本将自动打包插件并更新 `update.json`.
|
||||
|
||||
### 5 发布(Release)
|
||||
|
||||
如果要构建和发布插件,运行如下指令:
|
||||
|
||||
```shell
|
||||
# A release-it command: version increase, npm run build, git push, and GitHub release
|
||||
# release-it: https://github.com/release-it/release-it
|
||||
npm run release
|
||||
```
|
||||
|
||||
> [!note]
|
||||
> 在此模板中,release-it 被配置为在本地升级版本、构建、推送提交和 git 标签,随后GitHub Action 将重新构建插件并将 XPI 发布到 GitHub Release.
|
||||
>
|
||||
> 如果你需要发布一个本地构建的 XPI,将 `package.json` 中的 `release-it.github.release` 设置为 `true`,然后移除 `.github/workflows/release.yml`. 此外,你还需要设置环境变量 `GITHUB_TOKEN`,获取 GitHub Token(https://github.com/settings/tokens).
|
||||
|
||||
#### 关于预发布
|
||||
|
||||
该模板将 `prerelease` 定义为插件的测试版,当你在 release-it 中选择 `prerelease` 版本 (版本号中带有 `-` ),构建脚本将创建一个 `update-beta.json` 给预发布版本使用,这将确保常规版本的用户不会自动更新到测试版,只有手动下载并安装了测试版的用户才能自动更新到下一个测试版. 当下一个正式版本更新时,脚本将同步更新 `update.json` 和 `update-beta.json`,这将使正式版和测试版用户都可以更新到最新的正式版.
|
||||
|
||||
> [!warning]
|
||||
> 严格来说,区分 Zotero 6 和 Zotero 7 兼容的插件版本应该通过 `update.json` 的 `addons.__addonID__.updates[]` 中分别配置 `applications.zotero.strict_min_version`,这样 Zotero 才能正确识别,详情在 Zotero 7 开发文档(https://www.zotero.org/support/dev/zotero_7_for_developers#updaterdf_updatesjson)获取.
|
||||
|
||||
## Details 更多细节
|
||||
|
||||
### 关于Hooks(About Hooks)
|
||||
|
||||
> 可以在 [`src/hooks.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/hooks.ts) 中查看更多
|
||||
|
||||
1. 当在 Zotero 中触发安装/启用/启动时,`bootstrap.js` > `startup` 被调用
|
||||
- 等待 Zotero 就绪
|
||||
- 加载 `index.js` (插件代码的主入口,从 `index.ts` 中构建)
|
||||
- 如果是 Zotero 7 以上的版本则注册资源
|
||||
2. 主入口 `index.js` 中,插件对象被注入到 `Zotero` ,并且 `hooks.ts` > `onStartup` 被调用.
|
||||
- 初始化插件需要的资源,包括通知监听器、首选项面板和UI元素.
|
||||
3. 当在 Zotero 中触发卸载/禁用时,`bootstrap.js` > `shutdown` 被调用.
|
||||
- `events.ts` > `onShutdown` 被调用. 移除 UI 元素、首选项面板或插件创建的任何内容.
|
||||
- 移除脚本并释放资源.
|
||||
|
||||
### 关于全局变量(About Global Variables)
|
||||
|
||||
> 可以在 [`src/index.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/index.ts)中查看更多
|
||||
|
||||
bootstrap插件在沙盒中运行,但沙盒中没有默认的全局变量,例如 `Zotero` 或 `window` 等我们曾在overlay插件环境中使用的变量.
|
||||
|
||||
此模板将以下变量注册到全局范围:
|
||||
|
||||
```ts
|
||||
Zotero, ZoteroPane, Zotero_Tabs, window, document, rootURI, ztoolkit, addon;
|
||||
```
|
||||
|
||||
### 创建元素 API(Create Elements API)
|
||||
|
||||
插件模板为 bootstrap 插件提供了一些新的API. 我们有两个原因使用这些 API,而不是使用 `createElement/createElementNS`:
|
||||
|
||||
- 在 bootstrap 模式下,插件必须在推出(禁用或卸载)时清理所有 UI 元素,这非常麻烦. 使用 `createElement`,插件模板将维护这些元素. 仅仅在退出时 `unregisterAll` .
|
||||
- Zotero 7 需要 createElement()/createElementNS() → createXULElement() 来表示其他的 XUL 元素,而 Zotero 6 并不支持 `createXULElement`. 类似于 React.createElement 的API `createElement` 检测 namespace(xul/html/svg) 并且自动创建元素,返回元素为对应的 TypeScript 元素类型.
|
||||
|
||||
```ts
|
||||
createElement(document, "div"); // returns HTMLDivElement
|
||||
createElement(document, "hbox"); // returns XUL.Box
|
||||
createElement(document, "button", { namespace: "xul" }); // manually set namespace. returns XUL.Button
|
||||
```
|
||||
|
||||
### 关于 Zotero API(About Zotero API)
|
||||
|
||||
Zotero 文档已过时且不完整,克隆 https://github.com/zotero/zotero 并全局搜索关键字.
|
||||
|
||||
> ⭐[zotero-types](https://github.com/windingwind/zotero-types) 提供了最常用的 Zotero API,在默认情况下它被包含在此模板中. 你的 IDE 将为大多数的 API 提供提醒.
|
||||
|
||||
猜你需要:查找所需 API的技巧
|
||||
|
||||
在 `.xhtml`/`.flt` 文件中搜索 UI 标签,然后在 locale 文件中找到对应的键. ,然后在 `.js`/`.jsx` 文件中搜索此键.
|
||||
|
||||
### 目录结构(Directory Structure)
|
||||
|
||||
本部分展示了模板的目录结构.
|
||||
|
||||
- 所有的 `.js/.ts` 代码都在 `./src`;
|
||||
- 插件配置文件:`./addon/manifest.json`;
|
||||
- UI 文件: `./addon/chrome/content/*.xhtml`.
|
||||
- 区域设置文件: `./addon/locale/**/*.flt`;
|
||||
- 首选项文件: `./addon/prefs.js`;
|
||||
> 不要在 `prefs.js` 中换行
|
||||
|
||||
```shell
|
||||
.
|
||||
|-- .eslintrc.json # eslint conf
|
||||
|-- .gitattributes # git conf
|
||||
|-- .github/ # github conf
|
||||
|-- .gitignore # git conf
|
||||
|-- .prettierrc # prettier conf
|
||||
|-- .release-it.json # release-it conf
|
||||
|-- .vscode # vs code conf
|
||||
| |-- extensions.json
|
||||
| |-- launch.json
|
||||
| |-- setting.json
|
||||
| `-- toolkit.code-snippets
|
||||
|-- package-lock.json # npm conf
|
||||
|-- package.json # npm conf
|
||||
|-- LICENSE
|
||||
|-- README.md
|
||||
|-- addon
|
||||
| |-- bootstrap.js # addon load/unload script, like a main.c
|
||||
| |-- chrome
|
||||
| | `-- content
|
||||
| | |-- icons/
|
||||
| | |-- preferences.xhtml # preference panel
|
||||
| | `-- zoteroPane.css
|
||||
| |-- locale # locale
|
||||
| | |-- en-US
|
||||
| | | |-- addon.ftl
|
||||
| | | `-- preferences.ftl
|
||||
| | `-- zh-CN
|
||||
| | |-- addon.ftl
|
||||
| | `-- preferences.ftl
|
||||
| |-- manifest.json # addon config
|
||||
| `-- prefs.js
|
||||
|-- build/ # build dir
|
||||
|-- scripts # scripts for dev
|
||||
| |-- build.mjs # script to build plugin
|
||||
| |-- scripts.mjs # scripts send to Zotero, such as reload, openDevTool, etc
|
||||
| |-- server.mjs # script to start a development server
|
||||
| |-- start.mjs # script to start Zotero process
|
||||
| |-- stop.mjs # script to kill Zotero process
|
||||
| |-- utils.mjs # utils functions for dev scripts
|
||||
| |-- update-template.json # template of `update.json`
|
||||
| `-- zotero-cmd-template.json # template of local env
|
||||
|-- src # source code
|
||||
| |-- addon.ts # base class
|
||||
| |-- hooks.ts # lifecycle hooks
|
||||
| |-- index.ts # main entry
|
||||
| |-- modules # sub modules
|
||||
| | |-- examples.ts
|
||||
| | `-- preferenceScript.ts
|
||||
| `-- utils # utilities
|
||||
| |-- locale.ts
|
||||
| |-- prefs.ts
|
||||
| |-- wait.ts
|
||||
| `-- window.ts
|
||||
|-- tsconfig.json # https://code.visualstudio.com/docs/languages/jsconfig
|
||||
|-- typings # ts typings
|
||||
| `-- global.d.ts
|
||||
`-- update.json
|
||||
```
|
||||
|
||||
## Disclaimer 免责声明
|
||||
|
||||
在 AGPL 下使用此代码. 不提供任何保证. 遵守你所在地区的法律!
|
||||
|
||||
如果你想更改许可,请通过 <wyzlshx@foxmail.com> 与我联系.
|
||||
146
package.json
146
package.json
|
|
@ -1,40 +1,134 @@
|
|||
{
|
||||
"name": "zotero-addon-template",
|
||||
"addonName": "Zotero Addon Template",
|
||||
"addonID": "addontemplate@euclpts.com",
|
||||
"addonRef": "addontemplate",
|
||||
"version": "0.0.1",
|
||||
"description": "Zotero Addon Template",
|
||||
"main": "src/index.js",
|
||||
"name": "zotero-tldr",
|
||||
"version": "1.0.7",
|
||||
"description": "TLDR(too long; didn't read) from sematic scholar",
|
||||
"config": {
|
||||
"addonName": "Zotero TLDR",
|
||||
"addonID": "zoterotldr@syt.com",
|
||||
"addonRef": "zoterotldr",
|
||||
"addonInstance": "ZoteroTLDR",
|
||||
"prefsPrefix": "extensions.zotero.zoterotldr",
|
||||
"releasepage": "https://github.com/syt2/zotero-tldr/releases",
|
||||
"updateJSON": "https://raw.githubusercontent.com/syt2/zotero-tldr/main/update.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"start": "node start.js",
|
||||
"stop": "node stop.js",
|
||||
"prerestart": "npm run build",
|
||||
"restart": "node restart.js",
|
||||
"release": "release-it",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"start": "node scripts/server.mjs",
|
||||
"build": "tsc --noEmit && node scripts/build.mjs production",
|
||||
"stop": "node scripts/stop.mjs",
|
||||
"lint": "prettier --write . && eslint . --ext .ts --fix",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"release": "release-it --only-version --preReleaseId=beta",
|
||||
"update-deps": "npm update --save"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/windingwind/zotero-addon-template.git"
|
||||
"url": "git+https://github.com/syt2/zotero-tldr.git"
|
||||
},
|
||||
"author": "windingwind",
|
||||
"author": "syt2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/windingwind/zotero-addon-template/issues"
|
||||
"url": "https://github.com/syt2/zotero-tldr/issues"
|
||||
},
|
||||
"homepage": "https://github.com/windingwind/zotero-addon-template#readme",
|
||||
"releasepage": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi",
|
||||
"updaterdf": "https://raw.githubusercontent.com/windingwind/zotero-addon-template/master/update.json",
|
||||
"homepage": "https://github.com/syt2/zotero-tldr#readme",
|
||||
"dependencies": {
|
||||
"compressing": "^1.5.1",
|
||||
"esbuild": "^0.15.16",
|
||||
"replace-in-file": "^6.3.2"
|
||||
"zotero-plugin-toolkit": "^2.3.29"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.7.20",
|
||||
"release-it": "^14.14.0",
|
||||
"zotero-types": "^0.0.8"
|
||||
"@types/node": "^20.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"@typescript-eslint/parser": "^7.1.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"compressing": "^1.10.0",
|
||||
"esbuild": "^0.20.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"prettier": "^3.1.1",
|
||||
"release-it": "^17.0.1",
|
||||
"replace-in-file": "^7.0.2",
|
||||
"typescript": "^5.3.3",
|
||||
"zotero-types": "^1.3.10"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"overrides": [],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-ts-comment": [
|
||||
"warn",
|
||||
{
|
||||
"ts-expect-error": "allow-with-description",
|
||||
"ts-ignore": "allow-with-description",
|
||||
"ts-nocheck": "allow-with-description",
|
||||
"ts-check": "allow-with-description"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": [
|
||||
"off",
|
||||
{
|
||||
"ignoreRestArgs": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/build/**",
|
||||
"**/logs/**",
|
||||
"**/dist/**",
|
||||
"**/node_modules/**",
|
||||
"**/scripts/**",
|
||||
"**/*.js",
|
||||
"**/*.bak"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "lf",
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.xhtml"
|
||||
],
|
||||
"options": {
|
||||
"htmlWhitespaceSensitivity": "css"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
"tagName": "V${version}"
|
||||
},
|
||||
"npm": {
|
||||
"publish": false
|
||||
},
|
||||
"github": {
|
||||
"release": false,
|
||||
"assets": [
|
||||
"build/*.xpi"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"before:init": "npm run lint",
|
||||
"after:bump": "npm run build"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
const { execSync } = require("child_process");
|
||||
const { killZotero, startZotero } = require("./zotero-cmd.json");
|
||||
|
||||
try {
|
||||
execSync(killZotero);
|
||||
} catch (e) {}
|
||||
|
||||
execSync(startZotero);
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
import details from "../package.json" assert { type: "json" };
|
||||
import {
|
||||
Logger,
|
||||
clearFolder,
|
||||
copyFileSync,
|
||||
copyFolderRecursiveSync,
|
||||
dateFormat,
|
||||
} from "./utils.mjs";
|
||||
import { zip } from "compressing";
|
||||
import { build } from "esbuild";
|
||||
import { existsSync, readdirSync, renameSync } from "fs";
|
||||
import path from "path";
|
||||
import { env, exit } from "process";
|
||||
import replaceInFile from "replace-in-file";
|
||||
|
||||
const { replaceInFileSync } = replaceInFile;
|
||||
|
||||
process.env.NODE_ENV =
|
||||
process.argv[2] === "production" ? "production" : "development";
|
||||
|
||||
const buildDir = "build";
|
||||
|
||||
const { name, author, description, homepage, version, config } = details;
|
||||
const isPreRelease = version.includes("-");
|
||||
|
||||
function replaceString(buildTime) {
|
||||
const replaceFrom = [
|
||||
/__author__/g,
|
||||
/__description__/g,
|
||||
/__homepage__/g,
|
||||
/__buildVersion__/g,
|
||||
/__buildTime__/g,
|
||||
];
|
||||
const replaceTo = [author, description, homepage, version, buildTime];
|
||||
|
||||
config.updateURL = isPreRelease
|
||||
? config.updateJSON.replace("update.json", "update-beta.json")
|
||||
: config.updateJSON;
|
||||
|
||||
replaceFrom.push(
|
||||
...Object.keys(config).map((k) => new RegExp(`__${k}__`, "g")),
|
||||
);
|
||||
replaceTo.push(...Object.values(config));
|
||||
|
||||
const replaceResult = replaceInFileSync({
|
||||
files: [
|
||||
`${buildDir}/addon/**/*.xhtml`,
|
||||
`${buildDir}/addon/**/*.html`,
|
||||
`${buildDir}/addon/**/*.css`,
|
||||
`${buildDir}/addon/**/*.json`,
|
||||
`${buildDir}/addon/prefs.js`,
|
||||
`${buildDir}/addon/manifest.json`,
|
||||
`${buildDir}/addon/bootstrap.js`,
|
||||
],
|
||||
from: replaceFrom,
|
||||
to: replaceTo,
|
||||
countMatches: true,
|
||||
});
|
||||
|
||||
// Logger.debug(
|
||||
// "[Build] Run replace in ",
|
||||
// replaceResult.filter((f) => f.hasChanged).map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`),
|
||||
// );
|
||||
}
|
||||
|
||||
function prepareLocaleFiles() {
|
||||
// Prefix Fluent messages in xhtml
|
||||
const MessagesInHTML = new Set();
|
||||
replaceInFileSync({
|
||||
files: [`${buildDir}/addon/**/*.xhtml`, `${buildDir}/addon/**/*.html`],
|
||||
processor: (input) => {
|
||||
const matchs = [...input.matchAll(/(data-l10n-id)="(\S*)"/g)];
|
||||
matchs.map((match) => {
|
||||
input = input.replace(
|
||||
match[0],
|
||||
`${match[1]}="${config.addonRef}-${match[2]}"`,
|
||||
);
|
||||
MessagesInHTML.add(match[2]);
|
||||
});
|
||||
return input;
|
||||
},
|
||||
});
|
||||
|
||||
// Walk the sub folders of `build/addon/locale`
|
||||
const localesPath = path.join(buildDir, "addon/locale"),
|
||||
localeNames = readdirSync(localesPath, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
for (const localeName of localeNames) {
|
||||
const localePath = path.join(localesPath, localeName);
|
||||
const ftlFiles = readdirSync(localePath, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.filter((dirent) => dirent.isFile())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
// rename *.ftl to addonRef-*.ftl
|
||||
for (const ftlFile of ftlFiles) {
|
||||
if (ftlFile.endsWith(".ftl")) {
|
||||
renameSync(
|
||||
path.join(localePath, ftlFile),
|
||||
path.join(localePath, `${config.addonRef}-${ftlFile}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix Fluent messages in each ftl
|
||||
const MessageInThisLang = new Set();
|
||||
replaceInFileSync({
|
||||
files: [`${buildDir}/addon/locale/${localeName}/*.ftl`],
|
||||
processor: (fltContent) => {
|
||||
const lines = fltContent.split("\n");
|
||||
const prefixedLines = lines.map((line) => {
|
||||
// https://regex101.com/r/lQ9x5p/1
|
||||
const match = line.match(
|
||||
/^(?<message>[a-zA-Z]\S*)([ ]*=[ ]*)(?<pattern>.*)$/m,
|
||||
);
|
||||
if (match) {
|
||||
MessageInThisLang.add(match.groups.message);
|
||||
return `${config.addonRef}-${line}`;
|
||||
} else {
|
||||
return line;
|
||||
}
|
||||
});
|
||||
return prefixedLines.join("\n");
|
||||
},
|
||||
});
|
||||
|
||||
// If a message in xhtml but not in ftl of current language, log it
|
||||
MessagesInHTML.forEach((message) => {
|
||||
if (!MessageInThisLang.has(message)) {
|
||||
Logger.error(`[Build] ${message} don't exist in ${localeName}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function prepareUpdateJson() {
|
||||
// If it is a pre-release, use update-beta.json
|
||||
if (!isPreRelease) {
|
||||
copyFileSync("scripts/update-template.json", "update.json");
|
||||
}
|
||||
if (existsSync("update-beta.json") || isPreRelease) {
|
||||
copyFileSync("scripts/update-template.json", "update-beta.json");
|
||||
}
|
||||
|
||||
const updateLink =
|
||||
config.updateLink ?? isPreRelease
|
||||
? `${config.releasePage}/download/v${version}/${name}.xpi`
|
||||
: `${config.releasePage}/latest/download/${name}.xpi`;
|
||||
|
||||
const replaceResult = replaceInFileSync({
|
||||
files: [
|
||||
"update-beta.json",
|
||||
isPreRelease ? "pass" : "update.json",
|
||||
`${buildDir}/addon/manifest.json`,
|
||||
],
|
||||
from: [
|
||||
/__addonID__/g,
|
||||
/__buildVersion__/g,
|
||||
/__updateLink__/g,
|
||||
/__updateURL__/g,
|
||||
],
|
||||
to: [config.addonID, version, updateLink, config.updateURL],
|
||||
countMatches: true,
|
||||
});
|
||||
|
||||
Logger.debug(
|
||||
`[Build] Prepare Update.json for ${
|
||||
isPreRelease
|
||||
? "\u001b[31m Prerelease \u001b[0m"
|
||||
: "\u001b[32m Release \u001b[0m"
|
||||
}`,
|
||||
replaceResult
|
||||
.filter((f) => f.hasChanged)
|
||||
.map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`),
|
||||
);
|
||||
}
|
||||
|
||||
export const esbuildOptions = {
|
||||
entryPoints: ["src/index.ts"],
|
||||
define: {
|
||||
__env__: `"${env.NODE_ENV}"`,
|
||||
},
|
||||
bundle: true,
|
||||
target: "firefox102",
|
||||
outfile: path.join(
|
||||
buildDir,
|
||||
`addon/chrome/content/scripts/${config.addonRef}.js`,
|
||||
),
|
||||
// Don't turn minify on
|
||||
minify: env.NODE_ENV === "production",
|
||||
};
|
||||
|
||||
export async function main() {
|
||||
const t = new Date();
|
||||
const buildTime = dateFormat("YYYY-mm-dd HH:MM:SS", new Date());
|
||||
|
||||
Logger.info(
|
||||
`[Build] BUILD_DIR=${buildDir}, VERSION=${version}, BUILD_TIME=${buildTime}, ENV=${[
|
||||
env.NODE_ENV,
|
||||
]}`,
|
||||
);
|
||||
|
||||
clearFolder(buildDir);
|
||||
copyFolderRecursiveSync("addon", buildDir);
|
||||
|
||||
Logger.debug("[Build] Replacing");
|
||||
replaceString(buildTime);
|
||||
|
||||
Logger.debug("[Build] Preparing locale files");
|
||||
prepareLocaleFiles();
|
||||
|
||||
Logger.debug("[Build] Running esbuild");
|
||||
await build(esbuildOptions);
|
||||
|
||||
Logger.debug("[Build] Addon prepare OK");
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
Logger.debug("[Build] Packing Addon");
|
||||
await zip.compressDir(
|
||||
path.join(buildDir, "addon"),
|
||||
path.join(buildDir, `${name}.xpi`),
|
||||
{
|
||||
ignoreBase: true,
|
||||
},
|
||||
);
|
||||
|
||||
prepareUpdateJson();
|
||||
|
||||
Logger.debug(
|
||||
`[Build] Finished in ${(new Date().getTime() - t.getTime()) / 1000} s.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
main().catch((err) => {
|
||||
Logger.error(err);
|
||||
exit(1);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import details from "../package.json" assert { type: "json" };
|
||||
|
||||
const { addonID, addonName } = details.config;
|
||||
const { version } = details;
|
||||
|
||||
export const reloadScript = `
|
||||
(async () => {
|
||||
Services.obs.notifyObservers(null, "startupcache-invalidate", null);
|
||||
const { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
|
||||
const addon = await AddonManager.getAddonByID("${addonID}");
|
||||
await addon.reload();
|
||||
const progressWindow = new Zotero.ProgressWindow({ closeOnClick: true });
|
||||
progressWindow.changeHeadline("${addonName} Hot Reload");
|
||||
progressWindow.progress = new progressWindow.ItemProgress(
|
||||
"chrome://zotero/skin/tick.png",
|
||||
"VERSION=${version}, BUILD=${new Date().toLocaleString()}. By zotero-plugin-toolkit"
|
||||
);
|
||||
progressWindow.progress.setProgress(100);
|
||||
progressWindow.show();
|
||||
progressWindow.startCloseTimer(5000);
|
||||
})()`;
|
||||
|
||||
export const openDevToolScript = `
|
||||
(async () => {
|
||||
|
||||
// const { BrowserToolboxLauncher } = ChromeUtils.import(
|
||||
// "resource://devtools/client/framework/browser-toolbox/Launcher.jsm",
|
||||
// );
|
||||
// BrowserToolboxLauncher.init();
|
||||
// TODO: Use the above code to open the devtool after https://github.com/zotero/zotero/pull/3387
|
||||
|
||||
Zotero.Prefs.set("devtools.debugger.remote-enabled", true, true);
|
||||
Zotero.Prefs.set("devtools.debugger.remote-port", 6100, true);
|
||||
Zotero.Prefs.set("devtools.debugger.prompt-connection", false, true);
|
||||
Zotero.Prefs.set("devtools.debugger.chrome-debugging-websocket", false, true);
|
||||
|
||||
env =
|
||||
Services.env ||
|
||||
Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||
|
||||
env.set("MOZ_BROWSER_TOOLBOX_PORT", 6100);
|
||||
Zotero.openInViewer(
|
||||
"chrome://devtools/content/framework/browser-toolbox/window.html",
|
||||
{
|
||||
onLoad: (doc) => {
|
||||
doc.querySelector("#status-message-container").style.visibility =
|
||||
"collapse";
|
||||
let toolboxBody;
|
||||
waitUntil(
|
||||
() => {
|
||||
toolboxBody = doc
|
||||
.querySelector(".devtools-toolbox-browsertoolbox-iframe")
|
||||
?.contentDocument?.querySelector(".theme-body");
|
||||
return toolboxBody;
|
||||
},
|
||||
() => {
|
||||
toolboxBody.style = "pointer-events: all !important";
|
||||
}
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function waitUntil(condition, callback, interval = 100, timeout = 10000) {
|
||||
const start = Date.now();
|
||||
const intervalId = setInterval(() => {
|
||||
if (condition()) {
|
||||
clearInterval(intervalId);
|
||||
callback();
|
||||
} else if (Date.now() - start > timeout) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
})()`;
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import { main as build, esbuildOptions } from "./build.mjs";
|
||||
import { openDevToolScript, reloadScript } from "./scripts.mjs";
|
||||
import { main as startZotero } from "./start.mjs";
|
||||
import { Logger } from "./utils.mjs";
|
||||
import cmd from "./zotero-cmd.json" assert { type: "json" };
|
||||
import { execSync } from "child_process";
|
||||
import chokidar from "chokidar";
|
||||
import { context } from "esbuild";
|
||||
import { exit } from "process";
|
||||
|
||||
process.env.NODE_ENV = "development";
|
||||
|
||||
const { zoteroBinPath, profilePath } = cmd.exec;
|
||||
|
||||
const startZoteroCmd = `"${zoteroBinPath}" --debugger --purgecaches -profile "${profilePath}"`;
|
||||
|
||||
async function watch() {
|
||||
const watcher = chokidar.watch(["src/**", "addon/**"], {
|
||||
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
||||
persistent: true,
|
||||
});
|
||||
|
||||
let esbuildCTX = await context(esbuildOptions);
|
||||
|
||||
watcher
|
||||
.on("ready", () => {
|
||||
Logger.info("Server Ready! \n");
|
||||
})
|
||||
.on("change", async (path) => {
|
||||
Logger.info(`${path} changed.`);
|
||||
if (path.startsWith("src")) {
|
||||
await esbuildCTX.rebuild();
|
||||
} else if (path.startsWith("addon")) {
|
||||
await build()
|
||||
// Do not abort the watcher when errors occur in builds triggered by the watcher.
|
||||
.catch((err) => {
|
||||
Logger.error(err);
|
||||
});
|
||||
}
|
||||
// reload
|
||||
reload();
|
||||
})
|
||||
.on("error", (err) => {
|
||||
Logger.error("Server start failed!", err);
|
||||
});
|
||||
}
|
||||
|
||||
function reload() {
|
||||
Logger.debug("Reloading...");
|
||||
const url = `zotero://ztoolkit-debug/?run=${encodeURIComponent(
|
||||
reloadScript,
|
||||
)}`;
|
||||
const command = `${startZoteroCmd} -url "${url}"`;
|
||||
execSync(command);
|
||||
}
|
||||
|
||||
function openDevTool() {
|
||||
Logger.debug("Open dev tools...");
|
||||
const url = `zotero://ztoolkit-debug/?run=${encodeURIComponent(
|
||||
openDevToolScript,
|
||||
)}`;
|
||||
const command = `${startZoteroCmd} -url "${url}"`;
|
||||
execSync(command);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// build
|
||||
await build();
|
||||
|
||||
// start Zotero
|
||||
startZotero(openDevTool);
|
||||
|
||||
// watch
|
||||
await watch();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
Logger.error(err);
|
||||
// execSync("node scripts/stop.mjs");
|
||||
exit(1);
|
||||
});
|
||||
|
||||
process.on("SIGINT", (code) => {
|
||||
execSync("node scripts/stop.mjs");
|
||||
Logger.info(`Server terminated with signal ${code}.`);
|
||||
exit(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import details from "../package.json" assert { type: "json" };
|
||||
import { Logger } from "./utils.mjs";
|
||||
import cmd from "./zotero-cmd.json" assert { type: "json" };
|
||||
import { spawn } from "child_process";
|
||||
import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
|
||||
import { clearFolder } from "./utils.mjs";
|
||||
import path from "path";
|
||||
import { exit } from "process";
|
||||
|
||||
const { addonID } = details.config;
|
||||
const { zoteroBinPath, profilePath, dataDir } = cmd.exec;
|
||||
|
||||
// Keep in sync with the addon's onStartup
|
||||
const loadDevToolWhen = `Plugin ${addonID} startup`;
|
||||
|
||||
const logPath = "logs";
|
||||
const logFilePath = path.join(logPath, "zotero.log");
|
||||
|
||||
if (!existsSync(zoteroBinPath)) {
|
||||
throw new Error("Zotero binary does not exist.");
|
||||
}
|
||||
|
||||
if (!existsSync(profilePath)) {
|
||||
throw new Error("The given Zotero profile does not exist.");
|
||||
}
|
||||
|
||||
function prepareDevEnv() {
|
||||
const addonProxyFilePath = path.join(profilePath, `extensions/${addonID}`);
|
||||
const buildPath = path.resolve("build/addon");
|
||||
|
||||
function writeAddonProxyFile() {
|
||||
writeFileSync(addonProxyFilePath, buildPath);
|
||||
Logger.debug(
|
||||
`Addon proxy file has been updated.
|
||||
File path: ${addonProxyFilePath}
|
||||
Addon path: ${buildPath} `,
|
||||
);
|
||||
}
|
||||
|
||||
if (existsSync(addonProxyFilePath)) {
|
||||
if (readFileSync(addonProxyFilePath, "utf-8") !== buildPath) {
|
||||
writeAddonProxyFile();
|
||||
}
|
||||
} else {
|
||||
writeAddonProxyFile();
|
||||
}
|
||||
|
||||
const addonXpiFilePath = path.join(profilePath, `extensions/${addonID}.xpi`);
|
||||
if (existsSync(addonXpiFilePath)) {
|
||||
rmSync(addonXpiFilePath);
|
||||
}
|
||||
|
||||
const prefsPath = path.join(profilePath, "prefs.js");
|
||||
if (existsSync(prefsPath)) {
|
||||
const PrefsLines = readFileSync(prefsPath, "utf-8").split("\n");
|
||||
const filteredLines = PrefsLines.map((line) => {
|
||||
if (
|
||||
line.includes("extensions.lastAppBuildId") ||
|
||||
line.includes("extensions.lastAppVersion")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (line.includes("extensions.zotero.dataDir") && dataDir !== "") {
|
||||
return `user_pref("extensions.zotero.dataDir", "${dataDir.replace(/\\\\?/g, "\\\\")}");`;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
const updatedPrefs = filteredLines.join("\n");
|
||||
writeFileSync(prefsPath, updatedPrefs, "utf-8");
|
||||
Logger.debug("The <profile>/prefs.js has been modified.");
|
||||
}
|
||||
}
|
||||
|
||||
function prepareLog() {
|
||||
clearFolder(logPath);
|
||||
writeFileSync(logFilePath, "");
|
||||
}
|
||||
|
||||
export function main(callback) {
|
||||
let isZoteroReady = false;
|
||||
|
||||
prepareDevEnv();
|
||||
|
||||
prepareLog();
|
||||
|
||||
const zoteroProcess = spawn(zoteroBinPath, [
|
||||
"--debugger",
|
||||
"--purgecaches",
|
||||
"-profile",
|
||||
profilePath,
|
||||
]);
|
||||
|
||||
zoteroProcess.stdout.on("data", (data) => {
|
||||
if (!isZoteroReady && data.toString().includes(loadDevToolWhen)) {
|
||||
isZoteroReady = true;
|
||||
callback();
|
||||
}
|
||||
writeFileSync(logFilePath, data, {
|
||||
flag: "a",
|
||||
});
|
||||
});
|
||||
|
||||
zoteroProcess.stderr.on("data", (data) => {
|
||||
writeFileSync(logFilePath, data, {
|
||||
flag: "a",
|
||||
});
|
||||
});
|
||||
|
||||
zoteroProcess.on("close", (code) => {
|
||||
Logger.info(`Zotero terminated with code ${code}.`);
|
||||
exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
// Handle interrupt signal (Ctrl+C) to gracefully terminate Zotero process
|
||||
zoteroProcess.kill();
|
||||
exit();
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { Logger, isRunning } from "./utils.mjs";
|
||||
import cmd from "./zotero-cmd.json" assert { type: "json" };
|
||||
import { execSync } from "child_process";
|
||||
import process from "process";
|
||||
|
||||
const { killZoteroWindows, killZoteroUnix } = cmd;
|
||||
|
||||
isRunning("zotero", (status) => {
|
||||
if (status) {
|
||||
killZotero();
|
||||
} else {
|
||||
Logger.warn("No Zotero running.");
|
||||
}
|
||||
});
|
||||
|
||||
function killZotero() {
|
||||
try {
|
||||
if (process.platform === "win32") {
|
||||
execSync(killZoteroWindows);
|
||||
} else {
|
||||
execSync(killZoteroUnix);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"addons": {
|
||||
"__addonID__": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "__buildVersion__",
|
||||
"update_link": "__updateLink__",
|
||||
"applications": {
|
||||
"zotero": {
|
||||
"strict_min_version": "6.999"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import { exec } from "child_process";
|
||||
import {
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "fs";
|
||||
import path from "path";
|
||||
|
||||
export function copyFileSync(source, target) {
|
||||
var targetFile = target;
|
||||
|
||||
// If target is a directory, a new file with the same name will be created
|
||||
if (existsSync(target)) {
|
||||
if (lstatSync(target).isDirectory()) {
|
||||
targetFile = path.join(target, path.basename(source));
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(targetFile, readFileSync(source));
|
||||
}
|
||||
|
||||
export function copyFolderRecursiveSync(source, target) {
|
||||
var files = [];
|
||||
|
||||
// Check if folder needs to be created or integrated
|
||||
var targetFolder = path.join(target, path.basename(source));
|
||||
if (!existsSync(targetFolder)) {
|
||||
mkdirSync(targetFolder);
|
||||
}
|
||||
|
||||
// Copy
|
||||
if (lstatSync(source).isDirectory()) {
|
||||
files = readdirSync(source);
|
||||
files.forEach(function (file) {
|
||||
var curSource = path.join(source, file);
|
||||
if (lstatSync(curSource).isDirectory()) {
|
||||
copyFolderRecursiveSync(curSource, targetFolder);
|
||||
} else {
|
||||
copyFileSync(curSource, targetFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function clearFolder(target) {
|
||||
if (existsSync(target)) {
|
||||
rmSync(target, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
mkdirSync(target, { recursive: true });
|
||||
}
|
||||
|
||||
export function dateFormat(fmt, date) {
|
||||
let ret;
|
||||
const opt = {
|
||||
"Y+": date.getFullYear().toString(),
|
||||
"m+": (date.getMonth() + 1).toString(),
|
||||
"d+": date.getDate().toString(),
|
||||
"H+": date.getHours().toString(),
|
||||
"M+": date.getMinutes().toString(),
|
||||
"S+": date.getSeconds().toString(),
|
||||
};
|
||||
for (let k in opt) {
|
||||
ret = new RegExp("(" + k + ")").exec(fmt);
|
||||
if (ret) {
|
||||
fmt = fmt.replace(
|
||||
ret[1],
|
||||
ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, "0"),
|
||||
);
|
||||
}
|
||||
}
|
||||
return fmt;
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
static log(...args) {
|
||||
console.log(...args);
|
||||
}
|
||||
|
||||
// red
|
||||
static error(...args) {
|
||||
console.error("\u001b[31m [ERROR]", ...args, "\u001b[0m");
|
||||
}
|
||||
|
||||
// yellow
|
||||
static warn(...args) {
|
||||
console.warn("\u001b[33m [WARN]", ...args, "\u001b[0m");
|
||||
}
|
||||
|
||||
// blue
|
||||
static debug(...args) {
|
||||
console.log("\u001b[34m [DEBUG]\u001b[0m", ...args);
|
||||
}
|
||||
|
||||
// green
|
||||
static info(...args) {
|
||||
console.log("\u001b[32m [INFO]", ...args, "\u001b[0m");
|
||||
}
|
||||
|
||||
// cyan
|
||||
static trace(...args) {
|
||||
console.log("\u001b[36m [TRACE]\u001b[0m", ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export function isRunning(query, cb) {
|
||||
let platform = process.platform;
|
||||
let cmd = "";
|
||||
switch (platform) {
|
||||
case "win32":
|
||||
cmd = `tasklist`;
|
||||
break;
|
||||
case "darwin":
|
||||
cmd = `ps -ax | grep ${query}`;
|
||||
break;
|
||||
case "linux":
|
||||
cmd = `ps -A`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
exec(cmd, (err, stdout, stderr) => {
|
||||
cb(stdout.toLowerCase().indexOf(query.toLowerCase()) > -1);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"usage": "Copy and rename this file to zotero-cmd.json. Edit the cmd.",
|
||||
"killZoteroWindows": "taskkill /f /im zotero.exe",
|
||||
"killZoteroUnix": "kill -9 $(ps -x | grep '[z]otero' | awk '{print $1}')",
|
||||
"exec": {
|
||||
"@comment-zoteroBinPath": "Please input the path of the Zotero binary file in `zoteroBinPath`.",
|
||||
"@comment-zoteroBinPath-tip": "The path delimiter should be escaped as `\\` for win32. The path is `*/Zotero.app/Contents/MacOS/zotero` for MacOS.",
|
||||
"zoteroBinPath": "/path/to/zotero.exe",
|
||||
|
||||
"@comment-profilePath": "Please input the path of the profile used for development in `profilePath`.",
|
||||
"@comment-profilePath-tip": "Start the profile manager by `/path/to/zotero.exe -p` to create a profile for development",
|
||||
"@comment-profilePath-see": "https://www.zotero.org/support/kb/profile_directory",
|
||||
"profilePath": "/path/to/profile",
|
||||
|
||||
"@comment-dataDir": "Please input the directory where the database is located in dataDir",
|
||||
"@comment-dataDir-tip": "If this field is kept empty, Zotero will start with the default data.",
|
||||
"@comment-dataDir-see": "https://www.zotero.org/support/zotero_data",
|
||||
"dataDir": ""
|
||||
}
|
||||
}
|
||||
42
src/addon.ts
42
src/addon.ts
|
|
@ -1,22 +1,34 @@
|
|||
import AddonEvents from "./events";
|
||||
import AddonPrefs from "./prefs";
|
||||
import AddonUtils from "./utils";
|
||||
import AddonViews from "./views";
|
||||
import { DialogHelper } from "zotero-plugin-toolkit/dist/helpers/dialog";
|
||||
import hooks from "./hooks";
|
||||
import { createZToolkit } from "./utils/ztoolkit";
|
||||
|
||||
class Addon {
|
||||
public Zotero: _ZoteroConstructable;
|
||||
public events: AddonEvents;
|
||||
public views: AddonViews;
|
||||
public prefs: AddonPrefs;
|
||||
public Utils: AddonUtils;
|
||||
// root path to access the resources
|
||||
public rootURI: string;
|
||||
public data: {
|
||||
alive: boolean;
|
||||
// Env type, see build.js
|
||||
env: "development" | "production";
|
||||
ztoolkit: ZToolkit;
|
||||
locale?: {
|
||||
current: any;
|
||||
};
|
||||
prefs?: {
|
||||
window: Window;
|
||||
};
|
||||
dialog?: DialogHelper;
|
||||
};
|
||||
// Lifecycle hooks
|
||||
public hooks: typeof hooks;
|
||||
// APIs
|
||||
public api: object;
|
||||
|
||||
constructor() {
|
||||
this.events = new AddonEvents(this);
|
||||
this.views = new AddonViews(this);
|
||||
this.prefs = new AddonPrefs(this);
|
||||
this.Utils = new AddonUtils(this);
|
||||
this.data = {
|
||||
alive: true,
|
||||
env: __env__,
|
||||
ztoolkit: createZToolkit(),
|
||||
};
|
||||
this.hooks = hooks;
|
||||
this.api = {};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,84 +0,0 @@
|
|||
import Addon from "./addon";
|
||||
import AddonModule from "./module";
|
||||
import { addonName } from "../package.json";
|
||||
|
||||
class AddonEvents extends AddonModule {
|
||||
private notifierCallback: any;
|
||||
constructor(parent: Addon) {
|
||||
super(parent);
|
||||
this.notifierCallback = {
|
||||
notify: async (
|
||||
event: string,
|
||||
type: string,
|
||||
ids: Array<string>,
|
||||
extraData: object
|
||||
) => {
|
||||
// You can add your code to the corresponding notify type
|
||||
if (
|
||||
event == "select" &&
|
||||
type == "tab" &&
|
||||
extraData[ids[0]].type == "reader"
|
||||
) {
|
||||
// Select a reader tab
|
||||
}
|
||||
if (event == "add" && type == "item") {
|
||||
// Add an item
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async onInit(_Zotero: _ZoteroConstructable, rootURI) {
|
||||
this._Addon.Zotero = _Zotero;
|
||||
this._Addon.rootURI = rootURI;
|
||||
// This function is the setup code of the addon
|
||||
this._Addon.Utils.Tool.log(`${addonName}: init called`);
|
||||
// alert(112233);
|
||||
|
||||
// Reset prefs
|
||||
this.resetState();
|
||||
|
||||
// Register the callback in Zotero as an item observer
|
||||
let notifierID = Zotero.Notifier.registerObserver(this.notifierCallback, [
|
||||
"tab",
|
||||
"item",
|
||||
"file",
|
||||
]);
|
||||
|
||||
// Unregister callback when the window closes (important to avoid a memory leak)
|
||||
Zotero.getMainWindow().addEventListener(
|
||||
"unload",
|
||||
function (e) {
|
||||
Zotero.Notifier.unregisterObserver(notifierID);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
this._Addon.views.initViews();
|
||||
this._Addon.views.initPrefs();
|
||||
}
|
||||
|
||||
private resetState(): void {
|
||||
/*
|
||||
For prefs that could be simply set to a static default value,
|
||||
Please use addon/defaults/preferences/defaults.js
|
||||
Reset other preferrences here.
|
||||
Uncomment to use the example code.
|
||||
*/
|
||||
// let testPref = Zotero.Prefs.get("addonTemplate.testPref");
|
||||
// if (typeof testPref === "undefined") {
|
||||
// Zotero.Prefs.set("addonTemplate.testPref", true);
|
||||
// }
|
||||
}
|
||||
|
||||
public onUnInit(): void {
|
||||
const Zotero = this._Addon.Zotero;
|
||||
this._Addon.Utils.Tool.log(`${addonName}: uninit called`);
|
||||
// Remove elements and do clean up
|
||||
this._Addon.views.unInitViews();
|
||||
// Remove addon object
|
||||
Zotero.AddonTemplate = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default AddonEvents;
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
import { RegisterFactory, UIFactory } from "./modules/Common";
|
||||
import { config } from "../package.json";
|
||||
import { getString, initLocale } from "./utils/locale";
|
||||
import { registerPrefsScripts } from "./modules/preferenceScript";
|
||||
import { createZToolkit } from "./utils/ztoolkit";
|
||||
import { tldrs } from "./modules/dataStorage";
|
||||
import { TLDRFetcher } from "./modules/tldrFetcher";
|
||||
|
||||
async function onStartup() {
|
||||
await Promise.all([
|
||||
Zotero.initializationPromise,
|
||||
Zotero.unlockPromise,
|
||||
Zotero.uiReadyPromise,
|
||||
]);
|
||||
|
||||
// TODO: Remove this after zotero#3387 is merged
|
||||
if (__env__ === "development") {
|
||||
// Keep in sync with the scripts/startup.mjs
|
||||
const loadDevToolWhen = `Plugin ${config.addonID} startup`;
|
||||
ztoolkit.log(loadDevToolWhen);
|
||||
}
|
||||
|
||||
initLocale();
|
||||
|
||||
await tldrs.getAsync();
|
||||
|
||||
RegisterFactory.registerNotifier();
|
||||
|
||||
await onMainWindowLoad(window);
|
||||
}
|
||||
|
||||
async function onMainWindowLoad(win: Window): Promise<void> {
|
||||
// Create ztoolkit for every window
|
||||
addon.data.ztoolkit = createZToolkit();
|
||||
|
||||
(win as any).MozXULElement.insertFTLIfNeeded(
|
||||
`${config.addonRef}-mainWindow.ftl`,
|
||||
);
|
||||
|
||||
UIFactory.registerRightClickMenuItem();
|
||||
|
||||
UIFactory.registerRightClickCollectionMenuItem();
|
||||
|
||||
UIFactory.registerTLDRItemBoxRow();
|
||||
|
||||
onLoad();
|
||||
}
|
||||
|
||||
async function onMainWindowUnload(win: Window): Promise<void> {
|
||||
ztoolkit.unregisterAll();
|
||||
addon.data.dialog?.window?.close();
|
||||
}
|
||||
|
||||
function onShutdown(): void {
|
||||
ztoolkit.unregisterAll();
|
||||
addon.data.dialog?.window?.close();
|
||||
// Remove addon object
|
||||
addon.data.alive = false;
|
||||
delete Zotero[config.addonInstance];
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is just an example of dispatcher for Notify events.
|
||||
* Any operations should be placed in a function to keep this funcion clear.
|
||||
*/
|
||||
async function onNotify(
|
||||
event: string,
|
||||
type: string,
|
||||
ids: Array<string | number>,
|
||||
extraData: { [key: string]: any },
|
||||
) {
|
||||
Zotero.log(`${event} ${type} ${ids}, ${extraData}`);
|
||||
if (event == "add" && type == "item" && ids.length > 0) {
|
||||
onNotifyAddItems(ids);
|
||||
} else if (event == "delete" && type == "item" && ids.length > 0) {
|
||||
noNotifyDeleteItem(ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is just an example of dispatcher for Preference UI events.
|
||||
* Any operations should be placed in a function to keep this funcion clear.
|
||||
* @param type event type
|
||||
* @param data event data
|
||||
*/
|
||||
async function onPrefsEvent(type: string, data: { [key: string]: any }) {
|
||||
switch (type) {
|
||||
case "load":
|
||||
registerPrefsScripts(data.window);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function onLoad() {
|
||||
(async () => {
|
||||
let needFetchItems: Zotero.Item[] = [];
|
||||
for (const lib of Zotero.Libraries.getAll()) {
|
||||
needFetchItems = needFetchItems.concat(
|
||||
(await Zotero.Items.getAll(lib.id)).filter((item: Zotero.Item) => {
|
||||
return item.isRegularItem();
|
||||
}),
|
||||
);
|
||||
}
|
||||
onUpdateItems(needFetchItems, false);
|
||||
})();
|
||||
}
|
||||
|
||||
function noNotifyDeleteItem(ids: (string | number)[]) {
|
||||
tldrs.modify((data) => {
|
||||
ids.forEach((id) => {
|
||||
delete data[id];
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
function onNotifyAddItems(ids: (string | number)[]) {
|
||||
const addedRegularItems: Zotero.Item[] = [];
|
||||
for (const id of ids) {
|
||||
const item = Zotero.Items.get(id);
|
||||
if (item.isRegularItem()) {
|
||||
addedRegularItems.push(item);
|
||||
}
|
||||
}
|
||||
(async function () {
|
||||
await Zotero.Promise.delay(3000);
|
||||
onUpdateItems(addedRegularItems, false);
|
||||
})();
|
||||
}
|
||||
|
||||
function onUpdateItems(items: Zotero.Item[], forceFetch: boolean = false) {
|
||||
items = items.filter((item: Zotero.Item) => {
|
||||
if (!item.getField("title")) {
|
||||
return false;
|
||||
}
|
||||
if (!forceFetch && item.key in tldrs.get()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (items.length <= 0) {
|
||||
return;
|
||||
}
|
||||
const newPopWin = (closeOnClick = true) => {
|
||||
return new ztoolkit.ProgressWindow(config.addonName, {
|
||||
closeOnClick: closeOnClick,
|
||||
}).createLine({
|
||||
text: `${getString("popWindow-waiting")}: ${items.length}; ${getString(
|
||||
"popWindow-succeed",
|
||||
)}: 0; ${getString("popWindow-failed")}: 0`,
|
||||
type: "default",
|
||||
progress: 0,
|
||||
});
|
||||
};
|
||||
const popupWin = newPopWin().show(-1);
|
||||
(async function () {
|
||||
const count = items.length;
|
||||
const failedItems: Zotero.Item[] = [];
|
||||
const succeedItems: Zotero.Item[] = [];
|
||||
await (async function () {
|
||||
for (const [index, item] of items.entries()) {
|
||||
(await new TLDRFetcher(item).fetchTLDR())
|
||||
? succeedItems.push(item)
|
||||
: failedItems.push(item);
|
||||
await Zotero.Promise.delay(50);
|
||||
popupWin.changeLine({
|
||||
progress: (index * 100) / count,
|
||||
text: `${getString("popWindow-waiting")}: ${
|
||||
count - index - 1
|
||||
}; ${getString("popWindow-succeed")}: ${
|
||||
succeedItems.length
|
||||
}; ${getString("popWindow-failed")}: ${failedItems.length}`,
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
await (async function () {
|
||||
popupWin.changeLine({
|
||||
type: "success",
|
||||
progress: 100,
|
||||
text: `${getString("popWindow-succeed")}: ${
|
||||
succeedItems.length
|
||||
}; ${getString("popWindow-failed")}: ${failedItems.length}`,
|
||||
});
|
||||
popupWin.startCloseTimer(3000);
|
||||
})();
|
||||
})();
|
||||
}
|
||||
|
||||
// Add your hooks here. For element click, etc.
|
||||
// Keep in mind hooks only do dispatch. Don't add code that does real jobs in hooks.
|
||||
// Otherwise the code would be hard to read and maintain.
|
||||
|
||||
export default {
|
||||
onStartup,
|
||||
onShutdown,
|
||||
onMainWindowLoad,
|
||||
onMainWindowUnload,
|
||||
onNotify,
|
||||
onPrefsEvent,
|
||||
onUpdateItems,
|
||||
};
|
||||
28
src/index.ts
28
src/index.ts
|
|
@ -1,7 +1,27 @@
|
|||
import { BasicTool } from "zotero-plugin-toolkit/dist/basic";
|
||||
import Addon from "./addon";
|
||||
import { config } from "../package.json";
|
||||
|
||||
if (!Zotero.AddonTemplate) {
|
||||
Zotero.AddonTemplate = new Addon();
|
||||
// @ts-ignore
|
||||
Zotero.AddonTemplate.events.onInit(Zotero, rootURI);
|
||||
const basicTool = new BasicTool();
|
||||
|
||||
if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
|
||||
defineGlobal("window");
|
||||
defineGlobal("document");
|
||||
defineGlobal("ZoteroPane");
|
||||
defineGlobal("Zotero_Tabs");
|
||||
_globalThis.addon = new Addon();
|
||||
defineGlobal("ztoolkit", () => {
|
||||
return _globalThis.addon.data.ztoolkit;
|
||||
});
|
||||
Zotero[config.addonInstance] = addon;
|
||||
}
|
||||
|
||||
function defineGlobal(name: Parameters<BasicTool["getGlobal"]>[0]): void;
|
||||
function defineGlobal(name: string, getter: () => any): void;
|
||||
function defineGlobal(name: string, getter?: () => any) {
|
||||
Object.defineProperty(_globalThis, name, {
|
||||
get() {
|
||||
return getter ? getter() : basicTool.getGlobal(name);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
import Addon from "./addon";
|
||||
|
||||
class AddonModule {
|
||||
protected _Addon: Addon;
|
||||
constructor(parent: Addon) {
|
||||
this._Addon = parent;
|
||||
}
|
||||
}
|
||||
|
||||
export default AddonModule;
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import { config } from "../../package.json";
|
||||
import { getString } from "../utils/locale";
|
||||
import { tldrs } from "./dataStorage";
|
||||
|
||||
export class RegisterFactory {
|
||||
// 注册zotero的通知
|
||||
static registerNotifier() {
|
||||
const callback = {
|
||||
notify: async (
|
||||
event: string,
|
||||
type: string,
|
||||
ids: number[] | string[],
|
||||
extraData: { [key: string]: any },
|
||||
) => {
|
||||
if (!addon?.data.alive) {
|
||||
this.unregisterNotifier(notifierID);
|
||||
return;
|
||||
}
|
||||
addon.hooks.onNotify(event, type, ids, extraData);
|
||||
},
|
||||
};
|
||||
|
||||
// Register the callback in Zotero as an item observer
|
||||
const notifierID = Zotero.Notifier.registerObserver(callback, ["item"]);
|
||||
|
||||
// Unregister callback when the window closes (important to avoid a memory leak)
|
||||
window.addEventListener(
|
||||
"unload",
|
||||
(e: Event) => {
|
||||
this.unregisterNotifier(notifierID);
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
private static unregisterNotifier(notifierID: string) {
|
||||
Zotero.Notifier.unregisterObserver(notifierID);
|
||||
}
|
||||
}
|
||||
|
||||
export class UIFactory {
|
||||
// item右键菜单
|
||||
static registerRightClickMenuItem() {
|
||||
const menuIcon = `chrome://${config.addonRef}/content/icons/favicon.png`;
|
||||
// item menuitem with icon
|
||||
ztoolkit.Menu.register("item", {
|
||||
tag: "menuitem",
|
||||
id: "zotero-itemmenu-tldr",
|
||||
label: getString("menuitem-updatetldrlabel"),
|
||||
commandListener: (ev) => {
|
||||
const selectedItems = ZoteroPane.getSelectedItems() ?? [];
|
||||
addon.hooks.onUpdateItems(selectedItems, selectedItems.length <= 1);
|
||||
},
|
||||
icon: menuIcon,
|
||||
});
|
||||
}
|
||||
|
||||
// collection右键菜单
|
||||
static registerRightClickCollectionMenuItem() {
|
||||
const menuIcon = `chrome://${config.addonRef}/content/icons/favicon.png`;
|
||||
ztoolkit.Menu.register("collection", {
|
||||
tag: "menuitem",
|
||||
id: "zotero-collectionmenu-tldr",
|
||||
label: getString("menucollection-updatetldrlabel"),
|
||||
commandListener: (ev) =>
|
||||
addon.hooks.onUpdateItems(
|
||||
ZoteroPane.getSelectedCollection()?.getChildItems() ?? [],
|
||||
false,
|
||||
),
|
||||
icon: menuIcon,
|
||||
});
|
||||
}
|
||||
|
||||
// tldr行
|
||||
static async registerTLDRItemBoxRow() {
|
||||
const itemTLDR = (item: Zotero.Item) => {
|
||||
const noteKey = tldrs.get()[item.key];
|
||||
if (noteKey) {
|
||||
const obj = Zotero.Items.getByLibraryAndKey(item.libraryID, noteKey);
|
||||
if (
|
||||
obj &&
|
||||
obj instanceof Zotero.Item &&
|
||||
item.getNotes().includes(obj.id)
|
||||
) {
|
||||
let str = obj.getNote();
|
||||
if (str.startsWith("<p>TL;DR</p>\n<p>")) {
|
||||
str = str.slice("<p>TL;DR</p>\n<p>".length);
|
||||
}
|
||||
if (str.endsWith("</p>")) {
|
||||
str = str.slice(0, -4);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
Zotero.ItemPaneManager.registerSection({
|
||||
paneID: config.addonRef,
|
||||
pluginID: config.addonID,
|
||||
header: {
|
||||
l10nID: `${config.addonRef}-itemPaneSection-header`,
|
||||
icon: `chrome://${config.addonRef}/content/icons/favicon@16.png`,
|
||||
},
|
||||
sidenav: {
|
||||
l10nID: `${config.addonRef}-itemPaneSection-sidenav`,
|
||||
icon: `chrome://${config.addonRef}/content/icons/favicon@20.png`,
|
||||
},
|
||||
onRender: ({ body, item }: any) => {
|
||||
let tldr = itemTLDR(item);
|
||||
if (tldr.length <= 0 && item.parentItem) {
|
||||
tldr = itemTLDR(item.parentItem);
|
||||
}
|
||||
body.textContent = tldr;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import { config } from "../../package.json";
|
||||
|
||||
export class Data<K extends string | number | symbol, V> {
|
||||
[x: string]: any;
|
||||
private dataType: string;
|
||||
private filePath?: string;
|
||||
private _data: Record<K, V>;
|
||||
|
||||
constructor(dataType: string) {
|
||||
this.dataType = dataType;
|
||||
this._data = {} as Record<K, V>;
|
||||
}
|
||||
|
||||
async getAsync() {
|
||||
await this.initDataIfNeed();
|
||||
return this.data;
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
async modify(
|
||||
action: (data: Record<K, V>) => Record<K, V> | Promise<Record<K, V>>,
|
||||
) {
|
||||
await this.initDataIfNeed();
|
||||
const data = this.data;
|
||||
const newData = await action(data);
|
||||
if (this.filePath) {
|
||||
try {
|
||||
await IOUtils.writeJSON(this.filePath, newData, {
|
||||
mode: "overwrite",
|
||||
compress: false,
|
||||
});
|
||||
this.data = newData;
|
||||
return newData;
|
||||
} catch (error) {
|
||||
return data;
|
||||
}
|
||||
} else {
|
||||
this.data = newData;
|
||||
return newData;
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
if (this.filePath) {
|
||||
try {
|
||||
await IOUtils.remove(this.filePath);
|
||||
this.data = {} as Record<K, V>;
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
this.data = {} as Record<K, V>;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private get data() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
private set data(value: Record<K, V>) {
|
||||
this._data = value;
|
||||
}
|
||||
|
||||
private async initDataIfNeed() {
|
||||
if (this.inited) {
|
||||
return;
|
||||
}
|
||||
this.inited = true;
|
||||
|
||||
const prefsFile = PathUtils.join(PathUtils.profileDir, "prefs.js");
|
||||
const prefs = await Zotero.Profile.readPrefsFromFile(prefsFile);
|
||||
let dir = prefs["extensions.zotero.dataDir"];
|
||||
if (dir) {
|
||||
dir = PathUtils.join(dir, config.addonName);
|
||||
} else {
|
||||
dir = PathUtils.join(
|
||||
PathUtils.profileDir,
|
||||
"extensions",
|
||||
config.addonName,
|
||||
);
|
||||
}
|
||||
IOUtils.makeDirectory(dir, {
|
||||
createAncestors: true,
|
||||
ignoreExisting: true,
|
||||
});
|
||||
this.filePath = PathUtils.join(dir, this.dataType);
|
||||
try {
|
||||
this.data = await IOUtils.readJSON(this.filePath, { decompress: false });
|
||||
} catch (error) {
|
||||
this.data = {} as Record<K, V>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DataStorage {
|
||||
private dataMap: { [key: string]: Data<any, any> } = {};
|
||||
|
||||
private static shared = new DataStorage();
|
||||
|
||||
static instance<K extends string | number | symbol, V>(
|
||||
dataType: string,
|
||||
): Data<K, V> {
|
||||
if (this.shared.dataMap[dataType] === undefined) {
|
||||
const data = new Data<K, V>(dataType);
|
||||
this.shared.dataMap[dataType] = data;
|
||||
return data;
|
||||
} else {
|
||||
return this.shared.dataMap[dataType];
|
||||
}
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
|
||||
export const tldrs = DataStorage.instance<string, string | false>(
|
||||
"fetchedItems.json",
|
||||
);
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { config } from "../../package.json";
|
||||
import { getString } from "../utils/locale";
|
||||
|
||||
export async function registerPrefsScripts(_window: Window) {
|
||||
// This function is called when the prefs window is opened
|
||||
// See addon/chrome/content/preferences.xul onpaneload
|
||||
if (!addon.data.prefs) {
|
||||
addon.data.prefs = {
|
||||
window: _window,
|
||||
};
|
||||
} else {
|
||||
addon.data.prefs.window = _window;
|
||||
}
|
||||
bindPrefEvents();
|
||||
}
|
||||
|
||||
function bindPrefEvents() {}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
import { tldrs } from "./dataStorage";
|
||||
|
||||
type SemanticScholarItemInfo = {
|
||||
title?: string;
|
||||
abstract?: string;
|
||||
tldr?: string;
|
||||
};
|
||||
|
||||
export class TLDRFetcher {
|
||||
private readonly zoteroItem: Zotero.Item;
|
||||
private readonly title?: string;
|
||||
private readonly abstract?: string;
|
||||
|
||||
constructor(item: Zotero.Item) {
|
||||
this.zoteroItem = item;
|
||||
if (item.isRegularItem()) {
|
||||
this.title = item.getField("title") as string;
|
||||
this.abstract = item.getField("abstractNote") as string;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchTLDR() {
|
||||
if (!this.title || this.title.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
const noteKey = (await tldrs.getAsync())[this.zoteroItem.key];
|
||||
try {
|
||||
const infos = await this.fetchRelevanceItemInfos(this.title);
|
||||
for (const info of infos) {
|
||||
let match = false;
|
||||
if (info.title && this.title && this.checkLCS(info.title, this.title)) {
|
||||
match = true;
|
||||
} else if (
|
||||
info.abstract &&
|
||||
this.abstract &&
|
||||
this.checkLCS(info.abstract, this.abstract)
|
||||
) {
|
||||
match = true;
|
||||
}
|
||||
if (match && info.tldr) {
|
||||
let note = new Zotero.Item("note");
|
||||
if (noteKey) {
|
||||
const obj = Zotero.Items.getByLibraryAndKey(
|
||||
this.zoteroItem.libraryID,
|
||||
noteKey,
|
||||
);
|
||||
if (
|
||||
obj &&
|
||||
obj instanceof Zotero.Item &&
|
||||
this.zoteroItem.getNotes().includes(obj.id)
|
||||
) {
|
||||
note = obj;
|
||||
}
|
||||
}
|
||||
note.setNote(`<p>TL;DR</p>\n<p>${info.tldr}</p>`);
|
||||
note.parentID = this.zoteroItem.id;
|
||||
await note.saveTx();
|
||||
await tldrs.modify((data: any) => {
|
||||
data[this.zoteroItem.key] = note.key;
|
||||
return data;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
await tldrs.modify((data: any) => {
|
||||
data[this.zoteroItem.key] = false;
|
||||
return data;
|
||||
});
|
||||
} catch (error) {
|
||||
Zotero.log(`post semantic scholar request error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchRelevanceItemInfos(
|
||||
title: string,
|
||||
): Promise<SemanticScholarItemInfo[]> {
|
||||
const semanticScholarURL = "https://www.semanticscholar.org/api/1/search";
|
||||
const params = {
|
||||
queryString: title,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
sort: "relevance",
|
||||
authors: [],
|
||||
coAuthors: [],
|
||||
venues: [],
|
||||
performTitleMatch: true,
|
||||
requireViewablePdf: false,
|
||||
includeTldrs: true,
|
||||
};
|
||||
const resp = await Zotero.HTTP.request("POST", semanticScholarURL, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
const results = JSON.parse(resp.response).results;
|
||||
return results.map((item: any) => {
|
||||
const result = {
|
||||
title: item.title.text,
|
||||
abstract: item.paperAbstract.text,
|
||||
tldr: undefined,
|
||||
};
|
||||
if (item.tldr) {
|
||||
result.tldr = item.tldr.text;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private checkLCS(pattern: string, content: string): boolean {
|
||||
const LCS = StringMatchUtils.longestCommonSubsequence(pattern, content);
|
||||
return LCS.length >= Math.max(pattern.length, content.length) * 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
class StringMatchUtils {
|
||||
static longestCommonSubsequence(text1: string, text2: string): string {
|
||||
const m = text1.length;
|
||||
const n = text2.length;
|
||||
|
||||
const dp: number[][] = new Array(m + 1);
|
||||
for (let i = 0; i <= m; i++) {
|
||||
dp[i] = new Array(n + 1).fill(0);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
if (text1[i - 1] === text2[j - 1]) {
|
||||
dp[i][j] = dp[i - 1][j - 1] + 1;
|
||||
} else {
|
||||
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let i = m,
|
||||
j = n;
|
||||
const lcs: string[] = [];
|
||||
while (i > 0 && j > 0) {
|
||||
if (text1[i - 1] === text2[j - 1]) {
|
||||
lcs.unshift(text1[i - 1]);
|
||||
i--;
|
||||
j--;
|
||||
} else if (dp[i - 1][j] > dp[i][j - 1]) {
|
||||
i--;
|
||||
} else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
return lcs.join("");
|
||||
}
|
||||
|
||||
// static minWindow(s: string, t: string): [number, number] | null {
|
||||
// const m = s.length, n = t.length
|
||||
// let start = -1, minLen = Number.MAX_SAFE_INTEGER, i = 0, j = 0, end;
|
||||
// while (i < m) {
|
||||
// if (s[i] == t[j]) {
|
||||
// if (++j == n) {
|
||||
// end = i + 1;
|
||||
// while (--j >= 0) {
|
||||
// while (s[i--] != t[j]);
|
||||
// }
|
||||
// ++i; ++j;
|
||||
// if (end - i < minLen) {
|
||||
// minLen = end - i;
|
||||
// start = i;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ++i;
|
||||
// }
|
||||
// return start == -1 ? null : [start, minLen];
|
||||
// }
|
||||
}
|
||||
47
src/prefs.ts
47
src/prefs.ts
|
|
@ -1,47 +0,0 @@
|
|||
import Addon from "./addon";
|
||||
import AddonModule from "./module";
|
||||
import { addonName, addonRef } from "../package.json";
|
||||
|
||||
class AddonPrefs extends AddonModule {
|
||||
private _window: Window;
|
||||
constructor(parent: Addon) {
|
||||
super(parent);
|
||||
}
|
||||
public initPreferences(_window: Window) {
|
||||
// This function is called when the prefs window is opened
|
||||
// See addon/chrome/content/preferences.xul onpaneload
|
||||
this._window = _window;
|
||||
this._Addon.Utils.Tool.log(`${addonName}: init preferences`);
|
||||
this.updatePrefsUI();
|
||||
this.bindPrefEvents();
|
||||
}
|
||||
|
||||
private updatePrefsUI() {
|
||||
// You can initialize some UI elements on prefs window
|
||||
// with this._window.document
|
||||
// Or bind some events to the elements
|
||||
this._Addon.Utils.Tool.log(`${addonName}: init preferences UI`);
|
||||
}
|
||||
|
||||
private bindPrefEvents() {
|
||||
this._window.document
|
||||
.querySelector(`#zotero-prefpane-${addonRef}-enable`)
|
||||
?.addEventListener("command", (e) => {
|
||||
this._Addon.Utils.Tool.log(e);
|
||||
this._window.alert(
|
||||
`Successfully changed to ${(e.target as XUL.Checkbox).checked}!`
|
||||
);
|
||||
});
|
||||
|
||||
this._window.document
|
||||
.querySelector(`#zotero-prefpane-${addonRef}-input`)
|
||||
?.addEventListener("change", (e) => {
|
||||
this._Addon.Utils.Tool.log(e);
|
||||
this._window.alert(
|
||||
`Successfully changed to ${(e.target as HTMLInputElement).value}!`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AddonPrefs;
|
||||
461
src/utils.ts
461
src/utils.ts
|
|
@ -1,461 +0,0 @@
|
|||
import Addon from "./addon";
|
||||
import AddonModule from "./module";
|
||||
|
||||
class AddonUtils extends AddonModule {
|
||||
public Compat: ZoteroCompat;
|
||||
public Tool: ZoteroTool;
|
||||
public UI: ZoteroUI;
|
||||
|
||||
constructor(parent: Addon) {
|
||||
super(parent);
|
||||
this.Compat = {
|
||||
// Get Zotero instance
|
||||
getZotero: () => {
|
||||
if (typeof Zotero === "undefined") {
|
||||
return Components.classes["@zotero.org/Zotero;1"].getService(
|
||||
Components.interfaces.nsISupports
|
||||
).wrappedJSObject;
|
||||
}
|
||||
return Zotero;
|
||||
},
|
||||
// Check if it's running on Zotero 7 (Firefox 102)
|
||||
isZotero7: () => Zotero.platformMajorVersion >= 102,
|
||||
// Firefox 102 support DOMParser natively
|
||||
getDOMParser: () => {
|
||||
if (this.Compat.isZotero7()) {
|
||||
return new DOMParser();
|
||||
}
|
||||
try {
|
||||
return new (this.Compat.getZotero().getMainWindow().DOMParser)();
|
||||
} catch (e) {
|
||||
return Components.classes[
|
||||
"@mozilla.org/xmlextras/domparser;1"
|
||||
].createInstance(Components.interfaces.nsIDOMParser);
|
||||
}
|
||||
},
|
||||
|
||||
// create XUL element
|
||||
createXULElement: (doc: Document, type: string) => {
|
||||
if (this.Compat.isZotero7()) {
|
||||
// @ts-ignore
|
||||
return doc.createXULElement(type);
|
||||
} else {
|
||||
return doc.createElementNS(
|
||||
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
|
||||
type
|
||||
) as XUL.Element;
|
||||
}
|
||||
},
|
||||
parseXHTMLToFragment: (
|
||||
str: string,
|
||||
entities: string[] = [],
|
||||
defaultXUL = true
|
||||
) => {
|
||||
// Adapted from MozXULElement.parseXULToFragment
|
||||
|
||||
/* eslint-disable indent */
|
||||
let parser = this.Compat.getDOMParser();
|
||||
// parser.forceEnableXULXBL();
|
||||
const xulns =
|
||||
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const htmlns = "http://www.w3.org/1999/xhtml";
|
||||
const wrappedStr = `${
|
||||
entities.length
|
||||
? `<!DOCTYPE bindings [ ${entities.reduce(
|
||||
(preamble, url, index) => {
|
||||
return (
|
||||
preamble +
|
||||
`<!ENTITY % _dtd-${index} SYSTEM "${url}"> %_dtd-${index}; `
|
||||
);
|
||||
},
|
||||
""
|
||||
)}]>`
|
||||
: ""
|
||||
}
|
||||
<html:div xmlns="${defaultXUL ? xulns : htmlns}"
|
||||
xmlns:xul="${xulns}" xmlns:html="${htmlns}">
|
||||
${str}
|
||||
</html:div>`;
|
||||
this.Tool.log(wrappedStr, parser);
|
||||
let doc = parser.parseFromString(wrappedStr, "text/xml");
|
||||
/* eslint-enable indent */
|
||||
console.log(doc);
|
||||
|
||||
if (doc.documentElement.localName === "parsererror") {
|
||||
throw new Error("not well-formed XHTML");
|
||||
}
|
||||
|
||||
// We use a range here so that we don't access the inner DOM elements from
|
||||
// JavaScript before they are imported and inserted into a document.
|
||||
let range = doc.createRange();
|
||||
range.selectNodeContents(doc.querySelector("div"));
|
||||
return range.extractContents();
|
||||
},
|
||||
prefPaneCache: { win: undefined, listeners: [], ids: [] },
|
||||
registerPrefPane: (options: PrefPaneOptions) => {
|
||||
const windowListener = {
|
||||
onOpenWindow: (xulWindow) => {
|
||||
const win: Window = xulWindow
|
||||
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
||||
.getInterface(Components.interfaces.nsIDOMWindow);
|
||||
win.addEventListener(
|
||||
"load",
|
||||
async () => {
|
||||
if (
|
||||
win.location.href ===
|
||||
"chrome://zotero/content/preferences/preferences.xul"
|
||||
) {
|
||||
this.Tool.log("registerPrefPane:detected", options);
|
||||
const Zotero = this.Compat.getZotero();
|
||||
options.id || (options.id = `plugin-${new Date().getTime()}`);
|
||||
const src = `<prefpane id="${
|
||||
options.id
|
||||
}" insertafter="zotero-prefpane-advanced" label="${
|
||||
options.label || options.pluginID
|
||||
}" image="${options.image || ""}">
|
||||
${(await Zotero.File.getContentsAsync(options.src)) as string}
|
||||
</prefpane>`;
|
||||
const frag = this.Compat.parseXHTMLToFragment(
|
||||
src,
|
||||
options.extraDTD,
|
||||
options.defaultXUL
|
||||
);
|
||||
this.Tool.log(frag);
|
||||
const prefWindow = win.document.querySelector("prefwindow");
|
||||
prefWindow.appendChild(frag);
|
||||
const prefPane = win.document.querySelector(`#${options.id}`);
|
||||
// @ts-ignore
|
||||
prefWindow.addPane(prefPane);
|
||||
this.Compat.prefPaneCache.win = win;
|
||||
this.Compat.prefPaneCache.listeners.push(windowListener);
|
||||
this.Compat.prefPaneCache.ids.push(options.id);
|
||||
if (options.onload) {
|
||||
options.onload(win);
|
||||
}
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
},
|
||||
};
|
||||
Services.wm.addListener(windowListener);
|
||||
},
|
||||
unregisterPrefPane: () => {
|
||||
this.Compat.prefPaneCache.listeners.forEach((l) =>
|
||||
Services.wm.removeListener(l)
|
||||
);
|
||||
const win = this.Compat.prefPaneCache.win;
|
||||
if (win && !win.closed) {
|
||||
this.Compat.prefPaneCache.ids.forEach((id) =>
|
||||
win.document.querySelector(id)?.remove()
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
this.Tool = {
|
||||
getCopyHelper: () => new CopyHelper(),
|
||||
openFilePicker: (
|
||||
title: string,
|
||||
mode: "open" | "save" | "folder",
|
||||
filters?: [string, string][],
|
||||
suggestion?: string
|
||||
) => {
|
||||
const fp = Components.classes[
|
||||
"@mozilla.org/filepicker;1"
|
||||
].createInstance(Components.interfaces.nsIFilePicker);
|
||||
|
||||
if (suggestion) fp.defaultString = suggestion;
|
||||
|
||||
mode = {
|
||||
open: Components.interfaces.nsIFilePicker.modeOpen,
|
||||
save: Components.interfaces.nsIFilePicker.modeSave,
|
||||
folder: Components.interfaces.nsIFilePicker.modeGetFolder,
|
||||
}[mode];
|
||||
|
||||
fp.init(window, title, mode);
|
||||
|
||||
for (const [label, ext] of filters || []) {
|
||||
fp.appendFilter(label, ext);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return new Promise((resolve) => {
|
||||
fp.open((userChoice) => {
|
||||
switch (userChoice) {
|
||||
case Components.interfaces.nsIFilePicker.returnOK:
|
||||
case Components.interfaces.nsIFilePicker.returnReplace:
|
||||
resolve(fp.file.path);
|
||||
break;
|
||||
|
||||
default: // aka returnCancel
|
||||
resolve("");
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
log: (...data: any[]) => {
|
||||
try {
|
||||
this._Addon.Zotero.getMainWindow().console.log(...data);
|
||||
for (const d of data) {
|
||||
this._Addon.Zotero.debug(d);
|
||||
}
|
||||
} catch (e) {
|
||||
this._Addon.Zotero.debug(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
this.UI = {
|
||||
addonElements: [],
|
||||
createElement: (
|
||||
doc: Document,
|
||||
tagName: string,
|
||||
namespace: "html" | "svg" | "xul" = "html"
|
||||
) => {
|
||||
namespace = namespace || "html";
|
||||
const namespaces = {
|
||||
html: "http://www.w3.org/1999/xhtml",
|
||||
svg: "http://www.w3.org/2000/svg",
|
||||
};
|
||||
if (tagName === "fragment") {
|
||||
return doc.createDocumentFragment();
|
||||
} else if (namespace === "xul") {
|
||||
const e = this.Compat.createXULElement(doc, tagName);
|
||||
this.UI.addonElements.push(e);
|
||||
return e;
|
||||
} else {
|
||||
const e = doc.createElementNS(namespaces[namespace], tagName) as
|
||||
| HTMLElement
|
||||
| SVGAElement;
|
||||
this.UI.addonElements.push(e);
|
||||
return e;
|
||||
}
|
||||
},
|
||||
removeAddonElements: () => {
|
||||
this.UI.addonElements.forEach((e) => {
|
||||
try {
|
||||
e?.remove();
|
||||
} catch (e) {
|
||||
this._Addon.Utils.Tool.log(e);
|
||||
}
|
||||
});
|
||||
},
|
||||
creatElementsFromJSON: (doc: Document, options: ElementOptions) => {
|
||||
this.Tool.log(options);
|
||||
if (
|
||||
options.id &&
|
||||
(options.checkExistanceParent
|
||||
? options.checkExistanceParent
|
||||
: doc
|
||||
).querySelector(`#${options.id}`)
|
||||
) {
|
||||
if (options.ignoreIfExists) {
|
||||
return undefined;
|
||||
}
|
||||
if (options.removeIfExists) {
|
||||
doc.querySelector(`#${options.id}`).remove();
|
||||
}
|
||||
}
|
||||
if (options.customCheck && !options.customCheck()) {
|
||||
return undefined;
|
||||
}
|
||||
const element = this.UI.createElement(
|
||||
doc,
|
||||
options.tag,
|
||||
options.namespace
|
||||
);
|
||||
|
||||
let _DocumentFragment: typeof DocumentFragment;
|
||||
if (typeof DocumentFragment === "undefined") {
|
||||
_DocumentFragment = (doc as any).ownerGlobal.DocumentFragment;
|
||||
} else {
|
||||
_DocumentFragment = DocumentFragment;
|
||||
}
|
||||
if (!(element instanceof _DocumentFragment)) {
|
||||
if (options.id) {
|
||||
element.id = options.id;
|
||||
}
|
||||
if (options.styles && Object.keys(options.styles).length) {
|
||||
Object.keys(options.styles).forEach((k) => {
|
||||
const v = options.styles[k];
|
||||
typeof v !== "undefined" && (element.style[k] = v);
|
||||
});
|
||||
}
|
||||
if (
|
||||
options.directAttributes &&
|
||||
Object.keys(options.directAttributes).length
|
||||
) {
|
||||
Object.keys(options.directAttributes).forEach((k) => {
|
||||
const v = options.directAttributes[k];
|
||||
typeof v !== "undefined" && (element[k] = v);
|
||||
});
|
||||
}
|
||||
if (options.attributes && Object.keys(options.attributes).length) {
|
||||
Object.keys(options.attributes).forEach((k) => {
|
||||
const v = options.attributes[k];
|
||||
typeof v !== "undefined" && element.setAttribute(k, String(v));
|
||||
});
|
||||
}
|
||||
if (options.listeners?.length) {
|
||||
options.listeners.forEach(([type, cbk, option]) => {
|
||||
typeof cbk !== "undefined" &&
|
||||
element.addEventListener(type, cbk, option);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (options.subElementOptions?.length) {
|
||||
const subElements = options.subElementOptions
|
||||
.map((_options) => this.UI.creatElementsFromJSON(doc, _options))
|
||||
.filter((e) => e);
|
||||
element.append(...subElements);
|
||||
}
|
||||
return element;
|
||||
},
|
||||
defaultMenuPopupSelectors: {
|
||||
menuFile: "#menu_FilePopup",
|
||||
menuEdit: "#menu_EditPopup",
|
||||
menuView: "#menu_viewPopup",
|
||||
menuGo: "#menu_goPopup",
|
||||
menuTools: "#menu_ToolsPopup",
|
||||
menuHelp: "#menu_HelpPopup",
|
||||
collection: "#zotero-collectionmenu",
|
||||
item: "#zotero-itemmenu",
|
||||
},
|
||||
insertMenuItem: (
|
||||
menuPopup: XUL.Menupopup | string,
|
||||
options: MenuitemOptions,
|
||||
insertPosition: "before" | "after" = "after",
|
||||
anchorElement: XUL.Element = undefined
|
||||
) => {
|
||||
const Zotero = this.Compat.getZotero();
|
||||
let popup: XUL.Menupopup;
|
||||
if (typeof menuPopup === "string") {
|
||||
if (
|
||||
!Object.keys(this.UI.defaultMenuPopupSelectors).includes(menuPopup)
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
popup = (Zotero.getMainWindow() as Window).document.querySelector(
|
||||
this.UI.defaultMenuPopupSelectors[menuPopup]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
popup = menuPopup;
|
||||
}
|
||||
if (!popup) {
|
||||
return false;
|
||||
}
|
||||
const doc: Document = popup.ownerDocument;
|
||||
const generateElementOptions = (
|
||||
menuitemOption: MenuitemOptions
|
||||
): ElementOptions => {
|
||||
let elementOption: ElementOptions = {
|
||||
tag: menuitemOption.tag,
|
||||
id: menuitemOption.id,
|
||||
namespace: "xul",
|
||||
attributes: {
|
||||
label: menuitemOption.label,
|
||||
hidden: Boolean(menuitemOption.hidden),
|
||||
disaled: Boolean(menuitemOption.disabled),
|
||||
class: menuitemOption.class || "",
|
||||
oncommand: menuitemOption.oncommand,
|
||||
},
|
||||
styles: menuitemOption.styles || {},
|
||||
listeners: [["command", menuitemOption.commandListener]],
|
||||
subElementOptions: [],
|
||||
};
|
||||
if (menuitemOption.icon) {
|
||||
elementOption.attributes["class"] += " menuitem-iconic";
|
||||
elementOption.styles[
|
||||
"list-style-image"
|
||||
] = `url(${menuitemOption.icon})`;
|
||||
}
|
||||
if (menuitemOption.tag === "menu") {
|
||||
elementOption.subElementOptions.push({
|
||||
tag: "menupopup",
|
||||
id: menuitemOption.popupId,
|
||||
namespace: "xul",
|
||||
attributes: { onpopupshowing: menuitemOption.onpopupshowing },
|
||||
subElementOptions: menuitemOption.subElementOptions.map(
|
||||
generateElementOptions
|
||||
),
|
||||
});
|
||||
}
|
||||
return elementOption;
|
||||
};
|
||||
const menuItem = this.UI.creatElementsFromJSON(
|
||||
doc,
|
||||
generateElementOptions(options)
|
||||
);
|
||||
if (!anchorElement) {
|
||||
anchorElement = (
|
||||
insertPosition === "after"
|
||||
? popup.lastElementChild
|
||||
: popup.firstElementChild
|
||||
) as XUL.Element;
|
||||
}
|
||||
anchorElement[insertPosition](menuItem);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CopyHelper {
|
||||
private transferable: any;
|
||||
private clipboardService: any;
|
||||
|
||||
constructor() {
|
||||
this.transferable = Components.classes[
|
||||
"@mozilla.org/widget/transferable;1"
|
||||
].createInstance(Components.interfaces.nsITransferable);
|
||||
this.clipboardService = Components.classes[
|
||||
"@mozilla.org/widget/clipboard;1"
|
||||
].getService(Components.interfaces.nsIClipboard);
|
||||
this.transferable.init(null);
|
||||
}
|
||||
|
||||
public addText(source: string, type: "text/html" | "text/unicode") {
|
||||
const str = Components.classes[
|
||||
"@mozilla.org/supports-string;1"
|
||||
].createInstance(Components.interfaces.nsISupportsString);
|
||||
str.data = source;
|
||||
this.transferable.addDataFlavor(type);
|
||||
this.transferable.setTransferData(type, str, source.length * 2);
|
||||
return this;
|
||||
}
|
||||
|
||||
public addImage(source: string) {
|
||||
let parts = source.split(",");
|
||||
if (!parts[0].includes("base64")) {
|
||||
return;
|
||||
}
|
||||
let mime = parts[0].match(/:(.*?);/)[1];
|
||||
let bstr = atob(parts[1]);
|
||||
let n = bstr.length;
|
||||
let u8arr = new Uint8Array(n);
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
let imgTools = Components.classes["@mozilla.org/image/tools;1"].getService(
|
||||
Components.interfaces.imgITools
|
||||
);
|
||||
let imgPtr = Components.classes[
|
||||
"@mozilla.org/supports-interface-pointer;1"
|
||||
].createInstance(Components.interfaces.nsISupportsInterfacePointer);
|
||||
imgPtr.data = imgTools.decodeImageFromArrayBuffer(u8arr.buffer, mime);
|
||||
this.transferable.addDataFlavor(mime);
|
||||
this.transferable.setTransferData(mime, imgPtr, 0);
|
||||
return this;
|
||||
}
|
||||
|
||||
public copy() {
|
||||
this.clipboardService.setData(
|
||||
this.transferable,
|
||||
null,
|
||||
Components.interfaces.nsIClipboard.kGlobalClipboard
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AddonUtils;
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import { config } from "../../package.json";
|
||||
|
||||
export { initLocale, getString };
|
||||
|
||||
/**
|
||||
* Initialize locale data
|
||||
*/
|
||||
function initLocale() {
|
||||
const l10n = new (
|
||||
typeof Localization === "undefined"
|
||||
? ztoolkit.getGlobal("Localization")
|
||||
: Localization
|
||||
)([`${config.addonRef}-addon.ftl`], true);
|
||||
addon.data.locale = {
|
||||
current: l10n,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locale string, see https://firefox-source-docs.mozilla.org/l10n/fluent/tutorial.html#fluent-translation-list-ftl
|
||||
* @param localString ftl key
|
||||
* @param options.branch branch name
|
||||
* @param options.args args
|
||||
* @example
|
||||
* ```ftl
|
||||
* # addon.ftl
|
||||
* addon-static-example = This is default branch!
|
||||
* .branch-example = This is a branch under addon-static-example!
|
||||
* addon-dynamic-example =
|
||||
{ $count ->
|
||||
[one] I have { $count } apple
|
||||
*[other] I have { $count } apples
|
||||
}
|
||||
* ```
|
||||
* ```js
|
||||
* getString("addon-static-example"); // This is default branch!
|
||||
* getString("addon-static-example", { branch: "branch-example" }); // This is a branch under addon-static-example!
|
||||
* getString("addon-dynamic-example", { args: { count: 1 } }); // I have 1 apple
|
||||
* getString("addon-dynamic-example", { args: { count: 2 } }); // I have 2 apples
|
||||
* ```
|
||||
*/
|
||||
function getString(localString: string): string;
|
||||
function getString(localString: string, branch: string): string;
|
||||
function getString(
|
||||
localeString: string,
|
||||
options: { branch?: string | undefined; args?: Record<string, unknown> },
|
||||
): string;
|
||||
function getString(...inputs: any[]) {
|
||||
if (inputs.length === 1) {
|
||||
return _getString(inputs[0]);
|
||||
} else if (inputs.length === 2) {
|
||||
if (typeof inputs[1] === "string") {
|
||||
return _getString(inputs[0], { branch: inputs[1] });
|
||||
} else {
|
||||
return _getString(inputs[0], inputs[1]);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid arguments");
|
||||
}
|
||||
}
|
||||
|
||||
function _getString(
|
||||
localeString: string,
|
||||
options: { branch?: string | undefined; args?: Record<string, unknown> } = {},
|
||||
): string {
|
||||
const localStringWithPrefix = `${config.addonRef}-${localeString}`;
|
||||
const { branch, args } = options;
|
||||
const pattern = addon.data.locale?.current.formatMessagesSync([
|
||||
{ id: localStringWithPrefix, args },
|
||||
])[0];
|
||||
if (!pattern) {
|
||||
return localStringWithPrefix;
|
||||
}
|
||||
if (branch && pattern.attributes) {
|
||||
return pattern.attributes[branch] || localStringWithPrefix;
|
||||
} else {
|
||||
return pattern.value || localStringWithPrefix;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { config } from "../../package.json";
|
||||
|
||||
/**
|
||||
* Get preference value.
|
||||
* Wrapper of `Zotero.Prefs.get`.
|
||||
* @param key
|
||||
*/
|
||||
export function getPref(key: string) {
|
||||
return Zotero.Prefs.get(`${config.prefsPrefix}.${key}`, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set preference value.
|
||||
* Wrapper of `Zotero.Prefs.set`.
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
export function setPref(key: string, value: string | number | boolean) {
|
||||
return Zotero.Prefs.set(`${config.prefsPrefix}.${key}`, value, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear preference value.
|
||||
* Wrapper of `Zotero.Prefs.clear`.
|
||||
* @param key
|
||||
*/
|
||||
export function clearPref(key: string) {
|
||||
return Zotero.Prefs.clear(`${config.prefsPrefix}.${key}`, true);
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Wait until the condition is `true` or timeout.
|
||||
* The callback is triggered if condition returns `true`.
|
||||
* @param condition
|
||||
* @param callback
|
||||
* @param interval
|
||||
* @param timeout
|
||||
*/
|
||||
export function waitUntil(
|
||||
condition: () => boolean,
|
||||
callback: () => void,
|
||||
interval = 100,
|
||||
timeout = 10000,
|
||||
) {
|
||||
const start = Date.now();
|
||||
const intervalId = ztoolkit.getGlobal("setInterval")(() => {
|
||||
if (condition()) {
|
||||
ztoolkit.getGlobal("clearInterval")(intervalId);
|
||||
callback();
|
||||
} else if (Date.now() - start > timeout) {
|
||||
ztoolkit.getGlobal("clearInterval")(intervalId);
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait async until the condition is `true` or timeout.
|
||||
* @param condition
|
||||
* @param interval
|
||||
* @param timeout
|
||||
*/
|
||||
export function waitUtilAsync(
|
||||
condition: () => boolean,
|
||||
interval = 100,
|
||||
timeout = 10000,
|
||||
) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const start = Date.now();
|
||||
const intervalId = ztoolkit.getGlobal("setInterval")(() => {
|
||||
if (condition()) {
|
||||
ztoolkit.getGlobal("clearInterval")(intervalId);
|
||||
resolve();
|
||||
} else if (Date.now() - start > timeout) {
|
||||
ztoolkit.getGlobal("clearInterval")(intervalId);
|
||||
reject();
|
||||
}
|
||||
}, interval);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
export { isWindowAlive };
|
||||
|
||||
/**
|
||||
* Check if the window is alive.
|
||||
* Useful to prevent opening duplicate windows.
|
||||
* @param win
|
||||
*/
|
||||
function isWindowAlive(win?: Window) {
|
||||
return win && !Components.utils.isDeadWrapper(win) && !win.closed;
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import ZoteroToolkit from "zotero-plugin-toolkit";
|
||||
import { config } from "../../package.json";
|
||||
|
||||
export { createZToolkit };
|
||||
|
||||
function createZToolkit() {
|
||||
const _ztoolkit = new ZoteroToolkit();
|
||||
/**
|
||||
* Alternatively, import toolkit modules you use to minify the plugin size.
|
||||
* You can add the modules under the `MyToolkit` class below and uncomment the following line.
|
||||
*/
|
||||
// const _ztoolkit = new MyToolkit();
|
||||
initZToolkit(_ztoolkit);
|
||||
return _ztoolkit;
|
||||
}
|
||||
|
||||
function initZToolkit(_ztoolkit: ReturnType<typeof createZToolkit>) {
|
||||
const env = __env__;
|
||||
_ztoolkit.basicOptions.log.prefix = `[${config.addonName}]`;
|
||||
_ztoolkit.basicOptions.log.disableConsole = env === "production";
|
||||
_ztoolkit.UI.basicOptions.ui.enableElementJSONLog = __env__ === "development";
|
||||
_ztoolkit.UI.basicOptions.ui.enableElementDOMLog = __env__ === "development";
|
||||
_ztoolkit.basicOptions.debug.disableDebugBridgePassword =
|
||||
__env__ === "development";
|
||||
_ztoolkit.basicOptions.api.pluginID = config.addonID;
|
||||
_ztoolkit.ProgressWindow.setIconURI(
|
||||
"default",
|
||||
`chrome://${config.addonRef}/content/icons/favicon.png`,
|
||||
);
|
||||
}
|
||||
|
||||
import { BasicTool, unregister } from "zotero-plugin-toolkit/dist/basic";
|
||||
import { UITool } from "zotero-plugin-toolkit/dist/tools/ui";
|
||||
import { PreferencePaneManager } from "zotero-plugin-toolkit/dist/managers/preferencePane";
|
||||
|
||||
class MyToolkit extends BasicTool {
|
||||
UI: UITool;
|
||||
PreferencePane: PreferencePaneManager;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.UI = new UITool(this);
|
||||
this.PreferencePane = new PreferencePaneManager(this);
|
||||
}
|
||||
|
||||
unregisterAll() {
|
||||
unregister(this);
|
||||
}
|
||||
}
|
||||
113
src/views.ts
113
src/views.ts
|
|
@ -1,113 +0,0 @@
|
|||
import Addon from "./addon";
|
||||
import AddonModule from "./module";
|
||||
const { addonRef, addonID } = require("../package.json");
|
||||
|
||||
class AddonViews extends AddonModule {
|
||||
// You can store some element in the object attributes
|
||||
private progressWindowIcon: object;
|
||||
|
||||
constructor(parent: Addon) {
|
||||
super(parent);
|
||||
this.progressWindowIcon = {
|
||||
success: "chrome://zotero/skin/tick.png",
|
||||
fail: "chrome://zotero/skin/cross.png",
|
||||
default: `chrome://${addonRef}/content/icons/favicon.png`,
|
||||
};
|
||||
}
|
||||
|
||||
public initViews() {
|
||||
const Zotero = this._Addon.Zotero;
|
||||
// You can init the UI elements that
|
||||
// cannot be initialized with overlay.xul
|
||||
this._Addon.Utils.Tool.log("Initializing UI");
|
||||
const menuIcon =
|
||||
'url("chrome://addontemplate/content/icons/favicon@0.5x.png")';
|
||||
// item menuitem with icon
|
||||
this._Addon.Utils.UI.insertMenuItem("item", {
|
||||
tag: "menuitem",
|
||||
id: "zotero-itemmenu-addontemplate-test",
|
||||
label: "Addon Template: Menuitem",
|
||||
oncommand: "alert('Hello World! Default Menuitem.')",
|
||||
icon: menuIcon,
|
||||
});
|
||||
// item menupopup with sub-menuitems
|
||||
this._Addon.Utils.UI.insertMenuItem(
|
||||
"item",
|
||||
{
|
||||
tag: "menu",
|
||||
label: "Addon Template: Menupopup",
|
||||
subElementOptions: [
|
||||
{
|
||||
tag: "menuitem",
|
||||
label: "Addon Template",
|
||||
oncommand: "alert('Hello World! Sub Menuitem.')",
|
||||
},
|
||||
],
|
||||
},
|
||||
"before",
|
||||
this._Addon.Zotero.getMainWindow().document.querySelector(
|
||||
"#zotero-itemmenu-addontemplate-test"
|
||||
)
|
||||
);
|
||||
this._Addon.Utils.UI.insertMenuItem("menuFile", {
|
||||
tag: "menuseparator",
|
||||
});
|
||||
// menu->File menuitem
|
||||
this._Addon.Utils.UI.insertMenuItem("menuFile", {
|
||||
tag: "menuitem",
|
||||
label: "Addon Template: File Menuitem",
|
||||
oncommand: "alert('Hello World! File Menuitem.')",
|
||||
});
|
||||
}
|
||||
|
||||
public initPrefs() {
|
||||
const Zotero = this._Addon.Zotero;
|
||||
this._Addon.Utils.Tool.log(this._Addon.rootURI);
|
||||
const prefOptions = {
|
||||
pluginID: addonID,
|
||||
src: this._Addon.rootURI + "chrome/content/preferences.xhtml",
|
||||
label: "Template",
|
||||
image: `chrome://${addonRef}/content/icons/favicon.png`,
|
||||
extraDTD: [`chrome://${addonRef}/locale/overlay.dtd`],
|
||||
defaultXUL: true,
|
||||
onload: (win: Window) => {
|
||||
this._Addon.prefs.initPreferences(win);
|
||||
},
|
||||
};
|
||||
if (this._Addon.Utils.Compat.isZotero7()) {
|
||||
Zotero.PreferencePanes.register(prefOptions);
|
||||
} else {
|
||||
this._Addon.Utils.Compat.registerPrefPane(prefOptions);
|
||||
}
|
||||
}
|
||||
|
||||
public unInitViews() {
|
||||
const Zotero = this._Addon.Zotero;
|
||||
this._Addon.Utils.Tool.log("Uninitializing UI");
|
||||
this._Addon.Utils.UI.removeAddonElements();
|
||||
if (!this._Addon.Utils.Compat.isZotero7()) {
|
||||
this._Addon.Utils.Compat.unregisterPrefPane();
|
||||
}
|
||||
}
|
||||
|
||||
public showProgressWindow(
|
||||
header: string,
|
||||
context: string,
|
||||
type: string = "default",
|
||||
t: number = 5000
|
||||
) {
|
||||
// A simple wrapper of the Zotero ProgressWindow
|
||||
let progressWindow = new Zotero.ProgressWindow({ closeOnClick: true });
|
||||
progressWindow.changeHeadline(header);
|
||||
progressWindow.progress = new progressWindow.ItemProgress(
|
||||
this.progressWindowIcon[type],
|
||||
context
|
||||
);
|
||||
progressWindow.show();
|
||||
if (t > 0) {
|
||||
progressWindow.startCloseTimer(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AddonViews;
|
||||
6
start.js
6
start.js
|
|
@ -1,6 +0,0 @@
|
|||
const { execSync } = require("child_process");
|
||||
const { exit } = require("process");
|
||||
const { startZotero } = require("./zotero-cmd.json");
|
||||
|
||||
execSync(startZotero);
|
||||
exit(0);
|
||||
6
stop.js
6
stop.js
|
|
@ -1,6 +0,0 @@
|
|||
const { execSync } = require("child_process");
|
||||
const { killZotero } = require("./zotero-cmd.json");
|
||||
|
||||
try {
|
||||
execSync(killZotero);
|
||||
} catch (e) {}
|
||||
|
|
@ -1,16 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"resolveJsonModule": true
|
||||
"target": "ES2016",
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"typing",
|
||||
"node_modules/zotero-types"
|
||||
],
|
||||
"exclude": [
|
||||
"builds",
|
||||
"addon"
|
||||
]
|
||||
}
|
||||
"include": ["src", "typings", "node_modules/zotero-types"],
|
||||
"exclude": ["build", "addon"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
declare interface ZoteroCompat {
|
||||
getZotero: () => _ZoteroConstructable;
|
||||
isZotero7: () => boolean;
|
||||
getDOMParser: () => DOMParser;
|
||||
createXULElement: (doc: Document, type: string) => XUL.Element;
|
||||
parseXHTMLToFragment: (
|
||||
str: string,
|
||||
entities: string[],
|
||||
defaultXUL?: boolean
|
||||
) => DocumentFragment;
|
||||
prefPaneCache: { win: Window; listeners: any[]; ids: string[] };
|
||||
registerPrefPane: (options: PrefPaneOptions) => void;
|
||||
unregisterPrefPane: () => void;
|
||||
}
|
||||
|
||||
declare interface ZoteroTool {
|
||||
getCopyHelper: () => CopyHelper;
|
||||
openFilePicker: (
|
||||
title: string,
|
||||
mode: "open" | "save" | "folder",
|
||||
filters?: [string, string][],
|
||||
suggestion?: string
|
||||
) => Promise<string>;
|
||||
log: (...data: any[]) => void;
|
||||
}
|
||||
|
||||
declare interface ZoteroUI {
|
||||
addonElements: Element[];
|
||||
createElement: (
|
||||
doc: Document,
|
||||
tagName: string,
|
||||
namespace: "html" | "svg" | "xul"
|
||||
) => XUL.Element | DocumentFragment | HTMLElement | SVGAElement;
|
||||
removeAddonElements: () => void;
|
||||
creatElementsFromJSON: (
|
||||
doc: Document,
|
||||
options: ElementOptions
|
||||
) => XUL.Element | DocumentFragment | HTMLElement | SVGAElement;
|
||||
defaultMenuPopupSelectors: {
|
||||
[key: string]: string;
|
||||
};
|
||||
insertMenuItem: (
|
||||
menuPopup: XUL.Menupopup | string,
|
||||
options: MenuitemOptions,
|
||||
insertPosition?: "before" | "after",
|
||||
anchorElement?: XUL.Element
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
declare interface ElementOptions {
|
||||
tag: string;
|
||||
id?: string;
|
||||
namespace?: "html" | "svg" | "xul";
|
||||
styles?: { [key: string]: string };
|
||||
directAttributes?: { [key: string]: string | boolean | number };
|
||||
attributes?: { [key: string]: string | boolean | number };
|
||||
listeners?: Array<
|
||||
| [
|
||||
string,
|
||||
EventListenerOrEventListenerObject,
|
||||
boolean | AddEventListenerOptions
|
||||
]
|
||||
| [string, EventListenerOrEventListenerObject]
|
||||
>;
|
||||
checkExistanceParent?: HTMLElement;
|
||||
ignoreIfExists?: boolean;
|
||||
removeIfExists?: boolean;
|
||||
customCheck?: () => boolean;
|
||||
subElementOptions?: Array<ElementOptions>;
|
||||
}
|
||||
|
||||
declare interface MenuitemOptions {
|
||||
tag: "menuitem" | "menu" | "menuseparator";
|
||||
id?: string;
|
||||
label?: string;
|
||||
// data url (chrome://xxx.png) or base64 url (data:image/png;base64,xxx)
|
||||
icon?: string;
|
||||
class?: string;
|
||||
styles?: { [key: string]: string };
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
oncommand?: string;
|
||||
commandListener?: EventListenerOrEventListenerObject;
|
||||
// Attributes below are used when type === "menu"
|
||||
popupId?: string;
|
||||
onpopupshowing?: string;
|
||||
subElementOptions?: Array<MenuitemOptions>;
|
||||
}
|
||||
|
||||
declare interface PrefPaneOptions {
|
||||
pluginID: string;
|
||||
src: string;
|
||||
id?: string;
|
||||
parent?: string;
|
||||
label?: string;
|
||||
image?: string;
|
||||
extraDTD?: string[];
|
||||
scripts?: string[];
|
||||
defaultXUL?: boolean;
|
||||
// Only for Zotero 6
|
||||
onload?: (win: Window) => any;
|
||||
}
|
||||
|
||||
declare class CopyHelper {
|
||||
addText: (source: string, type: "text/html" | "text/unicode") => CopyHelper;
|
||||
addImage: (source: string) => CopyHelper;
|
||||
copy: () => void;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
declare const _globalThis: {
|
||||
[key: string]: any;
|
||||
Zotero: _ZoteroTypes.Zotero;
|
||||
ZoteroPane: _ZoteroTypes.ZoteroPane;
|
||||
Zotero_Tabs: typeof Zotero_Tabs;
|
||||
window: Window;
|
||||
document: Document;
|
||||
ztoolkit: ZToolkit;
|
||||
addon: typeof addon;
|
||||
};
|
||||
|
||||
declare type ZToolkit = ReturnType<
|
||||
typeof import("../src/utils/ztoolkit").createZToolkit
|
||||
>;
|
||||
|
||||
declare const ztoolkit: ZToolkit;
|
||||
|
||||
declare const rootURI: string;
|
||||
|
||||
declare const addon: import("../src/addon").default;
|
||||
|
||||
declare const __env__: "production" | "development";
|
||||
|
||||
declare class Localization {}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"addons": {
|
||||
"__addonID__": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "__buildVersion__",
|
||||
"update_link": "__releasepage__",
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"strict_min_version": "60.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "__buildVersion__",
|
||||
"update_link": "__releasepage__",
|
||||
"applications": {
|
||||
"zotero": {
|
||||
"strict_min_version": "6.999"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||
<rdf:Description rdf:about="urn:mozilla:extension:__addonID__">
|
||||
<em:updates>
|
||||
<rdf:Seq>
|
||||
<rdf:li>
|
||||
<rdf:Description>
|
||||
<em:version>__buildVersion__</em:version>
|
||||
<em:targetApplication>
|
||||
<rdf:Description>
|
||||
<em:id>zotero@chnm.gmu.edu</em:id>
|
||||
<em:minVersion>6.999</em:minVersion>
|
||||
<em:maxVersion>*</em:maxVersion>
|
||||
<em:updateLink>__releasepage__</em:updateLink>
|
||||
</rdf:Description>
|
||||
</em:targetApplication>
|
||||
<em:targetApplication>
|
||||
<rdf:Description>
|
||||
<em:id>juris-m@juris-m.github.io</em:id>
|
||||
<em:minVersion>6.999</em:minVersion>
|
||||
<em:maxVersion>*</em:maxVersion>
|
||||
<em:updateLink>__releasepage__</em:updateLink>
|
||||
</rdf:Description>
|
||||
</em:targetApplication>
|
||||
</rdf:Description>
|
||||
</rdf:li>
|
||||
</rdf:Seq>
|
||||
</em:updates>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
15
update.json
15
update.json
|
|
@ -1,19 +1,10 @@
|
|||
{
|
||||
"addons": {
|
||||
"addontemplate@euclpts.com": {
|
||||
"zoterotldr@syt.com": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"update_link": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi",
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"strict_min_version": "60.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"update_link": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi",
|
||||
"version": "1.0.7",
|
||||
"update_link": "undefined/latest/download/zotero-tldr.xpi",
|
||||
"applications": {
|
||||
"zotero": {
|
||||
"strict_min_version": "6.999"
|
||||
|
|
|
|||
30
update.rdf
30
update.rdf
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||
<rdf:Description rdf:about="urn:mozilla:extension:addontemplate@euclpts.com">
|
||||
<em:updates>
|
||||
<rdf:Seq>
|
||||
<rdf:li>
|
||||
<rdf:Description>
|
||||
<em:version>0.0.1</em:version>
|
||||
<em:targetApplication>
|
||||
<rdf:Description>
|
||||
<em:id>zotero@chnm.gmu.edu</em:id>
|
||||
<em:minVersion>6.999</em:minVersion>
|
||||
<em:maxVersion>*</em:maxVersion>
|
||||
<em:updateLink>https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi</em:updateLink>
|
||||
</rdf:Description>
|
||||
</em:targetApplication>
|
||||
<em:targetApplication>
|
||||
<rdf:Description>
|
||||
<em:id>juris-m@juris-m.github.io</em:id>
|
||||
<em:minVersion>6.999</em:minVersion>
|
||||
<em:maxVersion>*</em:maxVersion>
|
||||
<em:updateLink>https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi</em:updateLink>
|
||||
</rdf:Description>
|
||||
</em:targetApplication>
|
||||
</rdf:Description>
|
||||
</rdf:li>
|
||||
</rdf:Seq>
|
||||
</em:updates>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"usage": "Copy and rename this file to zotero-cmd.json. Edit the cmd.",
|
||||
"killZotero": "taskkill /f /im zotero.exe",
|
||||
"startZotero": "/path/to/zotero.exe --debugger --purgecaches"
|
||||
}
|
||||
Loading…
Reference in New Issue