diff --git a/README.md b/README.md index 7d283c5..276acdc 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,32 @@ This is an addon/plugin template for [Zotero](https://www.zotero.org/). - Some sample code of UI and lifecycle. - ⭐Compatibilities for Zotero 6 & Zotero 7.(using [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit)) +## Examples + +This repo provides examples for [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit) APIs. + +Search `@example` in `src/examples.ts`. The examples are called in `src/hooks.ts`. + +### Basic Examples + +- registerNotifier +- registerPrefs, unregisterPrefs + +### UI Examples + +![image](https://user-images.githubusercontent.com/33902321/209274492-7aa94912-af38-4154-af46-dc8f59640de3.png) + +- registerStyleSheet(the official make-it-red example) +- registerRightClickMenuItem +- registerRightClickMenuPopup +- registerWindowMenuWithSeprator +- registerExtraColumn +- registerExtraColumnWithCustomCell +- registerCustomCellRenderer +- registerLibraryTabPanel +- registerReaderTabPanel +- unregisterUIExamples + ## Quick Start Guide - Fork this repo; @@ -46,58 +72,38 @@ This is an addon/plugin template for [Zotero](https://www.zotero.org/). - Run `npm run build` to build the plugin in production mode. Run `npm run build-dev` to build the plugin in development mode. The xpi for installation and the built code is under builds folder. > What the difference between dev & prod? +> > - This environment variable is stored in `Zotero.AddonTemplate.env`. The outputs to console is disabled in prod mode. > - You can decide what users cannot see/use based on this variable. -### About Life Cycle +### About Hooks + +> See also `src/hooks.ts` 1. When install/enable/startup triggered from Zotero, `bootstrap.js` > `startup` is called - Wait for Zotero ready - Prepare global variables `ctx`. They are available globally in the plugin scope - Load `index.js` (the main entrance of plugin code, built from `index.ts`) - Register resources if Zotero 7+ -2. In the main entrance `index.js`, the plugin object is injected under `Zotero` and `events.ts` > `onInit` is called. - - Initialize anything you want, including notify listeners, preference panes(`initPrefs`), and UI elements(`initViews`). +2. In the main entrance `index.js`, the plugin object is injected under `Zotero` and `hooks.ts` > `onStartup` is called. + - Initialize anything you want, including notify listeners, preference panes, and UI elements. 3. When uninstall/disabled triggered from Zotero, `bootstrap.js` > `shutdown` is called. - - `events.ts` > `onUninit` is called. Remove UI elements(`unInitViews`), preference panes(`uninitPrefs`), or anything created by the plugin. + - `events.ts` > `onShutdown` is called. Remove UI elements, preference panes, or anything created by the plugin. - Remove scripts and release resources. ### About Global Variables +> See also `src/index.ts` + The bootstrapped plugin runs in a sandbox, which does not have default global variables like `Zotero` or `window`, which we used to have in the overlay plugins' window environment. This template registers the following variables to the global scope: ```ts -Zotero, ZoteroPane, Zotero_Tabs, window, document, rootURI, ZToolkit +Zotero, ZoteroPane, Zotero_Tabs, window, document, rootURI, ztoolkit, addon; ``` -See `src/events.ts` > `initGlobalVariables` for more details. - - -### Examples - -See https://github.com/windingwind/zotero-plugin-toolkit for more detailed API documentations. - -#### Menu (file, edit, view, ...) & Right-click Menu (item, collection/library) - -**File Menu** - -![image](https://user-images.githubusercontent.com/33902321/208077117-e9ae3ca8-f9c7-4549-8835-1de5ea8c665f.png) - -https://github.com/windingwind/zotero-addon-template/blob/574ce88b9fd3535a9d062db51cf16e99dda35288/src/views.ts#L52-L60 - -**Item Menu** - -![image](https://user-images.githubusercontent.com/33902321/208078502-75d547b7-1cff-4538-802a-3d5127a8b617.png) - -https://github.com/windingwind/zotero-addon-template/blob/574ce88b9fd3535a9d062db51cf16e99dda35288/src/views.ts#L23-L51 - -`insertMenuItem` resolved the input object and inject the menu items. - -You can choose an anchor element and insert before/after it using `insertPosition` and `anchorElement`. Default the insert position is the end of the menu. - -#### Preference, for both Zotero 6 and Zotero 7 (all in bootstrap) +### About Preference Zotero 6 doesn't support preference pane injection in bootstrap mode, thus I write a register for Zotero 6 or lower. @@ -133,7 +139,7 @@ Remember to call `unregisterPrefPane()` on plugin unload. https://github.com/windingwind/zotero-addon-template/blob/574ce88b9fd3535a9d062db51cf16e99dda35288/src/views.ts#L88-L90 -#### Create Elements API +### Create Elements API The plugin template provides new APIs for bootstrap plugins. We have two reasons to use these APIs, instead of the `createElement/createElementNS`: @@ -142,30 +148,13 @@ The plugin template provides new APIs for bootstrap plugins. We have two reasons There are more advanced APIs for creating elements in batch: `creatElementsFromJSON`. Input an element tree in JSON and return a fragment/element. These elements are also maintained by this plugin template. -#### Extra Column in Library - -Using [Zotero Plugin Toolkit:ItemTreeTool](https://github.com/windingwind/zotero-plugin-toolkit/blob/HEAD/docs/zotero-plugin-toolkit.itemtreetool.md) to register an extra column in `src/views.ts`. - -```ts -ZToolkit.ItemTree.registerExample(); -``` -This will register a column with dataKey `test`. Looks like: - -![image](https://user-images.githubusercontent.com/33902321/209274492-7aa94912-af38-4154-af46-dc8f59640de3.png) - -Remember to unregister it when exiting. - -```ts -ZToolkit.ItemTree.unregister("test"); -``` - ### Directory Structure This section shows the directory structure of a template. - All `.js/.ts` code files are in `./src`; -- Addon config files: `./addon/chrome.manifest`, `./addon/install.rdf`; -- UI files: `./addon/chrome/content/*.xul`. The `overlay.xul` also defines the main entrance; +- Addon config files: `./addon/chrome.manifest`, `./addon/install.rdf`, and `./addon/manifest.json`; +- UI files: `./addon/chrome/content/*.xhtml`. - Locale files: `./addon/chrome/locale/[*.dtd, *.properties]`; - Resource files: `./addon/chrome/skin/default/__addonRef__/*.dtd`; - Preferences file: `./addon/chrome/defaults/preferences/defaults.js`; @@ -184,8 +173,9 @@ This section shows the directory structure of a template. ├─.github # github conf │ ├─addon # addon dir -│ │ chrome.manifest #addon conf -│ │ install.rdf # addon install conf +│ │ chrome.manifest # for Zotero 6 +│ │ manifest.json # for Zotero 7 +│ │ install.rdf # addon install conf, for Zotero 6 │ │ bootstrap.js # addon load/unload script, like a main.c │ │ │ └─chrome @@ -211,10 +201,9 @@ This section shows the directory structure of a template. │ └─src # source code │ index.ts # main entry - │ module.ts # module class │ addon.ts # base class - │ events.ts # events class - │ views.ts # UI class + │ hooks.ts # lifecycle hooks + │ examples.ts # examples factory │ locale.ts # Locale class for properties files └─ prefs.ts # preferences class diff --git a/addon/bootstrap.js b/addon/bootstrap.js index 03f9488..c9f4d15 100644 --- a/addon/bootstrap.js +++ b/addon/bootstrap.js @@ -102,7 +102,7 @@ function shutdown({ id, version, resourceURI, rootURI }, reason) { Components.interfaces.nsISupports ).wrappedJSObject; } - Zotero.AddonTemplate.events.onUnInit(Zotero); + Zotero.AddonTemplate.hooks.onShutdown(); Cc["@mozilla.org/intl/stringbundle;1"] .getService(Components.interfaces.nsIStringBundleService) diff --git a/addon/chrome/locale/en-US/addon.properties b/addon/chrome/locale/en-US/addon.properties index 4536e81..c263b00 100644 --- a/addon/chrome/locale/en-US/addon.properties +++ b/addon/chrome/locale/en-US/addon.properties @@ -1,3 +1,5 @@ +startup.begin=Addon is loading +startup.finish=Addon is ready menuitem.label=Addon Template: Menuitem menupopup.label=Addon Template: Menupopup menuitem.submenulabel=Addon Template diff --git a/addon/chrome/locale/zh-CN/addon.properties b/addon/chrome/locale/zh-CN/addon.properties index 7cbbcc4..351dca2 100644 --- a/addon/chrome/locale/zh-CN/addon.properties +++ b/addon/chrome/locale/zh-CN/addon.properties @@ -1,7 +1,9 @@ -menuitem.label=Addon Template: 菜单 -menupopup.label=Addon Template: 弹出菜单 -menuitem.submenulabel=Addon Template -menuitem.filemenulabel=Addon Template: 文件菜单 +startup.begin=插件加载中 +startup.finish=插件已就绪 +menuitem.label=插件模板: 菜单 +menupopup.label=插件模板: 弹出菜单 +menuitem.submenulabel=插件模板:子菜单 +menuitem.filemenulabel=插件模板: 文件菜单 prefs.title=插件模板 tabpanel.lib.tab.label=库标签 tabpanel.reader.tab.label=阅读器标签 \ No newline at end of file diff --git a/build.js b/build.js index e5fe633..b2ff60d 100644 --- a/build.js +++ b/build.js @@ -101,7 +101,7 @@ async function main() { .build({ entryPoints: ["src/index.ts"], define: { - __env__: process.env.NODE_ENV, + __env__: `"${process.env.NODE_ENV}"`, }, bundle: true, // Entry should be the same as addon/chrome/content/overlay.xul diff --git a/package.json b/package.json index b2a28db..7162bfc 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,6 @@ "esbuild": "^0.16.10", "release-it": "^14.14.3", "replace-in-file": "^6.3.5", - "zotero-types": "^0.1.2" + "zotero-types": "^0.1.4" } } diff --git a/src/addon.ts b/src/addon.ts index fc4f719..76445ed 100644 --- a/src/addon.ts +++ b/src/addon.ts @@ -1,25 +1,24 @@ -import AddonEvents from "./events"; +import AddonHooks from "./hooks"; import AddonPrefs from "./prefs"; -import AddonViews from "./views"; import AddonLocale from "./locale"; class Addon { // Env type, see build.js public env!: "development" | "production"; + // If addon is disabled/removed, set it false + public alive: boolean; // Lifecycle events - public events: AddonEvents; - // UI operations - public views: AddonViews; + public hooks: AddonHooks; // Scripts for prefpane window public prefs: AddonPrefs; // Runtime locale with .properties public locale: AddonLocale; constructor() { - this.events = new AddonEvents(this); - this.views = new AddonViews(this); - this.prefs = new AddonPrefs(this); - this.locale = new AddonLocale(this); + this.alive = true; + this.hooks = new AddonHooks(); + this.prefs = new AddonPrefs(); + this.locale = new AddonLocale(); } } diff --git a/src/events.ts b/src/events.ts deleted file mode 100644 index 245da07..0000000 --- a/src/events.ts +++ /dev/null @@ -1,119 +0,0 @@ -import Addon from "./addon"; -import AddonModule from "./module"; -import { config } from "../package.json"; -import ZoteroToolkit from "zotero-plugin-toolkit"; - -class AddonEvents extends AddonModule { - constructor(parent: Addon) { - super(parent); - } - - // This function is the setup code of the addon - public async onInit() { - this.initGlobalVariables(); - // @ts-ignore - const development = "development"; - const production = "production"; - // The env will be replaced after esbuild - // @ts-ignore - this._Addon.env = __env__; - ZToolkit.Tool.logOptionsGlobal.disableConsole = - this._Addon.env === "production"; - ZToolkit.Tool.log("init called"); - - // Initialize locale provider - this._Addon.locale.initLocale(); - // Initialize preference window - this.initPrefs(); - // Initialize notifier callback - this.initNotifier(); - // Initialize UI elements - this._Addon.views.initViews(); - } - - public onUnInit(): void { - ZToolkit.Tool.log("uninit called"); - this.unInitPrefs(); - // Remove elements and do clean up - this._Addon.views.unInitViews(); - // Remove addon object - Zotero.AddonTemplate = undefined; - } - - private initGlobalVariables() { - _globalThis.ZToolkit = new ZoteroToolkit(); - ZToolkit.Tool.logOptionsGlobal.prefix = `[${config.addonName}]`; - _globalThis.Zotero = ZToolkit.Compat.getGlobal("Zotero"); - _globalThis.ZoteroPane = ZToolkit.Compat.getGlobal("ZoteroPane"); - _globalThis.Zotero_Tabs = ZToolkit.Compat.getGlobal("Zotero_Tabs"); - _globalThis.window = ZToolkit.Compat.getGlobal("window"); - _globalThis.document = ZToolkit.Compat.getGlobal("document"); - ZToolkit.Tool.log("initializeing global variables"); - } - - private initNotifier() { - const callback = { - notify: async ( - event: string, - type: string, - ids: Array, - extraData: { [key: string]: any } - ) => { - // You can add your code to the corresponding notify type - if ( - event == "select" && - type == "tab" && - extraData[ids[0]].type == "reader" - ) { - // Select a reader tab - } - if (event == "add" && type == "item") { - // Add an item - } - }, - }; - - // Register the callback in Zotero as an item observer - let notifierID = Zotero.Notifier.registerObserver(callback, [ - "tab", - "item", - "file", - ]); - - // Unregister callback when the window closes (important to avoid a memory leak) - Zotero.getMainWindow().addEventListener( - "unload", - function (e: Event) { - Zotero.Notifier.unregisterObserver(notifierID); - }, - false - ); - } - - private initPrefs() { - const prefOptions = { - pluginID: config.addonID, - src: rootURI + "chrome/content/preferences.xhtml", - label: this._Addon.locale.getString("prefs.title"), - image: `chrome://${config.addonRef}/content/icons/favicon.png`, - extraDTD: [`chrome://${config.addonRef}/locale/overlay.dtd`], - defaultXUL: true, - onload: (win: Window) => { - this._Addon.prefs.initPreferences(win); - }, - }; - if (ZToolkit.Compat.isZotero7()) { - Zotero.PreferencePanes.register(prefOptions); - } else { - ZToolkit.Compat.registerPrefPane(prefOptions); - } - } - - private unInitPrefs() { - if (!ZToolkit.Compat.isZotero7()) { - ZToolkit.Compat.unregisterPrefPane(); - } - } -} - -export default AddonEvents; diff --git a/src/views.ts b/src/examples.ts similarity index 54% rename from src/views.ts rename to src/examples.ts index f659611..434fb9f 100644 --- a/src/views.ts +++ b/src/examples.ts @@ -1,27 +1,100 @@ -import Addon from "./addon"; -import AddonModule from "./module"; +import { log } from "zotero-plugin-toolkit/dist/utils"; import { config } from "../package.json"; -class AddonViews extends AddonModule { - // You can store some element in the object attributes - private progressWindowIcon: { [key: string]: string }; +export function example(type?: string): MethodDecorator { + return function ( + target: Object, + propertyKey: string | symbol, + descriptor: PropertyDescriptor + ) { + log("Calling example", target, type, propertyKey, descriptor); + return descriptor; + }; +} - constructor(parent: Addon) { - super(parent); - this.progressWindowIcon = { - success: "chrome://zotero/skin/tick.png", - fail: "chrome://zotero/skin/cross.png", - default: `chrome://${config.addonRef}/content/icons/favicon.png`, +export class BasicExampleFactory { + @example() + static registerNotifier() { + const callback = { + notify: async ( + event: string, + type: string, + ids: Array, + extraData: { [key: string]: any } + ) => { + if (!addon.alive) { + this.unregisterNotifier(notifierID); + return; + } + ztoolkit.Tool.log("notify", event, type, ids, extraData); + // You can add your code to the corresponding notify type + if ( + event == "select" && + type == "tab" && + extraData[ids[0]].type == "reader" + ) { + // Select a reader tab + } + if (event == "add" && type == "item") { + // Add an item + } + }, }; + + // Register the callback in Zotero as an item observer + const notifierID = Zotero.Notifier.registerObserver(callback, [ + "tab", + "item", + "file", + ]); + + // Unregister callback when the window closes (important to avoid a memory leak) + window.addEventListener( + "unload", + (e: Event) => { + this.unregisterNotifier(notifierID); + }, + false + ); } - public initViews() { - // You can init the UI elements that - // cannot be initialized with overlay.xul - ZToolkit.Tool.log("Initializing UI"); + @example() + private static unregisterNotifier(notifierID: string) { + Zotero.Notifier.unregisterObserver(notifierID); + } - // register style sheet - const styles = ZToolkit.UI.creatElementsFromJSON(document, { + @example() + static registerPrefs() { + const prefOptions = { + pluginID: config.addonID, + src: rootURI + "chrome/content/preferences.xhtml", + label: addon.locale.getString("prefs.title"), + image: `chrome://${config.addonRef}/content/icons/favicon.png`, + extraDTD: [`chrome://${config.addonRef}/locale/overlay.dtd`], + defaultXUL: true, + onload: (win: Window) => { + addon.prefs.initPreferences(win); + }, + }; + if (ztoolkit.Compat.isZotero7()) { + Zotero.PreferencePanes.register(prefOptions); + } else { + ztoolkit.Compat.registerPrefPane(prefOptions); + } + } + + @example() + static unregisterPrefs() { + if (!ztoolkit.Compat.isZotero7()) { + ztoolkit.Compat.unregisterPrefPane(); + } + } +} + +export class UIExampleFactory { + @example() + static registerStyleSheet() { + const styles = ztoolkit.UI.creatElementsFromJSON(document, { tag: "link", directAttributes: { type: "text/css", @@ -33,26 +106,32 @@ class AddonViews extends AddonModule { document .getElementById("zotero-item-pane-content") ?.classList.add("makeItRed"); + } + @example() + static registerRightClickMenuItem() { const menuIcon = `chrome://${config.addonRef}/content/icons/favicon@0.5x.png`; // item menuitem with icon - ZToolkit.UI.insertMenuItem("item", { + ztoolkit.UI.insertMenuItem("item", { tag: "menuitem", id: "zotero-itemmenu-addontemplate-test", - label: this._Addon.locale.getString("menuitem.label"), + label: addon.locale.getString("menuitem.label"), oncommand: "alert('Hello World! Default Menuitem.')", icon: menuIcon, }); - // item menupopup with sub-menuitems - ZToolkit.UI.insertMenuItem( + } + + @example() + static registerRightClickMenuPopup() { + ztoolkit.UI.insertMenuItem( "item", { tag: "menu", - label: this._Addon.locale.getString("menupopup.label"), + label: addon.locale.getString("menupopup.label"), subElementOptions: [ { tag: "menuitem", - label: this._Addon.locale.getString("menuitem.submenulabel"), + label: addon.locale.getString("menuitem.submenulabel"), oncommand: "alert('Hello World! Sub Menuitem.')", }, ], @@ -62,24 +141,24 @@ class AddonViews extends AddonModule { "#zotero-itemmenu-addontemplate-test" ) as XUL.MenuItem ); - ZToolkit.UI.insertMenuItem("menuFile", { + } + + @example() + static registerWindowMenuWithSeprator() { + ztoolkit.UI.insertMenuItem("menuFile", { tag: "menuseparator", }); // menu->File menuitem - ZToolkit.UI.insertMenuItem("menuFile", { + ztoolkit.UI.insertMenuItem("menuFile", { tag: "menuitem", - label: this._Addon.locale.getString("menuitem.filemenulabel"), + label: addon.locale.getString("menuitem.filemenulabel"), oncommand: "alert('Hello World! File Menuitem.')", }); - /** - * Example: menu items ends - */ + } - /** - * Example: extra column starts - */ - // Initialize extra columns - ZToolkit.ItemTree.register( + @example() + static async registerExtraColumn() { + await ztoolkit.ItemTree.register( "test1", "text column", ( @@ -94,7 +173,11 @@ class AddonViews extends AddonModule { iconPath: "chrome://zotero/skin/cross.png", } ); - ZToolkit.ItemTree.register( + } + + @example() + static async registerExtraColumnWithCustomCell() { + await ztoolkit.ItemTree.register( "test2", "custom column", ( @@ -117,15 +200,11 @@ class AddonViews extends AddonModule { }, } ); - /** - * Example: extra column ends - */ + } - /** - * Example: custom cell starts - */ - // Customize cells - ZToolkit.ItemTree.addRenderCellHook( + @example() + static async registerCustomCellRenderer() { + await ztoolkit.ItemTree.addRenderCellHook( "title", (index: number, data: string, column: any, original: Function) => { const span = original(index, data, column) as HTMLSpanElement; @@ -134,17 +213,17 @@ class AddonViews extends AddonModule { return span; } ); - /** - * Example: custom cell ends - */ + // @ts-ignore + // This is a private method. Make it public in toolkit. + await ztoolkit.ItemTree.refresh(); + } - /** - * Example: extra library tab starts - */ - const libTabId = ZToolkit.UI.registerLibraryTabPanel( - this._Addon.locale.getString("tabpanel.lib.tab.label"), + @example() + static registerLibraryTabPanel() { + const tabId = ztoolkit.UI.registerLibraryTabPanel( + addon.locale.getString("tabpanel.lib.tab.label"), (panel: XUL.Element, win: Window) => { - const elem = ZToolkit.UI.creatElementsFromJSON(win.document, { + const elem = ztoolkit.UI.creatElementsFromJSON(win.document, { tag: "vbox", namespace: "xul", subElementOptions: [ @@ -172,7 +251,7 @@ class AddonViews extends AddonModule { { type: "click", listener: () => { - ZToolkit.UI.unregisterLibraryTabPanel(libTabId); + ztoolkit.UI.unregisterLibraryTabPanel(tabId); }, }, ], @@ -185,30 +264,26 @@ class AddonViews extends AddonModule { targetIndex: 1, } ); - /** - * Example: extra library tab ends - */ + } - /** - * Example: extra reader tab starts - */ - const readerTabId = `${config.addonRef}-extra-reader-tab`; - ZToolkit.UI.registerReaderTabPanel( - this._Addon.locale.getString("tabpanel.reader.tab.label"), + @example() + static async registerReaderTabPanel() { + const tabId = await ztoolkit.UI.registerReaderTabPanel( + addon.locale.getString("tabpanel.reader.tab.label"), ( - panel: XUL.Element, + panel: XUL.TabPanel | undefined, deck: XUL.Deck, win: Window, reader: _ZoteroReaderInstance ) => { if (!panel) { - ZToolkit.Tool.log( + ztoolkit.Tool.log( "This reader do not have right-side bar. Adding reader tab skipped." ); return; } - ZToolkit.Tool.log(reader); - const elem = ZToolkit.UI.creatElementsFromJSON(win.document, { + ztoolkit.Tool.log(reader); + const elem = ztoolkit.UI.creatElementsFromJSON(win.document, { tag: "vbox", id: `${config.addonRef}-${reader._instanceID}-extra-reader-tab-div`, namespace: "xul", @@ -254,7 +329,7 @@ class AddonViews extends AddonModule { { type: "click", listener: () => { - ZToolkit.UI.unregisterReaderTabPanel(readerTabId); + ztoolkit.UI.unregisterReaderTabPanel(tabId); }, }, ], @@ -264,49 +339,13 @@ class AddonViews extends AddonModule { panel.append(elem); }, { - tabId: readerTabId, targetIndex: 1, } ); - /** - * Example: extra reader tab ends - */ } - public unInitViews() { - ZToolkit.Tool.log("Uninitializing UI"); - ZToolkit.unregisterAll(); - // toolkit.UI.removeAddonElements(); - // // Remove extra columns - // toolkit.ItemTree.unregister("test1"); - // toolkit.ItemTree.unregister("test2"); - - // // Remove title cell patch - // toolkit.ItemTree.removeRenderCellHook("title"); - - // toolkit.UI.unregisterReaderTabPanel( - // `${config.addonRef}-extra-reader-tab` - // ); - } - - public showProgressWindow( - header: string, - context: string, - type: string = "default", - t: number = 5000 - ) { - // A simple wrapper of the Zotero ProgressWindow - let progressWindow = new Zotero.ProgressWindow({ closeOnClick: true }); - progressWindow.changeHeadline(header); - progressWindow.progress = new progressWindow.ItemProgress( - this.progressWindowIcon[type], - context - ); - progressWindow.show(); - if (t > 0) { - progressWindow.startCloseTimer(t); - } + @example() + static unregisterUIExamples() { + ztoolkit.unregisterAll(); } } - -export default AddonViews; diff --git a/src/hooks.ts b/src/hooks.ts new file mode 100644 index 0000000..580d92d --- /dev/null +++ b/src/hooks.ts @@ -0,0 +1,62 @@ +import { BasicExampleFactory, UIExampleFactory } from "./examples"; +import { changeProgressWindowLine, showProgressWindow } from "./tools/progress"; +import { config } from "../package.json"; + +class AddonHooks { + public async onStartup() { + addon.locale.initLocale(); + + const w = showProgressWindow( + config.addonName, + addon.locale.getString("startup.begin"), + "default", + -1 + ); + changeProgressWindowLine(w, { newProgress: 0 }); + + BasicExampleFactory.registerPrefs(); + + BasicExampleFactory.registerNotifier(); + + await Zotero.Promise.delay(1000); + changeProgressWindowLine(w, { + newProgress: 30, + newText: `[30%] ${addon.locale.getString("startup.begin")}`, + }); + + UIExampleFactory.registerStyleSheet(); + + UIExampleFactory.registerRightClickMenuItem(); + + UIExampleFactory.registerRightClickMenuPopup(); + + UIExampleFactory.registerWindowMenuWithSeprator(); + + await UIExampleFactory.registerExtraColumn(); + + await UIExampleFactory.registerExtraColumnWithCustomCell(); + + await UIExampleFactory.registerCustomCellRenderer(); + + UIExampleFactory.registerLibraryTabPanel(); + + await UIExampleFactory.registerReaderTabPanel(); + + await Zotero.Promise.delay(1000); + changeProgressWindowLine(w, { + newProgress: 100, + newText: `[100%] ${addon.locale.getString("startup.finish")}`, + }); + w.startCloseTimer(5000); + } + + public onShutdown(): void { + BasicExampleFactory.unregisterPrefs(); + UIExampleFactory.unregisterUIExamples(); + // Remove addon object + addon.alive = false; + delete Zotero.AddonTemplate; + } +} + +export default AddonHooks; diff --git a/src/index.ts b/src/index.ts index 9267ac4..16ed824 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,22 @@ +import ZoteroToolkit from "zotero-plugin-toolkit"; +import { getGlobal } from "zotero-plugin-toolkit/dist/utils"; import Addon from "./addon"; +import { config } from "../package.json"; -if (!Zotero.AddonTemplate) { - Zotero.AddonTemplate = new Addon(); - // @ts-ignore - Zotero.AddonTemplate.events.onInit(); +if (!getGlobal("Zotero").AddonTemplate) { + // Set global variables + _globalThis.Zotero = getGlobal("Zotero"); + _globalThis.ZoteroPane = getGlobal("ZoteroPane"); + _globalThis.Zotero_Tabs = getGlobal("Zotero_Tabs"); + _globalThis.window = getGlobal("window"); + _globalThis.document = getGlobal("document"); + _globalThis.ztoolkit = new ZoteroToolkit(); + _globalThis.addon = new Addon(); + // The env will be replaced after esbuild + addon.env = __env__; + ztoolkit.Tool.logOptionsGlobal.prefix = `[${config.addonName}]`; + ztoolkit.Tool.logOptionsGlobal.disableConsole = addon.env === "production"; + Zotero.AddonTemplate = addon; + // Trigger addon hook for initialization + addon.hooks.onStartup(); } diff --git a/src/locale.ts b/src/locale.ts index e21bb2e..bc2ee83 100644 --- a/src/locale.ts +++ b/src/locale.ts @@ -1,7 +1,6 @@ -import AddonModule from "./module"; import { config } from "../package.json"; -class AddonLocale extends AddonModule { +class AddonLocale { private stringBundle: any; public initLocale() { diff --git a/src/module.ts b/src/module.ts deleted file mode 100644 index 3b01dca..0000000 --- a/src/module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Addon from "./addon"; - -class AddonModule { - protected _Addon: Addon; - constructor(parent: Addon) { - this._Addon = parent; - } -} - -export default AddonModule; diff --git a/src/prefs.ts b/src/prefs.ts index 9093792..e954a03 100644 --- a/src/prefs.ts +++ b/src/prefs.ts @@ -1,17 +1,12 @@ -import Addon from "./addon"; -import AddonModule from "./module"; import { config } from "../package.json"; -class AddonPrefs extends AddonModule { +class AddonPrefs { private _window!: Window; - constructor(parent: Addon) { - super(parent); - } + public initPreferences(_window: Window) { // This function is called when the prefs window is opened // See addon/chrome/content/preferences.xul onpaneload this._window = _window; - ZToolkit.Tool.log("init preferences"); this.updatePrefsUI(); this.bindPrefEvents(); } @@ -20,14 +15,13 @@ class AddonPrefs extends AddonModule { // You can initialize some UI elements on prefs window // with this._window.document // Or bind some events to the elements - ZToolkit.Tool.log("init preferences UI"); } private bindPrefEvents() { this._window.document .querySelector(`#zotero-prefpane-${config.addonRef}-enable`) ?.addEventListener("command", (e) => { - ZToolkit.Tool.log(e); + ztoolkit.Tool.log(e); this._window.alert( `Successfully changed to ${(e.target as XUL.Checkbox).checked}!` ); @@ -36,7 +30,7 @@ class AddonPrefs extends AddonModule { this._window.document .querySelector(`#zotero-prefpane-${config.addonRef}-input`) ?.addEventListener("change", (e) => { - ZToolkit.Tool.log(e); + ztoolkit.Tool.log(e); this._window.alert( `Successfully changed to ${(e.target as HTMLInputElement).value}!` ); diff --git a/src/tools/progress.ts b/src/tools/progress.ts new file mode 100644 index 0000000..751f410 --- /dev/null +++ b/src/tools/progress.ts @@ -0,0 +1,48 @@ +import { config } from "../../package.json"; + +const progressWindowIcon = { + success: "chrome://zotero/skin/tick.png", + fail: "chrome://zotero/skin/cross.png", + default: `chrome://${config.addonRef}/content/icons/favicon.png`, +}; + +export function showProgressWindow( + header: string, + context: string, + type: "success" | "fail" | "default" = "default", + t: number = 5000 +): _ZoteroProgressWindow { + // A simple wrapper of the Zotero ProgressWindow + let progressWindow = new Zotero.ProgressWindow({ + closeOnClick: true, + }) as _ZoteroProgressWindow; + progressWindow.changeHeadline(header); + // @ts-ignore + progressWindow.progress = new progressWindow.ItemProgress( + progressWindowIcon[type], + context + ); + progressWindow.show(); + if (t > 0) { + progressWindow.startCloseTimer(t); + } + return progressWindow; +} + +export function changeProgressWindowLine( + progressWindow: _ZoteroProgressWindow, + options: { + newText?: string; + newIcon?: string; + newProgress?: number; + } +) { + // @ts-ignore + const progress = progressWindow.progress as _ZoteroItemProgress; + if (!progress) { + return; + } + options.newText && progress.setText(options.newText); + options.newIcon && progress.setIcon(options.newIcon); + options.newProgress && progress.setProgress(options.newProgress); +} diff --git a/tsconfig.json b/tsconfig.json index 46480e0..283a0d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "experimentalDecorators": true, "module": "commonjs", "target": "ES2016", "resolveJsonModule": true, diff --git a/typing/global.d.ts b/typing/global.d.ts index 7453f61..fa52c31 100644 --- a/typing/global.d.ts +++ b/typing/global.d.ts @@ -5,9 +5,14 @@ declare const _globalThis: { Zotero_Tabs: typeof Zotero_Tabs; window: Window; document: Document; - ZToolkit: typeof ZToolkit; + ztoolkit: typeof ztoolkit; + addon: typeof addon; }; -declare const ZToolkit: import("zotero-plugin-toolkit").ZoteroToolkit; +declare const ztoolkit: import("zotero-plugin-toolkit").ZoteroToolkit; declare const rootURI: string; + +declare const addon: import("../src/addon").default; + +declare const __env__: "production" | "development";