Compare commits

..

No commits in common. "main" and "0.1.3" have entirely different histories.
main ... 0.1.3

67 changed files with 1945 additions and 2302 deletions

1
.gitattributes vendored
View File

@ -1 +0,0 @@
* text=auto eol=lf

21
.github/renovate.json vendored
View File

@ -1,21 +0,0 @@
{
"$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
}
}

View File

@ -1,43 +0,0 @@
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._

8
.gitignore vendored
View File

@ -1,8 +1,4 @@
build
logs
**/builds
node_modules
package-lock.json
pnpm-lock.yaml
yarn.lock
zotero-cmd.json
.DS_Store
zotero-cmd.json

View File

@ -1,7 +0,0 @@
build
logs
node_modules
package-lock.json
yarn.lock
pnpm-lock.yaml
# zotero-cmd.json

13
.release-it.json Normal file
View File

@ -0,0 +1,13 @@
{
"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}."
}
}

View File

@ -1,7 +0,0 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"macabeus.vscode-fluent"
]
}

42
.vscode/launch.json vendored
View File

@ -1,22 +1,22 @@
{
// 使 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"]
}
]
}
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Restart",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "restart"],
},
{
"type": "node",
"request": "launch",
"name": "Restart in Prod Mode",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "restart-prod"],
}
]
}

View File

@ -1,7 +0,0 @@
{
"editor.formatOnType": false,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}

View File

@ -1,45 +0,0 @@
{
"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}",
"});",
],
},
}

367
README.md
View File

