Compare commits
No commits in common. "main" and "v1.1.0" have entirely different histories.
|
|
@ -3,41 +3,31 @@ name: Release
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- V**
|
||||
- v**
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
release-it:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GitHub_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
# cache: npm
|
||||
|
||||
- name: Install deps
|
||||
run: npm install
|
||||
|
||||
- name: Release to GitHub
|
||||
# if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v')
|
||||
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._
|
||||
npm run release -- --no-increment --no-git --github.release --ci --verbose
|
||||
|
|
|
|||
|
|
@ -4,5 +4,4 @@ node_modules
|
|||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
zotero-cmd.json
|
||||
.DS_Store
|
||||
zotero-cmd.json
|
||||
|
|
@ -18,13 +18,13 @@
|
|||
"\tremoveIfExists: ${13:true},",
|
||||
"\tcustomCheck: (doc: Document, options: ElementOptions) => ${14:true},",
|
||||
"\tchildren: [$15]",
|
||||
"}, ${16:container});",
|
||||
],
|
||||
"}, ${16:container});"
|
||||
]
|
||||
},
|
||||
"appendElement - minimum": {
|
||||
"scope": "javascript,typescript",
|
||||
"prefix": "appendElement",
|
||||
"body": "appendElement({ tag: '$1' }, $2);",
|
||||
"body": "appendElement({ tag: '$1' }, $2);"
|
||||
},
|
||||
"register Notifier": {
|
||||
"scope": "javascript,typescript",
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
"\t) => {",
|
||||
"\t\t$0",
|
||||
"\t}",
|
||||
"});",
|
||||
],
|
||||
},
|
||||
"});"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
420
README.md
420
README.md
|
|
@ -1,17 +1,417 @@
|
|||
# Zotero TL;DR
|
||||
# Zotero Plugin Template
|
||||
|
||||
[](https://www.zotero.org)
|
||||
[](https://www.zotero.org)
|
||||
[](https://github.com/windingwind/zotero-plugin-template)
|
||||
|
||||
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/).
|
||||
|
||||
## Install
|
||||
[English](README.md) | [简体中文](doc/README-zhCN.md)
|
||||
|
||||
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)
|
||||
[📖 Plugin Development Documentation](https://zotero.yuque.com/books/share/8d230829-6004-4934-b4c6-685a7001bfa0/vec88d) (Chinese, outdated)
|
||||
|
||||
## Usage
|
||||
[📖 Plugin Development Documentation for Zotero 7](https://www.zotero.org/support/dev/zotero_7_for_developers)
|
||||
|
||||
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 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)
|
||||
|
||||
[ℹ️ Zotero Type Definitions](https://github.com/windingwind/zotero-types)
|
||||
|
||||
[📜 Zotero Source Code](https://github.com/zotero/zotero)
|
||||
|
||||
[📌 Zotero Plugin Template](https://github.com/windingwind/zotero-plugin-template) (This repo)
|
||||
|
||||
> [!tip]
|
||||
> 👁 Watch this repo so that you can be notified whenever there are fixes & updates.
|
||||
|
||||
## Plugins built with this template
|
||||
|
||||
[](https://github.com/windingwind/zotero-better-notes)
|
||||
[](https://github.com/windingwind/zotero-pdf-preview)
|
||||
[](https://github.com/windingwind/zotero-pdf-translate)
|
||||
[](https://github.com/windingwind/zotero-tag)
|
||||
[](https://github.com/iShareStuff/ZoteroTheme)
|
||||
[](https://github.com/MuiseDestiny/zotero-reference)
|
||||
[](https://github.com/MuiseDestiny/zotero-citation)
|
||||
[](https://github.com/MuiseDestiny/ZoteroStyle)
|
||||
[](https://github.com/volatile-static/Chartero)
|
||||
[](https://github.com/l0o0/tara)
|
||||
[](https://github.com/redleafnew/delitemwithatt)
|
||||
[](https://github.com/redleafnew/zotero-updateifsE)
|
||||
[](https://github.com/northword/zotero-format-metadata)
|
||||
[](https://github.com/inciteful-xyz/inciteful-zotero-plugin)
|
||||
[](https://github.com/MuiseDestiny/zotero-gpt)
|
||||
[](https://github.com/zoushucai/zotero-journalabbr)
|
||||
[](https://github.com/MuiseDestiny/zotero-figure)
|
||||
[](https://github.com/l0o0/jasminum)
|
||||
[](https://github.com/lifan0127/ai-research-assistant)
|
||||
|
||||
If you are using this repo, I recommended that you put the following badge on your README:
|
||||
|
||||
[](https://github.com/windingwind/zotero-plugin-template)
|
||||
|
||||
```md
|
||||
[](https://github.com/windingwind/zotero-plugin-template)
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Event-driven, functional programming, under extensive skeleton;
|
||||
- Simple and user-friendly, works out-of-the-box.
|
||||
- ⭐ [New!] Auto hot reload! Whenever the source code is modified, automatically compile and reload. [See here→](#auto-hot-reload)
|
||||
- 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 develop/build/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));
|
||||
- Prettier and ES Lint integration.
|
||||
|
||||
> [!warning]
|
||||
> The localization system is upgraded (dtd is deprecated and we do not use .properties anymore). Only supports Zotero 7.0.0-beta.12 or higher now. If you want to support Zotero 6, you may need to use `dtd`, `properties`, and `ftl` at the same time. See the staled branch `zotero6-bootstrap`.
|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
- registerStyleSheet(the official make-it-red example)
|
||||
- registerRightClickMenuItem
|
||||
- registerRightClickMenuPopup
|
||||
- registerWindowMenuWithSeprator
|
||||
- registerExtraColumn
|
||||
- registerExtraColumnWithCustomCell
|
||||
- registerCustomItemBoxRow
|
||||
- registerLibraryTabPanel
|
||||
- registerReaderTabPanel
|
||||
|
||||
### Preference Pane Examples
|
||||
|
||||

|
||||
|
||||
- Preferences bindings
|
||||
- UI Events
|
||||
- Table
|
||||
- Locale
|
||||
|
||||
See [`src/modules/preferenceScript.ts`](./src/modules/preferenceScript.ts)
|
||||
|
||||
### HelperExamples
|
||||
|
||||

|
||||
|
||||
- dialogExample
|
||||
- clipboardExample
|
||||
- filePickerExample
|
||||
- progressWindowExample
|
||||
- vtableExample(See Preference Pane Examples)
|
||||
|
||||
### PromptExamples
|
||||
|
||||
An Obsidian-style prompt(popup command input) module. It accepts text command to run callback, with optional display in the popup.
|
||||
|
||||
Activate with `Shift+P`.
|
||||
|
||||

|
||||
|
||||
- registerAlertPromptExample
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
### 0 Requirement
|
||||
|
||||
1. Install a beta version of Zotero: <https://www.zotero.org/support/beta_builds>
|
||||
2. Install [Node.js](https://nodejs.org/en/) and Git
|
||||
|
||||
> [!note]
|
||||
> This guide assumes that you have an initial understanding of the basic structure and workings of the Zotero plugin. If you don't, please refer to the [documentation](https://www.zotero.org/support/dev/zotero_7_for_developers) and official plugin examples [Make It Red](https://github.com/zotero/make-it-red) first.
|
||||
|
||||
### 1 Creat Your Repo
|
||||
|
||||
1. Click `Use this template`
|
||||
2. Git clone your new repo
|
||||
<details >
|
||||
<summary>💡 Start with GitHub Codespace</summary>
|
||||
|
||||
_GitHub CodeSpace_ enables you getting started without the need to download code/IDE/dependencies locally.
|
||||
|
||||
Replace the steps above and build you first plugin in 30 seconds!
|
||||
|
||||
- Goto top of the [homepage](https://github.com/windingwind/zotero-plugin-template), click the green button `Use this template`, click `Open in codespace`. You may need to login to your GitHub account.
|
||||
- Wait for codespace to load.
|
||||
|
||||
</details>
|
||||
|
||||
3. Enter the repo folder
|
||||
|
||||
### 2 Config Template Settings and Enviroment
|
||||
|
||||
1. Modify the settings in `./package.json`, including:
|
||||
|
||||
```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]
|
||||
> Be careful to set the addonID and addonRef to avoid conflict.
|
||||
|
||||
If you need to host your XPI packages outside of GitHub, remove `releasePage` and add `updateLink` with the value set to your XPI download URL.
|
||||
|
||||
2. 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.
|
||||
> Put the path of the profile into the `profilePath` in `zotero-cmd.json` to specify which profile to use.
|
||||
|
||||
```sh
|
||||
cp ./scripts/zotero-cmd-default.json ./scripts/zotero-cmd.json
|
||||
vim ./scripts/zotero-cmd.json
|
||||
```
|
||||
|
||||
3. Install dependencies with `npm install`
|
||||
|
||||
> If you are using `pnpm` as the package manager for your project, you need to add `public-hoist-pattern[]=*@types/bluebird*` to `.npmrc`, see <https://github.com/windingwind/zotero-types?tab=readme-ov-file#usage>.
|
||||
|
||||
### 3 Coding
|
||||
|
||||
Start development server with `npm start`, it will:
|
||||
|
||||
- Prebuild the plugin in development mode
|
||||
- Start Zotero with plugin loaded from `build/`
|
||||
- Open devtool
|
||||
- Watch `src/**` and `addon/**`.
|
||||
- If `src/**` changed, run esbuild and reload
|
||||
- If `addon/**` has changed, rebuild the plugin (in development mode) and reload
|
||||
|
||||
#### Auto Hot Reload
|
||||
|
||||
Tired of endless restarting? Forget about it!
|
||||
|
||||
1. Run `npm start`.
|
||||
2. Coding. (Yes, that's all)
|
||||
|
||||
When file changes are detected in `src` or `addon`, the plugin will be automatically compiled and reloaded.
|
||||
|
||||
<details style="text-indent: 2em">
|
||||
<summary>💡 Steps to add this feature to an existing plugin</summary>
|
||||
|
||||
1. Copy `scripts/**.mjs`
|
||||
2. Copy `server`, `build`, and `stop` commands in `package.json`
|
||||
3. Run `npm install --save-dev chokidar`
|
||||
4. Done.
|
||||
|
||||
</details>
|
||||
|
||||
#### 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>
|
||||
|
||||
### 4 Build
|
||||
|
||||
Run `npm run build` to build the plugin in production mode, and the xpi for installation and the built code is under `build` folder.
|
||||
|
||||
Steps in `scripts/build.mjs`:
|
||||
|
||||
- Create/empty `build/`.
|
||||
- Copy `addon/**` to `build/addon/**`
|
||||
- Replace placeholders: use `replace-in-file` to replace keywords and configurations defined in `package.json` in non-build files (`xhtml`, `json`, et al.).
|
||||
- Prepare locale files to [avaid conflict](https://www.zotero.org/support/dev/zotero_7_for_developers#avoiding_localization_conflicts)
|
||||
- Rename `**/*.flt` to `**/${addonRef}-*.flt`
|
||||
- Prefix each fluent message with `addonRef-`
|
||||
- Use Esbuild to build `.ts` source code to `.js`, build `src/index.ts` to `./build/addon/chrome/content/scripts`.
|
||||
- (Production mode only) Zip the `./build/addon` to `./build/*.xpi`
|
||||
- (Production mode only) Prepare `update.json` or `update-beta.json`
|
||||
|
||||
> [!note]
|
||||
>
|
||||
> **What's the difference between dev & prod?**
|
||||
>
|
||||
> - This environment variable is stored in `Zotero.${addonInstance}.data.env`. The outputs to console is disabled in prod mode.
|
||||
> - You can decide what users cannot see/use based on this variable.
|
||||
> - In production mode, the build script will pack the plugin and update the `update.json`
|
||||
|
||||
### 5 Release
|
||||
|
||||
To build and release, use
|
||||
|
||||
```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]
|
||||
> In this template, release-it is configured to locally bump the version, build, and push commits and git.tags, subsequently GitHub Action will rebuild the plugin and publish the XPI to GitHub Release.
|
||||
>
|
||||
> If you need to release a locally built XPI, set `release-it.github.release` to `true` in `package.json` and remove `.github/workflows/release.yml`. Besides that, you need to set the environment variable `GITHUB_TOKEN`, get it in <https://github.com/settings/tokens>
|
||||
|
||||
#### About Prerelease
|
||||
|
||||
The template defines `prerelease` as the beta version of the plugin, when you select a `prerelease` version in release-it (with `-` in the version number), the build script will create a new `update-beta.json` for prerelease use, which ensures that users of the regular version won't be able to update to the beta, only users who have manually downloaded and installed the beta will be able to update to the next beta automatically. When the next regular release is updated, both `update.json` and `update-beta.json` will be updated so that both regular and beta users can update to the new regular release.
|
||||
|
||||
> [!warning]
|
||||
> Strictly, distinguishing between Zotero 6 and Zotero 7 compatible plugin versions should be done by configuring `applications.zotero.strict_min_version` in `addons.__addonID__.updates[]` of `update.json` respectively, so that Zotero recognizes it properly, see <https://www.zotero.org/support/dev/zotero_7_for_developers#updaterdf_updatesjson>.
|
||||
|
||||
## Details
|
||||
|
||||
### About Hooks
|
||||
|
||||
> See also [`src/hooks.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/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/main/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;
|
||||
```
|
||||
|
||||
### 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 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 `.xhtml`/`.flt` 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/manifest.json`;
|
||||
- UI files: `./addon/chrome/content/*.xhtml`.
|
||||
- Locale files: `./addon/locale/**/*.flt`;
|
||||
- Preferences file: `./addon/prefs.js`;
|
||||
> Don't break the lines in the `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-default.json # example 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
|
||||
|
||||
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>
|
||||
|
|
|
|||
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 |
|
|
@ -0,0 +1,36 @@
|
|||
<linkset>
|
||||
<html:link rel="localization" href="__addonRef__-preferences.ftl" />
|
||||
</linkset>
|
||||
<vbox
|
||||
id="zotero-prefpane-__addonRef__"
|
||||
onload="Zotero.__addonInstance__.hooks.onPrefsEvent('load', {window})"
|
||||
>
|
||||
<groupbox>
|
||||
<label><html:h2 data-l10n-id="pref-title"></html:h2></label>
|
||||
<checkbox
|
||||
id="zotero-prefpane-__addonRef__-enable"
|
||||
preference="extensions.zotero.__addonRef__.enable"
|
||||
data-l10n-id="pref-enable"
|
||||
/>
|
||||
<hbox>
|
||||
<html:label
|
||||
for="zotero-prefpane-__addonRef__-input"
|
||||
data-l10n-id="pref-input"
|
||||
></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>
|
||||
<html:label
|
||||
data-l10n-id="pref-help"
|
||||
data-l10n-args='{"time": "__buildTime__","name": "__addonName__","version":"__buildVersion__"}'
|
||||
></html:label>
|
||||
</vbox>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.makeItRed {
|
||||
background-color: tomato;
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
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
|
||||
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
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
itemPaneSection-header =
|
||||
.label = TLDR
|
||||
itemPaneSection-sidenav =
|
||||
.tooltiptext = TLDR
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
pref-title = Addon Template Example
|
||||
pref-enable =
|
||||
.label = Enable
|
||||
pref-input = Input
|
||||
pref-help = { $name } Build { $version } { $time }
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
menuitem-updatetldrlabel = 更新TLDR
|
||||
menucollection-updatetldrlabel = 批量更新TLDR
|
||||
itembox-tldrlabel = TLDR
|
||||
tldr-unrelated = 未关联TLDR
|
||||
tldr-itemnotfound = 未搜索到此条目
|
||||
popWindow-succeed = 成功
|
||||
popWindow-failed = 失败
|
||||
popWindow-waiting = 等待
|
||||
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 = 阅读器标签
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
itemPaneSection-header =
|
||||
.label = TLDR
|
||||
itemPaneSection-sidenav =
|
||||
.tooltiptext = TLDR
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
pref-title = 插件模板设置示例
|
||||
pref-enable =
|
||||
.label = 开启
|
||||
pref-input = 输入
|
||||
pref-help = { $name } Build { $version } { $time }
|
||||
|
|
@ -1 +1,3 @@
|
|||
/* eslint-disable no-undef */
|
||||
pref("__prefsPrefix__.enable", true);
|
||||
pref("__prefsPrefix__.input", "This is input");
|
||||
|
|
|
|||
|
|
@ -7,22 +7,7 @@
|
|||
|
||||
[English](../README.md) | [简体中文](./README-zhCN.md)
|
||||
|
||||
📖 [插件开发文档](https://zotero.yuque.com/books/share/8d230829-6004-4934-b4c6-685a7001bfa0/vec88d) (中文版,已过时)
|
||||
|
||||
[📖 Zotero 7 插件开发文档](https://www.zotero.org/support/dev/zotero_7_for_developers)
|
||||
|
||||
🛠️ [Zotero 插件工具包](https://github.com/windingwind/zotero-plugin-toolkit) | [API 文档](https://github.com/windingwind/zotero-plugin-toolkit/blob/master/docs/zotero-plugin-toolkit.md)
|
||||
|
||||
ℹ️ [Zotero 类型定义](https://github.com/windingwind/zotero-types)
|
||||
|
||||
📜 [Zotero 源代码](https://github.com/zotero/zotero)
|
||||
|
||||
📌 [Zotero 插件模板](https://github.com/windingwind/zotero-plugin-template) (即本仓库)
|
||||
|
||||
> [!tip]
|
||||
> 👁 Watch 本仓库,以及时收到修复或更新的通知.
|
||||
|
||||
## 使用此模板构建的插件
|
||||
使用此模板创建的一些插件:
|
||||
|
||||
[](https://github.com/windingwind/zotero-better-notes)
|
||||
[](https://github.com/windingwind/zotero-pdf-preview)
|
||||
|
|
@ -41,10 +26,21 @@
|
|||
[](https://github.com/MuiseDestiny/zotero-gpt)
|
||||
[](https://github.com/zoushucai/zotero-journalabbr)
|
||||
[](https://github.com/MuiseDestiny/zotero-figure)
|
||||
[](https://github.com/MuiseDestiny/zotero-file)
|
||||
[](https://github.com/l0o0/jasminum)
|
||||
[](https://github.com/lifan0127/ai-research-assistant)
|
||||
|
||||
[](https://github.com/daeh/zotero-markdb-connect)
|
||||
📖 [插件开发文档](https://zotero.yuque.com/books/share/8d230829-6004-4934-b4c6-685a7001bfa0/vec88d) (中文版)
|
||||
|
||||
🛠️ [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) (即当前库)
|
||||
|
||||
> 👁 关注此库,以便在有修复或更新时及时收到通知.
|
||||
|
||||
如果你正在使用此库,我建议你将这个标志 ([](https://github.com/windingwind/zotero-plugin-template)) 放在 README 文件中:
|
||||
|
||||
|
|
@ -54,21 +50,19 @@
|
|||
|
||||
## Features 特性
|
||||
|
||||
> ❗Zotero系统已升级(dtd 已弃用,我们将不在使用 .properties). 主分支将只支持 Zotero 7.0.0-beta.12 或更高版本. 如果需要支持 Zotero 6,可能需要同时使用`dtd`、`properties` 和`ftl`. 请参考此库的 `zotero6-bootstrap` 分支.
|
||||
|
||||
- 事件驱动、函数式编程的可扩展框架;
|
||||
- 简单易用,开箱即用;
|
||||
- ⭐[新特性!]自动热重载!每当修改源码时,都会自动编译并重新加载插件;[详情请跳转→](#自动热重载)
|
||||
- `src/modules/examples.ts` 中有丰富的示例,涵盖了插件中常用的大部分API (使用的插件工具包 zotero-plugin-toolkit,仓库地址 https://github.com/windingwind/zotero-plugin-toolkit);
|
||||
- ⭐[新特性!]自动热重载!每当修改源码时,都会自动编译并重新加载插件;[详情请跳转→](#auto-hot-reload)
|
||||
- `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);
|
||||
- 为使用 JavaScript 编写的Zotero源码提供全面的类型定义支持(使用类型定义包[zotero-types](https://github.com/windingwind/zotero-types));
|
||||
- 全局变量和环境设置;
|
||||
- 插件开发/构建/发布工作流:
|
||||
- 自动生成/更新插件id和版本、更新配置和设置环境变量 (`development`/`production`);
|
||||
- 插件构建/测试/发布工作流:
|
||||
- 自动生成/更新插件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 示例
|
||||
|
||||
|
|
@ -108,7 +102,7 @@
|
|||
|
||||
- Preferences bindings
|
||||
- UI Events
|
||||
- Table
|
||||
- Tabel
|
||||
- Locale
|
||||
|
||||
详情参见 [`src/modules/preferenceScript.ts`](./src/modules/preferenceScript.ts)
|
||||
|
|
@ -135,89 +129,108 @@ Obsidian风格的指令输入模块,它通过接受文本来运行插件,并
|
|||
|
||||
## Quick Start Guide 快速入门指南
|
||||
|
||||
### 0 前置要求(Requirement)
|
||||
### 安装预构建 `xpi`
|
||||
|
||||
1. 安装测试版 Zotero:https://www.zotero.org/support/beta_builds
|
||||
2. 安装 Node.js(https://nodejs.org/en/)和 Git(https://git-scm.com/)
|
||||
通过直接在GitHub中下载构建好的 `xpi` 文件并将其安装到Zotero中来了解示例的工作原理.
|
||||
|
||||
> [!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)
|
||||
> 该库构建好的xpi文件不具有任何实际功能,它可能不随Zotero更新而随时更新.
|
||||
>
|
||||
> `xpi` 文件实际上是一个zip压缩包,然而,请不要直接修改它,而是修改源代码并重新构建它.
|
||||
|
||||
1. 点击 `Use this template`;
|
||||
2. 使用 `git clone` 克隆上一步生成的仓库;
|
||||
<details >
|
||||
<summary>💡 从 GitHub Codespace 开始</summary>
|
||||
### 从源码构建(Build from Source)
|
||||
|
||||
_GitHub CodeSpace_ 使你可以直接开始开发而无需在本地下载代码/IDE/依赖.
|
||||
- Fork 此库或者使用 `Use this template`;
|
||||
- 使用 `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>
|
||||
- 去 [homepage](https://github.com/windingwind/zotero-plugin-template)顶部,点击绿色按钮`Use this template`,点击 `Open in codespace`, 你需要登录你的GitHub账号.
|
||||
- 等待 codespace 加载.
|
||||
- 修改 `./package.json` 中的设置,包括:
|
||||
</details>
|
||||
|
||||
3. 进入项目文件夹;
|
||||
```json5
|
||||
{
|
||||
version,
|
||||
author,
|
||||
description,
|
||||
homepage,
|
||||
config {
|
||||
releasepage, // URL to releases(`.xpi`)
|
||||
updaterdf, // URL to update.json
|
||||
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}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2 配置模板和开发环境(Config Template Settings and Enviroment)
|
||||
> 注意设置 addonID 和 addonRef 以避免冲突.
|
||||
|
||||
1. 修改 `./package.json` 中的设置,包括:
|
||||
- 运行 `npm install` 以设置插件并安装相关依赖. 如果你没有安装 Node.js,请在[此处下载](https://nodejs.org/en/);
|
||||
- 运行 `npm run build` 以在生产模式下构建插件,运行 `npm run build-dev` 以在开发模式下构建插件. 用于安装的 xpi 文件和用于构建的代码在 `build` 文件夹下.
|
||||
|
||||
```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
|
||||
},
|
||||
}
|
||||
```
|
||||
> Dev & prod 两者有什么区别?
|
||||
>
|
||||
> - 此环境变量存储在 `Zotero.${addonInstance}.data.env` 中,控制台输出在生产模式下被禁用.
|
||||
> - 你可以根据此变量决定用户无法查看/使用的内容.
|
||||
|
||||
> [!warning]
|
||||
> 注意设置 addonID 和 addonRef 以避免冲突.
|
||||
### 发布(Release)
|
||||
|
||||
如果你需要在GitHub以外的地方托管你的 XPI 包,请删除 `releasePage` 并添加 `updateLink`,并将值设置为你的 XPI 下载地址.
|
||||
如果要构建和发布插件,运行如下指令:
|
||||
|
||||
2. 复制 Zotero 启动配置,填入 Zotero 可执行文件路径和 profile 路径.
|
||||
```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. 安装 Zotero: <https://www.zotero.org/support/beta_builds> (Zotero 7 beta: <https://www.zotero.org/support/dev/zotero_7_for_developers>)
|
||||
|
||||
2. 安装 Firefox 102 (适用于 Zotero 7)
|
||||
|
||||
3. 复制 zotero 命令行配置文件,修改开始安装 beta Zotero 的命令.
|
||||
|
||||
> (可选项) 此操作仅需执行一次: 使用 `/path/to/zotero -p` 启动 Zotero,创建一个新的配置文件并用作开发配置文件.
|
||||
> 将配置文件的路径 `profilePath` 放入 `zotero-cmd.json` 中,以指定要使用的配置文件.
|
||||
|
||||
```sh
|
||||
cp ./scripts/zotero-cmd-template.json ./scripts/zotero-cmd.json
|
||||
cp ./scripts/zotero-cmd-default.json ./scripts/zotero-cmd.json
|
||||
vim ./scripts/zotero-cmd.json
|
||||
```
|
||||
|
||||
3. 运行 `npm install` 以安装相关依赖
|
||||
4. 构建插件并使用 `npm run restart` 重启 Zotero.
|
||||
|
||||
> 如果你使用 `pnpm` 作为包管理器,你需要添加 `public-hoist-pattern[]=*@types/bluebird*` 到`.npmrc`, 详情请查看 zotero-types(https://github.com/windingwind/zotero-types?tab=readme-ov-file#usage)的文档.
|
||||
5. 启动 Firefox 102 (Zotero 7)
|
||||
|
||||
### 3 开始开发(Coding)
|
||||
6. 在 Firefox 中,转到devtools,转到设置,单击 "enable remote debugging" ,同时,旁边的按钮也是关于调试的。
|
||||
|
||||
使用 `npm start` 启动开发服务器,它将:
|
||||
> 在 FirFox 102 中输入 `about:debugging#/setup` .
|
||||
|
||||
- 在开发模式下预构建插件
|
||||
- 启动 Zotero ,并让其从 `build/` 中加载插件
|
||||
- 打开开发者工具(devtool)
|
||||
- 监听 `src/**` 和 `addon/**`.
|
||||
- 如果 `src/**` 修改了,运行 esbuild 并且重新加载
|
||||
- 如果 `addon/**` 修改了,(在开发模式下)重新构建插件并且重新加载
|
||||
7. 在 Zotero 中,进入设置-高级-编辑器,搜索 "debugging" 然后单击 "allow remote debugging".
|
||||
|
||||
#### 自动热重载
|
||||
8. 在 Firefox 中连接 Zotero. 在 FireFox 102中,在远程调试页面底部输入 `localhost:6100` 然后单击 `add`.
|
||||
|
||||
9. 在远程调试页面左侧栏点击 `connect`.
|
||||
|
||||
10. 点击 "Inspect Main Process"
|
||||
|
||||
### 自动热重载(Auto Hot Reload)
|
||||
|
||||
厌倦了无休止的重启吗?忘掉它,拥抱热加载!
|
||||
|
||||
1. 运行 `npm start`.
|
||||
1. 运行 `npm run start-watch`. (如果Zotero已经在运行,请使用 `npm run watch`)
|
||||
2. 编码. (是的,就这么简单)
|
||||
|
||||
当检测到 `src` 或 `addon` 中的文件修改时,插件将自动编译并重新加载.
|
||||
|
|
@ -225,76 +238,27 @@ Obsidian风格的指令输入模块,它通过接受文本来运行插件,并
|
|||
<details style="text-indent: 2em">
|
||||
<summary>💡 将此功能添加到现有插件的步骤</summary>
|
||||
|
||||
1. 复制 `scripts/**.mjs`
|
||||
2. 复制 `server` 、`build` 和 `stop` 命令到 `package.json`
|
||||
3. 运行 `npm install --save-dev chokidar`
|
||||
1. 复制 `scripts/reload.mjs`
|
||||
2. 复制 `reload` 、`watch` 和 `start-watch` 命令 `package.json`
|
||||
3. 运行 `npm install --save-dev chokidar-cli`
|
||||
4. 结束.
|
||||
|
||||
</details>
|
||||
|
||||
#### 在 Zotero 中 Debug
|
||||
### 在 Zotero 中调试
|
||||
|
||||
你还可以:
|
||||
|
||||
- 在 Tools->Developer->Run Javascript 中测试代码片段;
|
||||
|
||||
- 使用 `Zotero.debug()` 调试输出. 在 Help->Debug Output Logging->View Output 查看输出;
|
||||
|
||||
- 调试 UI. Zotero 建立在 Firefox XUL 框架之上. 使用 [XUL Explorer](https://udn.realityripple.com/docs/Archive/Mozilla/XUL_Explorer) 等软件调试 XUL UI.
|
||||
|
||||
> XUL 文档: <http://www.devdoc.net/web/developer.mozilla.org/en-US/docs/XUL.html>
|
||||
|
||||
### 4 构建(Build)
|
||||
|
||||
运行 `npm run build` 在生产模式下构建插件,构建的结果位于 `build/` 目录中.
|
||||
|
||||
`scripts/build.mjs` 的运行步骤:
|
||||
|
||||
- 创建/清空 `build/`
|
||||
- 复制 `addon/**` 到 `build/addon/**`
|
||||
- 替换占位符:使用 `replace-in-file` 去替换在 `package.json` 中定义的关键字和配置 (`xhtml`、`.flt` 等)
|
||||
- 准备本地化文件以避免冲突,查看官方文档了解更多(https://www.zotero.org/support/dev/zotero_7_for_developers#avoiding_localization_conflicts)
|
||||
- 重命名`**/*.flt` 为 `**/${addonRef}-*.flt`
|
||||
- 在每个消息前加上 `addonRef-`
|
||||
- 使用 Esbuild 来将 `.ts` 源码构建为 `.js`,从 `src/index.ts` 构建到`./build/addon/chrome/content/scripts`
|
||||
- (仅在生产模式下工作) 压缩 `./build/addon` 目录为 `./build/*.xpi`
|
||||
- (仅在生产模式下工作) 准备 `update.json` 或 `update-beta.json`
|
||||
|
||||
> [!note]
|
||||
>
|
||||
> **Dev & prod 两者有什么区别?**
|
||||
>
|
||||
> - 此环境变量存储在 `Zotero.${addonInstance}.data.env` 中,控制台输出在生产模式下被禁用.
|
||||
> - 你可以根据此变量决定用户无法查看/使用的内容.
|
||||
> - 在生产模式下,构建脚本将自动打包插件并更新 `update.json`.
|
||||
|
||||
### 5 发布(Release)
|
||||
|
||||
如果要构建和发布插件,运行如下指令:
|
||||
|
||||
```shell
|
||||
# A release-it command: version increase, npm run build, git push, and GitHub release
|
||||
# release-it: https://github.com/release-it/release-it
|
||||
npm run release
|
||||
```
|
||||
|
||||
> [!note]
|
||||
> 在此模板中,release-it 被配置为在本地升级版本、构建、推送提交和 git 标签,随后GitHub Action 将重新构建插件并将 XPI 发布到 GitHub Release.
|
||||
>
|
||||
> 如果你需要发布一个本地构建的 XPI,将 `package.json` 中的 `release-it.github.release` 设置为 `true`,然后移除 `.github/workflows/release.yml`. 此外,你还需要设置环境变量 `GITHUB_TOKEN`,获取 GitHub Token(https://github.com/settings/tokens).
|
||||
|
||||
#### 关于预发布
|
||||
|
||||
该模板将 `prerelease` 定义为插件的测试版,当你在 release-it 中选择 `prerelease` 版本 (版本号中带有 `-` ),构建脚本将创建一个 `update-beta.json` 给预发布版本使用,这将确保常规版本的用户不会自动更新到测试版,只有手动下载并安装了测试版的用户才能自动更新到下一个测试版. 当下一个正式版本更新时,脚本将同步更新 `update.json` 和 `update-beta.json`,这将使正式版和测试版用户都可以更新到最新的正式版.
|
||||
|
||||
> [!warning]
|
||||
> 严格来说,区分 Zotero 6 和 Zotero 7 兼容的插件版本应该通过 `update.json` 的 `addons.__addonID__.updates[]` 中分别配置 `applications.zotero.strict_min_version`,这样 Zotero 才能正确识别,详情在 Zotero 7 开发文档(https://www.zotero.org/support/dev/zotero_7_for_developers#updaterdf_updatesjson)获取.
|
||||
|
||||
## Details 更多细节
|
||||
|
||||
### 关于Hooks(About Hooks)
|
||||
|
||||
> 可以在 [`src/hooks.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/hooks.ts) 中查看更多
|
||||
> 可以在 [`src/hooks.ts`](https://github.com/windingwind/zotero-plugin-template/blob/bootstrap/src/hooks.ts) 中查看更多
|
||||
|
||||
1. 当在 Zotero 中触发安装/启用/启动时,`bootstrap.js` > `startup` 被调用
|
||||
- 等待 Zotero 就绪
|
||||
|
|
@ -308,9 +272,9 @@ npm run release
|
|||
|
||||
### 关于全局变量(About Global Variables)
|
||||
|
||||
> 可以在 [`src/index.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/index.ts)中查看更多
|
||||
> 可以在 [`src/index.ts`](https://github.com/windingwind/zotero-plugin-template/blob/bootstrap/src/index.ts) 中查看更多
|
||||
|
||||
bootstrap插件在沙盒中运行,但沙盒中没有默认的全局变量,例如 `Zotero` 或 `window` 等我们曾在overlay插件环境中使用的变量.
|
||||
引导插件在沙盒中运行,但沙盒中没有默认的全局变量,例如 `Zotero` 或 `window` 等我们曾在覆盖插件环境中使用的变量.
|
||||
|
||||
此模板将以下变量注册到全局范围:
|
||||
|
||||
|
|
@ -331,9 +295,23 @@ createElement(document, "hbox"); // returns XUL.Box
|
|||
createElement(document, "button", { namespace: "xul" }); // manually set namespace. returns XUL.Button
|
||||
```
|
||||
|
||||
### 关于构建(About Build)
|
||||
|
||||
使用 Esbuild 将 `.ts` 源代码构建为 `.js`.
|
||||
|
||||
使用 `replace-in-file` 去替换在 `package.json` 中定义的关键字和配置 (`xhtml`、`.flt` 等).
|
||||
|
||||
步骤 `scripts/build.mjs`:
|
||||
|
||||
1. 清理 `./build`
|
||||
2. 复制 `./addon` 到 `./build`
|
||||
3. Esbuild 到 `./build/addon/chrome/content/scripts`
|
||||
4. 替换`__buildVersion__` 和 `__buildTime__` 在 `./build/addon`
|
||||
5. 压缩 `./build/addon` 到 `./build/*.xpi`
|
||||
|
||||
### 关于 Zotero API(About Zotero API)
|
||||
|
||||
Zotero 文档已过时且不完整,克隆 https://github.com/zotero/zotero 并全局搜索关键字.
|
||||
Zotero 文档已过时且不完整,git clone https://github.com/zotero/zotero 并全局搜索关键字.
|
||||
|
||||
> ⭐[zotero-types](https://github.com/windingwind/zotero-types) 提供了最常用的 Zotero API,在默认情况下它被包含在此模板中. 你的 IDE 将为大多数的 API 提供提醒.
|
||||
|
||||
|
|
@ -387,14 +365,11 @@ Zotero 文档已过时且不完整,克隆 https://github.com/zotero/zotero 并
|
|||
| `-- 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
|
||||
| |-- build.mjs # esbuild and replace
|
||||
| |-- reload.mjs
|
||||
| |-- start.mjs
|
||||
| |-- stop.mjs
|
||||
| `-- zotero-cmd-default.json
|
||||
|-- src # source code
|
||||
| |-- addon.ts # base class
|
||||
| |-- hooks.ts # lifecycle hooks
|
||||
|
|
@ -410,6 +385,7 @@ Zotero 文档已过时且不完整,克隆 https://github.com/zotero/zotero 并
|
|||
|-- tsconfig.json # https://code.visualstudio.com/docs/languages/jsconfig
|
||||
|-- typings # ts typings
|
||||
| `-- global.d.ts
|
||||
|-- update-template.json # template of `update.json`
|
||||
`-- update.json
|
||||
```
|
||||
|
||||
|
|
|
|||
47
package.json
47
package.json
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"name": "zotero-tldr",
|
||||
"version": "1.0.7",
|
||||
"description": "TLDR(too long; didn't read) from sematic scholar",
|
||||
"name": "zotero-addon-template",
|
||||
"version": "1.1.0",
|
||||
"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",
|
||||
"addonInstance": "AddonTemplate",
|
||||
"prefsPrefix": "extensions.zotero.addontemplate",
|
||||
"releasePage": "https://github.com/windingwind/zotero-addon-template/releases",
|
||||
"updateJSON": "https://raw.githubusercontent.com/windingwind/zotero-addon-template/main/update.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
|
|
@ -18,36 +18,36 @@
|
|||
"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",
|
||||
"release": "release-it",
|
||||
"update-deps": "npm update --save"
|
||||
},
|
||||
"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": "^2.3.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"@typescript-eslint/parser": "^7.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"compressing": "^1.10.0",
|
||||
"esbuild": "^0.20.1",
|
||||
"esbuild": "^0.19.8",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier": "^3.1.0",
|
||||
"release-it": "^17.0.1",
|
||||
"replace-in-file": "^7.0.2",
|
||||
"typescript": "^5.3.3",
|
||||
"zotero-types": "^1.3.10"
|
||||
"zotero-types": "^1.3.7"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
|
|
@ -115,7 +115,7 @@
|
|||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
"tagName": "V${version}"
|
||||
"tagName": "v${version}"
|
||||
},
|
||||
"npm": {
|
||||
"publish": false
|
||||
|
|
@ -128,7 +128,8 @@
|
|||
},
|
||||
"hooks": {
|
||||
"before:init": "npm run lint",
|
||||
"after:bump": "npm run build"
|
||||
"after:bump": "npm run build",
|
||||
"after:release": "echo Successfully released ${name} v${version} to ${repo.repository}."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,8 +45,6 @@ function replaceString(buildTime) {
|
|||
const replaceResult = replaceInFileSync({
|
||||
files: [
|
||||
`${buildDir}/addon/**/*.xhtml`,
|
||||
`${buildDir}/addon/**/*.html`,
|
||||
`${buildDir}/addon/**/*.css`,
|
||||
`${buildDir}/addon/**/*.json`,
|
||||
`${buildDir}/addon/prefs.js`,
|
||||
`${buildDir}/addon/manifest.json`,
|
||||
|
|
@ -64,75 +62,83 @@ function replaceString(buildTime) {
|
|||
}
|
||||
|
||||
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 builds/addon/locale folder's sub folders and rename *.ftl to addonRef-*.ftl
|
||||
const localeDir = path.join(buildDir, "addon/locale");
|
||||
const localeFolders = readdirSync(localeDir, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
// 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, {
|
||||
for (const localeSubFolder of localeFolders) {
|
||||
const localeSubDir = path.join(localeDir, localeSubFolder);
|
||||
const localeSubFiles = readdirSync(localeSubDir, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.filter((dirent) => dirent.isFile())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
// rename *.ftl to addonRef-*.ftl
|
||||
for (const ftlFile of ftlFiles) {
|
||||
if (ftlFile.endsWith(".ftl")) {
|
||||
for (const localeSubFile of localeSubFiles) {
|
||||
if (localeSubFile.endsWith(".ftl")) {
|
||||
renameSync(
|
||||
path.join(localePath, ftlFile),
|
||||
path.join(localePath, `${config.addonRef}-${ftlFile}`),
|
||||
path.join(localeSubDir, localeSubFile),
|
||||
path.join(localeSubDir, `${config.addonRef}-${localeSubFile}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
const localeMessage = new Set();
|
||||
const localeMessageMiss = new Set();
|
||||
|
||||
const replaceResultFlt = replaceInFileSync({
|
||||
files: [`${buildDir}/addon/locale/**/*.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) {
|
||||
localeMessage.add(match.groups.message);
|
||||
return `${config.addonRef}-${line}`;
|
||||
} else {
|
||||
return line;
|
||||
}
|
||||
});
|
||||
return prefixedLines.join("\n");
|
||||
},
|
||||
});
|
||||
|
||||
const replaceResultXhtml = replaceInFileSync({
|
||||
files: [`${buildDir}/addon/**/*.xhtml`],
|
||||
processor: (input) => {
|
||||
const matchs = [...input.matchAll(/(data-l10n-id)="(\S*)"/g)];
|
||||
matchs.map((match) => {
|
||||
if (localeMessage.has(match[2])) {
|
||||
input = input.replace(
|
||||
match[0],
|
||||
`${match[1]}="${config.addonRef}-${match[2]}"`,
|
||||
);
|
||||
if (match) {
|
||||
MessageInThisLang.add(match.groups.message);
|
||||
return `${config.addonRef}-${line}`;
|
||||
} else {
|
||||
return line;
|
||||
}
|
||||
});
|
||||
return prefixedLines.join("\n");
|
||||
},
|
||||
});
|
||||
} else {
|
||||
localeMessageMiss.add(match[2]);
|
||||
}
|
||||
});
|
||||
return input;
|
||||
},
|
||||
});
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
});
|
||||
Logger.debug(
|
||||
"[Build] Prepare locale files OK",
|
||||
// replaceResultFlt.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`),
|
||||
// replaceResultXhtml.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`),
|
||||
);
|
||||
|
||||
if (localeMessageMiss.size !== 0) {
|
||||
Logger.warn(
|
||||
`[Build] Fluent message [${new Array(
|
||||
...localeMessageMiss,
|
||||
)}] do not exsit in addon's locale files.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -204,21 +210,19 @@ export async function main() {
|
|||
);
|
||||
|
||||
clearFolder(buildDir);
|
||||
|
||||
copyFolderRecursiveSync("addon", buildDir);
|
||||
|
||||
Logger.debug("[Build] Replacing");
|
||||
replaceString(buildTime);
|
||||
Logger.debug("[Build] Replace OK");
|
||||
|
||||
Logger.debug("[Build] Preparing locale files");
|
||||
prepareLocaleFiles();
|
||||
|
||||
Logger.debug("[Build] Running esbuild");
|
||||
await build(esbuildOptions);
|
||||
Logger.debug("[Build] Run esbuild OK");
|
||||
|
||||
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`),
|
||||
|
|
@ -226,6 +230,7 @@ export async function main() {
|
|||
ignoreBase: true,
|
||||
},
|
||||
);
|
||||
Logger.debug("[Build] Addon pack OK");
|
||||
|
||||
prepareUpdateJson();
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,10 @@ async function main() {
|
|||
await build();
|
||||
|
||||
// start Zotero
|
||||
startZotero(openDevTool);
|
||||
startZotero();
|
||||
setTimeout(() => {
|
||||
openDevTool();
|
||||
}, 2000);
|
||||
|
||||
// watch
|
||||
await watch();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ 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 { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import { clearFolder } from "./utils.mjs";
|
||||
import path from "path";
|
||||
import { exit } from "process";
|
||||
|
|
@ -10,9 +10,6 @@ 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");
|
||||
|
||||
|
|
@ -45,11 +42,6 @@ function prepareDevEnv() {
|
|||
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");
|
||||
|
|
@ -61,7 +53,7 @@ function prepareDevEnv() {
|
|||
return;
|
||||
}
|
||||
if (line.includes("extensions.zotero.dataDir") && dataDir !== "") {
|
||||
return `user_pref("extensions.zotero.dataDir", "${dataDir.replace(/\\\\?/g, "\\\\")}");`;
|
||||
return `user_pref("extensions.zotero.dataDir", "${dataDir}");`;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
|
@ -76,9 +68,7 @@ function prepareLog() {
|
|||
writeFileSync(logFilePath, "");
|
||||
}
|
||||
|
||||
export function main(callback) {
|
||||
let isZoteroReady = false;
|
||||
|
||||
export function main() {
|
||||
prepareDevEnv();
|
||||
|
||||
prepareLog();
|
||||
|
|
@ -91,10 +81,6 @@ export function main(callback) {
|
|||
]);
|
||||
|
||||
zoteroProcess.stdout.on("data", (data) => {
|
||||
if (!isZoteroReady && data.toString().includes(loadDevToolWhen)) {
|
||||
isZoteroReady = true;
|
||||
callback();
|
||||
}
|
||||
writeFileSync(logFilePath, data, {
|
||||
flag: "a",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"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}')",
|
||||
"killZoteroUnix": "kill -9 $(ps -x | grep zotero)",
|
||||
"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.",
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { ColumnOptions } from "zotero-plugin-toolkit/dist/helpers/virtualizedTable";
|
||||
import { DialogHelper } from "zotero-plugin-toolkit/dist/helpers/dialog";
|
||||
import hooks from "./hooks";
|
||||
import { createZToolkit } from "./utils/ztoolkit";
|
||||
|
|
@ -13,6 +14,8 @@ class Addon {
|
|||
};
|
||||
prefs?: {
|
||||
window: Window;
|
||||
columns: Array<ColumnOptions>;
|
||||
rows: Array<{ [dataKey: string]: string }>;
|
||||
};
|
||||
dialog?: DialogHelper;
|
||||
};
|
||||
|
|
|
|||
219
src/hooks.ts
219
src/hooks.ts
|
|
@ -1,10 +1,14 @@
|
|||
import { RegisterFactory, UIFactory } from "./modules/Common";
|
||||
import {
|
||||
BasicExampleFactory,
|
||||
HelperExampleFactory,
|
||||
KeyExampleFactory,
|
||||
PromptExampleFactory,
|
||||
UIExampleFactory,
|
||||
} from "./modules/examples";
|
||||
import { config } from "../package.json";
|
||||
import { getString, initLocale } from "./utils/locale";
|
||||
import { registerPrefsScripts } from "./modules/preferenceScript";
|
||||
import { createZToolkit } from "./utils/ztoolkit";
|
||||
import { tldrs } from "./modules/dataStorage";
|
||||
import { TLDRFetcher } from "./modules/tldrFetcher";
|
||||
|
||||
async function onStartup() {
|
||||
await Promise.all([
|
||||
|
|
@ -12,19 +16,11 @@ 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();
|
||||
BasicExampleFactory.registerPrefs();
|
||||
|
||||
RegisterFactory.registerNotifier();
|
||||
BasicExampleFactory.registerNotifier();
|
||||
|
||||
await onMainWindowLoad(window);
|
||||
}
|
||||
|
|
@ -33,17 +29,58 @@ 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`,
|
||||
);
|
||||
const popupWin = new ztoolkit.ProgressWindow(config.addonName, {
|
||||
closeOnClick: true,
|
||||
closeTime: -1,
|
||||
})
|
||||
.createLine({
|
||||
text: getString("startup-begin"),
|
||||
type: "default",
|
||||
progress: 0,
|
||||
})
|
||||
.show();
|
||||
|
||||
UIFactory.registerRightClickMenuItem();
|
||||
KeyExampleFactory.registerShortcuts();
|
||||
|
||||
UIFactory.registerRightClickCollectionMenuItem();
|
||||
await Zotero.Promise.delay(1000);
|
||||
popupWin.changeLine({
|
||||
progress: 30,
|
||||
text: `[30%] ${getString("startup-begin")}`,
|
||||
});
|
||||
|
||||
UIFactory.registerTLDRItemBoxRow();
|
||||
UIExampleFactory.registerStyleSheet();
|
||||
|
||||
onLoad();
|
||||
UIExampleFactory.registerRightClickMenuItem();
|
||||
|
||||
UIExampleFactory.registerRightClickMenuPopup();
|
||||
|
||||
UIExampleFactory.registerWindowMenuWithSeparator();
|
||||
|
||||
await UIExampleFactory.registerExtraColumn();
|
||||
|
||||
await UIExampleFactory.registerExtraColumnWithCustomCell();
|
||||
|
||||
await UIExampleFactory.registerCustomItemBoxRow();
|
||||
|
||||
UIExampleFactory.registerLibraryTabPanel();
|
||||
|
||||
await UIExampleFactory.registerReaderTabPanel();
|
||||
|
||||
PromptExampleFactory.registerNormalCommandExample();
|
||||
|
||||
PromptExampleFactory.registerAnonymousCommandExample();
|
||||
|
||||
PromptExampleFactory.registerConditionalCommandExample();
|
||||
|
||||
await Zotero.Promise.delay(1000);
|
||||
|
||||
popupWin.changeLine({
|
||||
progress: 100,
|
||||
text: `[100%] ${getString("startup-finish")}`,
|
||||
});
|
||||
popupWin.startCloseTimer(5000);
|
||||
|
||||
addon.hooks.onDialogEvents("dialogExample");
|
||||
}
|
||||
|
||||
async function onMainWindowUnload(win: Window): Promise<void> {
|
||||
|
|
@ -69,11 +106,16 @@ async function onNotify(
|
|||
ids: Array<string | number>,
|
||||
extraData: { [key: string]: any },
|
||||
) {
|
||||
Zotero.log(`${event} ${type} ${ids}, ${extraData}`);
|
||||
if (event == "add" && type == "item" && ids.length > 0) {
|
||||
onNotifyAddItems(ids);
|
||||
} else if (event == "delete" && type == "item" && ids.length > 0) {
|
||||
noNotifyDeleteItem(ids);
|
||||
// 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,105 +135,47 @@ 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.exampleShortcutConflictingCallback();
|
||||
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,
|
||||
|
|
@ -200,5 +184,6 @@ export default {
|
|||
onMainWindowUnload,
|
||||
onNotify,
|
||||
onPrefsEvent,
|
||||
onUpdateItems,
|
||||
onShortcuts,
|
||||
onDialogEvents,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
);
|
||||
|
|
@ -0,0 +1,966 @@
|
|||
import { config } from "../../package.json";
|
||||
import { getString } from "../utils/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: 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, [
|
||||
"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`,
|
||||
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.${config.addonInstance}.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 exampleShortcutConflictingCallback() {
|
||||
const conflictingGroups = ztoolkit.Shortcut.checkAllKeyConflicting();
|
||||
new ztoolkit.ProgressWindow("Check Key Conflicting")
|
||||
.createLine({
|
||||
text: `${conflictingGroups.length} groups of conflicting keys found. Details are in the debug output/console.`,
|
||||
})
|
||||
.show(-1);
|
||||
ztoolkit.log(
|
||||
"Conflicting:",
|
||||
conflictingGroups,
|
||||
"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 registerWindowMenuWithSeparator() {
|
||||
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);
|
||||
},
|
||||
{
|
||||
renderCell(index, data, column) {
|
||||
ztoolkit.log("Custom column cell is rendered!");
|
||||
const span = document.createElementNS(
|
||||
"http://www.w3.org/1999/xhtml",
|
||||
"span",
|
||||
);
|
||||
span.className = `cell ${column.className}`;
|
||||
span.style.background = "#0dd068";
|
||||
span.innerText = "⭐" + data;
|
||||
return span;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@example
|
||||
static async registerCustomItemBoxRow() {
|
||||
await ztoolkit.ItemBox.register(
|
||||
"itemBoxFieldEditable",
|
||||
"Editable Custom Field",
|
||||
(field, unformatted, includeBaseMapped, item, original) => {
|
||||
return (
|
||||
ztoolkit.ExtraField.getExtraField(item, "itemBoxFieldEditable") || ""
|
||||
);
|
||||
},
|
||||
{
|
||||
editable: true,
|
||||
setFieldHook: (field, value, loadIn, item, original) => {
|
||||
window.alert("Custom itemBox value is changed and saved to extra!");
|
||||
ztoolkit.ExtraField.setExtraField(
|
||||
item,
|
||||
"itemBoxFieldEditable",
|
||||
value,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
index: 1,
|
||||
},
|
||||
);
|
||||
|
||||
await ztoolkit.ItemBox.register(
|
||||
"itemBoxFieldNonEditable",
|
||||
"Non-Editable Custom Field",
|
||||
(field, unformatted, includeBaseMapped, item, original) => {
|
||||
return (
|
||||
"[CANNOT EDIT THIS]" + (item.getField("title") as string).slice(0, 10)
|
||||
);
|
||||
},
|
||||
{
|
||||
editable: false,
|
||||
index: 2,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@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 PromptExampleFactory {
|
||||
@example
|
||||
static registerNormalCommandExample() {
|
||||
ztoolkit.Prompt.register([
|
||||
{
|
||||
name: "Normal Command Test",
|
||||
label: "Plugin Template",
|
||||
callback(prompt) {
|
||||
ztoolkit.getGlobal("alert")("Command triggered!");
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@example
|
||||
static registerAnonymousCommandExample() {
|
||||
ztoolkit.Prompt.register([
|
||||
{
|
||||
id: "search",
|
||||
callback: async (prompt) => {
|
||||
// https://github.com/zotero/zotero/blob/7262465109c21919b56a7ab214f7c7a8e1e63909/chrome/content/zotero/integration/quickFormat.js#L589
|
||||
function getItemDescription(item: Zotero.Item) {
|
||||
const nodes = [];
|
||||
let str = "";
|
||||
let author,
|
||||
authorDate = "";
|
||||
if (item.firstCreator) {
|
||||
author = authorDate = item.firstCreator;
|
||||
}
|
||||
let date = item.getField("date", true, true) as string;
|
||||
if (date && (date = date.substr(0, 4)) !== "0000") {
|
||||
authorDate += " (" + parseInt(date) + ")";
|
||||
}
|
||||
authorDate = authorDate.trim();
|
||||
if (authorDate) nodes.push(authorDate);
|
||||
|
||||
const publicationTitle = item.getField(
|
||||
"publicationTitle",
|
||||
false,
|
||||
true,
|
||||
);
|
||||
if (publicationTitle) {
|
||||
nodes.push(`<i>${publicationTitle}</i>`);
|
||||
}
|
||||
let volumeIssue = item.getField("volume");
|
||||
const issue = item.getField("issue");
|
||||
if (issue) volumeIssue += "(" + issue + ")";
|
||||
if (volumeIssue) nodes.push(volumeIssue);
|
||||
|
||||
const publisherPlace = [];
|
||||
let field;
|
||||
if ((field = item.getField("publisher")))
|
||||
publisherPlace.push(field);
|
||||
if ((field = item.getField("place"))) publisherPlace.push(field);
|
||||
if (publisherPlace.length) nodes.push(publisherPlace.join(": "));
|
||||
|
||||
const pages = item.getField("pages");
|
||||
if (pages) nodes.push(pages);
|
||||
|
||||
if (!nodes.length) {
|
||||
const url = item.getField("url");
|
||||
if (url) nodes.push(url);
|
||||
}
|
||||
|
||||
// compile everything together
|
||||
for (let i = 0, n = nodes.length; i < n; i++) {
|
||||
const node = nodes[i];
|
||||
|
||||
if (i != 0) str += ", ";
|
||||
|
||||
if (typeof node === "object") {
|
||||
const label = document.createElement("label");
|
||||
label.setAttribute("value", str);
|
||||
label.setAttribute("crop", "end");
|
||||
str = "";
|
||||
} else {
|
||||
str += node;
|
||||
}
|
||||
}
|
||||
str.length && (str += ".");
|
||||
return str;
|
||||
}
|
||||
function filter(ids: number[]) {
|
||||
ids = ids.filter(async (id) => {
|
||||
const item = (await Zotero.Items.getAsync(id)) as Zotero.Item;
|
||||
return item.isRegularItem() && !(item as any).isFeedItem;
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
const text = prompt.inputNode.value;
|
||||
prompt.showTip("Searching...");
|
||||
const s = new Zotero.Search();
|
||||
s.addCondition("quicksearch-titleCreatorYear", "contains", text);
|
||||
s.addCondition("itemType", "isNot", "attachment");
|
||||
let ids = await s.search();
|
||||
// prompt.exit will remove current container element.
|
||||
// @ts-ignore ignore
|
||||
prompt.exit();
|
||||
const container = prompt.createCommandsContainer();
|
||||
container.classList.add("suggestions");
|
||||
ids = filter(ids);
|
||||
console.log(ids.length);
|
||||
if (ids.length == 0) {
|
||||
const s = new Zotero.Search();
|
||||
const operators = [
|
||||
"is",
|
||||
"isNot",
|
||||
"true",
|
||||
"false",
|
||||
"isInTheLast",
|
||||
"isBefore",
|
||||
"isAfter",
|
||||
"contains",
|
||||
"doesNotContain",
|
||||
"beginsWith",
|
||||
];
|
||||
let hasValidCondition = false;
|
||||
let joinMode = "all";
|
||||
if (/\s*\|\|\s*/.test(text)) {
|
||||
joinMode = "any";
|
||||
}
|
||||
text.split(/\s*(&&|\|\|)\s*/g).forEach((conditinString: string) => {
|
||||
const conditions = conditinString.split(/\s+/g);
|
||||
if (
|
||||
conditions.length == 3 &&
|
||||
operators.indexOf(conditions[1]) != -1
|
||||
) {
|
||||
hasValidCondition = true;
|
||||
s.addCondition(
|
||||
"joinMode",
|
||||
joinMode as Zotero.Search.Operator,
|
||||
"",
|
||||
);
|
||||
s.addCondition(
|
||||
conditions[0] as string,
|
||||
conditions[1] as Zotero.Search.Operator,
|
||||
conditions[2] as string,
|
||||
);
|
||||
}
|
||||
});
|
||||
if (hasValidCondition) {
|
||||
ids = await s.search();
|
||||
}
|
||||
}
|
||||
ids = filter(ids);
|
||||
console.log(ids.length);
|
||||
if (ids.length > 0) {
|
||||
ids.forEach((id: number) => {
|
||||
const item = Zotero.Items.get(id);
|
||||
const title = item.getField("title");
|
||||
const ele = ztoolkit.UI.createElement(document, "div", {
|
||||
namespace: "html",
|
||||
classList: ["command"],
|
||||
listeners: [
|
||||
{
|
||||
type: "mousemove",
|
||||
listener: function () {
|
||||
// @ts-ignore ignore
|
||||
prompt.selectItem(this);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "click",
|
||||
listener: () => {
|
||||
prompt.promptNode.style.display = "none";
|
||||
Zotero_Tabs.select("zotero-pane");
|
||||
ZoteroPane.selectItem(item.id);
|
||||
},
|
||||
},
|
||||
],
|
||||
styles: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "start",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
tag: "span",
|
||||
styles: {
|
||||
fontWeight: "bold",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
properties: {
|
||||
innerText: title,
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "span",
|
||||
styles: {
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
properties: {
|
||||
innerHTML: getItemDescription(item),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
container.appendChild(ele);
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore ignore
|
||||
prompt.exit();
|
||||
prompt.showTip("Not Found.");
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@example
|
||||
static registerConditionalCommandExample() {
|
||||
ztoolkit.Prompt.register([
|
||||
{
|
||||
name: "Conditional Command Test",
|
||||
label: "Plugin Template",
|
||||
// The when function is executed when Prompt UI is woken up by `Shift + P`, and this command does not display when false is returned.
|
||||
when: () => {
|
||||
const items = ZoteroPane.getSelectedItems();
|
||||
return items.length > 0;
|
||||
},
|
||||
callback(prompt) {
|
||||
prompt.inputNode.placeholder = "Hello World!";
|
||||
const items = ZoteroPane.getSelectedItems();
|
||||
ztoolkit.getGlobal("alert")(
|
||||
`You select ${items.length} items!\n\n${items
|
||||
.map(
|
||||
(item, index) =>
|
||||
String(index + 1) + ". " + item.getDisplayTitle(),
|
||||
)
|
||||
.join("\n")}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
addon.data.dialog = dialogHelper;
|
||||
await dialogData.unloadLock.promise;
|
||||
addon.data.dialog = undefined;
|
||||
addon.data.alive &&
|
||||
ztoolkit.getGlobal("alert")(
|
||||
`Close dialog with ${dialogData._lastButtonId}.\nCheckbox: ${dialogData.checkboxValue}\nInput: ${dialogData.inputValue}.`,
|
||||
);
|
||||
ztoolkit.log(dialogData);
|
||||
}
|
||||
|
||||
@example
|
||||
static clipboardExample() {
|
||||
new ztoolkit.Clipboard()
|
||||
.addText(
|
||||
"",
|
||||
"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");
|
||||
}
|
||||
}
|
||||
|
|
@ -7,11 +7,125 @@ export async function registerPrefsScripts(_window: Window) {
|
|||
if (!addon.data.prefs) {
|
||||
addon.data.prefs = {
|
||||
window: _window,
|
||||
columns: [
|
||||
{
|
||||
dataKey: "title",
|
||||
label: getString("prefs-table-title"),
|
||||
fixedWidth: true,
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
dataKey: "detail",
|
||||
label: getString("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();
|
||||
if (addon.data.prefs?.window == undefined) return;
|
||||
const tableHelper = new ztoolkit.VirtualizedTable(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,
|
||||
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}!`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
// }
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@
|
|||
"target": "ES2016",
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src", "typings", "node_modules/zotero-types"],
|
||||
"exclude": ["build", "addon"],
|
||||
"exclude": ["build", "addon"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"addons": {
|
||||
"zoterotldr@syt.com": {
|
||||
"addontemplate@euclpts.com": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "1.0.7",
|
||||
"update_link": "undefined/latest/download/zotero-tldr.xpi",
|
||||
"version": "1.1.0",
|
||||
"update_link": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi",
|
||||
"applications": {
|
||||
"zotero": {
|
||||
"strict_min_version": "6.999"
|
||||
|
|
|
|||
Loading…
Reference in New Issue