diff --git a/.eslintrc.json b/.eslintrc.json
index 0f26c1d..5aa2af8 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -4,7 +4,11 @@
"es2021": true
},
"root": true,
- "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "prettier"
+ ],
"overrides": [],
"parser": "@typescript-eslint/parser",
"parserOptions": {
@@ -13,18 +17,17 @@
},
"plugins": ["@typescript-eslint"],
"rules": {
- "@typescript-eslint/ban-ts-comment": [
- "warn",
- {
- "ts-expect-error": "allow-with-description",
- "ts-ignore": "allow-with-description",
- "ts-nocheck": "allow-with-description",
- "ts-check": "allow-with-description"
- }
- ],
+ "@typescript-eslint/ban-ts-comment": ["warn", "allow-with-description"],
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": ["off", { "ignoreRestArgs": true }],
"@typescript-eslint/no-non-null-assertion": "off"
},
- "ignorePatterns": ["**/build/**", "**/dist/**", "**/node_modules/**", "**/scripts/**", "**/*.js", "**/*.bak"]
+ "ignorePatterns": [
+ "**/builds/**",
+ "**/dist/**",
+ "**/node_modules/**",
+ "**/scripts/**",
+ "**/*.js",
+ "**/*.bak"
+ ]
}
diff --git a/.gitignore b/.gitignore
index d277962..4192fd6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
**/build
+**/builds
node_modules
package-lock.json
-zotero-cmd.json
\ No newline at end of file
+zotero-cmd.json
diff --git a/.idea/zoplicate.iml b/.idea/zoplicate.iml
index 10b7162..b02fdaa 100644
--- a/.idea/zoplicate.iml
+++ b/.idea/zoplicate.iml
@@ -6,6 +6,7 @@
+
diff --git a/.release-it.json b/.release-it.json
index 69da605..3d4ffa5 100644
--- a/.release-it.json
+++ b/.release-it.json
@@ -4,7 +4,7 @@
},
"github": {
"release": true,
- "assets": ["build/*.xpi"]
+ "assets": ["builds/*.xpi"]
},
"hooks": {
"before:init": "npm run lint",
diff --git a/README.md b/README.md
index 87aa48f..d4c7d01 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,12 @@
-[![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/ChenglongMa/zoplicate?include_prereleases)](https://github.com/ChenglongMa/zoplicate/releases/latest)
-[![Github All Releases](https://img.shields.io/github/downloads/ChenglongMa/zoplicate/total.svg)](https://github.com/ChenglongMa/zoplicate/releases)
+[![GitHub package.json version (branch)](https://img.shields.io/github/package-json/v/ChenglongMa/zoplicate/zotero-6)
+](https://github.com/ChenglongMa/zoplicate/releases/tag/zotero6)
+[![Github All Releases](https://img.shields.io/github/downloads/ChenglongMa/zoplicate/latest/total)](https://github.com/ChenglongMa/zoplicate/releases)
+[![GitHub release (by tag)](https://img.shields.io/github/downloads/ChenglongMa/zoplicate/zotero6/total)](https://github.com/ChenglongMa/zoplicate/releases/tag/zotero6)
![GitHub License](https://img.shields.io/github/license/ChenglongMa/zoplicate)
-[![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-6-green?style=flat-square&logo=zotero&logoColor=CC2936)](https://www.zotero.org)
[![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template)
A plugin that does one thing only: **Detect** and **Manage** duplicate items in [![zotero](https://www.zotero.org/support/lib/exe/fetch.php?tok=2735f1&media=https%3A%2F%2Fwww.zotero.org%2Fstatic%2Fimages%2Fpromote%2Fzotero-logo-128x31.png)](https://www.zotero.org/).
@@ -22,12 +24,14 @@ The actions you can take are:
# Install
-1. Go to the [release page](https://github.com/ChenglongMa/zoplicate/releases) to download [the latest `.xpi` file](https://github.com/ChenglongMa/zoplicate/releases/latest/download/zoplicate.xpi).
+1. Go to the [zotero 6 release page](https://github.com/ChenglongMa/zoplicate/releases/tag/zotero6) to download [the `.xpi` file for zotero 6](https://github.com/ChenglongMa/zoplicate/releases/download/zotero6/zoplicate.xpi).
- If you are using FireFox, right-click on the link of the XPI file and select "Save As...".
2. Then, in Zotero, click `Tools` -> `Add-ons` and drag the `.xpi` onto the Add-ons window.
See [how to install a Zotero addon](https://www.zotero.org/support/plugins).
-Note: The latest version is only supported for Zotero 7.
+## Note
+* [The latest version](https://github.com/ChenglongMa/zoplicate/releases/latest) is only supported for Zotero 7.
+* This version is only supported for Zotero 6.
# Usage
diff --git a/addon/bootstrap.js b/addon/bootstrap.js
index 5d415aa..7045b97 100644
--- a/addon/bootstrap.js
+++ b/addon/bootstrap.js
@@ -5,23 +5,85 @@
* [2] https://www.zotero.org/support/dev/zotero_7_for_developers
*/
+if (typeof Zotero == "undefined") {
+ var Zotero;
+}
+
var chromeHandle;
+// In Zotero 6, bootstrap methods are called before Zotero is initialized, and using include.js
+// to get the Zotero XPCOM service would risk breaking Zotero startup. Instead, wait for the main
+// Zotero window to open and get the Zotero object from there.
+//
+// In Zotero 7, bootstrap methods are not called until Zotero is initialized, and the 'Zotero' is
+// automatically made available.
+async function waitForZotero() {
+ if (typeof Zotero != "undefined") {
+ await Zotero.initializationPromise;
+ }
+
+ var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+ var windows = Services.wm.getEnumerator("navigator:browser");
+ var found = false;
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext();
+ if (win.Zotero) {
+ Zotero = win.Zotero;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ await new Promise((resolve) => {
+ var listener = {
+ onOpenWindow: function (aWindow) {
+ // Wait for the window to finish loading
+ let domWindow = aWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
+ domWindow.addEventListener(
+ "load",
+ function () {
+ domWindow.removeEventListener("load", arguments.callee, false);
+ if (domWindow.Zotero) {
+ Services.wm.removeListener(listener);
+ Zotero = domWindow.Zotero;
+ resolve();
+ }
+ },
+ false
+ );
+ },
+ };
+ Services.wm.addListener(listener);
+ });
+ }
+ await Zotero.initializationPromise;
+}
+
function install(data, reason) {}
async function startup({ id, version, resourceURI, rootURI }, reason) {
- await Zotero.initializationPromise;
+ await waitForZotero();
// String 'rootURI' introduced in Zotero 7
if (!rootURI) {
rootURI = resourceURI.spec;
}
- var aomStartup = Components.classes["@mozilla.org/addons/addon-manager-startup;1"].getService(
- Components.interfaces.amIAddonManagerStartup,
- );
- var manifestURI = Services.io.newURI(rootURI + "manifest.json");
- chromeHandle = aomStartup.registerChrome(manifestURI, [["content", "__addonRef__", rootURI + "chrome/content/"]]);
+ if (Zotero.platformMajorVersion >= 102) {
+ var aomStartup = Components.classes[
+ "@mozilla.org/addons/addon-manager-startup;1"
+ ].getService(Components.interfaces.amIAddonManagerStartup);
+ var manifestURI = Services.io.newURI(rootURI + "manifest.json");
+ chromeHandle = aomStartup.registerChrome(manifestURI, [
+ ["content", "__addonRef__", rootURI + "chrome/content/"],
+ ["locale", "__addonRef__", "en-US", rootURI + "chrome/locale/en-US/"],
+ ["locale", "__addonRef__", "zh-CN", rootURI + "chrome/locale/zh-CN/"],
+ ]);
+ } else {
+ setDefaultPrefs(rootURI);
+ }
/**
* Global variables for plugin code.
@@ -34,30 +96,28 @@ async function startup({ id, version, resourceURI, rootURI }, reason) {
};
ctx._globalThis = ctx;
- Services.scriptloader.loadSubScript(`${rootURI}/chrome/content/scripts/__addonRef__.js`, ctx);
-}
-
-async function onMainWindowLoad({ window }, reason) {
- Zotero.__addonInstance__?.hooks.onMainWindowLoad(window);
-}
-
-async function onMainWindowUnload({ window }, reason) {
- Zotero.__addonInstance__?.hooks.onMainWindowUnload(window);
+ Services.scriptloader.loadSubScript(
+ `${rootURI}/chrome/content/scripts/index.js`,
+ ctx
+ );
}
function shutdown({ id, version, resourceURI, rootURI }, reason) {
if (reason === APP_SHUTDOWN) {
return;
}
-
if (typeof Zotero === "undefined") {
- Zotero = Components.classes["@zotero.org/Zotero;1"].getService(Components.interfaces.nsISupports).wrappedJSObject;
+ Zotero = Components.classes["@zotero.org/Zotero;1"].getService(
+ Components.interfaces.nsISupports
+ ).wrappedJSObject;
}
- Zotero.__addonInstance__?.hooks.onShutdown();
+ Zotero.__addonInstance__.hooks.onShutdown();
- Cc["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService).flushBundles();
+ Cc["@mozilla.org/intl/stringbundle;1"]
+ .getService(Components.interfaces.nsIStringBundleService)
+ .flushBundles();
- Cu.unload(`${rootURI}/chrome/content/scripts/__addonRef__.js`);
+ Cu.unload(`${rootURI}/chrome/content/scripts/index.js`);
if (chromeHandle) {
chromeHandle.destruct();
@@ -66,3 +126,27 @@ function shutdown({ id, version, resourceURI, rootURI }, reason) {
}
function uninstall(data, reason) {}
+
+// Loads default preferences from defaults/preferences/prefs.js in Zotero 6
+function setDefaultPrefs(rootURI) {
+ var branch = Services.prefs.getDefaultBranch("");
+ var obj = {
+ pref(pref, value) {
+ switch (typeof value) {
+ case "boolean":
+ branch.setBoolPref(pref, value);
+ break;
+ case "string":
+ branch.setStringPref(pref, value);
+ break;
+ case "number":
+ branch.setIntPref(pref, value);
+ break;
+ default:
+ Zotero.logError(`Invalid type '${typeof value}' for pref '${pref}'`);
+ }
+ },
+ };
+ Zotero.getMainWindow().console.log(rootURI + "prefs.js");
+ Services.scriptloader.loadSubScript(rootURI + "prefs.js", obj);
+}
diff --git a/addon/chrome.manifest b/addon/chrome.manifest
new file mode 100644
index 0000000..713e1b0
--- /dev/null
+++ b/addon/chrome.manifest
@@ -0,0 +1,3 @@
+content __addonRef__ chrome/content/
+locale __addonRef__ en-US chrome/locale/en-US/
+locale __addonRef__ zh-CN chrome/locale/zh-CN/
diff --git a/addon/chrome/content/preferences.xhtml b/addon/chrome/content/preferences.xhtml
index d9352b6..464248c 100644
--- a/addon/chrome/content/preferences.xhtml
+++ b/addon/chrome/content/preferences.xhtml
@@ -1,33 +1,24 @@
-
-
-
-
-
+
-
-
-
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
+
+
+
diff --git a/addon/chrome/locale/en-US/addon.properties b/addon/chrome/locale/en-US/addon.properties
new file mode 100644
index 0000000..2acb6ea
--- /dev/null
+++ b/addon/chrome/locale/en-US/addon.properties
@@ -0,0 +1,30 @@
+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.table.title = Title
+prefs.table.detail = Detail
+tabpanel.lib.tab.label = Lib Tab
+tabpanel.reader.tab.label = Reader Tab
+
+prefs.title = Zoplicate
+general.cancel = Cancel
+
+du.dialog.title = Found Duplicate Items
+du.dialog.header = The following items have existed in your library. How would you like to process them?
+
+du.dialog.table.title = Title
+du.dialog.table.keep = Keep This
+du.dialog.table.discard = Keep Others
+du.dialog.table.cancel = Keep All
+du.dialog.as.default = Use this action by default
+
+du.dialog.button.apply = Apply
+du.dialog.button.go.duplicates = Go to Duplicates
+du.dialog.button.cancel = Cancel
+
+du.progress.text = Processing Duplicates...
+du.progress.done = All duplicates have been processed.
+
diff --git a/addon/chrome/locale/en-US/overlay.dtd b/addon/chrome/locale/en-US/overlay.dtd
new file mode 100644
index 0000000..958fe53
--- /dev/null
+++ b/addon/chrome/locale/en-US/overlay.dtd
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/addon/chrome/locale/zh-CN/addon.properties b/addon/chrome/locale/zh-CN/addon.properties
new file mode 100644
index 0000000..90d97a4
--- /dev/null
+++ b/addon/chrome/locale/zh-CN/addon.properties
@@ -0,0 +1,30 @@
+startup.begin = 插件加载中
+startup.finish = 插件已就绪
+menuitem.label = 插件模板: 帮助工具样例
+menupopup.label = 插件模板: 弹出菜单
+menuitem.submenulabel = 插件模板:子菜单
+menuitem.filemenulabel = 插件模板: 文件菜单
+prefs.table.title = 标题
+prefs.table.detail = 详情
+tabpanel.lib.tab.label = 库标签
+tabpanel.reader.tab.label = 阅读器标签
+
+
+prefs.title = Zoplicate
+general.cancel = 取消
+
+du.dialog.title = 检测到重复条目
+du.dialog.header = 以下条目已存在于您的库中。您想如何处理它们?
+
+du.dialog.table.title = 标题
+du.dialog.table.keep = 保留最新的
+du.dialog.table.discard = 保留已有的
+du.dialog.table.cancel = 保留全部
+du.dialog.as.default = 将此操作设为默认值
+
+du.dialog.button.apply = 应用
+du.dialog.button.go.duplicates = 手动合并
+du.dialog.button.cancel = 取消
+
+du.progress.text = 正在处理重复条目...
+du.progress.done = 所有重复条目已处理完成。
diff --git a/addon/chrome/locale/zh-CN/overlay.dtd b/addon/chrome/locale/zh-CN/overlay.dtd
new file mode 100644
index 0000000..9b88f1a
--- /dev/null
+++ b/addon/chrome/locale/zh-CN/overlay.dtd
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/addon/install.rdf b/addon/install.rdf
new file mode 100644
index 0000000..a0c3322
--- /dev/null
+++ b/addon/install.rdf
@@ -0,0 +1,35 @@
+
+
+
+
+
+ zotero@chnm.gmu.edu
+ 5.0
+ 6.999
+
+
+
+
+ juris-m@juris-m.github.io
+ 5.0
+ *
+
+
+
+
diff --git a/addon/locale/en-US/addon.ftl b/addon/locale/en-US/addon.ftl
deleted file mode 100644
index 43cf09b..0000000
--- a/addon/locale/en-US/addon.ftl
+++ /dev/null
@@ -1,33 +0,0 @@
-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-table-title = Title
-prefs-table-detail = Detail
-tabpanel-lib-tab-label = Lib Tab
-tabpanel-reader-tab-label = Reader Tab
-
-# General
-prefs-title = Zoplicate
-general-cancel = Cancel
-
-# Duplicate Dialog
-du-dialog-title = Found Duplicate Items
-du-dialog-header = The following items have existed in your library. How would you like to process them?
-
-du-dialog-table-title = Title
-du-dialog-table-keep = Keep This
-du-dialog-table-discard = Keep Others
-du-dialog-table-cancel = Keep All
-du-dialog-as-default = Use this action by default
-
-## Buttons
-du-dialog-button-apply = Apply
-du-dialog-button-go-duplicates = Go to Duplicates
-du-dialog-button-cancel = Cancel
-
-## Messages
-du-progress-text = Processing Duplicates...
-du-progress-done = All duplicates have been processed.
diff --git a/addon/locale/en-US/preferences.ftl b/addon/locale/en-US/preferences.ftl
deleted file mode 100644
index e2f6afe..0000000
--- a/addon/locale/en-US/preferences.ftl
+++ /dev/null
@@ -1,13 +0,0 @@
-pref-title = Default Preferences
-pref-default-action-title = Default action to process duplicate items
-
-pref-default-action-keep-this =
- .label = [Keep This]: Save the last imported item and delete the rest
-pref-default-action-keep-others =
- .label = [Keep Others]: Delete the last imported item and save the rest
-pref-default-action-keep-all =
- .label = [Keep All]: Save all items
-pref-default-action-always-ask =
- .label = [Always Ask]: Ask for action every time
-
-pref-help = { $name } Build { $version } { $time }
diff --git a/addon/locale/zh-CN/addon.ftl b/addon/locale/zh-CN/addon.ftl
deleted file mode 100644
index 8a1d691..0000000
--- a/addon/locale/zh-CN/addon.ftl
+++ /dev/null
@@ -1,34 +0,0 @@
-startup-begin = 插件加载中
-startup-finish = 插件已就绪
-menuitem-label = 插件模板: 帮助工具样例
-menupopup-label = 插件模板: 弹出菜单
-menuitem-submenulabel = 插件模板:子菜单
-menuitem-filemenulabel = 插件模板: 文件菜单
-prefs-table-title = 标题
-prefs-table-detail = 详情
-tabpanel-lib-tab-label = 库标签
-tabpanel-reader-tab-label = 阅读器标签
-
-
-# General
-prefs-title = Zoplicate
-general-cancel = 取消
-
-# Duplicate Dialog
-du-dialog-title = 检测到重复条目
-du-dialog-header = 以下条目已存在于您的库中。您想如何处理它们?
-
-du-dialog-table-title = 标题
-du-dialog-table-keep = 保留最新的
-du-dialog-table-discard = 保留已有的
-du-dialog-table-cancel = 保留全部
-du-dialog-as-default = 将此操作设为默认值
-
-## Buttons
-du-dialog-button-apply = 应用
-du-dialog-button-go-duplicates = 手动合并
-du-dialog-button-cancel = 取消
-
-## Messages
-du-progress-text = 正在处理重复条目...
-du-progress-done = 所有重复条目已处理完成。
diff --git a/addon/locale/zh-CN/preferences.ftl b/addon/locale/zh-CN/preferences.ftl
deleted file mode 100644
index be4f8b3..0000000
--- a/addon/locale/zh-CN/preferences.ftl
+++ /dev/null
@@ -1,13 +0,0 @@
-pref-title = 默认设置
-pref-default-action-title = 处理重复条目的默认操作
-
-pref-default-action-keep-this =
- .label = [保留最新的]: 保留新导入条目,删除库中原有的
-pref-default-action-keep-others =
- .label = [保留已有的]: 保留库中原有条目,删除新导入的
-pref-default-action-keep-all =
- .label = [保留全部]: 保留所有条目,不删除任何项
-pref-default-action-always-ask =
- .label = [始终询问]: 每次都询问我该如何处理
-
-pref-help = { $name } Build { $version } { $time }
diff --git a/addon/manifest.json b/addon/manifest.json
index 0c356c6..a92439a 100644
--- a/addon/manifest.json
+++ b/addon/manifest.json
@@ -12,7 +12,7 @@
"applications": {
"zotero": {
"id": "__addonID__",
- "update_url": "__updateURL__",
+ "update_url": "__updaterdf__",
"strict_min_version": "6.999",
"strict_max_version": "7.0.*"
}
diff --git a/docs/settings.png b/docs/settings.png
index 9d6dfb3..d1f133e 100644
Binary files a/docs/settings.png and b/docs/settings.png differ
diff --git a/package.json b/package.json
index f16247c..dc708e0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "zoplicate",
- "version": "1.0.0",
+ "version": "0.6.0",
"description": "Detect and manage duplicate items in Zotero.",
"config": {
"addonName": "Zoplicate",
@@ -8,9 +8,8 @@
"addonRef": "zoplicate",
"addonInstance": "Zoplicate",
"prefsPrefix": "extensions.zotero.zoplicate",
- "releasePage": "https://github.com/ChenglongMa/zoplicate/releases",
- "updateJSON": "https://raw.githubusercontent.com/ChenglongMa/zoplicate/main/update.json",
- "updateBetaJSON": "https://raw.githubusercontent.com/ChenglongMa/zoplicate/main/update-beta.json"
+ "releasepage": "https://github.com/ChenglongMa/zoplicate/releases/download/zotero6/zoplicate.xpi",
+ "updaterdf": "https://raw.githubusercontent.com/ChenglongMa/zoplicate/zotero-6/update.json"
},
"main": "src/index.ts",
"scripts": {
@@ -42,23 +41,24 @@
},
"homepage": "https://chenglongma.com/zoplicate/",
"dependencies": {
- "zotero-plugin-toolkit": "^2.3.6"
+ "zotero-plugin-toolkit": "2.1.7"
},
"devDependencies": {
- "@types/node": "^20.6.0",
- "@typescript-eslint/eslint-plugin": "^6.6.0",
- "@typescript-eslint/parser": "^6.6.0",
+ "@types/node": "^20.3.0",
+ "@typescript-eslint/eslint-plugin": "^5.59.9",
+ "@typescript-eslint/parser": "^5.59.9",
"chokidar-cli": "^3.0.0",
- "compressing": "^1.10.0",
- "concurrently": "^8.2.1",
+ "compressing": "^1.9.0",
+ "concurrently": "^8.2.0",
"cross-env": "^7.0.3",
- "esbuild": "^0.19.2",
- "eslint": "^8.49.0",
- "eslint-config-prettier": "^9.0.0",
- "prettier": "^3.0.3",
- "release-it": "^17.0.0",
- "replace-in-file": "^7.0.1",
- "typescript": "^5.2.2",
- "zotero-types": "^1.3.5"
+ "esbuild": "^0.18.1",
+ "eslint": "^8.42.0",
+ "eslint-config-prettier": "^8.8.0",
+ "minimist": "^1.2.8",
+ "prettier": "2.8.8",
+ "release-it": "^15.11.0",
+ "replace-in-file": "6.3.5",
+ "typescript": "^5.1.3",
+ "zotero-types": "^1.0.15"
}
}
diff --git a/scripts/build.mjs b/scripts/build.mjs
index 5987d38..4b6a961 100644
--- a/scripts/build.mjs
+++ b/scripts/build.mjs
@@ -1,35 +1,29 @@
import { build } from "esbuild";
import { zip } from "compressing";
-import path from "path";
-import { existsSync, lstatSync, writeFileSync, readFileSync, mkdirSync, readdirSync, rmSync, renameSync } from "fs";
+import { join, basename } from "path";
+import {
+ existsSync,
+ lstatSync,
+ writeFileSync,
+ readFileSync,
+ mkdirSync,
+ readdirSync,
+ rmSync,
+} from "fs";
import { env, exit } from "process";
import replaceInFile from "replace-in-file";
-const { replaceInFileSync } = replaceInFile;
+const { sync } = replaceInFile;
import details from "../package.json" assert { type: "json" };
const { name, author, description, homepage, version, config } = details;
-const t = new Date();
-const buildTime = dateFormat("YYYY-mm-dd HH:MM:SS", new Date());
-const buildDir = "build";
-
-const isPreRelease = version.includes("-");
-
-// If it is a pre-release, use update-beta.json
-config.updateURL = isPreRelease ? config.updateBetaJSON : config.updateJSON;
-
-const updateJSONFile = isPreRelease ? "update-beta.json" : "update.json";
-const updateLink = isPreRelease
- ? `${config.releasePage}/download/v${version}/${name}.xpi`
- : `${config.releasePage}/latest/download/${name}.xpi`;
-
function copyFileSync(source, target) {
var targetFile = target;
// If target is a directory, a new file with the same name will be created
if (existsSync(target)) {
if (lstatSync(target).isDirectory()) {
- targetFile = path.join(target, path.basename(source));
+ targetFile = join(target, basename(source));
}
}
@@ -40,7 +34,7 @@ function copyFolderRecursiveSync(source, target) {
var files = [];
// Check if folder needs to be created or integrated
- var targetFolder = path.join(target, path.basename(source));
+ var targetFolder = join(target, basename(source));
if (!existsSync(targetFolder)) {
mkdirSync(targetFolder);
}
@@ -49,7 +43,7 @@ function copyFolderRecursiveSync(source, target) {
if (lstatSync(source).isDirectory()) {
files = readdirSync(source);
files.forEach(function (file) {
- var curSource = path.join(source, file);
+ var curSource = join(source, file);
if (lstatSync(curSource).isDirectory()) {
copyFolderRecursiveSync(curSource, targetFolder);
} else {
@@ -80,161 +74,105 @@ function dateFormat(fmt, date) {
for (let k in opt) {
ret = new RegExp("(" + k + ")").exec(fmt);
if (ret) {
- fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, "0"));
+ fmt = fmt.replace(
+ ret[1],
+ ret[1].length === 1 ? opt[k] : opt[k].padStart(ret[1].length, "0")
+ );
}
}
return fmt;
}
-function renameLocaleFiles() {
- 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}`),
- );
- }
- }
- }
-}
+async function main() {
+ const t = new Date();
+ const buildTime = dateFormat("YYYY-mm-dd HH:MM:SS", t);
+ const buildDir = "builds";
+
+ console.log(
+ `[Build] BUILD_DIR=${buildDir}, VERSION=${version}, BUILD_TIME=${buildTime}, ENV=${[
+ env.NODE_ENV,
+ ]}`
+ );
+
+ clearFolder(buildDir);
+
+ copyFolderRecursiveSync("addon", buildDir);
+
+ copyFileSync("update-template.json", "update.json");
+ copyFileSync("update-template.rdf", "update.rdf");
+
+ await build({
+ entryPoints: ["src/index.ts"],
+ define: {
+ __env__: `"${env.NODE_ENV}"`,
+ },
+ bundle: true,
+ target: "firefox60",
+ outfile: join(buildDir, "addon/chrome/content/scripts/index.js"),
+ // Don't turn minify on
+ // minify: true,
+ }).catch(() => exit(1));
+
+ console.log("[Build] Run esbuild OK");
-function replaceString() {
const replaceFrom = [
/__author__/g,
/__description__/g,
/__homepage__/g,
/__buildVersion__/g,
/__buildTime__/g,
- /__updateLink__/g,
];
- const replaceTo = [author, description, homepage, version, buildTime, updateLink];
- replaceFrom.push(...Object.keys(config).map((k) => new RegExp(`__${k}__`, "g")));
+ const replaceTo = [author, description, homepage, version, buildTime];
+
+ replaceFrom.push(
+ ...Object.keys(config).map((k) => new RegExp(`__${k}__`, "g"))
+ );
replaceTo.push(...Object.values(config));
const optionsAddon = {
files: [
- `${buildDir}/addon/**/*.xhtml`,
- `${buildDir}/addon/**/*.html`,
- `${buildDir}/addon/**/*.json`,
- `${buildDir}/addon/prefs.js`,
- `${buildDir}/addon/manifest.json`,
- `${buildDir}/addon/bootstrap.js`,
+ join(buildDir, "**/*.rdf"),
+ join(buildDir, "**/*.dtd"),
+ join(buildDir, "**/*.xul"),
+ join(buildDir, "**/*.xhtml"),
+ join(buildDir, "**/*.json"),
+ join(buildDir, "addon/prefs.js"),
+ join(buildDir, "addon/chrome.manifest"),
+ join(buildDir, "addon/manifest.json"),
+ join(buildDir, "addon/bootstrap.js"),
+ "update.json",
+ "update.rdf",
],
from: replaceFrom,
to: replaceTo,
countMatches: true,
};
- optionsAddon.files.push(updateJSONFile);
-
- const replaceResult = replaceInFileSync(optionsAddon);
-
- 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(/^(?[a-zA-Z]\S*)([ ]*=[ ]*)(?.*)$/m);
- if (match) {
- localeMessage.add(match.groups.message);
- return `${config.addonRef}-${line}`;
- } else {
- return line;
- }
- });
- return prefixedLines.join("\n");
- },
- });
-
- const replaceResultXhtml = replaceInFileSync({
- files: [`${buildDir}/addon/**/*.xhtml`],
- processor: (input) => {
- const matchs = [...input.matchAll(/(data-l10n-id)="(\S*)"/g)];
- matchs.map((match) => {
- if (localeMessage.has(match[2])) {
- input = input.replace(match[0], `${match[1]}="${config.addonRef}-${match[2]}"`);
- } else {
- localeMessageMiss.add(match[2]);
- }
- });
- return input;
- },
- });
-
+ const replaceResult = sync(optionsAddon);
console.log(
"[Build] Run replace in ",
- replaceResult.filter((f) => f.hasChanged).map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`),
- replaceResultFlt.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`),
- replaceResultXhtml.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`),
+ replaceResult
+ .filter((f) => f.hasChanged)
+ .map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`)
);
- if (localeMessageMiss.size !== 0) {
- console.warn(
- `[Build] [Warn] Fluent message [${new Array(...localeMessageMiss)}] do not exsit in addon's locale files.`,
- );
- }
-}
-
-async function esbuild() {
- await build({
- entryPoints: ["src/index.ts"],
- define: {
- __env__: `"${env.NODE_ENV}"`,
- },
- bundle: true,
- target: "firefox102",
- outfile: path.join(buildDir, `addon/chrome/content/scripts/${config.addonRef}.js`),
- // Don't turn minify on
- // minify: true,
- }).catch(() => exit(1));
-}
-
-async function main() {
- console.log(`[Build] BUILD_DIR=${buildDir}, VERSION=${version}, BUILD_TIME=${buildTime}, ENV=${[env.NODE_ENV]}`);
-
- clearFolder(buildDir);
-
- copyFolderRecursiveSync("addon", buildDir);
-
- copyFileSync("update-template.json", updateJSONFile);
-
- await esbuild();
-
- console.log("[Build] Run esbuild OK");
-
- replaceString();
-
console.log("[Build] Replace OK");
- // Walk the builds/addon/locale folder's sub folders and rename *.ftl to addonRef-*.ftl
- renameLocaleFiles();
-
console.log("[Build] Addon prepare OK");
- await zip.compressDir(path.join(buildDir, "addon"), path.join(buildDir, `${name}.xpi`), {
- ignoreBase: true,
- });
+ await zip.compressDir(
+ join(buildDir, "addon"),
+ join(buildDir, `${name}.xpi`),
+ {
+ ignoreBase: true,
+ }
+ );
console.log("[Build] Addon pack OK");
- console.log(`[Build] Finished in ${(new Date().getTime() - t.getTime()) / 1000} s.`);
+ console.log(
+ `[Build] Finished in ${(new Date().getTime() - t.getTime()) / 1000} s.`
+ );
}
main().catch((err) => {
diff --git a/scripts/reload.mjs b/scripts/reload.mjs
index 5cf84c7..0a9fbff 100644
--- a/scripts/reload.mjs
+++ b/scripts/reload.mjs
@@ -1,13 +1,20 @@
-import { exit } from "process";
+import { exit, argv } from "process";
+import minimist from "minimist";
import { execSync } from "child_process";
import details from "../package.json" assert { type: "json" };
+const { addonID, addonName } = details.config;
+const version = details.version;
import cmd from "./zotero-cmd.json" assert { type: "json" };
+const { exec } = cmd;
-const { addonID, addonName } = details.config;
-const { version } = details;
-const { zoteroBinPath, profilePath } = cmd.exec;
+// Run node reload.js -h for help
+const args = minimist(argv.slice(2));
-const startZotero = `"${zoteroBinPath}" --debugger --purgecaches -profile "${profilePath}"`;
+const zoteroPath = exec[args.zotero || args.z || Object.keys(exec)[0]];
+const profile = args.profile || args.p;
+const startZotero = `"${zoteroPath}" --debugger --purgecaches ${
+ profile ? `-p ${profile}` : ""
+}`;
const script = `
(async () => {
diff --git a/scripts/start.mjs b/scripts/start.mjs
index 19ed624..31459b4 100644
--- a/scripts/start.mjs
+++ b/scripts/start.mjs
@@ -1,66 +1,28 @@
+import process from "process";
import { execSync } from "child_process";
import { exit } from "process";
-import { existsSync, writeFileSync, readFileSync, mkdirSync } from "fs";
-import path from "path";
-import details from "../package.json" assert { type: "json" };
+import minimist from "minimist";
import cmd from "./zotero-cmd.json" assert { type: "json" };
-
-const { addonID } = details.config;
-const { zoteroBinPath, profilePath, dataDir } = cmd.exec;
-
-if (!existsSync(zoteroBinPath)) {
- throw new Error("Zotero binary does not exist.");
+const { exec } = cmd;
+
+// Run node start.js -h for help
+const args = minimist(process.argv.slice(2));
+
+if (args.help || args.h) {
+ console.log("Start Zotero Args:");
+ console.log(
+ "--zotero(-z): Zotero exec key in zotero-cmd.json. Default the first one."
+ );
+ console.log("--profile(-p): Zotero profile name.");
+ exit(0);
}
-if (existsSync(profilePath)) {
- const addonProxyFilePath = path.join(profilePath, `extensions/${addonID}`);
- const buildPath = path.resolve("build/addon");
-
- if (!existsSync(path.join(buildPath, "./manifest.json"))) {
- throw new Error(`The built file does not exist, maybe you need to build the addon first.`);
- }
-
- function writeAddonProxyFile() {
- writeFileSync(addonProxyFilePath, buildPath);
- console.log(
- `[info] Addon proxy file has been updated.
- File path: ${addonProxyFilePath}
- Addon path: ${buildPath} `,
- );
- }
-
- if (existsSync(addonProxyFilePath)) {
- if (readFileSync(addonProxyFilePath, "utf-8") !== buildPath) {
- writeAddonProxyFile();
- }
- } else {
- if (existsSync(profilePath) && !existsSync(path.join(profilePath, "extensions"))) {
- mkdirSync(path.join(profilePath, "extensions"));
- }
- writeAddonProxyFile();
- }
-
- const prefsPath = path.join(profilePath, "prefs.js");
- if (existsSync(prefsPath)) {
- const PrefsLines = readFileSync(prefsPath, "utf-8").split("\n");
- const filteredLines = PrefsLines.map((line) => {
- if (line.includes("extensions.lastAppBuildId") || line.includes("extensions.lastAppVersion")) {
- return;
- }
- if (line.includes("extensions.zotero.dataDir") && dataDir !== "") {
- return `user_pref("extensions.zotero.dataDir", "${dataDir}");`;
- }
- return line;
- });
- const updatedPrefs = filteredLines.join("\n");
- writeFileSync(prefsPath, updatedPrefs, "utf-8");
- console.log("[info] The /prefs.js has been modified.");
- }
-} else {
- throw new Error("The given Zotero profile does not exist.");
-}
+const zoteroPath = exec[args.zotero || args.z || Object.keys(exec)[0]];
+const profile = args.profile || args.p;
-const startZotero = `"${zoteroBinPath}" --debugger --purgecaches -profile "${profilePath}"`;
+const startZotero = `"${zoteroPath}" --debugger --purgecaches ${
+ profile ? `-p ${profile}` : ""
+}`;
execSync(startZotero);
exit(0);
diff --git a/scripts/zotero-cmd-default.json b/scripts/zotero-cmd-default.json
index c02a99f..de012a8 100644
--- a/scripts/zotero-cmd-default.json
+++ b/scripts/zotero-cmd-default.json
@@ -3,18 +3,6 @@
"killZoteroWindows": "taskkill /f /im zotero.exe",
"killZoteroUnix": "kill -9 $(ps -x | grep zotero)",
"exec": {
- "@comment-zoteroBinPath": "Please input the path of the Zotero binary file in `zoteroBinPath`.",
- "@comment-zoteroBinPath-tip": "The path delimiter should be escaped as `\\` for win32. The path is `*/Zotero.app/Contents/MacOS/zotero` for MacOS.",
- "zoteroBinPath": "/path/to/zotero.exe",
-
- "@comment-profilePath": "Please input the path of the profile used for development in `profilePath`.",
- "@comment-profilePath-tip": "Start the profile manager by `/path/to/zotero.exe -p` to create a profile for development",
- "@comment-profilePath-see": "https://www.zotero.org/support/kb/profile_directory",
- "profilePath": "/path/to/profile",
-
- "@comment-dataDir": "Please input the directory where the database is located in dataDir",
- "@comment-dataDir-tip": "If this field is kept empty, Zotero will start with the default data.",
- "@comment-dataDir-see": "https://www.zotero.org/support/zotero_data",
- "dataDir": ""
+ "7": "C:\\Program Files\\Zotero\\zotero.exe"
}
}
diff --git a/src/addon.ts b/src/addon.ts
index 67eb8f8..2f0bb94 100644
--- a/src/addon.ts
+++ b/src/addon.ts
@@ -1,7 +1,7 @@
+import ZoteroToolkit from "zotero-plugin-toolkit/dist/index";
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";
import { Action } from "./utils/action";
class Addon {
@@ -9,9 +9,10 @@ class Addon {
alive: boolean;
// Env type, see build.js
env: "development" | "production";
- ztoolkit: ZToolkit;
+ // ztoolkit: MyToolkit;
+ ztoolkit: ZoteroToolkit;
locale?: {
- current: any;
+ stringBundle: any;
};
prefs?: {
window: Window;
@@ -32,7 +33,8 @@ class Addon {
this.data = {
alive: true,
env: __env__,
- ztoolkit: createZToolkit(),
+ // ztoolkit: new MyToolkit(),
+ ztoolkit: new ZoteroToolkit(),
dialogs: {},
};
this.hooks = hooks;
@@ -40,4 +42,38 @@ class Addon {
}
}
+/**
+ * Alternatively, import toolkit modules you use to minify the plugin size.
+ *
+ * Steps to replace the default `ztoolkit: ZoteroToolkit` with your `ztoolkit: MyToolkit`:
+ *
+ * 1. Uncomment this file's line 30: `ztoolkit: new MyToolkit(),`
+ * and comment line 31: `ztoolkit: new ZoteroToolkit(),`.
+ * 2. Uncomment this file's line 10: `ztoolkit: MyToolkit;` in this file
+ * and comment line 11: `ztoolkit: ZoteroToolkit;`.
+ * 3. Uncomment `./typing/global.d.ts` line 12: `declare const ztoolkit: import("../src/addon").MyToolkit;`
+ * and comment line 13: `declare const ztoolkit: import("zotero-plugin-toolkit").ZoteroToolkit;`.
+ *
+ * You can now add the modules under the `MyToolkit` class.
+ */
+
+import { BasicTool, unregister } from "zotero-plugin-toolkit/dist/basic";
+import { UITool } from "zotero-plugin-toolkit/dist/tools/ui";
+import { PreferencePaneManager } from "zotero-plugin-toolkit/dist/managers/preferencePane";
+
+export class MyToolkit extends BasicTool {
+ UI: UITool;
+ PreferencePane: PreferencePaneManager;
+
+ constructor() {
+ super();
+ this.UI = new UITool(this);
+ this.PreferencePane = new PreferencePaneManager(this);
+ }
+
+ unregisterAll() {
+ unregister(this);
+ }
+}
+
export default Addon;
diff --git a/src/hooks.ts b/src/hooks.ts
index 1c9f8bd..c0da9e2 100644
--- a/src/hooks.ts
+++ b/src/hooks.ts
@@ -1,52 +1,46 @@
import { config } from "../package.json";
import { getString, initLocale } from "./utils/locale";
import { registerPrefs, registerPrefsScripts } from "./modules/preferenceScript";
-import { createZToolkit } from "./utils/ztoolkit";
import { Notifier } from "./modules/notifier";
async function onStartup() {
- await Promise.all([Zotero.initializationPromise, Zotero.unlockPromise, Zotero.uiReadyPromise]);
+ await Promise.all([
+ Zotero.initializationPromise,
+ Zotero.unlockPromise,
+ Zotero.uiReadyPromise,
+ ]);
initLocale();
-
+ ztoolkit.ProgressWindow.setIconURI(
+ "default",
+ `chrome://${config.addonRef}/content/icons/favicon.png`
+ );
registerPrefs();
Notifier.registerNotifier();
-
- await onMainWindowLoad(window);
-}
-
-async function onMainWindowLoad(win: Window): Promise {
- // 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();
-
- await Zotero.Promise.delay(1000);
- popupWin.changeLine({
- progress: 30,
- text: `[30%] ${getString("startup-begin")}`,
- });
- await Zotero.Promise.delay(1000);
-
- popupWin.changeLine({
- progress: 100,
- text: `[100%] ${getString("startup-finish")}`,
- });
- popupWin.startCloseTimer(5000);
-}
-
-async function onMainWindowUnload(win: Window): Promise {
- ztoolkit.unregisterAll();
- addon.data.dialogs.dialog?.window?.close();
+ //
+ // const popupWin = new ztoolkit.ProgressWindow(config.addonName, {
+ // closeOnClick: true,
+ // closeTime: -1,
+ // })
+ // .createLine({
+ // text: getString("startup.begin"),
+ // type: "default",
+ // progress: 0,
+ // })
+ // .show();
+ //
+ // await Zotero.Promise.delay(1000);
+ // popupWin.changeLine({
+ // progress: 30,
+ // text: `[30%] ${getString("startup.begin")}`,
+ // });
+ // await Zotero.Promise.delay(1000);
+ //
+ // popupWin.changeLine({
+ // progress: 100,
+ // text: `[100%] ${getString("startup.finish")}`,
+ // });
+ // popupWin.startCloseTimer(5000);
}
function onShutdown(): void {
@@ -96,8 +90,6 @@ async function onDialogEvents(type: string) {}
export default {
onStartup,
onShutdown,
- onMainWindowLoad,
- onMainWindowUnload,
onNotify,
onPrefsEvent,
onShortcuts,
diff --git a/src/index.ts b/src/index.ts
index 86c6a19..5326e2b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,25 +7,21 @@ const basicTool = new BasicTool();
if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
// Set global variables
_globalThis.Zotero = basicTool.getGlobal("Zotero");
- defineGlobal("window");
- defineGlobal("document");
- defineGlobal("ZoteroPane");
- defineGlobal("Zotero_Tabs");
+ _globalThis.ZoteroPane = basicTool.getGlobal("ZoteroPane");
+ _globalThis.Zotero_Tabs = basicTool.getGlobal("Zotero_Tabs");
+ _globalThis.window = basicTool.getGlobal("window");
+ _globalThis.document = basicTool.getGlobal("document");
_globalThis.addon = new Addon();
- defineGlobal("ztoolkit", () => {
- return _globalThis.addon.data.ztoolkit;
- });
+ _globalThis.ztoolkit = addon.data.ztoolkit;
+ ztoolkit.basicOptions.log.prefix = `[${config.addonName}]`;
+ ztoolkit.basicOptions.log.disableConsole = addon.data.env === "production";
+ ztoolkit.UI.basicOptions.ui.enableElementJSONLog =
+ addon.data.env === "development";
+ ztoolkit.UI.basicOptions.ui.enableElementDOMLog =
+ addon.data.env === "development";
+ ztoolkit.basicOptions.debug.disableDebugBridgePassword =
+ addon.data.env === "development";
Zotero[config.addonInstance] = addon;
// Trigger addon hook for initialization
addon.hooks.onStartup();
}
-
-function defineGlobal(name: Parameters[0]): void;
-function defineGlobal(name: string, getter: () => any): void;
-function defineGlobal(name: string, getter?: () => any) {
- Object.defineProperty(_globalThis, name, {
- get() {
- return getter ? getter() : basicTool.getGlobal(name);
- },
- });
-}
diff --git a/src/modules/duplicates.ts b/src/modules/duplicates.ts
index 61e9b0e..c152834 100644
--- a/src/modules/duplicates.ts
+++ b/src/modules/duplicates.ts
@@ -47,7 +47,7 @@ export class Duplicates {
} else {
// If dialog is not opened, create dialog
this.dialog = await this.createDialog();
- this.dialog.open(getString("du-dialog-title"), {
+ this.dialog.open(getString("du.dialog.title"), {
centerscreen: true,
resizable: false,
fitContent: true,
@@ -63,11 +63,11 @@ export class Duplicates {
const selectedItems: number[] = [];
if (duplicateMaps.size === 0) return { itemsToTrash, selectedItems };
- const popWin = new ztoolkit.ProgressWindow(getString("du-dialog-title"), {
+ const popWin = new ztoolkit.ProgressWindow(getString("du.dialog.title"), {
closeOnClick: true,
})
.createLine({
- text: getString("du-progress-text"),
+ text: getString("du.progress.text"),
type: "default",
progress: 0,
})
@@ -83,7 +83,7 @@ export class Duplicates {
}
}
popWin.changeLine({
- text: getString("du-progress-text"),
+ text: getString("du.progress.text"),
type: "default",
progress: 30,
});
@@ -92,7 +92,7 @@ export class Duplicates {
Zotero.Items.trashTx(itemsToTrash);
}
popWin.changeLine({
- text: getString("du-progress-text"),
+ text: getString("du.progress.text"),
type: "default",
progress: 80,
});
@@ -101,7 +101,7 @@ export class Duplicates {
}
popWin.changeLine({
- text: getString("du-progress-done"),
+ text: getString("du.progress.done"),
type: "success",
progress: 100,
});
@@ -193,7 +193,7 @@ export class Duplicates {
const tableBody = await this.updateTable();
return new ztoolkit.Dialog(3, 1)
.setDialogData(this.dialogData)
- .addCell(0, 0, { tag: "h2", properties: { innerHTML: getString("du-dialog-header") } })
+ .addCell(0, 0, { tag: "h2", properties: { innerHTML: getString("du.dialog.header") } })
.addCell(1, 0, {
tag: "table",
id: "data_table",
@@ -217,7 +217,7 @@ export class Duplicates {
tag: "th",
namespace: "html",
properties: {
- innerHTML: getString("du-dialog-table-title"),
+ innerHTML: getString("du.dialog.table.title"),
},
},
this.createTh(Action.KEEP),
@@ -254,16 +254,16 @@ export class Duplicates {
attributes: {
for: "act_as_default",
},
- properties: { innerHTML: getString("du-dialog-as-default") },
+ properties: { innerHTML: getString("du.dialog.as.default") },
},
],
})
- .addButton(getString("du-dialog-button-apply"), "btn_process", {
+ .addButton(getString("du.dialog.button.apply"), "btn_process", {
callback: (e) => {
Duplicates.processDuplicates(this.duplicateMaps!);
},
})
- .addButton(getString("du-dialog-button-go-duplicates"), "btn_go_duplicate", {
+ .addButton(getString("du.dialog.button.go.duplicates"), "btn_go_duplicate", {
callback: (e) => {
const libraryID = ZoteroPane.getSelectedLibraryID();
const type = "duplicates";
@@ -273,7 +273,7 @@ export class Duplicates {
ZoteroPane.setVirtual(libraryID, type, show, select);
},
})
- .addButton(getString("general-cancel"), "btn_cancel");
+ .addButton(getString("general.cancel"), "btn_cancel");
}
private async updateTable(): Promise {
@@ -405,7 +405,7 @@ export class Duplicates {
attributes: {
for: `act_${action}`,
},
- properties: { innerHTML: getString(`du-dialog-table-${action}`) },
+ properties: { innerHTML: getString(`du.dialog.table.${action}`) },
},
],
};
diff --git a/src/modules/preferenceScript.ts b/src/modules/preferenceScript.ts
index 0e4fcbe..f7d00fe 100644
--- a/src/modules/preferenceScript.ts
+++ b/src/modules/preferenceScript.ts
@@ -5,9 +5,10 @@ export function registerPrefs() {
ztoolkit.PreferencePane.register({
pluginID: config.addonID,
src: rootURI + "chrome/content/preferences.xhtml",
- label: getString("prefs-title"),
+ label: getString("prefs.title"),
image: `chrome://${config.addonRef}/content/icons/favicon.png`,
helpURL: homepage,
+ extraDTD: [`chrome://${config.addonRef}/locale/overlay.dtd`],
defaultXUL: true,
});
}
diff --git a/src/utils/locale.ts b/src/utils/locale.ts
index ff40d35..2b83153 100644
--- a/src/utils/locale.ts
+++ b/src/utils/locale.ts
@@ -1,76 +1,29 @@
import { config } from "../../package.json";
-export { initLocale, getString };
-
/**
* Initialize locale data
*/
-function initLocale() {
- const l10n = new (typeof Localization === "undefined" ? ztoolkit.getGlobal("Localization") : Localization)(
- [`${config.addonRef}-addon.ftl`],
- true,
- );
+export function initLocale() {
addon.data.locale = {
- current: l10n,
+ stringBundle: Components.classes["@mozilla.org/intl/stringbundle;1"]
+ .getService(Components.interfaces.nsIStringBundleService)
+ .createBundle(`chrome://${config.addonRef}/locale/addon.properties`),
};
}
/**
- * Get locale string, see https://firefox-source-docs.mozilla.org/l10n/fluent/tutorial.html#fluent-translation-list-ftl
- * @param localString ftl key
- * @param options.branch branch name
- * @param options.args args
- * @example
- * ```ftl
- * # addon.ftl
- * addon-static-example = This is default branch!
- * .branch-example = This is a branch under addon-static-example!
- * addon-dynamic-example =
- { $count ->
- [one] I have { $count } apple
- *[other] I have { $count } apples
- }
- * ```
- * ```js
- * getString("addon-static-example"); // This is default branch!
- * getString("addon-static-example", { branch: "branch-example" }); // This is a branch under addon-static-example!
- * getString("addon-dynamic-example", { args: { count: 1 } }); // I have 1 apple
- * getString("addon-dynamic-example", { args: { count: 2 } }); // I have 2 apples
- * ```
+ * Get locale string
+ * @param localString
+ * @param noReload
*/
-function getString(localString: string): string;
-function getString(localString: string, branch: string): string;
-function getString(
- localeString: string,
- options: { branch?: string | undefined; args?: Record },
-): string;
-function getString(...inputs: any[]) {
- if (inputs.length === 1) {
- return _getString(inputs[0]);
- } else if (inputs.length === 2) {
- if (typeof inputs[1] === "string") {
- return _getString(inputs[0], { branch: inputs[1] });
- } else {
- return _getString(inputs[0], inputs[1]);
+export function getString(localString: string, noReload = false): string {
+ try {
+ return addon.data.locale?.stringBundle.GetStringFromName(localString);
+ } catch (e) {
+ if (!noReload) {
+ initLocale();
+ return getString(localString, true);
}
- } else {
- throw new Error("Invalid arguments");
- }
-}
-
-function _getString(
- localeString: string,
- options: { branch?: string | undefined; args?: Record } = {},
-): string {
- const localStringWithPrefix = `${config.addonRef}-${localeString}`;
- const { branch, args } = options;
- const pattern = addon.data.locale?.current.formatMessagesSync([{ id: localStringWithPrefix, args }])[0];
- if (!pattern) {
- return localStringWithPrefix;
- }
- if (branch && pattern.attributes) {
- return pattern.attributes[branch] || localStringWithPrefix;
- } else {
- return pattern.value || localStringWithPrefix;
+ return localString;
}
}
diff --git a/src/utils/wait.ts b/src/utils/wait.ts
index ac76569..604237f 100644
--- a/src/utils/wait.ts
+++ b/src/utils/wait.ts
@@ -6,7 +6,12 @@
* @param interval
* @param timeout
*/
-export function waitUntil(condition: () => boolean, callback: () => void, interval = 100, timeout = 10000) {
+export function waitUntil(
+ condition: () => boolean,
+ callback: () => void,
+ interval = 100,
+ timeout = 10000
+) {
const start = Date.now();
const intervalId = ztoolkit.getGlobal("setInterval")(() => {
if (condition()) {
@@ -24,7 +29,11 @@ export function waitUntil(condition: () => boolean, callback: () => void, interv
* @param interval
* @param timeout
*/
-export function waitUtilAsync(condition: () => boolean, interval = 100, timeout = 10000) {
+export function waitUtilAsync(
+ condition: () => boolean,
+ interval = 100,
+ timeout = 10000
+) {
return new Promise((resolve, reject) => {
const start = Date.now();
const intervalId = ztoolkit.getGlobal("setInterval")(() => {
diff --git a/src/utils/window.ts b/src/utils/window.ts
index 270631f..4b072fa 100644
--- a/src/utils/window.ts
+++ b/src/utils/window.ts
@@ -1,4 +1,6 @@
-export { isWindowAlive };
+import { getString } from "./locale";
+
+export { isWindowAlive, localeWindow };
/**
* Check if the window is alive.
@@ -8,3 +10,57 @@ export { isWindowAlive };
function isWindowAlive(win?: Window) {
return win && !Components.utils.isDeadWrapper(win) && !win.closed;
}
+
+/**
+ * Locale the elements in window with the locale-target attribute.
+ * Useful when the window is created dynamically.
+ * @example
+ * In HTML:
+ * ```html
+ * elem.text
+ * ```
+ * In `addon/chrome/locale/en-US/addon.properties`:
+ * ```properties
+ * elem.text=Hello World
+ * elem.title=Locale example
+ * ```
+ * In `addon/chrome/locale/zh-CN/addon.properties`:
+ * ```properties
+ * elem.text=你好世界
+ * elem.title=多语言样例
+ * ```
+ * After locale:
+ *
+ * if locale is "en-US"
+ * ```html
+ * Hello World
+ * ```
+ * else if locale is "zh-CN"
+ * ```html
+ * 你好世界
+ * ```
+ * @param win
+ */
+function localeWindow(win: Window) {
+ Array.from(win.document.querySelectorAll("*[locale-target]")).forEach(
+ (elem) => {
+ const errorInfo = "Locale Error";
+ const locales = elem.getAttribute("locale-target")?.split(",");
+ locales?.forEach((key) => {
+ const isProp = key in elem;
+ try {
+ const localeString = getString(
+ (isProp ? (elem as any)[key] : elem.getAttribute(key)).trim() || ""
+ );
+ isProp
+ ? ((elem as any)[key] = localeString)
+ : elem.setAttribute(key, localeString);
+ } catch (error) {
+ isProp
+ ? ((elem as any)[key] = errorInfo)
+ : elem.setAttribute(key, errorInfo);
+ }
+ });
+ }
+ );
+}
diff --git a/tsconfig.json b/tsconfig.json
index a033072..65c5124 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,6 +7,6 @@
"skipLibCheck": true,
"strict": true
},
- "include": ["src", "typings", "node_modules/zotero-types"],
- "exclude": ["build", "addon"]
+ "include": ["src", "typing", "node_modules/zotero-types"],
+ "exclude": ["builds", "addon"]
}
diff --git a/typings/global.d.ts b/typing/global.d.ts
similarity index 66%
rename from typings/global.d.ts
rename to typing/global.d.ts
index 727c2ad..ae6b862 100644
--- a/typings/global.d.ts
+++ b/typing/global.d.ts
@@ -5,18 +5,15 @@ declare const _globalThis: {
Zotero_Tabs: typeof Zotero_Tabs;
window: Window;
document: Document;
- ztoolkit: ZToolkit;
+ ztoolkit: typeof ztoolkit;
addon: typeof addon;
};
-declare type ZToolkit = ReturnType;
-
-declare const ztoolkit: ZToolkit;
+// declare const ztoolkit: import("../src/addon").MyToolkit;
+declare const ztoolkit: import("zotero-plugin-toolkit").ZoteroToolkit;
declare const rootURI: string;
declare const addon: import("../src/addon").default;
declare const __env__: "production" | "development";
-
-declare class Localization {}
diff --git a/update-template.json b/update-template.json
index 8f65a4c..7dca98e 100644
--- a/update-template.json
+++ b/update-template.json
@@ -4,7 +4,16 @@
"updates": [
{
"version": "__buildVersion__",
- "update_link": "__updateLink__",
+ "update_link": "__releasepage__",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "60.0"
+ }
+ }
+ },
+ {
+ "version": "__buildVersion__",
+ "update_link": "__releasepage__",
"applications": {
"zotero": {
"strict_min_version": "6.999"
diff --git a/update-template.rdf b/update-template.rdf
new file mode 100644
index 0000000..4d57597
--- /dev/null
+++ b/update-template.rdf
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ __buildVersion__
+
+
+ zotero@chnm.gmu.edu
+ 5.999
+ *
+ __releasepage__
+
+
+
+
+ juris-m@juris-m.github.io
+ 5.999
+ *
+ __releasepage__
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/update.json b/update.json
index 1d1f1e0..c06c1b5 100644
--- a/update.json
+++ b/update.json
@@ -3,8 +3,17 @@
"zoplicate@chenglongma.com": {
"updates": [
{
- "version": "1.0.0",
- "update_link": "https://github.com/ChenglongMa/zoplicate/releases/latest/download/zoplicate.xpi",
+ "version": "0.6.0",
+ "update_link": "https://github.com/ChenglongMa/zoplicate/releases/download/zotero6/zoplicate.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "60.0"
+ }
+ }
+ },
+ {
+ "version": "0.6.0",
+ "update_link": "https://github.com/ChenglongMa/zoplicate/releases/download/zotero6/zoplicate.xpi",
"applications": {
"zotero": {
"strict_min_version": "6.999"
diff --git a/update.rdf b/update.rdf
new file mode 100644
index 0000000..1e17037
--- /dev/null
+++ b/update.rdf
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ 0.6.0
+
+
+ zotero@chnm.gmu.edu
+ 5.999
+ *
+ https://github.com/ChenglongMa/zoplicate/releases/download/zotero6/zoplicate.xpi
+
+
+
+
+ juris-m@juris-m.github.io
+ 5.999
+ *
+ https://github.com/ChenglongMa/zoplicate/releases/download/zotero6/zoplicate.xpi
+
+
+
+
+
+
+
+
\ No newline at end of file