@ -1,17 +1,362 @@
# Zotero TL;DR
# Zotero Plugin Template
[![zotero target version](https://img.shields.io/badge/Zotero-7-red?style=flat-square&logo=zotero&logoColor=CC2936)](https://www.zotero.org)
[![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template)
![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-better-notes?label=zotero-better-notes)
![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-pdf-preview?label=zotero-pdf-preview)
![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-pdf-translate?label=zotero-pdf-translate)
![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-tag?label=zotero-tag)
![GitHub Repo stars](https://img.shields.io/github/stars/iShareStuff/ZoteroTheme?label=zotero-theme)
![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-reference?label=zotero-reference)
![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/ZoteroStyle?label=zotero-style)
![GitHub Repo stars](https://img.shields.io/github/stars/volatile-static/Chartero?label=Chartero)
![GitHub Repo stars](https://img.shields.io/github/stars/l0o0/tara?label=tara)
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.
This is a plugin template for [Zotero](https://www.zotero.org/). Plugins using this template are shown above.
## Install
📖[Plugin Development Documentation](https://zotero.yuque.com/books/share/8d230829-6004-4934-b4c6-685a7001bfa0/vec88d)(Chinese, provides English translation)
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)
🛠️[Zotero Plugin Toolkit](https://github.com/windingwind/zotero-plugin-toolkit) | [API Documentation](https://github.com/windingwind/zotero-plugin-toolkit/blob/master/docs/zotero-plugin-toolkit.md)
## Usage
[Zotero Type Definitions](https://github.com/windingwind/zotero-types)
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.
📜[Zotero Source Code](https://github.com/zotero/zotero)
📌[Zotero Plugin Template](https://github.com/windingwind/zotero-plugin-template)(This repo)
> 👍You are currently in `bootstrap` extension mode. To use `overlay` mode, plsase switch to `overlay` branch in git.
> 👁 Watch this repo so that you can be notified whenever there are fixes & updates.
## Features
- Event-driven, functional programming, under extensive skeleton;
- Simple and user-friendly, works out-of-the-box.
- Abundant examples in `src/modules/examples.ts`, covering most of the commonly used APIs in plugins(using [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit));
- TypeScript support:
- Full type definition support for the whole Zotero project, which is written in JavaScript(using [zotero-types](https://github.com/windingwind/zotero-types));
- Global variables and environment setup;
- Plugin build/test/release workflow:
- Automatically generate/update plugin id/version, update configrations, and set environment variables(`development/production`);
- Automatically build and reload code in Zotero;
- Automatically release to GitHub(using [release-it](https://github.com/release-it/release-it));
- ⭐[New!]Compatibilities for Zotero 6 & Zotero 7.(using [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit))
## Examples
This repo provides examples for [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit) APIs.
Search `@example` in `src/examples.ts`. The examples are called in `src/hooks.ts`.
### Basic Examples
- registerNotifier
- registerPrefs, unregisterPrefs
### Shortcut Keys Examples
- registerShortcuts
- exampleShortcutLargerCallback
- exampleShortcutSmallerCallback
- exampleShortcutConflictionCallback
### UI Examples
![image](https://user-images.githubusercontent.com/33902321/211739774-cc5c2df8-5fd9-42f0-9cdf-0f2e5946d427.png)
- registerStyleSheet(the official make-it-red example)
- registerRightClickMenuItem
- registerRightClickMenuPopup
- registerWindowMenuWithSeprator
- registerExtraColumn
- registerExtraColumnWithCustomCell
- registerCustomCellRenderer
- registerLibraryTabPanel
- registerReaderTabPanel
### Preference Pane Examples
![image](https://user-images.githubusercontent.com/33902321/211737987-cd7c5c87-9177-4159-b975-dc67690d0490.png)
- Preferences bindings
- UI Events
- Tabel
- Locale
See [`src/modules/preferenceScript.ts`](./src/modules/preferenceScript.ts)
### HelperExamples
- dialogExample
- clipboardExample
- filePickerExample
- progressWindowExample
- vtableExample(See Preference Pane Examples)
## Quick Start Guide
### Install Pre-built `xpi`
See how the examples work by directly downloading the `xpi` file from GitHub release and install it to your Zotero.
This is also how your plugin will be released and used by others.
> The release do not promise any real functions. It is probably not up-to-date.
>
> The `xpi` package is a zip file. However, please don't modify it directly. Modify the source code and build it.
### Build from Source
- Fork this repo/Click `Use this template`;
- Git clone the forked repo;
- Enter the repo folder;
- Modify the settings in `./package.json`, including:
```
version,
author,
description,
homepage,
config {
releasepage,
updaterdf,
addonName,
addonID,
addonRef
}
```
> Be careful to set the addonID and addonRef to avoid confliction.
- Run `npm install` to set up 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 in production mode. Run `npm run build-dev` to build the plugin in development mode. The xpi for installation and the built code is under `builds` folder.
> What the difference between dev & prod?
>
> - This environment variable is stored in `Zotero.AddonTemplate.data.env`. The outputs to console is disabled in prod mode.
> - You can decide what users cannot see/use based on this variable.
### Release
To build and release, use
```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
```
### Setup Development Environment
1. Install a beta version of Zotero: https://www.zotero.org/support/beta_builds (Zotero 7 beta: https://www.zotero.org/support/dev/zotero_7_for_developers)
2. Install Firefox 60(for Zotero 6)/Firefox 102(for Zotero 7)
3. Copy zotero command line config file. Modify the commands that starts your installation of the beta Zotero.
> (Optional) Do this only once: Start the beta Zotero with `/path/to/zotero -p`. Create a new profile and use it as your development profile.
> Use `/path/to/zotero -p {profile_name}` to specify which profile to run with.
```sh
cp ./scripts/zotero-cmd-default.json ./scripts/zotero-cmd.json
vim ./scripts/zotero-cmd.json
```
4. Setup plugin development environment following this [link](https://www.zotero.org/support/dev/client_coding/plugin_development#setting_up_a_plugin_development_environment).
5. Build plugin and restart Zotero with `npm run restart`.
6. Launch Firefox 60(Zotero 6)/Firefox 102(Zotero 7)
7. In Firefox, go to devtools, go to settings, click "enable remote debugging" and the one next to it that's also about debugging
> Press `shift+F8` in FF 60, or enter `about:debugging#/setup` in FF 102.
8. In Zotero, go to setting, advanced, config editor, look up "debugging" and click on "allow remote debugging".
9. Connect to Zotero in Firefox.
> In FF 60, click the hamburger menu in the top right -> web developer -> Connect..., then enter `localhost:6100`.
> In FF 102, enter `localhost:6100` in the bottom input of remote-debugging page and click `add`.
10. Click `connect` in the leftside-bar of Firefox remote-debugging page.
11. Click "Inspect Main Process"
### Debug in Zotero
You can also:
- Test code snipastes in Tools->Developer->Run Javascript;
- Debug output with `Zotero.debug()`. Find the outputs in Help->Debug Output Logging->View Output;
- Debug UI. 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 Documentation: http://www.devdoc.net/web/developer.mozilla.org/en-US/docs/XUL.html
## Details
### About Hooks
> See also [`src/hooks.ts`](https://github.com/windingwind/zotero-plugin-template/blob/bootstrap/src/hooks.ts)
1. When install/enable/startup triggered from Zotero, `bootstrap.js` > `startup` is called
- Wait for Zotero ready
- Load `index.js` (the main entrance of plugin code, built from `index.ts`)
- Register resources if Zotero 7+
2. In the main entrance `index.js`, the plugin object is injected under `Zotero` and `hooks.ts` > `onStartup` is called.
- Initialize anything you want, including notify listeners, preference panes, and UI elements.
3. When uninstall/disabled triggered from Zotero, `bootstrap.js` > `shutdown` is called.
- `events.ts` > `onShutdown` is called. Remove UI elements, preference panes, or anything created by the plugin.
- Remove scripts and release resources.
### About Global Variables
> See also [`src/index.ts`](https://github.com/windingwind/zotero-plugin-template/blob/bootstrap/src/index.ts)
The bootstrapped plugin runs in a sandbox, which does not have default global variables like `Zotero` or `window`, which we used to have in the overlay plugins' window environment.
This template registers the following variables to the global scope:
```ts
Zotero, ZoteroPane, Zotero_Tabs, window, document, rootURI, ztoolkit, addon;
```
### About Preference
Zotero 6 doesn't support preference pane injection in bootstrap mode, thus I write a register for Zotero 6 or lower.
You only need to maintain one `preferences.xhtml` which runs natively on Zotero 7 and let the plugin template handle it when it is running on Zotero 6.
<table style="margin-left: auto; margin-right: auto;">
<tr>
<td>
<img width="350px" src="https://user-images.githubusercontent.com/33902321/208080125-2a776a98-f427-4c81-8924-7877bf803e3d.png"/>
<div>Zotero 7</div>
</td>
<td>
<img width="300px" src="https://user-images.githubusercontent.com/33902321/208080491-b7006c08-2679-4f85-9a28-dba8e622d745.png"/>
<div>Zotero 6</div>
</td>
</tr>
</table>
https://github.com/windingwind/zotero-plugin-template/blob/08d72a4e2b3bacff574f537bbd06cb33e6b22480/src/modules/examples.ts#L73-L85
> `<preferences>` element is deprecated. Please use the full pref-key in the elements' `preference` attribute. Like:
```xml
<checkbox label="&zotero.__addonRef__.pref.enable.label;" preference="extensions.zotero.__addonRef__.enable" />
```
The elements with `preference` attributes will bind to Zotero preferences.
Remember to call `unregister()` on plugin unload.
### Create Elements API
The plugin template provides new APIs for bootstrap plugins. We have two reasons to use these APIs, instead of the `createElement/createElementNS`:
- In bootstrap mode, plugins have to clean up all UI elements on exit (disable or uninstall), which is very annoying. Using the `createElement`, the plugin template will maintain these elements. Just `unregisterAll` at the exit.
- Zotero 7 requires createElement()/createElementNS() → createXULElement() for remaining XUL elements, while Zotero 6 doesn't support `createXULElement`. The React.createElement-like API `createElement` detects namespace(xul/html/svg) and creates elements automatically, with the return element in the corresponding TS element type.
```ts
createElement(document, "div"); // returns HTMLDivElement
createElement(document, "hbox"); // returns XUL.Box
createElement(document, "button", { namespace: "xul" }); // manually set namespace. returns XUL.Button
```
### About Build
Use Esbuild to build `.ts` source code to `.js`.
Use `replace-in-file` to replace keywords and configurations defined in `package.json` in non-build files (`.xul/xhtml`, `.dtd`, and `.properties`).
Steps in `scripts/build.js`:
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`
### About Zotero API
Zotero docs are outdated and incomplete. Clone https://github.com/zotero/zotero and search the keyword globally.
> ⭐The [zotero-types](https://github.com/windingwind/zotero-types) provides most frequently used Zotero APIs. It's included in this template by default. Your IDE would provide hint for most of the APIs.
A trick for finding the API you want:
Search the UI label in `.xul`(`.xhtml`)/`.dtd`/`.properties` files, find the corresponding key in locale file. Then search this keys in `.js`/`.jsx` files.
### 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`, and `./addon/manifest.json`;
- UI files: `./addon/chrome/content/*.xhtml`.
- Locale files: `./addon/chrome/locale/[*.dtd, *.properties]`;
- 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 # for Zotero 6
│ │ manifest.json # for Zotero 7
│ │ install.rdf # addon install conf, for Zotero 6
│ │ bootstrap.js # addon load/unload script, like a main.c
│ │
│ └─chrome
│ ├─content # UI
│ │ │ preferences.xhtml
│ │ │
│ │ ├─icons
│ │ │ favicon.png
│ │ │ favicon@0.5x.png
│ │ │
│ │ └─scripts
│ └─locale # locale
│ ├─en-US
│ │ overlay.dtd
│ │ addon.properties
│ │
│ ├─zh-CN
│ | overlay.dtd
│ └─ addon.properties
├─builds # build dir
│ └─.xpi
└─src # source code
│ index.ts # main entry
│ addon.ts # base class
│ hooks.ts # lifecycle hooks
|
└─modules # sub modules
│ examples.ts # examples factory
│ locale.ts # locale .properties
│ preferenceScript.ts # script runs in preferences.xhtml
└─ progressWindow.ts # progressWindow tool
```
## 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`)

113
addon/bootstrap.js vendored
View File

@ -1,73 +1,114 @@
/**
* 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
*/
/* 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;
}
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 Zotero.initializationPromise;
await waitForZotero();
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/"],
]);
}
// String 'rootURI' introduced in Zotero 7
if (!rootURI) {
rootURI = resourceURI.spec;
}
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.
*/
// Global variables for plugin code
const ctx = {
rootURI,
};
ctx._globalThis = ctx;
Services.scriptloader.loadSubScript(
`${rootURI}/chrome/content/scripts/__addonRef__.js`,
ctx,
`${rootURI}/chrome/content/scripts/index.js`,
ctx
);
Zotero.__addonInstance__.hooks.onStartup();
}
async function onMainWindowLoad({ window }, reason) {
Zotero.__addonInstance__?.hooks.onMainWindowLoad(window);
}
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.__addonInstance__?.hooks.onShutdown();
Zotero.AddonTemplate.hooks.onShutdown();
Cc["@mozilla.org/intl/stringbundle;1"]
.getService(Components.interfaces.nsIStringBundleService)
.flushBundles();
Cu.unload(`${rootURI}/chrome/content/scripts/__addonRef__.js`);
Cu.unload(`${rootURI}/chrome/content/scripts/index.js`);
if (chromeHandle) {
chromeHandle.destruct();

3
addon/chrome.manifest Normal file
View File

@ -0,0 +1,3 @@
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: 4.1 KiB

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,31 @@
<vbox
id="zotero-prefpane-__addonRef__"
onload="Zotero.AddonTemplate.hooks.onPrefsEvent('load', {window})"
>
<groupbox>
<label><html:h2>&zotero.__addonRef__.pref.title;</html:h2></label>
<checkbox
id="zotero-prefpane-__addonRef__-enable"
label="&zotero.__addonRef__.pref.enable.label;"
preference="extensions.zotero.__addonRef__.enable"
/>
<hbox>
<html:label for="zotero-prefpane-__addonRef__-input"
>&zotero.__addonRef__.pref.input.label;</html:label
>
<html:input
type="text"
id="zotero-prefpane-__addonRef__-input"
preference="extensions.zotero.__addonRef__.input"
></html:input>
</hbox>
<hbox class="virtualized-table-container" flex="1" height="300px">
<html:div id="__addonRef__-table-container" />
</hbox>
</groupbox>
</vbox>
<vbox>
<label
value="&zotero.__addonRef__.help.version.label; &zotero.__addonRef__.help.releasetime.label;"
></label>
</vbox>

View File

@ -0,0 +1,3 @@
.makeItRed {
background-color: tomato;
}

View File

@ -0,0 +1,11 @@
startup.begin=Addon is loading
startup.finish=Addon is ready
menuitem.label=Addon Template: Helper Examples
menupopup.label=Addon Template: Menupopup
menuitem.submenulabel=Addon Template
menuitem.filemenulabel=Addon Template: File Menuitem
prefs.title=Template
prefs.table.title=Title
prefs.table.detail=Detail
tabpanel.lib.tab.label=Lib Tab
tabpanel.reader.tab.label=Reader Tab

View File

@ -0,0 +1,7 @@
<!ENTITY zotero.__addonRef__.pref.title "Addon Template Example">
<!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__">

View File

@ -0,0 +1,11 @@
startup.begin=插件加载中
startup.finish=插件已就绪
menuitem.label=插件模板: 帮助工具样例
menupopup.label=插件模板: 弹出菜单
menuitem.submenulabel=插件模板:子菜单
menuitem.filemenulabel=插件模板: 文件菜单
prefs.title=插件模板
prefs.table.title=标题
prefs.table.detail=详情
tabpanel.lib.tab.label=库标签
tabpanel.reader.tab.label=阅读器标签

View File

@ -0,0 +1,7 @@
<!ENTITY zotero.__addonRef__.pref.title "插件模板设置示例">
<!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__">

View File

@ -0,0 +1,2 @@
pref("extensions.zotero.__addonRef__.enable", true);
pref("extensions.zotero.__addonRef__.input", "This is input");

35
addon/install.rdf Normal file
View File

@ -0,0 +1,35 @@
<?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: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>

View File

@ -1,8 +0,0 @@
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

View File

@ -1,4 +0,0 @@
itemPaneSection-header =
.label = TLDR
itemPaneSection-sidenav =
.tooltiptext = TLDR

View File

@ -1,8 +0,0 @@
menuitem-updatetldrlabel = 更新TLDR
menucollection-updatetldrlabel = 批量更新TLDR
itembox-tldrlabel = TLDR
tldr-unrelated = 未关联TLDR
tldr-itemnotfound = 未搜索到此条目
popWindow-succeed = 成功
popWindow-failed = 失败
popWindow-waiting = 等待

View File

@ -1,4 +0,0 @@
itemPaneSection-header =
.label = TLDR
itemPaneSection-sidenav =
.tooltiptext = TLDR

View File

@ -3,7 +3,6 @@
"name": "__addonName__",
"version": "__buildVersion__",
"description": "__description__",
"homepage_url": "__homepage__",
"author": "__author__",
"icons": {
"48": "chrome/content/icons/favicon@0.5x.png",
@ -12,7 +11,7 @@
"applications": {
"zotero": {
"id": "__addonID__",
"update_url": "__updateURL__",
"update_url": "__updaterdf__",
"strict_min_version": "6.999",
"strict_max_version": "7.0.*"
}

View File

@ -1 +0,0 @@
/* eslint-disable no-undef */

View File

@ -1,420 +0,0 @@
# Zotero Plugin Template
[![zotero target version](https://img.shields.io/badge/Zotero-7-green?style=flat-square&logo=zotero&logoColor=CC2936)](https://www.zotero.org)
[![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template)
这是 [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 本仓库,以及时收到修复或更新的通知.
## 使用此模板构建的插件
[![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-better-notes?label=zotero-better-notes&style=flat-square)](https://github.com/windingwind/zotero-better-notes)
[![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-pdf-preview?label=zotero-pdf-preview&style=flat-square)](https://github.com/windingwind/zotero-pdf-preview)
[![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-pdf-translate?label=zotero-pdf-translate&style=flat-square)](https://github.com/windingwind/zotero-pdf-translate)
[![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-tag?label=zotero-tag&style=flat-square)](https://github.com/windingwind/zotero-tag)
[![GitHub Repo stars](https://img.shields.io/github/stars/iShareStuff/ZoteroTheme?label=zotero-theme&style=flat-square)](https://github.com/iShareStuff/ZoteroTheme)
[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-reference?label=zotero-reference&style=flat-square)](https://github.com/MuiseDestiny/zotero-reference)
[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-citation?label=zotero-citation&style=flat-square)](https://github.com/MuiseDestiny/zotero-citation)
[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/ZoteroStyle?label=zotero-style&style=flat-square)](https://github.com/MuiseDestiny/ZoteroStyle)
[![GitHub Repo stars](https://img.shields.io/github/stars/volatile-static/Chartero?label=Chartero&style=flat-square)](https://github.com/volatile-static/Chartero)
[![GitHub Repo stars](https://img.shields.io/github/stars/l0o0/tara?label=tara&style=flat-square)](https://github.com/l0o0/tara)
[![GitHub Repo stars](https://img.shields.io/github/stars/redleafnew/delitemwithatt?label=delitemwithatt&style=flat-square)](https://github.com/redleafnew/delitemwithatt)
[![GitHub Repo stars](https://img.shields.io/github/stars/redleafnew/zotero-updateifsE?label=zotero-updateifsE&style=flat-square)](https://github.com/redleafnew/zotero-updateifsE)
[![GitHub Repo stars](https://img.shields.io/github/stars/northword/zotero-format-metadata?label=zotero-format-metadata&style=flat-square)](https://github.com/northword/zotero-format-metadata)
[![GitHub Repo stars](https://img.shields.io/github/stars/inciteful-xyz/inciteful-zotero-plugin?label=inciteful-zotero-plugin&style=flat-square)](https://github.com/inciteful-xyz/inciteful-zotero-plugin)
[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-gpt?label=zotero-gpt&style=flat-square)](https://github.com/MuiseDestiny/zotero-gpt)
[![GitHub Repo stars](https://img.shields.io/github/stars/zoushucai/zotero-journalabbr?label=zotero-journalabbr&style=flat-square)](https://github.com/zoushucai/zotero-journalabbr)
[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-figure?label=zotero-figure&style=flat-square)](https://github.com/MuiseDestiny/zotero-figure)
[![GitHub Repo stars](https://img.shields.io/github/stars/l0o0/jasminum?label=jasminum&style=flat-square)](https://github.com/l0o0/jasminum)
[![GitHub Repo stars](https://img.shields.io/github/stars/lifan0127/ai-research-assistant?label=ai-research-assistant&style=flat-square)](https://github.com/lifan0127/ai-research-assistant)
[![GitHub Repo stars](https://img.shields.io/github/stars/daeh/zotero-markdb-connect?label=zotero-markdb-connect&style=flat-square)](https://github.com/daeh/zotero-markdb-connect)
如果你正在使用此库,我建议你将这个标志 ([![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template)) 放在 README 文件中:
```md
[![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template)
```
## 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)
![image](https://user-images.githubusercontent.com/33902321/211739774-cc5c2df8-5fd9-42f0-9cdf-0f2e5946d427.png)
- registerStyleSheet(the official make-it-red example)
- registerRightClickMenuItem
- registerRightClickMenuPopup
- registerWindowMenuWithSeprator
- registerExtraColumn
- registerExtraColumnWithCustomCell
- registerCustomItemBoxRow
- registerLibraryTabPanel
- registerReaderTabPanel
### 首选项面板示例(Preference Pane Examples)
![image](https://user-images.githubusercontent.com/33902321/211737987-cd7c5c87-9177-4159-b975-dc67690d0490.png)
- Preferences bindings
- UI Events
- Table
- Locale
详情参见 [`src/modules/preferenceScript.ts`](./src/modules/preferenceScript.ts)
### 帮助示例(HelperExamples)
![image](https://user-images.githubusercontent.com/33902321/215119473-e7d0d0ef-6d96-437e-b989-4805ffcde6cf.png)
- dialogExample
- clipboardExample
- filePickerExample
- progressWindowExample
- vtableExample(See Preference Pane Examples)
### 指令行示例(PromptExamples)
Obsidian风格的指令输入模块它通过接受文本来运行插件并在弹出窗口中显示可选项.
使用 `Shift+P` 激活.
![image](https://user-images.githubusercontent.com/33902321/215120009-e7c7ed27-33a0-44fe-b021-06c272481a92.png)
- registerAlertPromptExample
## Quick Start Guide 快速入门指南
### 0 前置要求(Requirement)
1. 安装测试版 Zoterohttps://www.zotero.org/support/beta_builds
2. 安装 Node.jshttps://nodejs.org/en/)和 Githttps://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-typeshttps://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 Tokenhttps://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> 与我联系.

View File

@ -1,134 +1,50 @@
{
"name": "zotero-tldr",
"version": "1.0.7",
"description": "TLDR(too long; didn't read) from sematic scholar",
"name": "zotero-addon-template",
"version": "0.1.3",
"description": "Zotero Addon Template",
"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"
"addonName": "Zotero Addon Template",
"addonID": "addontemplate@euclpts.com",
"addonRef": "addontemplate",
"releasepage": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi",
"updaterdf": "https://raw.githubusercontent.com/windingwind/zotero-addon-template/bootstrap/update.json"
},
"main": "src/index.ts",
"scripts": {
"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"
"build-dev": "cross-env NODE_ENV=development node scripts/build.js",
"build-prod": "cross-env NODE_ENV=production node scripts/build.js",
"build": "npm run build-prod",
"start-z6": "node scripts/start.js --z 6",
"start-z7": "node scripts/start.js --z 7",
"start": "node scripts/start.js",
"stop": "node scripts/stop.js",
"restart-dev": "npm run build-dev && npm run stop && npm run start",
"restart-prod": "npm run build-prod && npm run stop && npm run start",
"restart": "npm run restart-dev",
"release": "release-it",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/syt2/zotero-tldr.git"
"url": "git+https://github.com/windingwind/zotero-addon-template.git"
},
"author": "syt2",
"author": "windingwind",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/syt2/zotero-tldr/issues"
"url": "https://github.com/windingwind/zotero-addon-template/issues"
},
"homepage": "https://github.com/syt2/zotero-tldr#readme",
"homepage": "https://github.com/windingwind/zotero-addon-template#readme",
"dependencies": {
"zotero-plugin-toolkit": "^2.3.29"
"zotero-plugin-toolkit": "^1.0.6"
},
"devDependencies": {
"@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"
}
"@types/node": "^18.11.17",
"compressing": "^1.6.3",
"cross-env": "^7.0.3",
"esbuild": "^0.16.10",
"release-it": "^15.6.0",
"replace-in-file": "^6.3.5",
"minimist": "^1.2.7",
"zotero-types": "^1.0.0"
}
}

182
scripts/build.js Normal file
View File

@ -0,0 +1,182 @@
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,
version,
config,
} = 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}, ENV=${[
process.env.NODE_ENV,
]}`
);
clearFolder(buildDir);
copyFolderRecursiveSync("addon", buildDir);
copyFileSync("update-template.json", "update.json");
copyFileSync("update-template.rdf", "update.rdf");
await esbuild
.build({
entryPoints: ["src/index.ts"],
define: {
__env__: `"${process.env.NODE_ENV}"`,
},
bundle: true,
outfile: path.join(buildDir, "addon/chrome/content/scripts/index.js"),
// Don't turn minify on
// 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,
config.releasepage,
config.updaterdf,
config.addonName,
config.addonID,
config.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();

View File

@ -1,243 +0,0 @@
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);
});
}

View File

@ -1,75 +0,0 @@
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);
}
})()`;

View File

@ -1,87 +0,0 @@
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);
});

25
scripts/start.js Normal file
View File

@ -0,0 +1,25 @@
const { execSync } = require("child_process");
const { exit } = require("process");
const { exec } = require("./zotero-cmd.json");
// Run node start.js -h for help
const args = require("minimist")(process.argv.slice(2));
if (args.help || args.h) {
console.log("Start Zotero Args:");
console.log(
"--zotero(-z): Zotero exec key in zotero-cmd.json. Default the first one."
);
console.log("--profile(-p): Zotero profile name.");
exit(0);
}
const zoteroPath = exec[args.zotero || args.z || Object.keys(exec)[0]];
const profile = args.profile || args.p;
const startZotero = `${zoteroPath} --debugger --purgecaches ${
profile ? `-p ${profile}` : ""
}`;
execSync(startZotero);
exit(0);

View File

@ -1,119 +0,0 @@
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();
});
}

10
scripts/stop.js Normal file
View File

@ -0,0 +1,10 @@
const { execSync } = require("child_process");
const { killZoteroWindows, killZoteroUnix } = require("./zotero-cmd.json");
try {
if (process.platform === "win32") {
execSync(killZoteroWindows);
} else {
execSync(killZoteroUnix);
}
} catch (e) {}

View File

@ -1,26 +0,0 @@
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);
}
}

View File

@ -1,17 +0,0 @@
{
"addons": {
"__addonID__": {
"updates": [
{
"version": "__buildVersion__",
"update_link": "__updateLink__",
"applications": {
"zotero": {
"strict_min_version": "6.999"
}
}
}
]
}
}
}

View File

@ -1,129 +0,0 @@
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);
});
}

View File

@ -0,0 +1,9 @@
{
"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 zotero)",
"exec": {
"6": "/path/to/zotero6.exe",
"7": "/path/to/zotero7.exe"
}
}

View File

@ -1,20 +0,0 @@
{
"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": ""
}
}

View File

@ -1,35 +1,72 @@
import { DialogHelper } from "zotero-plugin-toolkit/dist/helpers/dialog";
import ZoteroToolkit from "zotero-plugin-toolkit/dist/index";
import { ColumnOptions } from "zotero-plugin-toolkit/dist/helpers/virtualizedTable";
import hooks from "./hooks";
import { createZToolkit } from "./utils/ztoolkit";
class Addon {
public data: {
alive: boolean;
// Env type, see build.js
env: "development" | "production";
ztoolkit: ZToolkit;
// ztoolkit: MyToolkit;
ztoolkit: ZoteroToolkit;
locale?: {
current: any;
stringBundle: any;
};
prefs?: {
window: Window;
columns: Array<ColumnOptions>;
rows: Array<{ [dataKey: string]: string }>;
};
dialog?: DialogHelper;
};
// Lifecycle hooks
public hooks: typeof hooks;
// APIs
public api: object;
public api: {};
constructor() {
this.data = {
alive: true,
env: __env__,
ztoolkit: createZToolkit(),
// ztoolkit: new MyToolkit(),
ztoolkit: new ZoteroToolkit(),
};
this.hooks = hooks;
this.api = {};
}
}
/**
* Alternatively, import toolkit modules you use to minify the plugin size.
*
* Steps to replace the default `ztoolkit: ZoteroToolkit` with your `ztoolkit: MyToolkit`:
*
* 1. Uncomment this file's line 30: `ztoolkit: new MyToolkit(),`
* and comment line 31: `ztoolkit: new ZoteroToolkit(),`.
* 2. Uncomment this file's line 10: `ztoolkit: MyToolkit;` in this file
* and comment line 11: `ztoolkit: ZoteroToolkit;`.
* 3. Uncomment `./typing/global.d.ts` line 12: `declare const ztoolkit: import("../src/addon").MyToolkit;`
* and comment line 13: `declare const ztoolkit: import("zotero-plugin-toolkit").ZoteroToolkit;`.
*
* You can now add the modules under the `MyToolkit` class.
*/
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";
export class MyToolkit extends BasicTool {
UI: UITool;
PreferencePane: PreferencePaneManager;
constructor() {
super();
this.UI = new UITool(this);
this.PreferencePane = new PreferencePaneManager(this);
}
unregisterAll() {
unregister(this);
}
}
export default Addon;

View File

@ -1,10 +1,12 @@
import { RegisterFactory, UIFactory } from "./modules/Common";
import {
BasicExampleFactory,
HelperExampleFactory,
KeyExampleFactory,
UIExampleFactory,
} from "./modules/examples";
import { config } from "../package.json";
import { getString, initLocale } from "./utils/locale";
import { getString, initLocale } from "./modules/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([
@ -12,51 +14,69 @@ async function onStartup() {
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`,
ztoolkit.ProgressWindow.setIconURI(
"default",
`chrome://${config.addonRef}/content/icons/favicon.png`
);
UIFactory.registerRightClickMenuItem();
const popupWin = new ztoolkit.ProgressWindow(config.addonName, {
closeOnClick: true,
closeTime: -1,
})
.createLine({
text: getString("startup.begin"),
type: "default",
progress: 0,
})
.show();
UIFactory.registerRightClickCollectionMenuItem();
BasicExampleFactory.registerPrefs();
UIFactory.registerTLDRItemBoxRow();
BasicExampleFactory.registerNotifier();
onLoad();
}
KeyExampleFactory.registerShortcuts();
async function onMainWindowUnload(win: Window): Promise<void> {
ztoolkit.unregisterAll();
addon.data.dialog?.window?.close();
await Zotero.Promise.delay(1000);
popupWin.changeLine({
progress: 30,
text: `[30%] ${getString("startup.begin")}`,
});
UIExampleFactory.registerStyleSheet();
UIExampleFactory.registerRightClickMenuItem();
UIExampleFactory.registerRightClickMenuPopup();
UIExampleFactory.registerWindowMenuWithSeprator();
await UIExampleFactory.registerExtraColumn();
await UIExampleFactory.registerExtraColumnWithCustomCell();
await UIExampleFactory.registerCustomCellRenderer();
UIExampleFactory.registerLibraryTabPanel();
await UIExampleFactory.registerReaderTabPanel();
await Zotero.Promise.delay(1000);
popupWin.changeLine({
progress: 100,
text: `[100%] ${getString("startup.finish")}`,
});
popupWin.startCloseTimer(5000);
addon.hooks.onDialogEvents("dialogExample");
}
function onShutdown(): void {
ztoolkit.unregisterAll();
addon.data.dialog?.window?.close();
// Remove addon object
addon.data.alive = false;
delete Zotero[config.addonInstance];
delete Zotero.AddonTemplate;
}
/**
@ -66,14 +86,19 @@ function onShutdown(): void {
async function onNotify(
event: string,
type: string,
ids: Array<string | number>,
extraData: { [key: string]: any },
ids: Array<string>,
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);
// You can add your code to the corresponding notify type
ztoolkit.log("notify", event, type, ids, extraData);
if (
event == "select" &&
type == "tab" &&
extraData[ids[0]].type == "reader"
) {
BasicExampleFactory.exampleNotifierCallback();
} else {
return;
}
}
@ -93,112 +118,53 @@ async function onPrefsEvent(type: string, data: { [key: string]: any }) {
}
}
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);
}
function onShortcuts(type: string) {
switch (type) {
case "larger":
KeyExampleFactory.exampleShortcutLargerCallback();
break;
case "smaller":
KeyExampleFactory.exampleShortcutSmallerCallback();
break;
case "confliction":
KeyExampleFactory.exampleShortcutConflictionCallback();
break;
default:
break;
}
(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;
function onDialogEvents(type: string) {
switch (type) {
case "dialogExample":
HelperExampleFactory.dialogExample();
break;
case "clipboardExample":
HelperExampleFactory.clipboardExample();
break;
case "filePickerExample":
HelperExampleFactory.filePickerExample();
break;
case "progressWindowExample":
HelperExampleFactory.progressWindowExample();
break;
case "vtableExample":
HelperExampleFactory.vtableExample();
break;
default:
break;
}
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.
// Otherwise the code would be hard to read and maintian.
export default {
onStartup,
onShutdown,
onMainWindowLoad,
onMainWindowUnload,
onNotify,
onPrefsEvent,
onUpdateItems,
onShortcuts,
onDialogEvents,
};

View File

@ -4,24 +4,20 @@ import { config } from "../package.json";
const basicTool = new BasicTool();
if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
defineGlobal("window");
defineGlobal("document");
defineGlobal("ZoteroPane");
defineGlobal("Zotero_Tabs");
if (!basicTool.getGlobal("Zotero").AddonTemplate) {
// Set global variables
_globalThis.Zotero = basicTool.getGlobal("Zotero");
_globalThis.ZoteroPane = basicTool.getGlobal("ZoteroPane");
_globalThis.Zotero_Tabs = basicTool.getGlobal("Zotero_Tabs");
_globalThis.window = basicTool.getGlobal("window");
_globalThis.document = basicTool.getGlobal("document");
_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);
},
});
_globalThis.ztoolkit = addon.data.ztoolkit;
ztoolkit.basicOptions.log.prefix = `[${config.addonName}]`;
ztoolkit.basicOptions.log.disableConsole = addon.data.env === "production";
ztoolkit.UI.basicOptions.ui.enableElementJSONLog =
addon.data.env === "development";
Zotero.AddonTemplate = addon;
// Trigger addon hook for initialization
addon.hooks.onStartup();
}

View File

@ -1,117 +0,0 @@
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;
},
});
}
}

View File

@ -1,124 +0,0 @@
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",
);

697
src/modules/examples.ts Normal file
View File

@ -0,0 +1,697 @@
import { config } from "../../package.json";
import { getString } from "./locale";
function example(
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args: any) {
try {
ztoolkit.log(`Calling example ${target.name}.${String(propertyKey)}`);
return original.apply(this, args);
} catch (e) {
ztoolkit.log(`Error in example ${target.name}.${String(propertyKey)}`, e);
throw e;
}
};
return descriptor;
}
export class BasicExampleFactory {
@example
static registerNotifier() {
const callback = {
notify: async (
event: string,
type: string,
ids: Array<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, [
"tab",
"item",
"file",
]);
// Unregister callback when the window closes (important to avoid a memory leak)
window.addEventListener(
"unload",
(e: Event) => {
this.unregisterNotifier(notifierID);
},
false
);
}
@example
static exampleNotifierCallback() {
new ztoolkit.ProgressWindow(config.addonName)
.createLine({
text: "Open Tab Detected!",
type: "success",
progress: 100,
})
.show();
}
@example
private static unregisterNotifier(notifierID: string) {
Zotero.Notifier.unregisterObserver(notifierID);
}
@example
static registerPrefs() {
const prefOptions = {
pluginID: config.addonID,
src: rootURI + "chrome/content/preferences.xhtml",
label: getString("prefs.title"),
image: `chrome://${config.addonRef}/content/icons/favicon.png`,
extraDTD: [`chrome://${config.addonRef}/locale/overlay.dtd`],
defaultXUL: true,
};
ztoolkit.PreferencePane.register(prefOptions);
}
}
export class KeyExampleFactory {
@example
static registerShortcuts() {
const keysetId = `${config.addonRef}-keyset`;
const cmdsetId = `${config.addonRef}-cmdset`;
const cmdSmallerId = `${config.addonRef}-cmd-smaller`;
// Register an event key for Alt+L
ztoolkit.Shortcut.register("event", {
id: `${config.addonRef}-key-larger`,
key: "L",
modifiers: "alt",
callback: (keyOptions) => {
addon.hooks.onShortcuts("larger");
},
});
// Register an element key using <key> for Alt+S
ztoolkit.Shortcut.register("element", {
id: `${config.addonRef}-key-smaller`,
key: "S",
modifiers: "alt",
xulData: {
document,
command: cmdSmallerId,
_parentId: keysetId,
_commandOptions: {
id: cmdSmallerId,
document,
_parentId: cmdsetId,
oncommand: "Zotero.AddonTemplate.hooks.onShortcuts('smaller')",
},
},
});
// Here we register an conflict key for Alt+S
// just to show how the confliction check works.
// This is something you should avoid in your plugin.
ztoolkit.Shortcut.register("event", {
id: `${config.addonRef}-key-smaller-conflict`,
key: "S",
modifiers: "alt",
callback: (keyOptions) => {
ztoolkit.getGlobal("alert")("Smaller! This is a conflict key.");
},
});
// Register an event key to check confliction
ztoolkit.Shortcut.register("event", {
id: `${config.addonRef}-key-check-conflict`,
key: "C",
modifiers: "alt",
callback: (keyOptions) => {
addon.hooks.onShortcuts("confliction");
},
});
new ztoolkit.ProgressWindow(config.addonName)
.createLine({
text: "Example Shortcuts: Alt+L/S/C",
type: "success",
})
.show();
}
@example
static exampleShortcutLargerCallback() {
new ztoolkit.ProgressWindow(config.addonName)
.createLine({
text: "Larger!",
type: "default",
})
.show();
}
@example
static exampleShortcutSmallerCallback() {
new ztoolkit.ProgressWindow(config.addonName)
.createLine({
text: "Smaller!",
type: "default",
})
.show();
}
@example
static exampleShortcutConflictionCallback() {
const conflictionGroups = ztoolkit.Shortcut.checkAllKeyConfliction();
new ztoolkit.ProgressWindow("Check Key Confliction")
.createLine({
text: `${conflictionGroups.length} groups of confliction keys found. Details are in the debug output/console.`,
})
.show(-1);
ztoolkit.log(
"Conflictions:",
conflictionGroups,
"All keys:",
ztoolkit.Shortcut.getAll()
);
}
}
export class UIExampleFactory {
@example
static registerStyleSheet() {
const styles = ztoolkit.UI.createElement(document, "link", {
properties: {
type: "text/css",
rel: "stylesheet",
href: `chrome://${config.addonRef}/content/zoteroPane.css`,
},
});
document.documentElement.appendChild(styles);
document
.getElementById("zotero-item-pane-content")
?.classList.add("makeItRed");
}
@example
static registerRightClickMenuItem() {
const menuIcon = `chrome://${config.addonRef}/content/icons/favicon@0.5x.png`;
// item menuitem with icon
ztoolkit.Menu.register("item", {
tag: "menuitem",
id: "zotero-itemmenu-addontemplate-test",
label: getString("menuitem.label"),
commandListener: (ev) => addon.hooks.onDialogEvents("dialogExample"),
icon: menuIcon,
});
}
@example
static registerRightClickMenuPopup() {
ztoolkit.Menu.register(
"item",
{
tag: "menu",
label: getString("menupopup.label"),
children: [
{
tag: "menuitem",
label: getString("menuitem.submenulabel"),
oncommand: "alert('Hello World! Sub Menuitem.')",
},
],
},
"before",
document.querySelector(
"#zotero-itemmenu-addontemplate-test"
) as XUL.MenuItem
);
}
@example
static registerWindowMenuWithSeprator() {
ztoolkit.Menu.register("menuFile", {
tag: "menuseparator",
});
// menu->File menuitem
ztoolkit.Menu.register("menuFile", {
tag: "menuitem",
label: getString("menuitem.filemenulabel"),
oncommand: "alert('Hello World! File Menuitem.')",
});
}
@example
static async registerExtraColumn() {
await ztoolkit.ItemTree.register(
"test1",
"text column",
(
field: string,
unformatted: boolean,
includeBaseMapped: boolean,
item: Zotero.Item
) => {
return field + String(item.id);
},
{
iconPath: "chrome://zotero/skin/cross.png",
}
);
}
@example
static async registerExtraColumnWithCustomCell() {
await ztoolkit.ItemTree.register(
"test2",
"custom column",
(
field: string,
unformatted: boolean,
includeBaseMapped: boolean,
item: Zotero.Item
) => {
return String(item.id);
},
{
renderCellHook(index, data, column) {
const span = document.createElementNS(
"http://www.w3.org/1999/xhtml",
"span"
);
span.style.background = "#0dd068";
span.innerText = "⭐" + data;
return span;
},
}
);
}
@example
static async registerCustomCellRenderer() {
await ztoolkit.ItemTree.addRenderCellHook(
"title",
(index: number, data: string, column: any, original: Function) => {
const span = original(index, data, column) as HTMLSpanElement;
span.style.background = "rgb(30, 30, 30)";
span.style.color = "rgb(156, 220, 240)";
return span;
}
);
// @ts-ignore
// This is a private method. Make it public in toolkit.
await ztoolkit.ItemTree.refresh();
}
@example
static registerLibraryTabPanel() {
const tabId = ztoolkit.LibraryTabPanel.register(
getString("tabpanel.lib.tab.label"),
(panel: XUL.Element, win: Window) => {
const elem = ztoolkit.UI.createElement(win.document, "vbox", {
children: [
{
tag: "h2",
properties: {
innerText: "Hello World!",
},
},
{
tag: "div",
properties: {
innerText: "This is a library tab.",
},
},
{
tag: "button",
namespace: "html",
properties: {
innerText: "Unregister",
},
listeners: [
{
type: "click",
listener: () => {
ztoolkit.LibraryTabPanel.unregister(tabId);
},
},
],
},
],
});
panel.append(elem);
},
{
targetIndex: 1,
}
);
}
@example
static async registerReaderTabPanel() {
const tabId = await ztoolkit.ReaderTabPanel.register(
getString("tabpanel.reader.tab.label"),
(
panel: XUL.TabPanel | undefined,
deck: XUL.Deck,
win: Window,
reader: _ZoteroTypes.ReaderInstance
) => {
if (!panel) {
ztoolkit.log(
"This reader do not have right-side bar. Adding reader tab skipped."
);
return;
}
ztoolkit.log(reader);
const elem = ztoolkit.UI.createElement(win.document, "vbox", {
id: `${config.addonRef}-${reader._instanceID}-extra-reader-tab-div`,
// This is important! Don't create content for multiple times
// ignoreIfExists: true,
removeIfExists: true,
children: [
{
tag: "h2",
properties: {
innerText: "Hello World!",
},
},
{
tag: "div",
properties: {
innerText: "This is a reader tab.",
},
},
{
tag: "div",
properties: {
innerText: `Reader: ${reader._title.slice(0, 20)}`,
},
},
{
tag: "div",
properties: {
innerText: `itemID: ${reader.itemID}.`,
},
},
{
tag: "button",
namespace: "html",
properties: {
innerText: "Unregister",
},
listeners: [
{
type: "click",
listener: () => {
ztoolkit.ReaderTabPanel.unregister(tabId);
},
},
],
},
],
});
panel.append(elem);
},
{
targetIndex: 1,
}
);
}
}
export class HelperExampleFactory {
@example
static async dialogExample() {
const dialogData: { [key: string | number]: any } = {
inputValue: "test",
checkboxValue: true,
loadCallback: () => {
ztoolkit.log(dialogData, "Dialog Opened!");
},
unloadCallback: () => {
ztoolkit.log(dialogData, "Dialog closed!");
},
};
const dialogHelper = new ztoolkit.Dialog(10, 2)
.addCell(0, 0, {
tag: "h1",
properties: { innerHTML: "Helper Examples" },
})
.addCell(1, 0, {
tag: "h2",
properties: { innerHTML: "Dialog Data Binding" },
})
.addCell(2, 0, {
tag: "p",
properties: {
innerHTML:
"Elements with attribute 'data-bind' are binded to the prop under 'dialogData' with the same name.",
},
styles: {
width: "200px",
},
})
.addCell(3, 0, {
tag: "label",
namespace: "html",
attributes: {
for: "dialog-checkbox",
},
properties: { innerHTML: "bind:checkbox" },
})
.addCell(
3,
1,
{
tag: "input",
namespace: "html",
id: "dialog-checkbox",
attributes: {
"data-bind": "checkboxValue",
"data-prop": "checked",
type: "checkbox",
},
properties: { label: "Cell 1,0" },
},
false
)
.addCell(4, 0, {
tag: "label",
namespace: "html",
attributes: {
for: "dialog-input",
},
properties: { innerHTML: "bind:input" },
})
.addCell(
4,
1,
{
tag: "input",
namespace: "html",
id: "dialog-input",
attributes: {
"data-bind": "inputValue",
"data-prop": "value",
type: "text",
},
},
false
)
.addCell(5, 0, {
tag: "h2",
properties: { innerHTML: "Toolkit Helper Examples" },
})
.addCell(
6,
0,
{
tag: "button",
namespace: "html",
attributes: {
type: "button",
},
listeners: [
{
type: "click",
listener: (e: Event) => {
addon.hooks.onDialogEvents("clipboardExample");
},
},
],
children: [
{
tag: "div",
styles: {
padding: "2.5px 15px",
},
properties: {
innerHTML: "example:clipboard",
},
},
],
},
false
)
.addCell(
7,
0,
{
tag: "button",
namespace: "html",
attributes: {
type: "button",
},
listeners: [
{
type: "click",
listener: (e: Event) => {
addon.hooks.onDialogEvents("filePickerExample");
},
},
],
children: [
{
tag: "div",
styles: {
padding: "2.5px 15px",
},
properties: {
innerHTML: "example:filepicker",
},
},
],
},
false
)
.addCell(
8,
0,
{
tag: "button",
namespace: "html",
attributes: {
type: "button",
},
listeners: [
{
type: "click",
listener: (e: Event) => {
addon.hooks.onDialogEvents("progressWindowExample");
},
},
],
children: [
{
tag: "div",
styles: {
padding: "2.5px 15px",
},
properties: {
innerHTML: "example:progressWindow",
},
},
],
},
false
)
.addCell(
9,
0,
{
tag: "button",
namespace: "html",
attributes: {
type: "button",
},
listeners: [
{
type: "click",
listener: (e: Event) => {
addon.hooks.onDialogEvents("vtableExample");
},
},
],
children: [
{
tag: "div",
styles: {
padding: "2.5px 15px",
},
properties: {
innerHTML: "example:virtualized-table",
},
},
],
},
false
)
.addButton("Confirm", "confirm")
.addButton("Cancel", "cancel")
.addButton("Help", "help", {
noClose: true,
callback: (e) => {
dialogHelper.window?.alert(
"Help Clicked! Dialog will not be closed."
);
},
})
.setDialogData(dialogData)
.open("Dialog Example");
await dialogData.unloadLock.promise;
ztoolkit.getGlobal("alert")(
`Close dialog with ${dialogData._lastButtonId}.\nCheckbox: ${dialogData.checkboxValue}\nInput: ${dialogData.inputValue}.`
);
ztoolkit.log(dialogData);
}
@example
static clipboardExample() {
new ztoolkit.Clibpoard()
.addText(
"![Plugin Template](https://github.com/windingwind/zotero-plugin-template)",
"text/unicode"
)
.addText(
'<a href="https://github.com/windingwind/zotero-plugin-template">Plugin Template</a>',
"text/html"
)
.copy();
ztoolkit.getGlobal("alert")("Copied!");
}
@example
static async filePickerExample() {
const path = await new ztoolkit.FilePicker(
"Import File",
"open",
[
["PNG File(*.png)", "*.png"],
["Any", "*.*"],
],
"image.png"
).open();
ztoolkit.getGlobal("alert")(`Selected ${path}`);
}
@example
static progressWindowExample() {
new ztoolkit.ProgressWindow(config.addonName)
.createLine({
text: "ProgressWindow Example!",
type: "success",
progress: 100,
})
.show();
}
@example
static vtableExample() {
ztoolkit.getGlobal("alert")("See src/modules/preferenceScript.ts");
}
}

13
src/modules/locale.ts Normal file
View File

@ -0,0 +1,13 @@
import { config } from "../../package.json";
export function initLocale() {
addon.data.locale = {
stringBundle: Components.classes["@mozilla.org/intl/stringbundle;1"]
.getService(Components.interfaces.nsIStringBundleService)
.createBundle(`chrome://${config.addonRef}/locale/addon.properties`),
};
}
export function getString(localString: string): string {
return addon.data.locale?.stringBundle.GetStringFromName(localString);
}

View File

@ -1,17 +1,134 @@
import { config } from "../../package.json";
import { getString } from "../utils/locale";
import { getString } from "./locale";
export async function registerPrefsScripts(_window: Window) {
export 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,
columns: [
{
dataKey: "title",
label: "prefs.table.title",
fixedWidth: true,
width: 100,
},
{
dataKey: "detail",
label: "prefs.table.detail",
},
],
rows: [
{
title: "Orange",
detail: "It's juicy",
},
{
title: "Banana",
detail: "It's sweet",
},
{
title: "Apple",
detail: "I mean the fruit APPLE",
},
],
};
} else {
addon.data.prefs.window = _window;
}
updatePrefsUI();
bindPrefEvents();
}
function bindPrefEvents() {}
async function updatePrefsUI() {
// You can initialize some UI elements on prefs window
// with addon.data.prefs.window.document
// Or bind some events to the elements
const renderLock = ztoolkit.getGlobal("Zotero").Promise.defer();
const tableHelper = new ztoolkit.VirtualizedTabel(addon.data.prefs?.window!)
.setContainerId(`${config.addonRef}-table-container`)
.setProp({
id: `${config.addonRef}-prefs-table`,
// Do not use setLocale, as it modifies the Zotero.Intl.strings
// Set locales directly to columns
columns: addon.data.prefs?.columns.map((column) =>
Object.assign(column, {
label: getString(column.label) || column.label,
})
),
showHeader: true,
multiSelect: true,
staticColumns: true,
disableFontSizeScaling: true,
})
.setProp("getRowCount", () => addon.data.prefs?.rows.length || 0)
.setProp(
"getRowData",
(index) =>
addon.data.prefs?.rows[index] || {
title: "no data",
detail: "no data",
}
)
// Show a progress window when selection changes
.setProp("onSelectionChange", (selection) => {
new ztoolkit.ProgressWindow(config.addonName)
.createLine({
text: `Selected line: ${addon.data.prefs?.rows
.filter((v, i) => selection.isSelected(i))
.map((row) => row.title)
.join(",")}`,
progress: 100,
})
.show();
})
// When pressing delete, delete selected line and refresh table.
// Returning false to prevent default event.
.setProp("onKeyDown", (event: KeyboardEvent) => {
if (event.key == "Delete" || (Zotero.isMac && event.key == "Backspace")) {
addon.data.prefs!.rows =
addon.data.prefs?.rows.filter(
(v, i) => !tableHelper.treeInstance.selection.isSelected(i)
) || [];
tableHelper.render();
return false;
}
return true;
})
// For find-as-you-type
.setProp(
"getRowString",
(index) => addon.data.prefs?.rows[index].title || ""
)
// Render the table.
.render(-1, () => {
renderLock.resolve();
});
await renderLock.promise;
ztoolkit.log("Preference table rendered!");
}
function bindPrefEvents() {
addon.data
.prefs!.window.document.querySelector(
`#zotero-prefpane-${config.addonRef}-enable`
)
?.addEventListener("command", (e) => {
ztoolkit.log(e);
addon.data.prefs!.window.alert(
`Successfully changed to ${(e.target as XUL.Checkbox).checked}!`
);
});
addon.data
.prefs!!.window.document.querySelector(
`#zotero-prefpane-${config.addonRef}-input`
)
?.addEventListener("change", (e) => {
ztoolkit.log(e);
addon.data.prefs!.window.alert(
`Successfully changed to ${(e.target as HTMLInputElement).value}!`
);
});
}

View File

@ -1,176 +0,0 @@
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];
// }
}

View File

@ -1,79 +0,0 @@
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;
}
}

View File

@ -1,29 +0,0 @@
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);
}

View File

@ -1,49 +0,0 @@
/**
* 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);
});
}

View File

@ -1,10 +0,0 @@
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;
}

View File

@ -1,49 +0,0 @@
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);
}
}

View File

@ -4,9 +4,15 @@
"module": "commonjs",
"target": "ES2016",
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"strict": true
},
"include": ["src", "typings", "node_modules/zotero-types"],
"exclude": ["build", "addon"],
}
"include": [
"src",
"typing",
"node_modules/zotero-types"
],
"exclude": [
"builds",
"addon"
]
}

View File

@ -1,24 +1,19 @@
declare const _globalThis: {
[key: string]: any;
Zotero: _ZoteroTypes.Zotero;
Zotero: _ZoteroConstructable;
ZoteroPane: _ZoteroTypes.ZoteroPane;
Zotero_Tabs: typeof Zotero_Tabs;
window: Window;
document: Document;
ztoolkit: ZToolkit;
ztoolkit: typeof ztoolkit;
addon: typeof addon;
};
declare type ZToolkit = ReturnType<
typeof import("../src/utils/ztoolkit").createZToolkit
>;
declare const ztoolkit: ZToolkit;
// declare const ztoolkit: import("../src/addon").MyToolkit;
declare const ztoolkit: import("zotero-plugin-toolkit").ZoteroToolkit;
declare const rootURI: string;
declare const addon: import("../src/addon").default;
declare const __env__: "production" | "development";
declare class Localization {}

26
update-template.json Normal file
View File

@ -0,0 +1,26 @@
{
"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"
}
}
}
]
}
}
}

30
update-template.rdf Normal file
View File

@ -0,0 +1,30 @@
<?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>

View File

@ -1,10 +1,19 @@
{
"addons": {
"zoterotldr@syt.com": {
"addontemplate@euclpts.com": {
"updates": [
{
"version": "1.0.7",
"update_link": "undefined/latest/download/zotero-tldr.xpi",
"version": "0.1.3",
"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.1.3",
"update_link": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi",
"applications": {
"zotero": {
"strict_min_version": "6.999"

30
update.rdf Normal file
View File

@ -0,0 +1,30 @@
<?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.1.3</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>