Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed129f2a45 | ||
|
|
2a9125aa99 | ||
|
|
5d4dde170a | ||
|
|
2d10915c7d | ||
|
|
18fac41190 | ||
|
|
f653f8a11a | ||
|
|
28cf7c86de | ||
|
|
6edfda0388 | ||
|
|
6056c4208f | ||
|
|
5cc74c4986 | ||
|
|
a26f5fd0a9 | ||
|
|
eec0d47a96 | ||
|
|
df847e59be | ||
|
|
50956fefff | ||
|
|
f93d22d29b | ||
|
|
a9d13a20c6 | ||
|
|
3c9695fa71 | ||
|
|
735da8a43a | ||
|
|
b56669ae8f | ||
|
|
66c0460513 | ||
|
|
f52eb9f100 | ||
|
|
0075dc8a43 | ||
|
|
cd14a26c11 | ||
|
|
fdd4d20a95 | ||
|
|
2cc3ac7622 | ||
|
|
0663e5b0b1 | ||
|
|
7738410680 | ||
|
|
7f436b320f | ||
|
|
296ce35e16 | ||
|
|
dad1df7b52 | ||
|
|
6ebb421185 | ||
|
|
861d6685a0 | ||
|
|
67498280a0 | ||
|
|
309447ca68 | ||
|
|
e9b9e7ef46 | ||
|
|
f1d5275e6f | ||
|
|
24c229f736 | ||
|
|
7fd2de3554 | ||
|
|
eed95d402e | ||
|
|
033bafa5bc | ||
|
|
03fadd7b25 |
|
|
@ -3,31 +3,41 @@ name: Release
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
- V**
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release-it:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GitHub_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
# cache: npm
|
||||
|
||||
- name: Install deps
|
||||
run: npm install
|
||||
|
||||
- name: Release to GitHub
|
||||
# if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
npm run release -- --no-increment --no-git --github.release --ci --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._
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@
|
|||
"\tremoveIfExists: ${13:true},",
|
||||
"\tcustomCheck: (doc: Document, options: ElementOptions) => ${14:true},",
|
||||
"\tchildren: [$15]",
|
||||
"}, ${16:container});"
|
||||
]
|
||||
"}, ${16:container});",
|
||||
],
|
||||
},
|
||||
"appendElement - minimum": {
|
||||
"scope": "javascript,typescript",
|
||||
"prefix": "appendElement",
|
||||
"body": "appendElement({ tag: '$1' }, $2);"
|
||||
"body": "appendElement({ tag: '$1' }, $2);",
|
||||
},
|
||||
"register Notifier": {
|
||||
"scope": "javascript,typescript",
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
"\t) => {",
|
||||
"\t\t$0",
|
||||
"\t}",
|
||||
"});"
|
||||
]
|
||||
}
|
||||
"});",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
|||
421
README.md
421
README.md
|
|
@ -1,418 +1,17 @@
|
|||
# Zotero Plugin Template
|
||||
# Zotero TL;DR
|
||||
|
||||
[](https://www.zotero.org)
|
||||
[](https://www.zotero.org)
|
||||
[](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)
|
||||
|
||||
[ℹ️ Zotero Type Definitions](https://github.com/windingwind/zotero-types)
|
||||
|
||||
[📜 Zotero Source Code](https://github.com/zotero/zotero)
|
||||
|
||||
[📌 Zotero Plugin Template](https://github.com/windingwind/zotero-plugin-template) (This repo)
|
||||
|
||||
> [!tip]
|
||||
> 👁 Watch this repo so that you can be notified whenever there are fixes & updates.
|
||||
|
||||
## Plugins built with this template
|
||||
|
||||
[](https://github.com/windingwind/zotero-better-notes)
|
||||
[](https://github.com/windingwind/zotero-pdf-preview)
|
||||
[](https://github.com/windingwind/zotero-pdf-translate)
|
||||
[](https://github.com/windingwind/zotero-tag)
|
||||
[](https://github.com/iShareStuff/ZoteroTheme)
|
||||
[](https://github.com/MuiseDestiny/zotero-reference)
|
||||
[](https://github.com/MuiseDestiny/zotero-citation)
|
||||
[](https://github.com/MuiseDestiny/ZoteroStyle)
|
||||
[](https://github.com/volatile-static/Chartero)
|
||||
[](https://github.com/l0o0/tara)
|
||||
[](https://github.com/redleafnew/delitemwithatt)
|
||||
[](https://github.com/redleafnew/zotero-updateifsE)
|
||||
[](https://github.com/northword/zotero-format-metadata)
|
||||
[](https://github.com/inciteful-xyz/inciteful-zotero-plugin)
|
||||
[](https://github.com/MuiseDestiny/zotero-gpt)
|
||||
[](https://github.com/zoushucai/zotero-journalabbr)
|
||||
[](https://github.com/MuiseDestiny/zotero-figure)
|
||||
[](https://github.com/l0o0/jasminum)
|
||||
[](https://github.com/lifan0127/ai-research-assistant)
|
||||
[](https://github.com/daeh/zotero-markdb-connect)
|
||||
|
||||
If you are using this repo, I recommended that you put the following badge on your README:
|
||||
|
||||
[](https://github.com/windingwind/zotero-plugin-template)
|
||||
|
||||
```md
|
||||
[](https://github.com/windingwind/zotero-plugin-template)
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Event-driven, functional programming, under extensive skeleton;
|
||||
- Simple and user-friendly, works out-of-the-box.
|
||||
- ⭐ [New!] Auto hot reload! Whenever the source code is modified, automatically compile and reload. [See here→](#auto-hot-reload)
|
||||
- Abundant examples in `src/modules/examples.ts`, covering most of the commonly used APIs in plugins (using [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit));
|
||||
- TypeScript support:
|
||||
- Full type definition support for the whole Zotero project, which is written in JavaScript (using [zotero-types](https://github.com/windingwind/zotero-types));
|
||||
- Global variables and environment setup;
|
||||
- Plugin develop/build/release workflow:
|
||||
- Automatically generate/update plugin id/version, update configrations, and set environment variables (`development` / `production`);
|
||||
- Automatically build and reload code in Zotero;
|
||||
- Automatically release to GitHub (using [release-it](https://github.com/release-it/release-it));
|
||||
- Prettier and ES Lint integration.
|
||||
|
||||
> [!warning]
|
||||
> The localization system is upgraded (`dtd` is deprecated and we do not use `.properties` anymore). Only supports Zotero 7.0.0-beta.12 or higher now. If you want to support Zotero 6, you may need to use `dtd`, `properties`, and `ftl` at the same time. See the staled branch `zotero6-bootstrap`.
|
||||
|
||||
## Examples
|
||||
|
||||
This repo provides examples for [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit) APIs.
|
||||
|
||||
Search `@example` in `src/examples.ts`. The examples are called in `src/hooks.ts`.
|
||||
|
||||
### Basic Examples
|
||||
|
||||
- registerNotifier
|
||||
- registerPrefs, unregisterPrefs
|
||||
|
||||
### Shortcut Keys Examples
|
||||
|
||||
- registerShortcuts
|
||||
- exampleShortcutLargerCallback
|
||||
- exampleShortcutSmallerCallback
|
||||
- exampleShortcutConflictionCallback
|
||||
|
||||
### UI Examples
|
||||
|
||||

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

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

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

|
||||
|
||||
- registerAlertPromptExample
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
### 0 Requirement
|
||||
|
||||
1. Install a beta version of Zotero: <https://www.zotero.org/support/beta_builds>
|
||||
2. Install [Node.js](https://nodejs.org/en/) and [Git](https://git-scm.com/)
|
||||
|
||||
> [!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-template.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 [avoid 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-template.json # template of local env
|
||||
|-- src # source code
|
||||
| |-- addon.ts # base class
|
||||
| |-- hooks.ts # lifecycle hooks
|
||||
| |-- index.ts # main entry
|
||||
| |-- modules # sub modules
|
||||
| | |-- examples.ts
|
||||
| | `-- preferenceScript.ts
|
||||
| `-- utils # utilities
|
||||
| |-- locale.ts
|
||||
| |-- prefs.ts
|
||||
| |-- wait.ts
|
||||
| `-- window.ts
|
||||
|-- tsconfig.json # https://code.visualstudio.com/docs/languages/jsconfig
|
||||
|-- typings # ts typings
|
||||
| `-- global.d.ts
|
||||
`-- update.json
|
||||
```
|
||||
|
||||
## Disclaimer
|
||||
|
||||
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>
|
||||
There are no configuration steps required.
|
||||
The add-on will automatically fetch the TL;DR information for all items.
|
||||
You can view the TLDR information in details on the right side.
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 677 B After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 836 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -1,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>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
.makeItRed {
|
||||
background-color: tomato;
|
||||
}
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
startup-begin = Addon is loading
|
||||
startup-finish = Addon is ready
|
||||
menuitem-label = Addon Template: Helper Examples
|
||||
menupopup-label = Addon Template: Menupopup
|
||||
menuitem-submenulabel = Addon Template
|
||||
menuitem-filemenulabel = Addon Template: File Menuitem
|
||||
prefs-title = Template
|
||||
prefs-table-title = Title
|
||||
prefs-table-detail = Detail
|
||||
tabpanel-lib-tab-label = Lib Tab
|
||||
tabpanel-reader-tab-label = Reader Tab
|
||||
menuitem-updatetldrlabel = update TLDR
|
||||
menucollection-updatetldrlabel = update TLDR
|
||||
itembox-tldrlabel = TLDR
|
||||
tldr-unrelated = TLDR Unrelated in Semantic scholar
|
||||
tldr-itemnotfound = Item Not Found in Semantic scholar
|
||||
popWindow-succeed = Succeed
|
||||
popWindow-failed = Failed
|
||||
popWindow-waiting = Waiting
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
itemPaneSection-header =
|
||||
.label = TLDR
|
||||
itemPaneSection-sidenav =
|
||||
.tooltiptext = TLDR
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
pref-title = Addon Template Example
|
||||
pref-enable =
|
||||
.label = Enable
|
||||
pref-input = Input
|
||||
pref-help = { $name } Build { $version } { $time }
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
startup-begin = 插件加载中
|
||||
startup-finish = 插件已就绪
|
||||
menuitem-label = 插件模板: 帮助工具样例
|
||||
menupopup-label = 插件模板: 弹出菜单
|
||||
menuitem-submenulabel = 插件模板:子菜单
|
||||
menuitem-filemenulabel = 插件模板: 文件菜单
|
||||
prefs-title = 插件模板
|
||||
prefs-table-title = 标题
|
||||
prefs-table-detail = 详情
|
||||
tabpanel-lib-tab-label = 库标签
|
||||
tabpanel-reader-tab-label = 阅读器标签
|
||||
menuitem-updatetldrlabel = 更新TLDR
|
||||
menucollection-updatetldrlabel = 批量更新TLDR
|
||||
itembox-tldrlabel = TLDR
|
||||
tldr-unrelated = 未关联TLDR
|
||||
tldr-itemnotfound = 未搜索到此条目
|
||||
popWindow-succeed = 成功
|
||||
popWindow-failed = 失败
|
||||
popWindow-waiting = 等待
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
itemPaneSection-header =
|
||||
.label = TLDR
|
||||
itemPaneSection-sidenav =
|
||||
.tooltiptext = TLDR
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
pref-title = 插件模板设置示例
|
||||
pref-enable =
|
||||
.label = 开启
|
||||
pref-input = 输入
|
||||
pref-help = { $name } Build { $version } { $time }
|
||||
|
|
@ -1,3 +1 @@
|
|||
/* eslint-disable no-undef */
|
||||
pref("__prefsPrefix__.enable", true);
|
||||
pref("__prefsPrefix__.input", "This is input");
|
||||
|
|
|
|||
48
package.json
48
package.json
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"name": "zotero-addon-template",
|
||||
"version": "1.1.1",
|
||||
"description": "Zotero Addon Template",
|
||||
"name": "zotero-tldr",
|
||||
"version": "1.0.7",
|
||||
"description": "TLDR(too long; didn't read) from sematic scholar",
|
||||
"config": {
|
||||
"addonName": "Zotero Addon Template",
|
||||
"addonID": "addontemplate@euclpts.com",
|
||||
"addonRef": "addontemplate",
|
||||
"addonInstance": "AddonTemplate",
|
||||
"prefsPrefix": "extensions.zotero.addontemplate",
|
||||
"releasePage": "https://github.com/windingwind/zotero-addon-template/releases",
|
||||
"updateJSON": "https://raw.githubusercontent.com/windingwind/zotero-addon-template/main/update.json"
|
||||
"addonName": "Zotero TLDR",
|
||||
"addonID": "zoterotldr@syt.com",
|
||||
"addonRef": "zoterotldr",
|
||||
"addonInstance": "ZoteroTLDR",
|
||||
"prefsPrefix": "extensions.zotero.zoterotldr",
|
||||
"releasepage": "https://github.com/syt2/zotero-tldr/releases",
|
||||
"updateJSON": "https://raw.githubusercontent.com/syt2/zotero-tldr/main/update.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
|
|
@ -23,24 +23,24 @@
|
|||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/windingwind/zotero-addon-template.git"
|
||||
"url": "git+https://github.com/syt2/zotero-tldr.git"
|
||||
},
|
||||
"author": "windingwind",
|
||||
"author": "syt2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/windingwind/zotero-addon-template/issues"
|
||||
"url": "https://github.com/syt2/zotero-tldr/issues"
|
||||
},
|
||||
"homepage": "https://github.com/windingwind/zotero-addon-template#readme",
|
||||
"homepage": "https://github.com/syt2/zotero-tldr#readme",
|
||||
"dependencies": {
|
||||
"zotero-plugin-toolkit": "^2.3.15"
|
||||
"zotero-plugin-toolkit": "^2.3.29"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"@typescript-eslint/parser": "^7.1.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"compressing": "^1.10.0",
|
||||
"esbuild": "^0.19.9",
|
||||
"esbuild": "^0.20.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"prettier": "^3.1.1",
|
||||
|
|
@ -115,7 +115,7 @@
|
|||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
"tagName": "v${version}"
|
||||
"tagName": "V${version}"
|
||||
},
|
||||
"npm": {
|
||||
"publish": false
|
||||
|
|
@ -124,17 +124,11 @@
|
|||
"release": false,
|
||||
"assets": [
|
||||
"build/*.xpi"
|
||||
],
|
||||
"comments": {
|
||||
"submit": true,
|
||||
"issue": ":rocket: _This issue has been resolved in v${version}. See [${releaseName}](${releaseUrl}) for release notes._",
|
||||
"pr": ":rocket: _This pull request is included in v${version}. See [${releaseName}](${releaseUrl}) for release notes._"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"before:init": "npm run lint",
|
||||
"after:bump": "npm run build",
|
||||
"after:release": "echo Successfully released ${name} v${version} to ${repo.repository}."
|
||||
"after:bump": "npm run build"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,83 +64,75 @@ function replaceString(buildTime) {
|
|||
}
|
||||
|
||||
function prepareLocaleFiles() {
|
||||
// Walk the builds/addon/locale folder's sub folders and rename *.ftl to addonRef-*.ftl
|
||||
const localeDir = path.join(buildDir, "addon/locale");
|
||||
const localeFolders = readdirSync(localeDir, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
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`],
|
||||
// Prefix Fluent messages in xhtml
|
||||
const MessagesInHTML = new Set();
|
||||
replaceInFileSync({
|
||||
files: [`${buildDir}/addon/**/*.xhtml`, `${buildDir}/addon/**/*.html`],
|
||||
processor: (input) => {
|
||||
const matchs = [...input.matchAll(/(data-l10n-id)="(\S*)"/g)];
|
||||
matchs.map((match) => {
|
||||
if (localeMessage.has(match[2])) {
|
||||
input = input.replace(
|
||||
match[0],
|
||||
`${match[1]}="${config.addonRef}-${match[2]}"`,
|
||||
);
|
||||
} else {
|
||||
localeMessageMiss.add(match[2]);
|
||||
}
|
||||
input = input.replace(
|
||||
match[0],
|
||||
`${match[1]}="${config.addonRef}-${match[2]}"`,
|
||||
);
|
||||
MessagesInHTML.add(match[2]);
|
||||
});
|
||||
return input;
|
||||
},
|
||||
});
|
||||
|
||||
Logger.debug(
|
||||
"[Build] Prepare locale files OK",
|
||||
// replaceResultFlt.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`),
|
||||
// replaceResultXhtml.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`),
|
||||
);
|
||||
// Walk the sub folders of `build/addon/locale`
|
||||
const localesPath = path.join(buildDir, "addon/locale"),
|
||||
localeNames = readdirSync(localesPath, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
if (localeMessageMiss.size !== 0) {
|
||||
Logger.warn(
|
||||
`[Build] Fluent message [${new Array(
|
||||
...localeMessageMiss,
|
||||
)}] do not exsit in addon's locale files.`,
|
||||
);
|
||||
for (const localeName of localeNames) {
|
||||
const localePath = path.join(localesPath, localeName);
|
||||
const ftlFiles = readdirSync(localePath, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.filter((dirent) => dirent.isFile())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
// rename *.ftl to addonRef-*.ftl
|
||||
for (const ftlFile of ftlFiles) {
|
||||
if (ftlFile.endsWith(".ftl")) {
|
||||
renameSync(
|
||||
path.join(localePath, ftlFile),
|
||||
path.join(localePath, `${config.addonRef}-${ftlFile}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix Fluent messages in each ftl
|
||||
const MessageInThisLang = new Set();
|
||||
replaceInFileSync({
|
||||
files: [`${buildDir}/addon/locale/${localeName}/*.ftl`],
|
||||
processor: (fltContent) => {
|
||||
const lines = fltContent.split("\n");
|
||||
const prefixedLines = lines.map((line) => {
|
||||
// https://regex101.com/r/lQ9x5p/1
|
||||
const match = line.match(
|
||||
/^(?<message>[a-zA-Z]\S*)([ ]*=[ ]*)(?<pattern>.*)$/m,
|
||||
);
|
||||
if (match) {
|
||||
MessageInThisLang.add(match.groups.message);
|
||||
return `${config.addonRef}-${line}`;
|
||||
} else {
|
||||
return line;
|
||||
}
|
||||
});
|
||||
return prefixedLines.join("\n");
|
||||
},
|
||||
});
|
||||
|
||||
// If a message in xhtml but not in ftl of current language, log it
|
||||
MessagesInHTML.forEach((message) => {
|
||||
if (!MessageInThisLang.has(message)) {
|
||||
Logger.error(`[Build] ${message} don't exist in ${localeName}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,19 +204,21 @@ export async function main() {
|
|||
);
|
||||
|
||||
clearFolder(buildDir);
|
||||
|
||||
copyFolderRecursiveSync("addon", buildDir);
|
||||
replaceString(buildTime);
|
||||
Logger.debug("[Build] Replace OK");
|
||||
|
||||
Logger.debug("[Build] Replacing");
|
||||
replaceString(buildTime);
|
||||
|
||||
Logger.debug("[Build] Preparing locale files");
|
||||
prepareLocaleFiles();
|
||||
|
||||
Logger.debug("[Build] Running esbuild");
|
||||
await build(esbuildOptions);
|
||||
Logger.debug("[Build] Run esbuild OK");
|
||||
|
||||
Logger.debug("[Build] Addon prepare OK");
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
Logger.debug("[Build] Packing Addon");
|
||||
await zip.compressDir(
|
||||
path.join(buildDir, "addon"),
|
||||
path.join(buildDir, `${name}.xpi`),
|
||||
|
|
@ -232,7 +226,6 @@ export async function main() {
|
|||
ignoreBase: true,
|
||||
},
|
||||
);
|
||||
Logger.debug("[Build] Addon pack OK");
|
||||
|
||||
prepareUpdateJson();
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ function prepareDevEnv() {
|
|||
return;
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { ColumnOptions } from "zotero-plugin-toolkit/dist/helpers/virtualizedTable";
|
||||
import { DialogHelper } from "zotero-plugin-toolkit/dist/helpers/dialog";
|
||||
import hooks from "./hooks";
|
||||
import { createZToolkit } from "./utils/ztoolkit";
|
||||
|
|
@ -14,8 +13,6 @@ class Addon {
|
|||
};
|
||||
prefs?: {
|
||||
window: Window;
|
||||
columns: Array<ColumnOptions>;
|
||||
rows: Array<{ [dataKey: string]: string }>;
|
||||
};
|
||||
dialog?: DialogHelper;
|
||||
};
|
||||
|
|
|
|||
208
src/hooks.ts
208
src/hooks.ts
|
|
@ -1,14 +1,10 @@
|
|||
import {
|
||||
BasicExampleFactory,
|
||||
HelperExampleFactory,
|
||||
KeyExampleFactory,
|
||||
PromptExampleFactory,
|
||||
UIExampleFactory,
|
||||
} from "./modules/examples";
|
||||
import { RegisterFactory, UIFactory } from "./modules/Common";
|
||||
import { config } from "../package.json";
|
||||
import { getString, initLocale } from "./utils/locale";
|
||||
import { registerPrefsScripts } from "./modules/preferenceScript";
|
||||
import { createZToolkit } from "./utils/ztoolkit";
|
||||
import { tldrs } from "./modules/dataStorage";
|
||||
import { TLDRFetcher } from "./modules/tldrFetcher";
|
||||
|
||||
async function onStartup() {
|
||||
await Promise.all([
|
||||
|
|
@ -26,11 +22,9 @@ async function onStartup() {
|
|||
|
||||
initLocale();
|
||||
|
||||
BasicExampleFactory.registerPrefs();
|
||||
await tldrs.getAsync();
|
||||
|
||||
BasicExampleFactory.registerNotifier();
|
||||
|
||||
KeyExampleFactory.registerShortcuts();
|
||||
RegisterFactory.registerNotifier();
|
||||
|
||||
await onMainWindowLoad(window);
|
||||
}
|
||||
|
|
@ -39,56 +33,17 @@ async function onMainWindowLoad(win: Window): Promise<void> {
|
|||
// Create ztoolkit for every window
|
||||
addon.data.ztoolkit = createZToolkit();
|
||||
|
||||
const popupWin = new ztoolkit.ProgressWindow(config.addonName, {
|
||||
closeOnClick: true,
|
||||
closeTime: -1,
|
||||
})
|
||||
.createLine({
|
||||
text: getString("startup-begin"),
|
||||
type: "default",
|
||||
progress: 0,
|
||||
})
|
||||
.show();
|
||||
(win as any).MozXULElement.insertFTLIfNeeded(
|
||||
`${config.addonRef}-mainWindow.ftl`,
|
||||
);
|
||||
|
||||
await Zotero.Promise.delay(1000);
|
||||
popupWin.changeLine({
|
||||
progress: 30,
|
||||
text: `[30%] ${getString("startup-begin")}`,
|
||||
});
|
||||
UIFactory.registerRightClickMenuItem();
|
||||
|
||||
UIExampleFactory.registerStyleSheet();
|
||||
UIFactory.registerRightClickCollectionMenuItem();
|
||||
|
||||
UIExampleFactory.registerRightClickMenuItem();
|
||||
UIFactory.registerTLDRItemBoxRow();
|
||||
|
||||
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");
|
||||
onLoad();
|
||||
}
|
||||
|
||||
async function onMainWindowUnload(win: Window): Promise<void> {
|
||||
|
|
@ -114,16 +69,11 @@ async function onNotify(
|
|||
ids: Array<string | number>,
|
||||
extraData: { [key: string]: any },
|
||||
) {
|
||||
// You can add your code to the corresponding notify type
|
||||
ztoolkit.log("notify", event, type, ids, extraData);
|
||||
if (
|
||||
event == "select" &&
|
||||
type == "tab" &&
|
||||
extraData[ids[0]].type == "reader"
|
||||
) {
|
||||
BasicExampleFactory.exampleNotifierCallback();
|
||||
} else {
|
||||
return;
|
||||
Zotero.log(`${event} ${type} ${ids}, ${extraData}`);
|
||||
if (event == "add" && type == "item" && ids.length > 0) {
|
||||
onNotifyAddItems(ids);
|
||||
} else if (event == "delete" && type == "item" && ids.length > 0) {
|
||||
noNotifyDeleteItem(ids);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -143,39 +93,100 @@ async function onPrefsEvent(type: string, data: { [key: string]: any }) {
|
|||
}
|
||||
}
|
||||
|
||||
function onShortcuts(type: string) {
|
||||
switch (type) {
|
||||
case "larger":
|
||||
KeyExampleFactory.exampleShortcutLargerCallback();
|
||||
break;
|
||||
case "smaller":
|
||||
KeyExampleFactory.exampleShortcutSmallerCallback();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
function onLoad() {
|
||||
(async () => {
|
||||
let needFetchItems: Zotero.Item[] = [];
|
||||
for (const lib of Zotero.Libraries.getAll()) {
|
||||
needFetchItems = needFetchItems.concat(
|
||||
(await Zotero.Items.getAll(lib.id)).filter((item: Zotero.Item) => {
|
||||
return item.isRegularItem();
|
||||
}),
|
||||
);
|
||||
}
|
||||
onUpdateItems(needFetchItems, false);
|
||||
})();
|
||||
}
|
||||
|
||||
function onDialogEvents(type: string) {
|
||||
switch (type) {
|
||||
case "dialogExample":
|
||||
HelperExampleFactory.dialogExample();
|
||||
break;
|
||||
case "clipboardExample":
|
||||
HelperExampleFactory.clipboardExample();
|
||||
break;
|
||||
case "filePickerExample":
|
||||
HelperExampleFactory.filePickerExample();
|
||||
break;
|
||||
case "progressWindowExample":
|
||||
HelperExampleFactory.progressWindowExample();
|
||||
break;
|
||||
case "vtableExample":
|
||||
HelperExampleFactory.vtableExample();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
function noNotifyDeleteItem(ids: (string | number)[]) {
|
||||
tldrs.modify((data) => {
|
||||
ids.forEach((id) => {
|
||||
delete data[id];
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
function onNotifyAddItems(ids: (string | number)[]) {
|
||||
const addedRegularItems: Zotero.Item[] = [];
|
||||
for (const id of ids) {
|
||||
const item = Zotero.Items.get(id);
|
||||
if (item.isRegularItem()) {
|
||||
addedRegularItems.push(item);
|
||||
}
|
||||
}
|
||||
(async function () {
|
||||
await Zotero.Promise.delay(3000);
|
||||
onUpdateItems(addedRegularItems, false);
|
||||
})();
|
||||
}
|
||||
|
||||
function onUpdateItems(items: Zotero.Item[], forceFetch: boolean = false) {
|
||||
items = items.filter((item: Zotero.Item) => {
|
||||
if (!item.getField("title")) {
|
||||
return false;
|
||||
}
|
||||
if (!forceFetch && item.key in tldrs.get()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (items.length <= 0) {
|
||||
return;
|
||||
}
|
||||
const newPopWin = (closeOnClick = true) => {
|
||||
return new ztoolkit.ProgressWindow(config.addonName, {
|
||||
closeOnClick: closeOnClick,
|
||||
}).createLine({
|
||||
text: `${getString("popWindow-waiting")}: ${items.length}; ${getString(
|
||||
"popWindow-succeed",
|
||||
)}: 0; ${getString("popWindow-failed")}: 0`,
|
||||
type: "default",
|
||||
progress: 0,
|
||||
});
|
||||
};
|
||||
const popupWin = newPopWin().show(-1);
|
||||
(async function () {
|
||||
const count = items.length;
|
||||
const failedItems: Zotero.Item[] = [];
|
||||
const succeedItems: Zotero.Item[] = [];
|
||||
await (async function () {
|
||||
for (const [index, item] of items.entries()) {
|
||||
(await new TLDRFetcher(item).fetchTLDR())
|
||||
? succeedItems.push(item)
|
||||
: failedItems.push(item);
|
||||
await Zotero.Promise.delay(50);
|
||||
popupWin.changeLine({
|
||||
progress: (index * 100) / count,
|
||||
text: `${getString("popWindow-waiting")}: ${
|
||||
count - index - 1
|
||||
}; ${getString("popWindow-succeed")}: ${
|
||||
succeedItems.length
|
||||
}; ${getString("popWindow-failed")}: ${failedItems.length}`,
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
await (async function () {
|
||||
popupWin.changeLine({
|
||||
type: "success",
|
||||
progress: 100,
|
||||
text: `${getString("popWindow-succeed")}: ${
|
||||
succeedItems.length
|
||||
}; ${getString("popWindow-failed")}: ${failedItems.length}`,
|
||||
});
|
||||
popupWin.startCloseTimer(3000);
|
||||
})();
|
||||
})();
|
||||
}
|
||||
|
||||
// Add your hooks here. For element click, etc.
|
||||
|
|
@ -189,6 +200,5 @@ export default {
|
|||
onMainWindowUnload,
|
||||
onNotify,
|
||||
onPrefsEvent,
|
||||
onShortcuts,
|
||||
onDialogEvents,
|
||||
onUpdateItems,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
import { config } from "../../package.json";
|
||||
import { getString } from "../utils/locale";
|
||||
import { tldrs } from "./dataStorage";
|
||||
|
||||
export class RegisterFactory {
|
||||
// 注册zotero的通知
|
||||
static registerNotifier() {
|
||||
const callback = {
|
||||
notify: async (
|
||||
event: string,
|
||||
type: string,
|
||||
ids: number[] | string[],
|
||||
extraData: { [key: string]: any },
|
||||
) => {
|
||||
if (!addon?.data.alive) {
|
||||
this.unregisterNotifier(notifierID);
|
||||
return;
|
||||
}
|
||||
addon.hooks.onNotify(event, type, ids, extraData);
|
||||
},
|
||||
};
|
||||
|
||||
// Register the callback in Zotero as an item observer
|
||||
const notifierID = Zotero.Notifier.registerObserver(callback, ["item"]);
|
||||
|
||||
// Unregister callback when the window closes (important to avoid a memory leak)
|
||||
window.addEventListener(
|
||||
"unload",
|
||||
(e: Event) => {
|
||||
this.unregisterNotifier(notifierID);
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
private static unregisterNotifier(notifierID: string) {
|
||||
Zotero.Notifier.unregisterObserver(notifierID);
|
||||
}
|
||||
}
|
||||
|
||||
export class UIFactory {
|
||||
// item右键菜单
|
||||
static registerRightClickMenuItem() {
|
||||
const menuIcon = `chrome://${config.addonRef}/content/icons/favicon.png`;
|
||||
// item menuitem with icon
|
||||
ztoolkit.Menu.register("item", {
|
||||
tag: "menuitem",
|
||||
id: "zotero-itemmenu-tldr",
|
||||
label: getString("menuitem-updatetldrlabel"),
|
||||
commandListener: (ev) => {
|
||||
const selectedItems = ZoteroPane.getSelectedItems() ?? [];
|
||||
addon.hooks.onUpdateItems(selectedItems, selectedItems.length <= 1);
|
||||
},
|
||||
icon: menuIcon,
|
||||
});
|
||||
}
|
||||
|
||||
// collection右键菜单
|
||||
static registerRightClickCollectionMenuItem() {
|
||||
const menuIcon = `chrome://${config.addonRef}/content/icons/favicon.png`;
|
||||
ztoolkit.Menu.register("collection", {
|
||||
tag: "menuitem",
|
||||
id: "zotero-collectionmenu-tldr",
|
||||
label: getString("menucollection-updatetldrlabel"),
|
||||
commandListener: (ev) =>
|
||||
addon.hooks.onUpdateItems(
|
||||
ZoteroPane.getSelectedCollection()?.getChildItems() ?? [],
|
||||
false,
|
||||
),
|
||||
icon: menuIcon,
|
||||
});
|
||||
}
|
||||
|
||||
// tldr行
|
||||
static async registerTLDRItemBoxRow() {
|
||||
const itemTLDR = (item: Zotero.Item) => {
|
||||
const noteKey = tldrs.get()[item.key];
|
||||
if (noteKey) {
|
||||
const obj = Zotero.Items.getByLibraryAndKey(item.libraryID, noteKey);
|
||||
if (
|
||||
obj &&
|
||||
obj instanceof Zotero.Item &&
|
||||
item.getNotes().includes(obj.id)
|
||||
) {
|
||||
let str = obj.getNote();
|
||||
if (str.startsWith("<p>TL;DR</p>\n<p>")) {
|
||||
str = str.slice("<p>TL;DR</p>\n<p>".length);
|
||||
}
|
||||
if (str.endsWith("</p>")) {
|
||||
str = str.slice(0, -4);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
Zotero.ItemPaneManager.registerSection({
|
||||
paneID: config.addonRef,
|
||||
pluginID: config.addonID,
|
||||
header: {
|
||||
l10nID: `${config.addonRef}-itemPaneSection-header`,
|
||||
icon: `chrome://${config.addonRef}/content/icons/favicon@16.png`,
|
||||
},
|
||||
sidenav: {
|
||||
l10nID: `${config.addonRef}-itemPaneSection-sidenav`,
|
||||
icon: `chrome://${config.addonRef}/content/icons/favicon@20.png`,
|
||||
},
|
||||
onRender: ({ body, item }: any) => {
|
||||
let tldr = itemTLDR(item);
|
||||
if (tldr.length <= 0 && item.parentItem) {
|
||||
tldr = itemTLDR(item.parentItem);
|
||||
}
|
||||
body.textContent = tldr;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import { config } from "../../package.json";
|
||||
|
||||
export class Data<K extends string | number | symbol, V> {
|
||||
[x: string]: any;
|
||||
private dataType: string;
|
||||
private filePath?: string;
|
||||
private _data: Record<K, V>;
|
||||
|
||||
constructor(dataType: string) {
|
||||
this.dataType = dataType;
|
||||
this._data = {} as Record<K, V>;
|
||||
}
|
||||
|
||||
async getAsync() {
|
||||
await this.initDataIfNeed();
|
||||
return this.data;
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
async modify(
|
||||
action: (data: Record<K, V>) => Record<K, V> | Promise<Record<K, V>>,
|
||||
) {
|
||||
await this.initDataIfNeed();
|
||||
const data = this.data;
|
||||
const newData = await action(data);
|
||||
if (this.filePath) {
|
||||
try {
|
||||
await IOUtils.writeJSON(this.filePath, newData, {
|
||||
mode: "overwrite",
|
||||
compress: false,
|
||||
});
|
||||
this.data = newData;
|
||||
return newData;
|
||||
} catch (error) {
|
||||
return data;
|
||||
}
|
||||
} else {
|
||||
this.data = newData;
|
||||
return newData;
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
if (this.filePath) {
|
||||
try {
|
||||
await IOUtils.remove(this.filePath);
|
||||
this.data = {} as Record<K, V>;
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
this.data = {} as Record<K, V>;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private get data() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
private set data(value: Record<K, V>) {
|
||||
this._data = value;
|
||||
}
|
||||
|
||||
private async initDataIfNeed() {
|
||||
if (this.inited) {
|
||||
return;
|
||||
}
|
||||
this.inited = true;
|
||||
|
||||
const prefsFile = PathUtils.join(PathUtils.profileDir, "prefs.js");
|
||||
const prefs = await Zotero.Profile.readPrefsFromFile(prefsFile);
|
||||
let dir = prefs["extensions.zotero.dataDir"];
|
||||
if (dir) {
|
||||
dir = PathUtils.join(dir, config.addonName);
|
||||
} else {
|
||||
dir = PathUtils.join(
|
||||
PathUtils.profileDir,
|
||||
"extensions",
|
||||
config.addonName,
|
||||
);
|
||||
}
|
||||
IOUtils.makeDirectory(dir, {
|
||||
createAncestors: true,
|
||||
ignoreExisting: true,
|
||||
});
|
||||
this.filePath = PathUtils.join(dir, this.dataType);
|
||||
try {
|
||||
this.data = await IOUtils.readJSON(this.filePath, { decompress: false });
|
||||
} catch (error) {
|
||||
this.data = {} as Record<K, V>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DataStorage {
|
||||
private dataMap: { [key: string]: Data<any, any> } = {};
|
||||
|
||||
private static shared = new DataStorage();
|
||||
|
||||
static instance<K extends string | number | symbol, V>(
|
||||
dataType: string,
|
||||
): Data<K, V> {
|
||||
if (this.shared.dataMap[dataType] === undefined) {
|
||||
const data = new Data<K, V>(dataType);
|
||||
this.shared.dataMap[dataType] = data;
|
||||
return data;
|
||||
} else {
|
||||
return this.shared.dataMap[dataType];
|
||||
}
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
|
||||
export const tldrs = DataStorage.instance<string, string | false>(
|
||||
"fetchedItems.json",
|
||||
);
|
||||
|
|
@ -1,912 +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() {
|
||||
// Register an event key for Alt+L
|
||||
ztoolkit.Keyboard.register((ev, keyOptions) => {
|
||||
ztoolkit.log(ev, keyOptions.keyboard);
|
||||
if (keyOptions.keyboard.equals("shift,l")) {
|
||||
addon.hooks.onShortcuts("larger");
|
||||
}
|
||||
if (ev.shiftKey && ev.key === "S") {
|
||||
addon.hooks.onShortcuts("smaller");
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
export class UIExampleFactory {
|
||||
@example
|
||||
static registerStyleSheet() {
|
||||
const styles = ztoolkit.UI.createElement(document, "link", {
|
||||
properties: {
|
||||
type: "text/css",
|
||||
rel: "stylesheet",
|
||||
href: `chrome://${config.addonRef}/content/zoteroPane.css`,
|
||||
},
|
||||
});
|
||||
document.documentElement.appendChild(styles);
|
||||
document
|
||||
.getElementById("zotero-item-pane-content")
|
||||
?.classList.add("makeItRed");
|
||||
}
|
||||
|
||||
@example
|
||||
static registerRightClickMenuItem() {
|
||||
const menuIcon = `chrome://${config.addonRef}/content/icons/favicon@0.5x.png`;
|
||||
// item menuitem with icon
|
||||
ztoolkit.Menu.register("item", {
|
||||
tag: "menuitem",
|
||||
id: "zotero-itemmenu-addontemplate-test",
|
||||
label: getString("menuitem-label"),
|
||||
commandListener: (ev) => addon.hooks.onDialogEvents("dialogExample"),
|
||||
icon: menuIcon,
|
||||
});
|
||||
}
|
||||
|
||||
@example
|
||||
static registerRightClickMenuPopup() {
|
||||
ztoolkit.Menu.register(
|
||||
"item",
|
||||
{
|
||||
tag: "menu",
|
||||
label: getString("menupopup-label"),
|
||||
children: [
|
||||
{
|
||||
tag: "menuitem",
|
||||
label: getString("menuitem-submenulabel"),
|
||||
oncommand: "alert('Hello World! Sub Menuitem.')",
|
||||
},
|
||||
],
|
||||
},
|
||||
"before",
|
||||
document.querySelector(
|
||||
"#zotero-itemmenu-addontemplate-test",
|
||||
) as XUL.MenuItem,
|
||||
);
|
||||
}
|
||||
|
||||
@example
|
||||
static registerWindowMenuWithSeparator() {
|
||||
ztoolkit.Menu.register("menuFile", {
|
||||
tag: "menuseparator",
|
||||
});
|
||||
// menu->File menuitem
|
||||
ztoolkit.Menu.register("menuFile", {
|
||||
tag: "menuitem",
|
||||
label: getString("menuitem-filemenulabel"),
|
||||
oncommand: "alert('Hello World! File Menuitem.')",
|
||||
});
|
||||
}
|
||||
|
||||
@example
|
||||
static async registerExtraColumn() {
|
||||
await ztoolkit.ItemTree.register(
|
||||
"test1",
|
||||
"text column",
|
||||
(
|
||||
field: string,
|
||||
unformatted: boolean,
|
||||
includeBaseMapped: boolean,
|
||||
item: Zotero.Item,
|
||||
) => {
|
||||
return field + String(item.id);
|
||||
},
|
||||
{
|
||||
iconPath: "chrome://zotero/skin/cross.png",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@example
|
||||
static async registerExtraColumnWithCustomCell() {
|
||||
await ztoolkit.ItemTree.register(
|
||||
"test2",
|
||||
"custom column",
|
||||
(
|
||||
field: string,
|
||||
unformatted: boolean,
|
||||
includeBaseMapped: boolean,
|
||||
item: Zotero.Item,
|
||||
) => {
|
||||
return String(item.id);
|
||||
},
|
||||
{
|
||||
renderCell(index, data, column) {
|
||||
ztoolkit.log("Custom column cell is rendered!");
|
||||
const span = document.createElementNS(
|
||||
"http://www.w3.org/1999/xhtml",
|
||||
"span",
|
||||
);
|
||||
span.className = `cell ${column.className}`;
|
||||
span.style.background = "#0dd068";
|
||||
span.innerText = "⭐" + data;
|
||||
return span;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@example
|
||||
static async registerCustomItemBoxRow() {
|
||||
await ztoolkit.ItemBox.register(
|
||||
"itemBoxFieldEditable",
|
||||
"Editable Custom Field",
|
||||
(field, unformatted, includeBaseMapped, item, original) => {
|
||||
return (
|
||||
ztoolkit.ExtraField.getExtraField(item, "itemBoxFieldEditable") || ""
|
||||
);
|
||||
},
|
||||
{
|
||||
editable: true,
|
||||
setFieldHook: (field, value, loadIn, item, original) => {
|
||||
window.alert("Custom itemBox value is changed and saved to extra!");
|
||||
ztoolkit.ExtraField.setExtraField(
|
||||
item,
|
||||
"itemBoxFieldEditable",
|
||||
value,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
index: 1,
|
||||
},
|
||||
);
|
||||
|
||||
await ztoolkit.ItemBox.register(
|
||||
"itemBoxFieldNonEditable",
|
||||
"Non-Editable Custom Field",
|
||||
(field, unformatted, includeBaseMapped, item, original) => {
|
||||
return (
|
||||
"[CANNOT EDIT THIS]" + (item.getField("title") as string).slice(0, 10)
|
||||
);
|
||||
},
|
||||
{
|
||||
editable: false,
|
||||
index: 2,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@example
|
||||
static registerLibraryTabPanel() {
|
||||
const tabId = ztoolkit.LibraryTabPanel.register(
|
||||
getString("tabpanel-lib-tab-label"),
|
||||
(panel: XUL.Element, win: Window) => {
|
||||
const elem = ztoolkit.UI.createElement(win.document, "vbox", {
|
||||
children: [
|
||||
{
|
||||
tag: "h2",
|
||||
properties: {
|
||||
innerText: "Hello World!",
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "div",
|
||||
properties: {
|
||||
innerText: "This is a library tab.",
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "button",
|
||||
namespace: "html",
|
||||
properties: {
|
||||
innerText: "Unregister",
|
||||
},
|
||||
listeners: [
|
||||
{
|
||||
type: "click",
|
||||
listener: () => {
|
||||
ztoolkit.LibraryTabPanel.unregister(tabId);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
panel.append(elem);
|
||||
},
|
||||
{
|
||||
targetIndex: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@example
|
||||
static async registerReaderTabPanel() {
|
||||
const tabId = await ztoolkit.ReaderTabPanel.register(
|
||||
getString("tabpanel-reader-tab-label"),
|
||||
(
|
||||
panel: XUL.TabPanel | undefined,
|
||||
deck: XUL.Deck,
|
||||
win: Window,
|
||||
reader: _ZoteroTypes.ReaderInstance,
|
||||
) => {
|
||||
if (!panel) {
|
||||
ztoolkit.log(
|
||||
"This reader do not have right-side bar. Adding reader tab skipped.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
ztoolkit.log(reader);
|
||||
const elem = ztoolkit.UI.createElement(win.document, "vbox", {
|
||||
id: `${config.addonRef}-${reader._instanceID}-extra-reader-tab-div`,
|
||||
// This is important! Don't create content for multiple times
|
||||
// ignoreIfExists: true,
|
||||
removeIfExists: true,
|
||||
children: [
|
||||
{
|
||||
tag: "h2",
|
||||
properties: {
|
||||
innerText: "Hello World!",
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "div",
|
||||
properties: {
|
||||
innerText: "This is a reader tab.",
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "div",
|
||||
properties: {
|
||||
innerText: `Reader: ${reader._title.slice(0, 20)}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "div",
|
||||
properties: {
|
||||
innerText: `itemID: ${reader.itemID}.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "button",
|
||||
namespace: "html",
|
||||
properties: {
|
||||
innerText: "Unregister",
|
||||
},
|
||||
listeners: [
|
||||
{
|
||||
type: "click",
|
||||
listener: () => {
|
||||
ztoolkit.ReaderTabPanel.unregister(tabId);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
panel.append(elem);
|
||||
},
|
||||
{
|
||||
targetIndex: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class PromptExampleFactory {
|
||||
@example
|
||||
static registerNormalCommandExample() {
|
||||
ztoolkit.Prompt.register([
|
||||
{
|
||||
name: "Normal Command Test",
|
||||
label: "Plugin Template",
|
||||
callback(prompt) {
|
||||
ztoolkit.getGlobal("alert")("Command triggered!");
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@example
|
||||
static registerAnonymousCommandExample() {
|
||||
ztoolkit.Prompt.register([
|
||||
{
|
||||
id: "search",
|
||||
callback: async (prompt) => {
|
||||
// https://github.com/zotero/zotero/blob/7262465109c21919b56a7ab214f7c7a8e1e63909/chrome/content/zotero/integration/quickFormat.js#L589
|
||||
function getItemDescription(item: Zotero.Item) {
|
||||
const nodes = [];
|
||||
let str = "";
|
||||
let author,
|
||||
authorDate = "";
|
||||
if (item.firstCreator) {
|
||||
author = authorDate = item.firstCreator;
|
||||
}
|
||||
let date = item.getField("date", true, true) as string;
|
||||
if (date && (date = date.substr(0, 4)) !== "0000") {
|
||||
authorDate += " (" + parseInt(date) + ")";
|
||||
}
|
||||
authorDate = authorDate.trim();
|
||||
if (authorDate) nodes.push(authorDate);
|
||||
|
||||
const publicationTitle = item.getField(
|
||||
"publicationTitle",
|
||||
false,
|
||||
true,
|
||||
);
|
||||
if (publicationTitle) {
|
||||
nodes.push(`<i>${publicationTitle}</i>`);
|
||||
}
|
||||
let volumeIssue = item.getField("volume");
|
||||
const issue = item.getField("issue");
|
||||
if (issue) volumeIssue += "(" + issue + ")";
|
||||
if (volumeIssue) nodes.push(volumeIssue);
|
||||
|
||||
const publisherPlace = [];
|
||||
let field;
|
||||
if ((field = item.getField("publisher")))
|
||||
publisherPlace.push(field);
|
||||
if ((field = item.getField("place"))) publisherPlace.push(field);
|
||||
if (publisherPlace.length) nodes.push(publisherPlace.join(": "));
|
||||
|
||||
const pages = item.getField("pages");
|
||||
if (pages) nodes.push(pages);
|
||||
|
||||
if (!nodes.length) {
|
||||
const url = item.getField("url");
|
||||
if (url) nodes.push(url);
|
||||
}
|
||||
|
||||
// compile everything together
|
||||
for (let i = 0, n = nodes.length; i < n; i++) {
|
||||
const node = nodes[i];
|
||||
|
||||
if (i != 0) str += ", ";
|
||||
|
||||
if (typeof node === "object") {
|
||||
const label = document.createElement("label");
|
||||
label.setAttribute("value", str);
|
||||
label.setAttribute("crop", "end");
|
||||
str = "";
|
||||
} else {
|
||||
str += node;
|
||||
}
|
||||
}
|
||||
str.length && (str += ".");
|
||||
return str;
|
||||
}
|
||||
function filter(ids: number[]) {
|
||||
ids = ids.filter(async (id) => {
|
||||
const item = (await Zotero.Items.getAsync(id)) as Zotero.Item;
|
||||
return item.isRegularItem() && !(item as any).isFeedItem;
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
const text = prompt.inputNode.value;
|
||||
prompt.showTip("Searching...");
|
||||
const s = new Zotero.Search();
|
||||
s.addCondition("quicksearch-titleCreatorYear", "contains", text);
|
||||
s.addCondition("itemType", "isNot", "attachment");
|
||||
let ids = await s.search();
|
||||
// prompt.exit will remove current container element.
|
||||
// @ts-ignore ignore
|
||||
prompt.exit();
|
||||
const container = prompt.createCommandsContainer();
|
||||
container.classList.add("suggestions");
|
||||
ids = filter(ids);
|
||||
console.log(ids.length);
|
||||
if (ids.length == 0) {
|
||||
const s = new Zotero.Search();
|
||||
const operators = [
|
||||
"is",
|
||||
"isNot",
|
||||
"true",
|
||||
"false",
|
||||
"isInTheLast",
|
||||
"isBefore",
|
||||
"isAfter",
|
||||
"contains",
|
||||
"doesNotContain",
|
||||
"beginsWith",
|
||||
];
|
||||
let hasValidCondition = false;
|
||||
let joinMode = "all";
|
||||
if (/\s*\|\|\s*/.test(text)) {
|
||||
joinMode = "any";
|
||||
}
|
||||
text.split(/\s*(&&|\|\|)\s*/g).forEach((conditinString: string) => {
|
||||
const conditions = conditinString.split(/\s+/g);
|
||||
if (
|
||||
conditions.length == 3 &&
|
||||
operators.indexOf(conditions[1]) != -1
|
||||
) {
|
||||
hasValidCondition = true;
|
||||
s.addCondition(
|
||||
"joinMode",
|
||||
joinMode as Zotero.Search.Operator,
|
||||
"",
|
||||
);
|
||||
s.addCondition(
|
||||
conditions[0] as string,
|
||||
conditions[1] as Zotero.Search.Operator,
|
||||
conditions[2] as string,
|
||||
);
|
||||
}
|
||||
});
|
||||
if (hasValidCondition) {
|
||||
ids = await s.search();
|
||||
}
|
||||
}
|
||||
ids = filter(ids);
|
||||
console.log(ids.length);
|
||||
if (ids.length > 0) {
|
||||
ids.forEach((id: number) => {
|
||||
const item = Zotero.Items.get(id);
|
||||
const title = item.getField("title");
|
||||
const ele = ztoolkit.UI.createElement(document, "div", {
|
||||
namespace: "html",
|
||||
classList: ["command"],
|
||||
listeners: [
|
||||
{
|
||||
type: "mousemove",
|
||||
listener: function () {
|
||||
// @ts-ignore ignore
|
||||
prompt.selectItem(this);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "click",
|
||||
listener: () => {
|
||||
prompt.promptNode.style.display = "none";
|
||||
Zotero_Tabs.select("zotero-pane");
|
||||
ZoteroPane.selectItem(item.id);
|
||||
},
|
||||
},
|
||||
],
|
||||
styles: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "start",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
tag: "span",
|
||||
styles: {
|
||||
fontWeight: "bold",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
properties: {
|
||||
innerText: title,
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "span",
|
||||
styles: {
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
properties: {
|
||||
innerHTML: getItemDescription(item),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
container.appendChild(ele);
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore ignore
|
||||
prompt.exit();
|
||||
prompt.showTip("Not Found.");
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@example
|
||||
static registerConditionalCommandExample() {
|
||||
ztoolkit.Prompt.register([
|
||||
{
|
||||
name: "Conditional Command Test",
|
||||
label: "Plugin Template",
|
||||
// The when function is executed when Prompt UI is woken up by `Shift + P`, and this command does not display when false is returned.
|
||||
when: () => {
|
||||
const items = ZoteroPane.getSelectedItems();
|
||||
return items.length > 0;
|
||||
},
|
||||
callback(prompt) {
|
||||
prompt.inputNode.placeholder = "Hello World!";
|
||||
const items = ZoteroPane.getSelectedItems();
|
||||
ztoolkit.getGlobal("alert")(
|
||||
`You select ${items.length} items!\n\n${items
|
||||
.map(
|
||||
(item, index) =>
|
||||
String(index + 1) + ". " + item.getDisplayTitle(),
|
||||
)
|
||||
.join("\n")}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export class HelperExampleFactory {
|
||||
@example
|
||||
static async dialogExample() {
|
||||
const dialogData: { [key: string | number]: any } = {
|
||||
inputValue: "test",
|
||||
checkboxValue: true,
|
||||
loadCallback: () => {
|
||||
ztoolkit.log(dialogData, "Dialog Opened!");
|
||||
},
|
||||
unloadCallback: () => {
|
||||
ztoolkit.log(dialogData, "Dialog closed!");
|
||||
},
|
||||
};
|
||||
const dialogHelper = new ztoolkit.Dialog(10, 2)
|
||||
.addCell(0, 0, {
|
||||
tag: "h1",
|
||||
properties: { innerHTML: "Helper Examples" },
|
||||
})
|
||||
.addCell(1, 0, {
|
||||
tag: "h2",
|
||||
properties: { innerHTML: "Dialog Data Binding" },
|
||||
})
|
||||
.addCell(2, 0, {
|
||||
tag: "p",
|
||||
properties: {
|
||||
innerHTML:
|
||||
"Elements with attribute 'data-bind' are binded to the prop under 'dialogData' with the same name.",
|
||||
},
|
||||
styles: {
|
||||
width: "200px",
|
||||
},
|
||||
})
|
||||
.addCell(3, 0, {
|
||||
tag: "label",
|
||||
namespace: "html",
|
||||
attributes: {
|
||||
for: "dialog-checkbox",
|
||||
},
|
||||
properties: { innerHTML: "bind:checkbox" },
|
||||
})
|
||||
.addCell(
|
||||
3,
|
||||
1,
|
||||
{
|
||||
tag: "input",
|
||||
namespace: "html",
|
||||
id: "dialog-checkbox",
|
||||
attributes: {
|
||||
"data-bind": "checkboxValue",
|
||||
"data-prop": "checked",
|
||||
type: "checkbox",
|
||||
},
|
||||
properties: { label: "Cell 1,0" },
|
||||
},
|
||||
false,
|
||||
)
|
||||
.addCell(4, 0, {
|
||||
tag: "label",
|
||||
namespace: "html",
|
||||
attributes: {
|
||||
for: "dialog-input",
|
||||
},
|
||||
properties: { innerHTML: "bind:input" },
|
||||
})
|
||||
.addCell(
|
||||
4,
|
||||
1,
|
||||
{
|
||||
tag: "input",
|
||||
namespace: "html",
|
||||
id: "dialog-input",
|
||||
attributes: {
|
||||
"data-bind": "inputValue",
|
||||
"data-prop": "value",
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
false,
|
||||
)
|
||||
.addCell(5, 0, {
|
||||
tag: "h2",
|
||||
properties: { innerHTML: "Toolkit Helper Examples" },
|
||||
})
|
||||
.addCell(
|
||||
6,
|
||||
0,
|
||||
{
|
||||
tag: "button",
|
||||
namespace: "html",
|
||||
attributes: {
|
||||
type: "button",
|
||||
},
|
||||
listeners: [
|
||||
{
|
||||
type: "click",
|
||||
listener: (e: Event) => {
|
||||
addon.hooks.onDialogEvents("clipboardExample");
|
||||
},
|
||||
},
|
||||
],
|
||||
children: [
|
||||
{
|
||||
tag: "div",
|
||||
styles: {
|
||||
padding: "2.5px 15px",
|
||||
},
|
||||
properties: {
|
||||
innerHTML: "example:clipboard",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
false,
|
||||
)
|
||||
.addCell(
|
||||
7,
|
||||
0,
|
||||
{
|
||||
tag: "button",
|
||||
namespace: "html",
|
||||
attributes: {
|
||||
type: "button",
|
||||
},
|
||||
listeners: [
|
||||
{
|
||||
type: "click",
|
||||
listener: (e: Event) => {
|
||||
addon.hooks.onDialogEvents("filePickerExample");
|
||||
},
|
||||
},
|
||||
],
|
||||
children: [
|
||||
{
|
||||
tag: "div",
|
||||
styles: {
|
||||
padding: "2.5px 15px",
|
||||
},
|
||||
properties: {
|
||||
innerHTML: "example:filepicker",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
false,
|
||||
)
|
||||
.addCell(
|
||||
8,
|
||||
0,
|
||||
{
|
||||
tag: "button",
|
||||
namespace: "html",
|
||||
attributes: {
|
||||
type: "button",
|
||||
},
|
||||
listeners: [
|
||||
{
|
||||
type: "click",
|
||||
listener: (e: Event) => {
|
||||
addon.hooks.onDialogEvents("progressWindowExample");
|
||||
},
|
||||
},
|
||||
],
|
||||
children: [
|
||||
{
|
||||
tag: "div",
|
||||
styles: {
|
||||
padding: "2.5px 15px",
|
||||
},
|
||||
properties: {
|
||||
innerHTML: "example:progressWindow",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
false,
|
||||
)
|
||||
.addCell(
|
||||
9,
|
||||
0,
|
||||
{
|
||||
tag: "button",
|
||||
namespace: "html",
|
||||
attributes: {
|
||||
type: "button",
|
||||
},
|
||||
listeners: [
|
||||
{
|
||||
type: "click",
|
||||
listener: (e: Event) => {
|
||||
addon.hooks.onDialogEvents("vtableExample");
|
||||
},
|
||||
},
|
||||
],
|
||||
children: [
|
||||
{
|
||||
tag: "div",
|
||||
styles: {
|
||||
padding: "2.5px 15px",
|
||||
},
|
||||
properties: {
|
||||
innerHTML: "example:virtualized-table",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
false,
|
||||
)
|
||||
.addButton("Confirm", "confirm")
|
||||
.addButton("Cancel", "cancel")
|
||||
.addButton("Help", "help", {
|
||||
noClose: true,
|
||||
callback: (e) => {
|
||||
dialogHelper.window?.alert(
|
||||
"Help Clicked! Dialog will not be closed.",
|
||||
);
|
||||
},
|
||||
})
|
||||
.setDialogData(dialogData)
|
||||
.open("Dialog Example");
|
||||
addon.data.dialog = dialogHelper;
|
||||
await dialogData.unloadLock.promise;
|
||||
addon.data.dialog = undefined;
|
||||
addon.data.alive &&
|
||||
ztoolkit.getGlobal("alert")(
|
||||
`Close dialog with ${dialogData._lastButtonId}.\nCheckbox: ${dialogData.checkboxValue}\nInput: ${dialogData.inputValue}.`,
|
||||
);
|
||||
ztoolkit.log(dialogData);
|
||||
}
|
||||
|
||||
@example
|
||||
static clipboardExample() {
|
||||
new ztoolkit.Clipboard()
|
||||
.addText(
|
||||
"",
|
||||
"text/unicode",
|
||||
)
|
||||
.addText(
|
||||
'<a href="https://github.com/windingwind/zotero-plugin-template">Plugin Template</a>',
|
||||
"text/html",
|
||||
)
|
||||
.copy();
|
||||
ztoolkit.getGlobal("alert")("Copied!");
|
||||
}
|
||||
|
||||
@example
|
||||
static async filePickerExample() {
|
||||
const path = await new ztoolkit.FilePicker(
|
||||
"Import File",
|
||||
"open",
|
||||
[
|
||||
["PNG File(*.png)", "*.png"],
|
||||
["Any", "*.*"],
|
||||
],
|
||||
"image.png",
|
||||
).open();
|
||||
ztoolkit.getGlobal("alert")(`Selected ${path}`);
|
||||
}
|
||||
|
||||
@example
|
||||
static progressWindowExample() {
|
||||
new ztoolkit.ProgressWindow(config.addonName)
|
||||
.createLine({
|
||||
text: "ProgressWindow Example!",
|
||||
type: "success",
|
||||
progress: 100,
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
@example
|
||||
static vtableExample() {
|
||||
ztoolkit.getGlobal("alert")("See src/modules/preferenceScript.ts");
|
||||
}
|
||||
}
|
||||
|
|
@ -7,125 +7,11 @@ export async function registerPrefsScripts(_window: Window) {
|
|||
if (!addon.data.prefs) {
|
||||
addon.data.prefs = {
|
||||
window: _window,
|
||||
columns: [
|
||||
{
|
||||
dataKey: "title",
|
||||
label: getString("prefs-table-title"),
|
||||
fixedWidth: true,
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
dataKey: "detail",
|
||||
label: getString("prefs-table-detail"),
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
title: "Orange",
|
||||
detail: "It's juicy",
|
||||
},
|
||||
{
|
||||
title: "Banana",
|
||||
detail: "It's sweet",
|
||||
},
|
||||
{
|
||||
title: "Apple",
|
||||
detail: "I mean the fruit APPLE",
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
addon.data.prefs.window = _window;
|
||||
}
|
||||
updatePrefsUI();
|
||||
bindPrefEvents();
|
||||
}
|
||||
|
||||
async function updatePrefsUI() {
|
||||
// You can initialize some UI elements on prefs window
|
||||
// with addon.data.prefs.window.document
|
||||
// Or bind some events to the elements
|
||||
const renderLock = ztoolkit.getGlobal("Zotero").Promise.defer();
|
||||
if (addon.data.prefs?.window == undefined) return;
|
||||
const tableHelper = new ztoolkit.VirtualizedTable(addon.data.prefs?.window)
|
||||
.setContainerId(`${config.addonRef}-table-container`)
|
||||
.setProp({
|
||||
id: `${config.addonRef}-prefs-table`,
|
||||
// Do not use setLocale, as it modifies the Zotero.Intl.strings
|
||||
// Set locales directly to columns
|
||||
columns: addon.data.prefs?.columns,
|
||||
showHeader: true,
|
||||
multiSelect: true,
|
||||
staticColumns: true,
|
||||
disableFontSizeScaling: true,
|
||||
})
|
||||
.setProp("getRowCount", () => addon.data.prefs?.rows.length || 0)
|
||||
.setProp(
|
||||
"getRowData",
|
||||
(index) =>
|
||||
addon.data.prefs?.rows[index] || {
|
||||
title: "no data",
|
||||
detail: "no data",
|
||||
},
|
||||
)
|
||||
// Show a progress window when selection changes
|
||||
.setProp("onSelectionChange", (selection) => {
|
||||
new ztoolkit.ProgressWindow(config.addonName)
|
||||
.createLine({
|
||||
text: `Selected line: ${addon.data.prefs?.rows
|
||||
.filter((v, i) => selection.isSelected(i))
|
||||
.map((row) => row.title)
|
||||
.join(",")}`,
|
||||
progress: 100,
|
||||
})
|
||||
.show();
|
||||
})
|
||||
// When pressing delete, delete selected line and refresh table.
|
||||
// Returning false to prevent default event.
|
||||
.setProp("onKeyDown", (event: KeyboardEvent) => {
|
||||
if (event.key == "Delete" || (Zotero.isMac && event.key == "Backspace")) {
|
||||
addon.data.prefs!.rows =
|
||||
addon.data.prefs?.rows.filter(
|
||||
(v, i) => !tableHelper.treeInstance.selection.isSelected(i),
|
||||
) || [];
|
||||
tableHelper.render();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
// For find-as-you-type
|
||||
.setProp(
|
||||
"getRowString",
|
||||
(index) => addon.data.prefs?.rows[index].title || "",
|
||||
)
|
||||
// Render the table.
|
||||
.render(-1, () => {
|
||||
renderLock.resolve();
|
||||
});
|
||||
await renderLock.promise;
|
||||
ztoolkit.log("Preference table rendered!");
|
||||
}
|
||||
|
||||
function bindPrefEvents() {
|
||||
addon.data
|
||||
.prefs!.window.document.querySelector(
|
||||
`#zotero-prefpane-${config.addonRef}-enable`,
|
||||
)
|
||||
?.addEventListener("command", (e) => {
|
||||
ztoolkit.log(e);
|
||||
addon.data.prefs!.window.alert(
|
||||
`Successfully changed to ${(e.target as XUL.Checkbox).checked}!`,
|
||||
);
|
||||
});
|
||||
|
||||
addon.data
|
||||
.prefs!.window.document.querySelector(
|
||||
`#zotero-prefpane-${config.addonRef}-input`,
|
||||
)
|
||||
?.addEventListener("change", (e) => {
|
||||
ztoolkit.log(e);
|
||||
addon.data.prefs!.window.alert(
|
||||
`Successfully changed to ${(e.target as HTMLInputElement).value}!`,
|
||||
);
|
||||
});
|
||||
}
|
||||
function bindPrefEvents() {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
import { tldrs } from "./dataStorage";
|
||||
|
||||
type SemanticScholarItemInfo = {
|
||||
title?: string;
|
||||
abstract?: string;
|
||||
tldr?: string;
|
||||
};
|
||||
|
||||
export class TLDRFetcher {
|
||||
private readonly zoteroItem: Zotero.Item;
|
||||
private readonly title?: string;
|
||||
private readonly abstract?: string;
|
||||
|
||||
constructor(item: Zotero.Item) {
|
||||
this.zoteroItem = item;
|
||||
if (item.isRegularItem()) {
|
||||
this.title = item.getField("title") as string;
|
||||
this.abstract = item.getField("abstractNote") as string;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchTLDR() {
|
||||
if (!this.title || this.title.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
const noteKey = (await tldrs.getAsync())[this.zoteroItem.key];
|
||||
try {
|
||||
const infos = await this.fetchRelevanceItemInfos(this.title);
|
||||
for (const info of infos) {
|
||||
let match = false;
|
||||
if (info.title && this.title && this.checkLCS(info.title, this.title)) {
|
||||
match = true;
|
||||
} else if (
|
||||
info.abstract &&
|
||||
this.abstract &&
|
||||
this.checkLCS(info.abstract, this.abstract)
|
||||
) {
|
||||
match = true;
|
||||
}
|
||||
if (match && info.tldr) {
|
||||
let note = new Zotero.Item("note");
|
||||
if (noteKey) {
|
||||
const obj = Zotero.Items.getByLibraryAndKey(
|
||||
this.zoteroItem.libraryID,
|
||||
noteKey,
|
||||
);
|
||||
if (
|
||||
obj &&
|
||||
obj instanceof Zotero.Item &&
|
||||
this.zoteroItem.getNotes().includes(obj.id)
|
||||
) {
|
||||
note = obj;
|
||||
}
|
||||
}
|
||||
note.setNote(`<p>TL;DR</p>\n<p>${info.tldr}</p>`);
|
||||
note.parentID = this.zoteroItem.id;
|
||||
await note.saveTx();
|
||||
await tldrs.modify((data: any) => {
|
||||
data[this.zoteroItem.key] = note.key;
|
||||
return data;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
await tldrs.modify((data: any) => {
|
||||
data[this.zoteroItem.key] = false;
|
||||
return data;
|
||||
});
|
||||
} catch (error) {
|
||||
Zotero.log(`post semantic scholar request error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchRelevanceItemInfos(
|
||||
title: string,
|
||||
): Promise<SemanticScholarItemInfo[]> {
|
||||
const semanticScholarURL = "https://www.semanticscholar.org/api/1/search";
|
||||
const params = {
|
||||
queryString: title,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
sort: "relevance",
|
||||
authors: [],
|
||||
coAuthors: [],
|
||||
venues: [],
|
||||
performTitleMatch: true,
|
||||
requireViewablePdf: false,
|
||||
includeTldrs: true,
|
||||
};
|
||||
const resp = await Zotero.HTTP.request("POST", semanticScholarURL, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
const results = JSON.parse(resp.response).results;
|
||||
return results.map((item: any) => {
|
||||
const result = {
|
||||
title: item.title.text,
|
||||
abstract: item.paperAbstract.text,
|
||||
tldr: undefined,
|
||||
};
|
||||
if (item.tldr) {
|
||||
result.tldr = item.tldr.text;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private checkLCS(pattern: string, content: string): boolean {
|
||||
const LCS = StringMatchUtils.longestCommonSubsequence(pattern, content);
|
||||
return LCS.length >= Math.max(pattern.length, content.length) * 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
class StringMatchUtils {
|
||||
static longestCommonSubsequence(text1: string, text2: string): string {
|
||||
const m = text1.length;
|
||||
const n = text2.length;
|
||||
|
||||
const dp: number[][] = new Array(m + 1);
|
||||
for (let i = 0; i <= m; i++) {
|
||||
dp[i] = new Array(n + 1).fill(0);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
if (text1[i - 1] === text2[j - 1]) {
|
||||
dp[i][j] = dp[i - 1][j - 1] + 1;
|
||||
} else {
|
||||
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let i = m,
|
||||
j = n;
|
||||
const lcs: string[] = [];
|
||||
while (i > 0 && j > 0) {
|
||||
if (text1[i - 1] === text2[j - 1]) {
|
||||
lcs.unshift(text1[i - 1]);
|
||||
i--;
|
||||
j--;
|
||||
} else if (dp[i - 1][j] > dp[i][j - 1]) {
|
||||
i--;
|
||||
} else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
return lcs.join("");
|
||||
}
|
||||
|
||||
// static minWindow(s: string, t: string): [number, number] | null {
|
||||
// const m = s.length, n = t.length
|
||||
// let start = -1, minLen = Number.MAX_SAFE_INTEGER, i = 0, j = 0, end;
|
||||
// while (i < m) {
|
||||
// if (s[i] == t[j]) {
|
||||
// if (++j == n) {
|
||||
// end = i + 1;
|
||||
// while (--j >= 0) {
|
||||
// while (s[i--] != t[j]);
|
||||
// }
|
||||
// ++i; ++j;
|
||||
// if (end - i < minLen) {
|
||||
// minLen = end - i;
|
||||
// start = i;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ++i;
|
||||
// }
|
||||
// return start == -1 ? null : [start, minLen];
|
||||
// }
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@
|
|||
"target": "ES2016",
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
"strict": true,
|
||||
},
|
||||
"include": ["src", "typings", "node_modules/zotero-types"],
|
||||
"exclude": ["build", "addon"]
|
||||
"exclude": ["build", "addon"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"addons": {
|
||||
"addontemplate@euclpts.com": {
|
||||
"zoterotldr@syt.com": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "1.1.1",
|
||||
"update_link": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi",
|
||||
"version": "1.0.7",
|
||||
"update_link": "undefined/latest/download/zotero-tldr.xpi",
|
||||
"applications": {
|
||||
"zotero": {
|
||||
"strict_min_version": "6.999"
|
||||
|
|
|
|||
Loading…
Reference in New Issue