Compare commits

...

53 Commits
v1.1.0 ... main

Author SHA1 Message Date
syt ed129f2a45
Merge pull request #12 from syt2/11-更新时报错-typeerror-tiscollection-is-not-a-function
fix isCollection func
2024-07-25 16:53:31 +08:00
ytshen 2a9125aa99 fix isCollection func 2024-07-25 16:52:52 +08:00
shenyutao 5d4dde170a Release 1.0.7 2024-04-17 18:33:55 +08:00
shenyutao 2d10915c7d fix error 2024-04-17 18:33:35 +08:00
ytshen 18fac41190 update release v -> V 2024-04-16 23:42:07 +08:00
ytshen f653f8a11a Release 1.0.6 2024-04-16 23:36:43 +08:00
ytshen 28cf7c86de update 2024-04-16 23:36:29 +08:00
ytshen 6edfda0388 update icon 2024-04-16 23:35:47 +08:00
ytshen 6056c4208f update pane registe 2024-04-16 23:30:59 +08:00
ytshen 5cc74c4986 Merge branch 'template'
# Conflicts:
#	README.md
#	addon/locale/en-US/preferences.ftl
#	package.json
#	src/hooks.ts
#	src/modules/examples.ts
#	update.json
2024-04-16 23:11:34 +08:00
ytshen a26f5fd0a9 update 2024-04-16 22:56:23 +08:00
windingwind eec0d47a96
Merge pull request #107 from windingwind/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-7.3.1
build(deps-dev): bump @typescript-eslint/eslint-plugin from 6.21.0 to 7.3.1
2024-03-25 22:14:44 +08:00
dependabot[bot] df847e59be
build(deps-dev): bump @typescript-eslint/eslint-plugin
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.21.0 to 7.3.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.3.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 23:25:48 +00:00
shenyutao 50956fefff Release 1.0.5 2024-03-13 17:15:07 +08:00
shenyutao f93d22d29b update 2024-03-13 17:14:35 +08:00
shenyutao a9d13a20c6 同时注册于row中 2024-03-13 17:13:42 +08:00
shenyutao 3c9695fa71 Release 1.0.4 2024-03-11 11:48:00 +08:00
shenyutao 735da8a43a update 2024-03-11 11:47:42 +08:00
shenyutao b56669ae8f update dependence 2024-03-11 11:46:32 +08:00
windingwind 66c0460513
Merge pull request #102 from windingwind/dependabot/npm_and_yarn/esbuild-0.20.1
build(deps-dev): bump esbuild from 0.19.12 to 0.20.1
2024-03-09 22:01:30 +08:00
windingwind f52eb9f100
Merge pull request #104 from windingwind/dependabot/npm_and_yarn/typescript-eslint/parser-7.1.1
build(deps-dev): bump @typescript-eslint/parser from 6.21.0 to 7.1.1
2024-03-09 22:01:18 +08:00
dependabot[bot] 0075dc8a43
build(deps-dev): bump @typescript-eslint/parser from 6.21.0 to 7.1.1
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.21.0 to 7.1.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.1.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-04 23:31:03 +00:00
dependabot[bot] cd14a26c11
build(deps-dev): bump esbuild from 0.19.12 to 0.20.1
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.19.12 to 0.20.1.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.19.12...v0.20.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-19 23:37:21 +00:00
shenyutao fdd4d20a95 move from itemBox row to note 2024-01-31 13:47:29 +08:00
Chénglóng Mǎ 2cc3ac7622
Update start.mjs (#97) 2024-01-12 10:06:01 +08:00
Northword 0663e5b0b1
fix: log miss ftl messages in build (#95) 2024-01-06 00:14:07 +08:00
Northword 7738410680
fix: comment for release (#93)
* fix: comment for release

* chore: update deps

* Delete release-it after:release hook
2024-01-06 00:13:29 +08:00
windingwind d1f6367825 Release 1.1.1 2023-12-28 10:43:50 +08:00
windingwind 2839b3ac53 chore: doc lint 2023-12-28 10:43:23 +08:00
Northword 7f6b7c54b2
fix: only prompt version when run release, and submit comment to closed issues, fix prerelease version number breaks auto update (#92)
* chore: only prompt version when run release

* chore: submit comment in issue when new version release
closes: #91

* fix: set default prerelease id to beta to resolve auto update wrong
2023-12-26 21:43:26 +08:00
syt fc8a4ac9d9
Remove original xpi file if exists in `extensions` folder (#90)
* remove original xpi file in extensions folder to avoid load add-on from xpi

* Update README.md
2023-12-18 15:49:00 +08:00
windingwind f5f2fd3297 add: build replace-in-file includes html and css 2023-12-18 00:03:28 +08:00
KikkiZ be0b82f23d
Fixes issue #86 (#87) 2023-12-17 19:19:26 +08:00
Dae 6f3a6d112e
update killZoteroUnix command to ignore grep pipe (#89) 2023-12-17 10:48:07 +08:00
windingwind 3d9f8a8a0d update: registerShortcuts 2023-12-14 22:33:10 +08:00
windingwind 032aa74f6e fix: devtool startup 2023-12-13 22:21:26 +08:00
windingwind 57028c7ba8 chore: change default dev tool start delay 2023-12-13 21:23:19 +08:00
windingwind 4eb662ea2f rename: cmd template 2023-12-12 23:01:33 +08:00
windingwind 56c427b26a add: DS_Store to gitignore 2023-12-12 22:30:41 +08:00
shenyutao 7f436b320f Release 1.0.3 2023-08-28 12:00:24 +08:00
shenyutao 296ce35e16 update store path 2023-08-28 12:00:04 +08:00
shenyutao dad1df7b52 Release 1.0.2 2023-08-25 16:14:49 +08:00
shenyutao 6ebb421185 update icon 2023-08-25 16:14:24 +08:00
shenyutao 861d6685a0 update readme 2023-08-25 15:49:42 +08:00
shenyutao 67498280a0 Release 1.0.1 2023-08-25 15:45:13 +08:00
shenyutao 309447ca68 ipv code 2023-08-25 15:44:44 +08:00
shenyutao e9b9e7ef46 ipv code & language 2023-08-25 15:43:17 +08:00
shenyutao f1d5275e6f Release 1.0.0 2023-08-18 18:31:42 +08:00
shenyutao 24c229f736 update code 2023-08-18 18:31:10 +08:00
shenyutao 7fd2de3554 update 2023-08-18 18:29:46 +08:00
shenyutao eed95d402e update 2023-08-17 16:11:25 +08:00
shenyutao 033bafa5bc init proj 2023-08-17 14:38:20 +08:00
shenyutao 03fadd7b25 init 2023-08-16 11:59:55 +08:00
32 changed files with 854 additions and 1914 deletions

View File

@ -3,31 +3,41 @@ name: Release
on: on:
push: push:
tags: tags:
- v** - V**
permissions: permissions:
contents: write contents: write
issues: write
pull-requests: write
jobs: jobs:
release-it: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
GITHUB_TOKEN: ${{ secrets.GitHub_TOKEN }} GITHUB_TOKEN: ${{ secrets.GitHub_TOKEN }}
steps: steps:
- uses: actions/checkout@v4 - name: Checkout
uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
# cache: npm
- name: Install deps - name: Install deps
run: npm install run: npm install
- name: Release to GitHub - name: Release to GitHub
# if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v')
run: | run: |
npm run release -- --no-increment --no-git --github.release --ci --verbose 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._

3
.gitignore vendored
View File

@ -4,4 +4,5 @@ node_modules
package-lock.json package-lock.json
pnpm-lock.yaml pnpm-lock.yaml
yarn.lock yarn.lock
zotero-cmd.json zotero-cmd.json
.DS_Store

View File

@ -18,13 +18,13 @@
"\tremoveIfExists: ${13:true},", "\tremoveIfExists: ${13:true},",
"\tcustomCheck: (doc: Document, options: ElementOptions) => ${14:true},", "\tcustomCheck: (doc: Document, options: ElementOptions) => ${14:true},",
"\tchildren: [$15]", "\tchildren: [$15]",
"}, ${16:container});" "}, ${16:container});",
] ],
}, },
"appendElement - minimum": { "appendElement - minimum": {
"scope": "javascript,typescript", "scope": "javascript,typescript",
"prefix": "appendElement", "prefix": "appendElement",
"body": "appendElement({ tag: '$1' }, $2);" "body": "appendElement({ tag: '$1' }, $2);",
}, },
"register Notifier": { "register Notifier": {
"scope": "javascript,typescript", "scope": "javascript,typescript",
@ -39,7 +39,7 @@
"\t) => {", "\t) => {",
"\t\t$0", "\t\t$0",
"\t}", "\t}",
"});" "});",
] ],
} },
} }

