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 @@ zoplicate banner -[![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