420
README.md
View File

@ -1,417 +1,17 @@
# Zotero Plugin Template # Zotero TL;DR
[![zotero target version](https://img.shields.io/badge/Zotero-7-green?style=flat-square&logo=zotero&logoColor=CC2936)](https://www.zotero.org) [![zotero target version](https://img.shields.io/badge/Zotero-7-red?style=flat-square&logo=zotero&logoColor=CC2936)](https://www.zotero.org)
[![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template) [![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template)
This is a plugin template for [Zotero](https://www.zotero.org/). 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.
[English](README.md) | [简体中文](doc/README-zhCN.md) ## Install
[📖 Plugin Development Documentation](https://zotero.yuque.com/books/share/8d230829-6004-4934-b4c6-685a7001bfa0/vec88d) (Chinese, outdated) 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 for Zotero 7](https://www.zotero.org/support/dev/zotero_7_for_developers) ## Usage
[🛠️ 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) There are no configuration steps required.
The add-on will automatically fetch the TL;DR information for all items.
[ Zotero Type Definitions](https://github.com/windingwind/zotero-types) You can view the TLDR information in details on the right side.
[📜 Zotero Source Code](https://github.com/zotero/zotero)
[📌 Zotero Plugin Template](https://github.com/windingwind/zotero-plugin-template) (This repo)
> [!tip]
> 👁 Watch this repo so that you can be notified whenever there are fixes & updates.
## Plugins built with this template
[![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-better-notes?label=zotero-better-notes&style=flat-square)](https://github.com/windingwind/zotero-better-notes)
[![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-pdf-preview?label=zotero-pdf-preview&style=flat-square)](https://github.com/windingwind/zotero-pdf-preview)
[![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-pdf-translate?label=zotero-pdf-translate&style=flat-square)](https://github.com/windingwind/zotero-pdf-translate)
[![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-tag?label=zotero-tag&style=flat-square)](https://github.com/windingwind/zotero-tag)
[![GitHub Repo stars](https://img.shields.io/github/stars/iShareStuff/ZoteroTheme?label=zotero-theme&style=flat-square)](https://github.com/iShareStuff/ZoteroTheme)
[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-reference?label=zotero-reference&style=flat-square)](https://github.com/MuiseDestiny/zotero-reference)
[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-citation?label=zotero-citation&style=flat-square)](https://github.com/MuiseDestiny/zotero-citation)
[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/ZoteroStyle?label=zotero-style&style=flat-square)](https://github.com/MuiseDestiny/ZoteroStyle)
[![GitHub Repo stars](https://img.shields.io/github/stars/volatile-static/Chartero?label=Chartero&style=flat-square)](https://github.com/volatile-static/Chartero)
[![GitHub Repo stars](https://img.shields.io/github/stars/l0o0/tara?label=tara&style=flat-square)](https://github.com/l0o0/tara)
[![GitHub Repo stars](https://img.shields.io/github/stars/redleafnew/delitemwithatt?label=delitemwithatt&style=flat-square)](https://github.com/redleafnew/delitemwithatt)
[![GitHub Repo stars](https://img.shields.io/github/stars/redleafnew/zotero-updateifsE?label=zotero-updateifsE&style=flat-square)](https://github.com/redleafnew/zotero-updateifsE)
[![GitHub Repo stars](https://img.shields.io/github/stars/northword/zotero-format-metadata?label=zotero-format-metadata&style=flat-square)](https://github.com/northword/zotero-format-metadata)
[![GitHub Repo stars](https://img.shields.io/github/stars/inciteful-xyz/inciteful-zotero-plugin?label=inciteful-zotero-plugin&style=flat-square)](https://github.com/inciteful-xyz/inciteful-zotero-plugin)
[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-gpt?label=zotero-gpt&style=flat-square)](https://github.com/MuiseDestiny/zotero-gpt)
[![GitHub Repo stars](https://img.shields.io/github/stars/zoushucai/zotero-journalabbr?label=zotero-journalabbr&style=flat-square)](https://github.com/zoushucai/zotero-journalabbr)
[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-figure?label=zotero-figure&style=flat-square)](https://github.com/MuiseDestiny/zotero-figure)
[![GitHub Repo stars](https://img.shields.io/github/stars/l0o0/jasminum?label=jasminum&style=flat-square)](https://github.com/l0o0/jasminum)
[![GitHub Repo stars](https://img.shields.io/github/stars/lifan0127/ai-research-assistant?label=ai-research-assistant&style=flat-square)](https://github.com/lifan0127/ai-research-assistant)
If you are using this repo, I recommended that you put the following badge on your README:
[![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template)
```md
[![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template)
```
## Features
- 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
![image](https://user-images.githubusercontent.com/33902321/211739774-cc5c2df8-5fd9-42f0-9cdf-0f2e5946d427.png)
- registerStyleSheet(the official make-it-red example)
- registerRightClickMenuItem
- registerRightClickMenuPopup
- registerWindowMenuWithSeprator
- registerExtraColumn
- registerExtraColumnWithCustomCell
- registerCustomItemBoxRow
- registerLibraryTabPanel
- registerReaderTabPanel
### Preference Pane Examples
![image](https://user-images.githubusercontent.com/33902321/211737987-cd7c5c87-9177-4159-b975-dc67690d0490.png)
- Preferences bindings
- UI Events
- Table
- Locale
See [`src/modules/preferenceScript.ts`](./src/modules/preferenceScript.ts)
### HelperExamples
![image](https://user-images.githubusercontent.com/33902321/215119473-e7d0d0ef-6d96-437e-b989-4805ffcde6cf.png)
- dialogExample
- clipboardExample
- filePickerExample
- progressWindowExample
- vtableExample(See Preference Pane Examples)
### PromptExamples
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`.
![image](https://user-images.githubusercontent.com/33902321/215120009-e7c7ed27-33a0-44fe-b021-06c272481a92.png)
- 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: 677 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

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

View File

@ -1,11 +1,8 @@
startup-begin = Addon is loading menuitem-updatetldrlabel = update TLDR
startup-finish = Addon is ready menucollection-updatetldrlabel = update TLDR
menuitem-label = Addon Template: Helper Examples itembox-tldrlabel = TLDR
menupopup-label = Addon Template: Menupopup tldr-unrelated = TLDR Unrelated in Semantic scholar
menuitem-submenulabel = Addon Template tldr-itemnotfound = Item Not Found in Semantic scholar
menuitem-filemenulabel = Addon Template: File Menuitem popWindow-succeed = Succeed
prefs-title = Template popWindow-failed = Failed
prefs-table-title = Title popWindow-waiting = Waiting
prefs-table-detail = Detail
tabpanel-lib-tab-label = Lib Tab
tabpanel-reader-tab-label = Reader Tab

View File

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

View File

@ -1,5 +0,0 @@
pref-title = Addon Template Example
pref-enable =
.label = Enable
pref-input = Input
pref-help = { $name } Build { $version } { $time }

View File

@ -1,11 +1,8 @@
startup-begin = 插件加载中 menuitem-updatetldrlabel = 更新TLDR
startup-finish = 插件已就绪 menucollection-updatetldrlabel = 批量更新TLDR
menuitem-label = 插件模板: 帮助工具样例 itembox-tldrlabel = TLDR
menupopup-label = 插件模板: 弹出菜单 tldr-unrelated = 未关联TLDR
menuitem-submenulabel = 插件模板:子菜单 tldr-itemnotfound = 未搜索到此条目
menuitem-filemenulabel = 插件模板: 文件菜单 popWindow-succeed = 成功
prefs-title = 插件模板 popWindow-failed = 失败
prefs-table-title = 标题 popWindow-waiting = 等待
prefs-table-detail = 详情
tabpanel-lib-tab-label = 库标签
tabpanel-reader-tab-label = 阅读器标签

View File

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

View File

@ -1,5 +0,0 @@
pref-title = 插件模板设置示例
pref-enable =
.label = 开启
pref-input = 输入
pref-help = { $name } Build { $version } { $time }

View File

@ -1,3 +1 @@
/* eslint-disable no-undef */ /* eslint-disable no-undef */
pref("__prefsPrefix__.enable", true);
pref("__prefsPrefix__.input", "This is input");

View File

@ -7,7 +7,22 @@
[English](../README.md) | [简体中文](./README-zhCN.md) [English](../README.md) | [简体中文](./README-zhCN.md)
使用此模板创建的一些插件: 📖 [插件开发文档](https://zotero.yuque.com/books/share/8d230829-6004-4934-b4c6-685a7001bfa0/vec88d) (中文版,已过时)
[📖 Zotero 7 插件开发文档](https://www.zotero.org/support/dev/zotero_7_for_developers)
🛠️ [Zotero 插件工具包](https://github.com/windingwind/zotero-plugin-toolkit) | [API 文档](https://github.com/windingwind/zotero-plugin-toolkit/blob/master/docs/zotero-plugin-toolkit.md)
[Zotero 类型定义](https://github.com/windingwind/zotero-types)
📜 [Zotero 源代码](https://github.com/zotero/zotero)
📌 [Zotero 插件模板](https://github.com/windingwind/zotero-plugin-template) (即本仓库)
> [!tip]
> 👁 Watch 本仓库,以及时收到修复或更新的通知.
## 使用此模板构建的插件
[![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-better-notes?label=zotero-better-notes&style=flat-square)](https://github.com/windingwind/zotero-better-notes) [![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-better-notes?label=zotero-better-notes&style=flat-square)](https://github.com/windingwind/zotero-better-notes)
[![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-pdf-preview?label=zotero-pdf-preview&style=flat-square)](https://github.com/windingwind/zotero-pdf-preview) [![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-pdf-preview?label=zotero-pdf-preview&style=flat-square)](https://github.com/windingwind/zotero-pdf-preview)
@ -26,21 +41,10 @@
[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-gpt?label=zotero-gpt&style=flat-square)](https://github.com/MuiseDestiny/zotero-gpt) [![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-gpt?label=zotero-gpt&style=flat-square)](https://github.com/MuiseDestiny/zotero-gpt)
[![GitHub Repo stars](https://img.shields.io/github/stars/zoushucai/zotero-journalabbr?label=zotero-journalabbr&style=flat-square)](https://github.com/zoushucai/zotero-journalabbr) [![GitHub Repo stars](https://img.shields.io/github/stars/zoushucai/zotero-journalabbr?label=zotero-journalabbr&style=flat-square)](https://github.com/zoushucai/zotero-journalabbr)
[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-figure?label=zotero-figure&style=flat-square)](https://github.com/MuiseDestiny/zotero-figure) [![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-figure?label=zotero-figure&style=flat-square)](https://github.com/MuiseDestiny/zotero-figure)
[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-file?label=WanderingFile&style=flat-square)](https://github.com/MuiseDestiny/zotero-file)
[![GitHub Repo stars](https://img.shields.io/github/stars/l0o0/jasminum?label=jasminum&style=flat-square)](https://github.com/l0o0/jasminum) [![GitHub Repo stars](https://img.shields.io/github/stars/l0o0/jasminum?label=jasminum&style=flat-square)](https://github.com/l0o0/jasminum)
[![GitHub Repo stars](https://img.shields.io/github/stars/lifan0127/ai-research-assistant?label=ai-research-assistant&style=flat-square)](https://github.com/lifan0127/ai-research-assistant) [![GitHub Repo stars](https://img.shields.io/github/stars/lifan0127/ai-research-assistant?label=ai-research-assistant&style=flat-square)](https://github.com/lifan0127/ai-research-assistant)
📖 [插件开发文档](https://zotero.yuque.com/books/share/8d230829-6004-4934-b4c6-685a7001bfa0/vec88d) (中文版) [![GitHub Repo stars](https://img.shields.io/github/stars/daeh/zotero-markdb-connect?label=zotero-markdb-connect&style=flat-square)](https://github.com/daeh/zotero-markdb-connect)
🛠️ [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) (即当前库)
> 👁 关注此库,以便在有修复或更新时及时收到通知.
如果你正在使用此库,我建议你将这个标志 ([![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template)) 放在 README 文件中: 如果你正在使用此库,我建议你将这个标志 ([![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template)) 放在 README 文件中:
@ -50,19 +54,21 @@
## Features 特性 ## Features 特性
> ❗Zotero系统已升级(dtd 已弃用,我们将不在使用 .properties). 主分支将只支持 Zotero 7.0.0-beta.12 或更高版本. 如果需要支持 Zotero 6可能需要同时使用`dtd`、`properties` 和`ftl`. 请参考此库的 `zotero6-bootstrap` 分支.
- 事件驱动、函数式编程的可扩展框架; - 事件驱动、函数式编程的可扩展框架;
- 简单易用,开箱即用; - 简单易用,开箱即用;
- ⭐[新特性!]自动热重载!每当修改源码时,都会自动编译并重新加载插件;[详情请跳转→](#auto-hot-reload) - ⭐[新特性!]自动热重载!每当修改源码时,都会自动编译并重新加载插件;[详情请跳转→](#自动热重载)
- `src/modules/examples.ts` 中有丰富的示例涵盖了插件中常用的大部分API(使用的插件工具包 [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit)) - `src/modules/examples.ts` 中有丰富的示例涵盖了插件中常用的大部分API (使用的插件工具包 zotero-plugin-toolkit仓库地址 https://github.com/windingwind/zotero-plugin-toolkit)
- TypeScript 支持: - 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 中构建和重新加载代码; - 自动在 Zotero 中构建和重新加载代码;
- 自动发布到GitHub (使用[release-it](https://github.com/release-it/release-it)); - 自动发布到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 示例 ## Examples 示例
@ -102,7 +108,7 @@
- Preferences bindings - Preferences bindings
- UI Events - UI Events
- Tabel - Table
- Locale - Locale
详情参见 [`src/modules/preferenceScript.ts`](./src/modules/preferenceScript.ts) 详情参见 [`src/modules/preferenceScript.ts`](./src/modules/preferenceScript.ts)
@ -129,108 +135,89 @@ Obsidian风格的指令输入模块它通过接受文本来运行插件
## Quick Start Guide 快速入门指南 ## Quick Start Guide 快速入门指南
### 安装预构建 `xpi` ### 0 前置要求(Requirement)
通过直接在GitHub中下载构建好的 `xpi` 文件并将其安装到Zotero中来了解示例的工作原理. 1. 安装测试版 Zoterohttps://www.zotero.org/support/beta_builds
2. 安装 Node.jshttps://nodejs.org/en/)和 Githttps://git-scm.com/
这也是你发布插件的格式,同时这也将是其他人可以直接使用的版本. > [!note]
> 本指南假定你已经对 Zotero 插件的基本结构和工作原理有初步的了解. 如果你还不了解请先参考官方文档https://www.zotero.org/support/dev/zotero_7_for_developers和官方插件样例 Make It Red仓库地址 https://github.com/zotero/make-it-red.
> 该库构建好的xpi文件不具有任何实际功能它可能不随Zotero更新而随时更新. ### 1 创建你的仓库(Create Your Repo)
>
> `xpi` 文件实际上是一个zip压缩包然而请不要直接修改它而是修改源代码并重新构建它.
### 从源码构建(Build from Source) 1. 点击 `Use this template`
2. 使用 `git clone` 克隆上一步生成的仓库;
<details >
<summary>💡 从 GitHub Codespace 开始</summary>
- Fork 此库或者使用 `Use this template` _GitHub CodeSpace_ 使你可以直接开始开发而无需在本地下载代码/IDE/依赖.
- 使用 `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 加载.
- 去 [homepage](https://github.com/windingwind/zotero-plugin-template)顶部,点击绿色按钮`Use this template`,点击 `Open in codespace` 你需要登录你的GitHub账号. </details>
- 等待 codespace 加载.
- 修改 `./package.json` 中的设置,包括:
</details>
```json5 3. 进入项目文件夹;
{
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}
}
}
```
> 注意设置 addonID 和 addonRef 以避免冲突. ### 2 配置模板和开发环境(Config Template Settings and Enviroment)
- 运行 `npm install` 以设置插件并安装相关依赖. 如果你没有安装 Node.js请在[此处下载](https://nodejs.org/en/); 1. 修改 `./package.json` 中的设置,包括:
- 运行 `npm run build` 以在生产模式下构建插件,运行 `npm run build-dev` 以在开发模式下构建插件. 用于安装的 xpi 文件和用于构建的代码在 `build` 文件夹下.
> Dev & prod 两者有什么区别? ```json5
> {
> - 此环境变量存储在 `Zotero.${addonInstance}.data.env` 中,控制台输出在生产模式下被禁用. 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
},
}
```
### 发布(Release) > [!warning]
> 注意设置 addonID 和 addonRef 以避免冲突.
如果要构建和发布插件,运行如下指令: 如果你需要在GitHub以外的地方托管你的 XPI 包,请删除 `releasePage` 并添加 `updateLink`,并将值设置为你的 XPI 下载地址.
```shell 2. 复制 Zotero 启动配置,填入 Zotero 可执行文件路径和 profile 路径.
# 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创建一个新的配置文件并用作开发配置文件. > (可选项) 此操作仅需执行一次: 使用 `/path/to/zotero -p` 启动 Zotero创建一个新的配置文件并用作开发配置文件.
> 将配置文件的路径 `profilePath` 放入 `zotero-cmd.json` 中,以指定要使用的配置文件. > 将配置文件的路径 `profilePath` 放入 `zotero-cmd.json` 中,以指定要使用的配置文件.
```sh ```sh
cp ./scripts/zotero-cmd-default.json ./scripts/zotero-cmd.json cp ./scripts/zotero-cmd-template.json ./scripts/zotero-cmd.json
vim ./scripts/zotero-cmd.json vim ./scripts/zotero-cmd.json
``` ```
4. 构建插件并使用 `npm run restart` 重启 Zotero. 3. 运行 `npm install` 以安装相关依赖
5. 启动 Firefox 102 (Zotero 7) > 如果你使用 `pnpm` 作为包管理器,你需要添加 `public-hoist-pattern[]=*@types/bluebird*` 到`.npmrc`, 详情请查看 zotero-typeshttps://github.com/windingwind/zotero-types?tab=readme-ov-file#usage的文档.
6. 在 Firefox 中转到devtools转到设置单击 "enable remote debugging" ,同时,旁边的按钮也是关于调试的。 ### 3 开始开发(Coding)
> 在 FirFox 102 中输入 `about:debugging#/setup` . 使用 `npm start` 启动开发服务器,它将:
7. 在 Zotero 中,进入设置-高级-编辑器,搜索 "debugging" 然后单击 "allow remote debugging". - 在开发模式下预构建插件
- 启动 Zotero ,并让其从 `build/` 中加载插件
- 打开开发者工具devtool
- 监听 `src/**``addon/**`.
- 如果 `src/**` 修改了,运行 esbuild 并且重新加载
- 如果 `addon/**` 修改了,(在开发模式下)重新构建插件并且重新加载
8. 在 Firefox 中连接 Zotero. 在 FireFox 102中在远程调试页面底部输入 `localhost:6100` 然后单击 `add`. #### 自动热重载
9. 在远程调试页面左侧栏点击 `connect`.
10. 点击 "Inspect Main Process"
### 自动热重载(Auto Hot Reload)
厌倦了无休止的重启吗?忘掉它,拥抱热加载! 厌倦了无休止的重启吗?忘掉它,拥抱热加载!
1. 运行 `npm run start-watch`. (如果Zotero已经在运行请使用 `npm run watch`) 1. 运行 `npm start`.
2. 编码. (是的,就这么简单) 2. 编码. (是的,就这么简单)
当检测到 `src``addon` 中的文件修改时,插件将自动编译并重新加载. 当检测到 `src``addon` 中的文件修改时,插件将自动编译并重新加载.
@ -238,27 +225,76 @@ npm run release
<details style="text-indent: 2em"> <details style="text-indent: 2em">
<summary>💡 将此功能添加到现有插件的步骤</summary> <summary>💡 将此功能添加到现有插件的步骤</summary>
1. 复制 `scripts/reload.mjs` 1. 复制 `scripts/**.mjs`
2. 复制 `reload` 、`watch` 和 `start-watch` 命令 `package.json` 2. 复制 `server` 、`build` 和 `stop` 命令到 `package.json`
3. 运行 `npm install --save-dev chokidar-cli` 3. 运行 `npm install --save-dev chokidar`
4. 结束. 4. 结束.
</details> </details>
### 在 Zotero 中调试 #### 在 Zotero 中 Debug
你还可以: 你还可以:
- 在 Tools->Developer->Run Javascript 中测试代码片段; - 在 Tools->Developer->Run Javascript 中测试代码片段;
- 使用 `Zotero.debug()` 调试输出. 在 Help->Debug Output Logging->View Output 查看输出; - 使用 `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. - 调试 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> > XUL 文档: <http://www.devdoc.net/web/developer.mozilla.org/en-US/docs/XUL.html>
### 4 构建(Build)
运行 `npm run build` 在生产模式下构建插件,构建的结果位于 `build/` 目录中.
`scripts/build.mjs` 的运行步骤:
- 创建/清空 `build/`
- 复制 `addon/**``build/addon/**`
- 替换占位符:使用 `replace-in-file` 去替换在 `package.json` 中定义的关键字和配置 (`xhtml`、`.flt` 等)
- 准备本地化文件以避免冲突查看官方文档了解更多https://www.zotero.org/support/dev/zotero_7_for_developers#avoiding_localization_conflicts
- 重命名`**/*.flt` 为 `**/${addonRef}-*.flt`
- 在每个消息前加上 `addonRef-`
- 使用 Esbuild 来将 `.ts` 源码构建为 `.js`,从 `src/index.ts` 构建到`./build/addon/chrome/content/scripts`
- (仅在生产模式下工作) 压缩 `./build/addon` 目录为 `./build/*.xpi`
- (仅在生产模式下工作) 准备 `update.json``update-beta.json`
> [!note]
>
> **Dev & prod 两者有什么区别?**
>
> - 此环境变量存储在 `Zotero.${addonInstance}.data.env` 中,控制台输出在生产模式下被禁用.
> - 你可以根据此变量决定用户无法查看/使用的内容.
> - 在生产模式下,构建脚本将自动打包插件并更新 `update.json`.
### 5 发布(Release)
如果要构建和发布插件,运行如下指令:
```shell
# A release-it command: version increase, npm run build, git push, and GitHub release
# release-it: https://github.com/release-it/release-it
npm run release
```
> [!note]
> 在此模板中release-it 被配置为在本地升级版本、构建、推送提交和 git 标签随后GitHub Action 将重新构建插件并将 XPI 发布到 GitHub Release.
>
> 如果你需要发布一个本地构建的 XPI`package.json` 中的 `release-it.github.release` 设置为 `true`,然后移除 `.github/workflows/release.yml`. 此外,你还需要设置环境变量 `GITHUB_TOKEN`,获取 GitHub Tokenhttps://github.com/settings/tokens.
#### 关于预发布
该模板将 `prerelease` 定义为插件的测试版,当你在 release-it 中选择 `prerelease` 版本 (版本号中带有 `-` ),构建脚本将创建一个 `update-beta.json` 给预发布版本使用,这将确保常规版本的用户不会自动更新到测试版,只有手动下载并安装了测试版的用户才能自动更新到下一个测试版. 当下一个正式版本更新时,脚本将同步更新 `update.json``update-beta.json`,这将使正式版和测试版用户都可以更新到最新的正式版.
> [!warning]
> 严格来说,区分 Zotero 6 和 Zotero 7 兼容的插件版本应该通过 `update.json``addons.__addonID__.updates[]` 中分别配置 `applications.zotero.strict_min_version`,这样 Zotero 才能正确识别,详情在 Zotero 7 开发文档https://www.zotero.org/support/dev/zotero_7_for_developers#updaterdf_updatesjson获取.
## Details 更多细节 ## Details 更多细节
### 关于Hooks(About Hooks) ### 关于Hooks(About Hooks)
> 可以在 [`src/hooks.ts`](https://github.com/windingwind/zotero-plugin-template/blob/bootstrap/src/hooks.ts) 中查看更多 > 可以在 [`src/hooks.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/hooks.ts) 中查看更多
1. 当在 Zotero 中触发安装/启用/启动时,`bootstrap.js` > `startup` 被调用 1. 当在 Zotero 中触发安装/启用/启动时,`bootstrap.js` > `startup` 被调用
- 等待 Zotero 就绪 - 等待 Zotero 就绪
@ -272,9 +308,9 @@ npm run release
### 关于全局变量(About Global Variables) ### 关于全局变量(About Global Variables)
> 可以在 [`src/index.ts`](https://github.com/windingwind/zotero-plugin-template/blob/bootstrap/src/index.ts) 中查看更多 > 可以在 [`src/index.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/index.ts)中查看更多
引导插件在沙盒中运行,但沙盒中没有默认的全局变量,例如 `Zotero``window` 等我们曾在覆盖插件环境中使用的变量. bootstrap插件在沙盒中运行,但沙盒中没有默认的全局变量,例如 `Zotero``window` 等我们曾在overlay插件环境中使用的变量.
此模板将以下变量注册到全局范围: 此模板将以下变量注册到全局范围:
@ -295,23 +331,9 @@ createElement(document, "hbox"); // returns XUL.Box
createElement(document, "button", { namespace: "xul" }); // manually set namespace. returns XUL.Button 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 API(About Zotero API)
Zotero 文档已过时且不完整,git clone https://github.com/zotero/zotero 并全局搜索关键字. Zotero 文档已过时且不完整,克隆 https://github.com/zotero/zotero 并全局搜索关键字.
> ⭐[zotero-types](https://github.com/windingwind/zotero-types) 提供了最常用的 Zotero API在默认情况下它被包含在此模板中. 你的 IDE 将为大多数的 API 提供提醒. > ⭐[zotero-types](https://github.com/windingwind/zotero-types) 提供了最常用的 Zotero API在默认情况下它被包含在此模板中. 你的 IDE 将为大多数的 API 提供提醒.
@ -365,11 +387,14 @@ Zotero 文档已过时且不完整git clone https://github.com/zotero/zotero
| `-- prefs.js | `-- prefs.js
|-- build/ # build dir |-- build/ # build dir
|-- scripts # scripts for dev |-- scripts # scripts for dev
| |-- build.mjs # esbuild and replace | |-- build.mjs # script to build plugin
| |-- reload.mjs | |-- scripts.mjs # scripts send to Zotero, such as reload, openDevTool, etc
| |-- start.mjs | |-- server.mjs # script to start a development server
| |-- stop.mjs | |-- start.mjs # script to start Zotero process
| `-- zotero-cmd-default.json | |-- stop.mjs # script to kill Zotero process
| |-- utils.mjs # utils functions for dev scripts
| |-- update-template.json # template of `update.json`
| `-- zotero-cmd-template.json # template of local env
|-- src # source code |-- src # source code
| |-- addon.ts # base class | |-- addon.ts # base class
| |-- hooks.ts # lifecycle hooks | |-- hooks.ts # lifecycle hooks
@ -385,7 +410,6 @@ Zotero 文档已过时且不完整git clone https://github.com/zotero/zotero
|-- tsconfig.json # https://code.visualstudio.com/docs/languages/jsconfig |-- tsconfig.json # https://code.visualstudio.com/docs/languages/jsconfig
|-- typings # ts typings |-- typings # ts typings
| `-- global.d.ts | `-- global.d.ts
|-- update-template.json # template of `update.json`
`-- update.json `-- update.json
``` ```

View File

@ -1,15 +1,15 @@
{ {
"name": "zotero-addon-template", "name": "zotero-tldr",
"version": "1.1.0", "version": "1.0.7",
"description": "Zotero Addon Template", "description": "TLDR(too long; didn't read) from sematic scholar",
"config": { "config": {
"addonName": "Zotero Addon Template", "addonName": "Zotero TLDR",
"addonID": "addontemplate@euclpts.com", "addonID": "zoterotldr@syt.com",
"addonRef": "addontemplate", "addonRef": "zoterotldr",
"addonInstance": "AddonTemplate", "addonInstance": "ZoteroTLDR",
"prefsPrefix": "extensions.zotero.addontemplate", "prefsPrefix": "extensions.zotero.zoterotldr",
"releasePage": "https://github.com/windingwind/zotero-addon-template/releases", "releasepage": "https://github.com/syt2/zotero-tldr/releases",
"updateJSON": "https://raw.githubusercontent.com/windingwind/zotero-addon-template/main/update.json" "updateJSON": "https://raw.githubusercontent.com/syt2/zotero-tldr/main/update.json"
}, },
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {
@ -18,36 +18,36 @@
"stop": "node scripts/stop.mjs", "stop": "node scripts/stop.mjs",
"lint": "prettier --write . && eslint . --ext .ts --fix", "lint": "prettier --write . && eslint . --ext .ts --fix",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"release": "release-it", "release": "release-it --only-version --preReleaseId=beta",
"update-deps": "npm update --save" "update-deps": "npm update --save"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/windingwind/zotero-addon-template.git" "url": "git+https://github.com/syt2/zotero-tldr.git"
}, },
"author": "windingwind", "author": "syt2",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"bugs": { "bugs": {
"url": "https://github.com/windingwind/zotero-addon-template/issues" "url": "https://github.com/syt2/zotero-tldr/issues"
}, },
"homepage": "https://github.com/windingwind/zotero-addon-template#readme", "homepage": "https://github.com/syt2/zotero-tldr#readme",
"dependencies": { "dependencies": {
"zotero-plugin-toolkit": "^2.3.11" "zotero-plugin-toolkit": "^2.3.29"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.10.4", "@types/node": "^20.10.4",
"@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^6.13.2", "@typescript-eslint/parser": "^7.1.1",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"compressing": "^1.10.0", "compressing": "^1.10.0",
"esbuild": "^0.19.8", "esbuild": "^0.20.1",
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"prettier": "^3.1.0", "prettier": "^3.1.1",
"release-it": "^17.0.1", "release-it": "^17.0.1",
"replace-in-file": "^7.0.2", "replace-in-file": "^7.0.2",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"zotero-types": "^1.3.7" "zotero-types": "^1.3.10"
}, },
"eslintConfig": { "eslintConfig": {
"env": { "env": {
@ -115,7 +115,7 @@
}, },
"release-it": { "release-it": {
"git": { "git": {
"tagName": "v${version}" "tagName": "V${version}"
}, },
"npm": { "npm": {
"publish": false "publish": false
@ -128,8 +128,7 @@
}, },
"hooks": { "hooks": {
"before:init": "npm run lint", "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}."
} }
} }
} }

View File

@ -45,6 +45,8 @@ function replaceString(buildTime) {
const replaceResult = replaceInFileSync({ const replaceResult = replaceInFileSync({
files: [ files: [
`${buildDir}/addon/**/*.xhtml`, `${buildDir}/addon/**/*.xhtml`,
`${buildDir}/addon/**/*.html`,
`${buildDir}/addon/**/*.css`,
`${buildDir}/addon/**/*.json`, `${buildDir}/addon/**/*.json`,
`${buildDir}/addon/prefs.js`, `${buildDir}/addon/prefs.js`,
`${buildDir}/addon/manifest.json`, `${buildDir}/addon/manifest.json`,
@ -62,83 +64,75 @@ function replaceString(buildTime) {
} }
function prepareLocaleFiles() { function prepareLocaleFiles() {
// Walk the builds/addon/locale folder's sub folders and rename *.ftl to addonRef-*.ftl // Prefix Fluent messages in xhtml
const localeDir = path.join(buildDir, "addon/locale"); const MessagesInHTML = new Set();
const localeFolders = readdirSync(localeDir, { withFileTypes: true }) replaceInFileSync({
.filter((dirent) => dirent.isDirectory()) files: [`${buildDir}/addon/**/*.xhtml`, `${buildDir}/addon/**/*.html`],
.map((dirent) => dirent.name);
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);
for (const localeSubFile of localeSubFiles) {
if (localeSubFile.endsWith(".ftl")) {
renameSync(
path.join(localeSubDir, localeSubFile),
path.join(localeSubDir, `${config.addonRef}-${localeSubFile}`),
);
}
}
}
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) => { processor: (input) => {
const matchs = [...input.matchAll(/(data-l10n-id)="(\S*)"/g)]; const matchs = [...input.matchAll(/(data-l10n-id)="(\S*)"/g)];
matchs.map((match) => { matchs.map((match) => {
if (localeMessage.has(match[2])) { input = input.replace(
input = input.replace( match[0],
match[0], `${match[1]}="${config.addonRef}-${match[2]}"`,
`${match[1]}="${config.addonRef}-${match[2]}"`, );
); MessagesInHTML.add(match[2]);
} else {
localeMessageMiss.add(match[2]);
}
}); });
return input; return input;
}, },
}); });
Logger.debug( // Walk the sub folders of `build/addon/locale`
"[Build] Prepare locale files OK", const localesPath = path.join(buildDir, "addon/locale"),
// replaceResultFlt.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`), localeNames = readdirSync(localesPath, { withFileTypes: true })
// replaceResultXhtml.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`), .filter((dirent) => dirent.isDirectory())
); .map((dirent) => dirent.name);
if (localeMessageMiss.size !== 0) { for (const localeName of localeNames) {
Logger.warn( const localePath = path.join(localesPath, localeName);
`[Build] Fluent message [${new Array( const ftlFiles = readdirSync(localePath, {
...localeMessageMiss, withFileTypes: true,
)}] do not exsit in addon's locale files.`, })
); .filter((dirent) => dirent.isFile())
.map((dirent) => dirent.name);
// rename *.ftl to addonRef-*.ftl
for (const ftlFile of ftlFiles) {
if (ftlFile.endsWith(".ftl")) {
renameSync(
path.join(localePath, ftlFile),
path.join(localePath, `${config.addonRef}-${ftlFile}`),
);
}
}
// Prefix Fluent messages in each ftl
const MessageInThisLang = new Set();
replaceInFileSync({
files: [`${buildDir}/addon/locale/${localeName}/*.ftl`],
processor: (fltContent) => {
const lines = fltContent.split("\n");
const prefixedLines = lines.map((line) => {
// https://regex101.com/r/lQ9x5p/1
const match = line.match(
/^(?<message>[a-zA-Z]\S*)([ ]*=[ ]*)(?<pattern>.*)$/m,
);
if (match) {
MessageInThisLang.add(match.groups.message);
return `${config.addonRef}-${line}`;
} else {
return line;
}
});
return prefixedLines.join("\n");
},
});
// If a message in xhtml but not in ftl of current language, log it
MessagesInHTML.forEach((message) => {
if (!MessageInThisLang.has(message)) {
Logger.error(`[Build] ${message} don't exist in ${localeName}`);
}
});
} }
} }
@ -210,19 +204,21 @@ export async function main() {
); );
clearFolder(buildDir); clearFolder(buildDir);
copyFolderRecursiveSync("addon", buildDir); copyFolderRecursiveSync("addon", buildDir);
replaceString(buildTime);
Logger.debug("[Build] Replace OK");
Logger.debug("[Build] Replacing");
replaceString(buildTime);
Logger.debug("[Build] Preparing locale files");
prepareLocaleFiles(); prepareLocaleFiles();
Logger.debug("[Build] Running esbuild");
await build(esbuildOptions); await build(esbuildOptions);
Logger.debug("[Build] Run esbuild OK");
Logger.debug("[Build] Addon prepare OK"); Logger.debug("[Build] Addon prepare OK");
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
Logger.debug("[Build] Packing Addon");
await zip.compressDir( await zip.compressDir(
path.join(buildDir, "addon"), path.join(buildDir, "addon"),
path.join(buildDir, `${name}.xpi`), path.join(buildDir, `${name}.xpi`),
@ -230,7 +226,6 @@ export async function main() {
ignoreBase: true, ignoreBase: true,
}, },
); );
Logger.debug("[Build] Addon pack OK");
prepareUpdateJson(); prepareUpdateJson();

View File

@ -68,10 +68,7 @@ async function main() {
await build(); await build();
// start Zotero // start Zotero
startZotero(); startZotero(openDevTool);
setTimeout(() => {
openDevTool();
}, 2000);
// watch // watch
await watch(); await watch();

View File

@ -2,7 +2,7 @@ import details from "../package.json" assert { type: "json" };
import { Logger } from "./utils.mjs"; import { Logger } from "./utils.mjs";
import cmd from "./zotero-cmd.json" assert { type: "json" }; import cmd from "./zotero-cmd.json" assert { type: "json" };
import { spawn } from "child_process"; import { spawn } from "child_process";
import { existsSync, readFileSync, writeFileSync } from "fs"; import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
import { clearFolder } from "./utils.mjs"; import { clearFolder } from "./utils.mjs";
import path from "path"; import path from "path";
import { exit } from "process"; import { exit } from "process";
@ -10,6 +10,9 @@ import { exit } from "process";
const { addonID } = details.config; const { addonID } = details.config;
const { zoteroBinPath, profilePath, dataDir } = cmd.exec; const { zoteroBinPath, profilePath, dataDir } = cmd.exec;
// Keep in sync with the addon's onStartup
const loadDevToolWhen = `Plugin ${addonID} startup`;
const logPath = "logs"; const logPath = "logs";
const logFilePath = path.join(logPath, "zotero.log"); const logFilePath = path.join(logPath, "zotero.log");
@ -42,6 +45,11 @@ function prepareDevEnv() {
writeAddonProxyFile(); writeAddonProxyFile();
} }
const addonXpiFilePath = path.join(profilePath, `extensions/${addonID}.xpi`);
if (existsSync(addonXpiFilePath)) {
rmSync(addonXpiFilePath);
}
const prefsPath = path.join(profilePath, "prefs.js"); const prefsPath = path.join(profilePath, "prefs.js");
if (existsSync(prefsPath)) { if (existsSync(prefsPath)) {
const PrefsLines = readFileSync(prefsPath, "utf-8").split("\n"); const PrefsLines = readFileSync(prefsPath, "utf-8").split("\n");
@ -53,7 +61,7 @@ function prepareDevEnv() {
return; return;
} }
if (line.includes("extensions.zotero.dataDir") && dataDir !== "") { if (line.includes("extensions.zotero.dataDir") && dataDir !== "") {
return `user_pref("extensions.zotero.dataDir", "${dataDir}");`; return `user_pref("extensions.zotero.dataDir", "${dataDir.replace(/\\\\?/g, "\\\\")}");`;
} }
return line; return line;
}); });
@ -68,7 +76,9 @@ function prepareLog() {
writeFileSync(logFilePath, ""); writeFileSync(logFilePath, "");
} }
export function main() { export function main(callback) {
let isZoteroReady = false;
prepareDevEnv(); prepareDevEnv();
prepareLog(); prepareLog();
@ -81,6 +91,10 @@ export function main() {
]); ]);
zoteroProcess.stdout.on("data", (data) => { zoteroProcess.stdout.on("data", (data) => {
if (!isZoteroReady && data.toString().includes(loadDevToolWhen)) {
isZoteroReady = true;
callback();
}
writeFileSync(logFilePath, data, { writeFileSync(logFilePath, data, {
flag: "a", flag: "a",
}); });

View File

@ -1,7 +1,7 @@
{ {
"usage": "Copy and rename this file to zotero-cmd.json. Edit the cmd.", "usage": "Copy and rename this file to zotero-cmd.json. Edit the cmd.",
"killZoteroWindows": "taskkill /f /im zotero.exe", "killZoteroWindows": "taskkill /f /im zotero.exe",
"killZoteroUnix": "kill -9 $(ps -x | grep zotero)", "killZoteroUnix": "kill -9 $(ps -x | grep '[z]otero' | awk '{print $1}')",
"exec": { "exec": {
"@comment-zoteroBinPath": "Please input the path of the Zotero binary file in `zoteroBinPath`.", "@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.", "@comment-zoteroBinPath-tip": "The path delimiter should be escaped as `\\` for win32. The path is `*/Zotero.app/Contents/MacOS/zotero` for MacOS.",

View File

@ -1,4 +1,3 @@
import { ColumnOptions } from "zotero-plugin-toolkit/dist/helpers/virtualizedTable";
import { DialogHelper } from "zotero-plugin-toolkit/dist/helpers/dialog"; import { DialogHelper } from "zotero-plugin-toolkit/dist/helpers/dialog";
import hooks from "./hooks"; import hooks from "./hooks";
import { createZToolkit } from "./utils/ztoolkit"; import { createZToolkit } from "./utils/ztoolkit";
@ -14,8 +13,6 @@ class Addon {
}; };
prefs?: { prefs?: {
window: Window; window: Window;
columns: Array<ColumnOptions>;
rows: Array<{ [dataKey: string]: string }>;
}; };
dialog?: DialogHelper; dialog?: DialogHelper;
}; };

View File

@ -1,14 +1,10 @@
import { import { RegisterFactory, UIFactory } from "./modules/Common";
BasicExampleFactory,
HelperExampleFactory,
KeyExampleFactory,
PromptExampleFactory,
UIExampleFactory,
} from "./modules/examples";
import { config } from "../package.json"; import { config } from "../package.json";
import { getString, initLocale } from "./utils/locale"; import { getString, initLocale } from "./utils/locale";
import { registerPrefsScripts } from "./modules/preferenceScript"; import { registerPrefsScripts } from "./modules/preferenceScript";
import { createZToolkit } from "./utils/ztoolkit"; import { createZToolkit } from "./utils/ztoolkit";
import { tldrs } from "./modules/dataStorage";
import { TLDRFetcher } from "./modules/tldrFetcher";
async function onStartup() { async function onStartup() {
await Promise.all([ await Promise.all([
@ -16,11 +12,19 @@ async function onStartup() {
Zotero.unlockPromise, Zotero.unlockPromise,
Zotero.uiReadyPromise, 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(); initLocale();
BasicExampleFactory.registerPrefs(); await tldrs.getAsync();
BasicExampleFactory.registerNotifier(); RegisterFactory.registerNotifier();
await onMainWindowLoad(window); await onMainWindowLoad(window);
} }
@ -29,58 +33,17 @@ async function onMainWindowLoad(win: Window): Promise<void> {
// Create ztoolkit for every window // Create ztoolkit for every window
addon.data.ztoolkit = createZToolkit(); addon.data.ztoolkit = createZToolkit();
const popupWin = new ztoolkit.ProgressWindow(config.addonName, { (win as any).MozXULElement.insertFTLIfNeeded(
closeOnClick: true, `${config.addonRef}-mainWindow.ftl`,
closeTime: -1, );
})
.createLine({
text: getString("startup-begin"),
type: "default",
progress: 0,
})
.show();
KeyExampleFactory.registerShortcuts(); UIFactory.registerRightClickMenuItem();
await Zotero.Promise.delay(1000); UIFactory.registerRightClickCollectionMenuItem();
popupWin.changeLine({
progress: 30,
text: `[30%] ${getString("startup-begin")}`,
});
UIExampleFactory.registerStyleSheet(); UIFactory.registerTLDRItemBoxRow();
UIExampleFactory.registerRightClickMenuItem(); onLoad();
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> { async function onMainWindowUnload(win: Window): Promise<void> {
@ -106,16 +69,11 @@ async function onNotify(
ids: Array<string | number>, ids: Array<string | number>,
extraData: { [key: string]: any }, extraData: { [key: string]: any },
) { ) {
// You can add your code to the corresponding notify type Zotero.log(`${event} ${type} ${ids}, ${extraData}`);
ztoolkit.log("notify", event, type, ids, extraData); if (event == "add" && type == "item" && ids.length > 0) {
if ( onNotifyAddItems(ids);
event == "select" && } else if (event == "delete" && type == "item" && ids.length > 0) {
type == "tab" && noNotifyDeleteItem(ids);
extraData[ids[0]].type == "reader"
) {
BasicExampleFactory.exampleNotifierCallback();
} else {
return;
} }
} }
@ -135,47 +93,105 @@ async function onPrefsEvent(type: string, data: { [key: string]: any }) {
} }
} }
function onShortcuts(type: string) { function onLoad() {
switch (type) { (async () => {
case "larger": let needFetchItems: Zotero.Item[] = [];
KeyExampleFactory.exampleShortcutLargerCallback(); for (const lib of Zotero.Libraries.getAll()) {
break; needFetchItems = needFetchItems.concat(
case "smaller": (await Zotero.Items.getAll(lib.id)).filter((item: Zotero.Item) => {
KeyExampleFactory.exampleShortcutSmallerCallback(); return item.isRegularItem();
break; }),
case "confliction": );
KeyExampleFactory.exampleShortcutConflictingCallback(); }
break; onUpdateItems(needFetchItems, false);
default: })();
break;
}
} }
function onDialogEvents(type: string) { function noNotifyDeleteItem(ids: (string | number)[]) {
switch (type) { tldrs.modify((data) => {
case "dialogExample": ids.forEach((id) => {
HelperExampleFactory.dialogExample(); delete data[id];
break; });
case "clipboardExample": return data;
HelperExampleFactory.clipboardExample(); });
break; }
case "filePickerExample":
HelperExampleFactory.filePickerExample(); function onNotifyAddItems(ids: (string | number)[]) {
break; const addedRegularItems: Zotero.Item[] = [];
case "progressWindowExample": for (const id of ids) {
HelperExampleFactory.progressWindowExample(); const item = Zotero.Items.get(id);
break; if (item.isRegularItem()) {
case "vtableExample": addedRegularItems.push(item);
HelperExampleFactory.vtableExample(); }
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;
}
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. // 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. // 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 maintian. // Otherwise the code would be hard to read and maintain.
export default { export default {
onStartup, onStartup,
@ -184,6 +200,5 @@ export default {
onMainWindowUnload, onMainWindowUnload,
onNotify, onNotify,
onPrefsEvent, onPrefsEvent,
onShortcuts, onUpdateItems,
onDialogEvents,
}; };

117
src/modules/Common.ts Normal file
View File

@ -0,0 +1,117 @@
import { config } from "../../package.json";
import { getString } from "../utils/locale";
import { tldrs } from "./dataStorage";
export class RegisterFactory {
// 注册zotero的通知
static registerNotifier() {
const callback = {
notify: async (
event: string,
type: string,
ids: number[] | string[],
extraData: { [key: string]: any },
) => {
if (!addon?.data.alive) {
this.unregisterNotifier(notifierID);
return;
}
addon.hooks.onNotify(event, type, ids, extraData);
},
};
// Register the callback in Zotero as an item observer
const notifierID = Zotero.Notifier.registerObserver(callback, ["item"]);
// Unregister callback when the window closes (important to avoid a memory leak)
window.addEventListener(
"unload",
(e: Event) => {
this.unregisterNotifier(notifierID);
},
false,
);
}
private static unregisterNotifier(notifierID: string) {
Zotero.Notifier.unregisterObserver(notifierID);
}
}
export class UIFactory {
// item右键菜单
static registerRightClickMenuItem() {
const menuIcon = `chrome://${config.addonRef}/content/icons/favicon.png`;
// item menuitem with icon
ztoolkit.Menu.register("item", {
tag: "menuitem",
id: "zotero-itemmenu-tldr",
label: getString("menuitem-updatetldrlabel"),
commandListener: (ev) => {
const selectedItems = ZoteroPane.getSelectedItems() ?? [];
addon.hooks.onUpdateItems(selectedItems, selectedItems.length <= 1);
},
icon: menuIcon,
});
}
// collection右键菜单
static registerRightClickCollectionMenuItem() {
const menuIcon = `chrome://${config.addonRef}/content/icons/favicon.png`;
ztoolkit.Menu.register("collection", {
tag: "menuitem",
id: "zotero-collectionmenu-tldr",
label: getString("menucollection-updatetldrlabel"),
commandListener: (ev) =>
addon.hooks.onUpdateItems(
ZoteroPane.getSelectedCollection()?.getChildItems() ?? [],
false,
),
icon: menuIcon,
});
}
// tldr行
static async registerTLDRItemBoxRow() {
const itemTLDR = (item: Zotero.Item) => {
const noteKey = tldrs.get()[item.key];
if (noteKey) {
const obj = Zotero.Items.getByLibraryAndKey(item.libraryID, noteKey);
if (
obj &&
obj instanceof Zotero.Item &&
item.getNotes().includes(obj.id)
) {
let str = obj.getNote();
if (str.startsWith("<p>TL;DR</p>\n<p>")) {
str = str.slice("<p>TL;DR</p>\n<p>".length);
}
if (str.endsWith("</p>")) {
str = str.slice(0, -4);
}
return str;
}
}
return "";
};
Zotero.ItemPaneManager.registerSection({
paneID: config.addonRef,
pluginID: config.addonID,
header: {
l10nID: `${config.addonRef}-itemPaneSection-header`,
icon: `chrome://${config.addonRef}/content/icons/favicon@16.png`,
},
sidenav: {
l10nID: `${config.addonRef}-itemPaneSection-sidenav`,
icon: `chrome://${config.addonRef}/content/icons/favicon@20.png`,
},
onRender: ({ body, item }: any) => {
let tldr = itemTLDR(item);
if (tldr.length <= 0 && item.parentItem) {
tldr = itemTLDR(item.parentItem);
}
body.textContent = tldr;
},
});
}
}

124
src/modules/dataStorage.ts Normal file
View File

@ -0,0 +1,124 @@
import { config } from "../../package.json";
export class Data<K extends string | number | symbol, V> {
[x: string]: any;
private dataType: string;
private filePath?: string;
private _data: Record<K, V>;
constructor(dataType: string) {
this.dataType = dataType;
this._data = {} as Record<K, V>;
}
async getAsync() {
await this.initDataIfNeed();
return this.data;
}
get() {
return this.data;
}
async modify(
action: (data: Record<K, V>) => Record<K, V> | Promise<Record<K, V>>,
) {
await this.initDataIfNeed();
const data = this.data;
const newData = await action(data);
if (this.filePath) {
try {
await IOUtils.writeJSON(this.filePath, newData, {
mode: "overwrite",
compress: false,
});
this.data = newData;
return newData;
} catch (error) {
return data;
}
} else {
this.data = newData;
return newData;
}
}
async delete() {
if (this.filePath) {
try {
await IOUtils.remove(this.filePath);
this.data = {} as Record<K, V>;
return true;
} catch (error) {
return false;
}
} else {
this.data = {} as Record<K, V>;
return true;
}
}
private get data() {
return this._data;
}
private set data(value: Record<K, V>) {
this._data = value;
}
private async initDataIfNeed() {
if (this.inited) {
return;
}
this.inited = true;
const prefsFile = PathUtils.join(PathUtils.profileDir, "prefs.js");
const prefs = await Zotero.Profile.readPrefsFromFile(prefsFile);
let dir = prefs["extensions.zotero.dataDir"];
if (dir) {
dir = PathUtils.join(dir, config.addonName);
} else {
dir = PathUtils.join(
PathUtils.profileDir,
"extensions",
config.addonName,
);
}
IOUtils.makeDirectory(dir, {
createAncestors: true,
ignoreExisting: true,
});
this.filePath = PathUtils.join(dir, this.dataType);
try {
this.data = await IOUtils.readJSON(this.filePath, { decompress: false });
} catch (error) {
this.data = {} as Record<K, V>;
}
}
}
export class DataStorage {
private dataMap: { [key: string]: Data<any, any> } = {};
private static shared = new DataStorage();
static instance<K extends string | number | symbol, V>(
dataType: string,
): Data<K, V> {
if (this.shared.dataMap[dataType] === undefined) {
const data = new Data<K, V>(dataType);
this.shared.dataMap[dataType] = data;
return data;
} else {
return this.shared.dataMap[dataType];
}
}
private constructor() {
// empty
}
}
export const tldrs = DataStorage.instance<string, string | false>(
"fetchedItems.json",
);

View File

@ -1,966 +0,0 @@
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(
"![Plugin Template](https://github.com/windingwind/zotero-plugin-template)",
"text/unicode",
)
.addText(
'<a href="https://github.com/windingwind/zotero-plugin-template">Plugin Template</a>',
"text/html",
)
.copy();
ztoolkit.getGlobal("alert")("Copied!");
}
@example
static async filePickerExample() {
const path = await new ztoolkit.FilePicker(
"Import File",
"open",
[
["PNG File(*.png)", "*.png"],
["Any", "*.*"],
],
"image.png",
).open();
ztoolkit.getGlobal("alert")(`Selected ${path}`);
}
@example
static progressWindowExample() {
new ztoolkit.ProgressWindow(config.addonName)
.createLine({
text: "ProgressWindow Example!",
type: "success",
progress: 100,
})
.show();
}
@example
static vtableExample() {
ztoolkit.getGlobal("alert")("See src/modules/preferenceScript.ts");
}
}

View File

@ -7,125 +7,11 @@ export async function registerPrefsScripts(_window: Window) {
if (!addon.data.prefs) { if (!addon.data.prefs) {
addon.data.prefs = { addon.data.prefs = {
window: _window, 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 { } else {
addon.data.prefs.window = _window; addon.data.prefs.window = _window;
} }
updatePrefsUI();
bindPrefEvents(); bindPrefEvents();
} }
async function updatePrefsUI() { function bindPrefEvents() {}
// 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}!`,
);
});
}

176
src/modules/tldrFetcher.ts Normal file
View File

@ -0,0 +1,176 @@
import { tldrs } from "./dataStorage";
type SemanticScholarItemInfo = {
title?: string;
abstract?: string;
tldr?: string;
};
export class TLDRFetcher {
private readonly zoteroItem: Zotero.Item;
private readonly title?: string;
private readonly abstract?: string;
constructor(item: Zotero.Item) {
this.zoteroItem = item;
if (item.isRegularItem()) {
this.title = item.getField("title") as string;
this.abstract = item.getField("abstractNote") as string;
}
}
async fetchTLDR() {
if (!this.title || this.title.length <= 0) {
return false;
}
const noteKey = (await tldrs.getAsync())[this.zoteroItem.key];
try {
const infos = await this.fetchRelevanceItemInfos(this.title);
for (const info of infos) {
let match = false;
if (info.title && this.title && this.checkLCS(info.title, this.title)) {
match = true;
} else if (
info.abstract &&
this.abstract &&
this.checkLCS(info.abstract, this.abstract)
) {
match = true;
}
if (match && info.tldr) {
let note = new Zotero.Item("note");
if (noteKey) {
const obj = Zotero.Items.getByLibraryAndKey(
this.zoteroItem.libraryID,
noteKey,
);
if (
obj &&
obj instanceof Zotero.Item &&
this.zoteroItem.getNotes().includes(obj.id)
) {
note = obj;
}
}
note.setNote(`<p>TL;DR</p>\n<p>${info.tldr}</p>`);
note.parentID = this.zoteroItem.id;
await note.saveTx();
await tldrs.modify((data: any) => {
data[this.zoteroItem.key] = note.key;
return data;
});
return true;
}
}
await tldrs.modify((data: any) => {
data[this.zoteroItem.key] = false;
return data;
});
} catch (error) {
Zotero.log(`post semantic scholar request error: ${error}`);
}
}
private async fetchRelevanceItemInfos(
title: string,
): Promise<SemanticScholarItemInfo[]> {
const semanticScholarURL = "https://www.semanticscholar.org/api/1/search";
const params = {
queryString: title,
page: 1,
pageSize: 10,
sort: "relevance",
authors: [],
coAuthors: [],
venues: [],
performTitleMatch: true,
requireViewablePdf: false,
includeTldrs: true,
};
const resp = await Zotero.HTTP.request("POST", semanticScholarURL, {
headers: { "Content-Type": "application/json" },
body: JSON.stringify(params),
});
if (resp.status === 200) {
const results = JSON.parse(resp.response).results;
return results.map((item: any) => {
const result = {
title: item.title.text,
abstract: item.paperAbstract.text,
tldr: undefined,
};
if (item.tldr) {
result.tldr = item.tldr.text;
}
return result;
});
}
return [];
}
private checkLCS(pattern: string, content: string): boolean {
const LCS = StringMatchUtils.longestCommonSubsequence(pattern, content);
return LCS.length >= Math.max(pattern.length, content.length) * 0.9;
}
}
class StringMatchUtils {
static longestCommonSubsequence(text1: string, text2: string): string {
const m = text1.length;
const n = text2.length;
const dp: number[][] = new Array(m + 1);
for (let i = 0; i <= m; i++) {
dp[i] = new Array(n + 1).fill(0);
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (text1[i - 1] === text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
let i = m,
j = n;
const lcs: string[] = [];
while (i > 0 && j > 0) {
if (text1[i - 1] === text2[j - 1]) {
lcs.unshift(text1[i - 1]);
i--;
j--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
i--;
} else {
j--;
}
}
return lcs.join("");
}
// static minWindow(s: string, t: string): [number, number] | null {
// const m = s.length, n = t.length
// let start = -1, minLen = Number.MAX_SAFE_INTEGER, i = 0, j = 0, end;
// while (i < m) {
// if (s[i] == t[j]) {
// if (++j == n) {
// end = i + 1;
// while (--j >= 0) {
// while (s[i--] != t[j]);
// }
// ++i; ++j;
// if (end - i < minLen) {
// minLen = end - i;
// start = i;
// }
// }
// }
// ++i;
// }
// return start == -1 ? null : [start, minLen];
// }
}

View File

@ -5,8 +5,8 @@
"target": "ES2016", "target": "ES2016",
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true "strict": true,
}, },
"include": ["src", "typings", "node_modules/zotero-types"], "include": ["src", "typings", "node_modules/zotero-types"],
"exclude": ["build", "addon"] "exclude": ["build", "addon"],
} }

View File

@ -1,10 +1,10 @@
{ {
"addons": { "addons": {
"addontemplate@euclpts.com": { "zoterotldr@syt.com": {
"updates": [ "updates": [
{ {
"version": "1.1.0", "version": "1.0.7",
"update_link": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi", "update_link": "undefined/latest/download/zotero-tldr.xpi",
"applications": { "applications": {
"zotero": { "zotero": {
"strict_min_version": "6.999" "strict_min_version": "6.999"