diff --git a/addon/chrome/content/preferences.xhtml b/addon/chrome/content/preferences.xhtml index af67264..8b13789 100644 --- a/addon/chrome/content/preferences.xhtml +++ b/addon/chrome/content/preferences.xhtml @@ -1,36 +1 @@ - - - - - - - - - - - - - - - - - - - + diff --git a/addon/chrome/content/zoteroPane.css b/addon/chrome/content/zoteroPane.css deleted file mode 100644 index 8c95c1e..0000000 --- a/addon/chrome/content/zoteroPane.css +++ /dev/null @@ -1,3 +0,0 @@ -.makeItRed { - background-color: tomato; -} diff --git a/addon/locale/en-US/addon.ftl b/addon/locale/en-US/addon.ftl index 892d267..04c1f65 100644 --- a/addon/locale/en-US/addon.ftl +++ b/addon/locale/en-US/addon.ftl @@ -1,11 +1,5 @@ -startup-begin = Addon is loading -startup-finish = Addon is ready -menuitem-label = Addon Template: Helper Examples -menupopup-label = Addon Template: Menupopup -menuitem-submenulabel = Addon Template -menuitem-filemenulabel = Addon Template: File Menuitem -prefs-title = Template -prefs-table-title = Title -prefs-table-detail = Detail -tabpanel-lib-tab-label = Lib Tab -tabpanel-reader-tab-label = Reader Tab \ No newline at end of file +menuitem-updatetldrlabel = update TLDR +menucollection-updatetldrlabel = update TLDR +itembox-tldrlabel = TLDR +tldr-unrelated = TLDR Unrelated in Semantic scholar +tldr-itemnotfound = Item Not Found in Semantic scholar \ No newline at end of file diff --git a/addon/locale/zh-CN/addon.ftl b/addon/locale/zh-CN/addon.ftl index 16d97d3..0882e0c 100644 --- a/addon/locale/zh-CN/addon.ftl +++ b/addon/locale/zh-CN/addon.ftl @@ -1,11 +1,4 @@ -startup-begin = 插件加载中 -startup-finish = 插件已就绪 -menuitem-label = 插件模板: 帮助工具样例 -menupopup-label = 插件模板: 弹出菜单 -menuitem-submenulabel = 插件模板:子菜单 -menuitem-filemenulabel = 插件模板: 文件菜单 -prefs-title = 插件模板 -prefs-table-title = 标题 -prefs-table-detail = 详情 -tabpanel-lib-tab-label = 库标签 -tabpanel-reader-tab-label = 阅读器标签 \ No newline at end of file +menuitem-updatetldrlabel = 更新TLDR +menucollection-updatetldrlabel = 批量更新TLDR +itembox-tldrlabel = TLDR +prefs-title = 插件模板 \ No newline at end of file diff --git a/addon/prefs.js b/addon/prefs.js index be8f47d..beff801 100644 --- a/addon/prefs.js +++ b/addon/prefs.js @@ -1,3 +1 @@ -/* eslint-disable no-undef */ -pref("__prefsPrefix__.enable", true); -pref("__prefsPrefix__.input", "This is input"); +/* eslint-disable no-undef */ \ No newline at end of file diff --git a/src/addon.ts b/src/addon.ts index a9a70af..cf2e864 100644 --- a/src/addon.ts +++ b/src/addon.ts @@ -14,8 +14,6 @@ class Addon { }; prefs?: { window: Window; - columns: Array; - rows: Array<{ [dataKey: string]: string }>; }; dialog?: DialogHelper; }; diff --git a/src/hooks.ts b/src/hooks.ts index 7b4efa2..e863bc2 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,14 +1,13 @@ import { - BasicExampleFactory, - HelperExampleFactory, - KeyExampleFactory, - PromptExampleFactory, - UIExampleFactory, -} from "./modules/examples"; + RegisterFactory, + UIFactory, +} from "./modules/Common"; import { config } from "../package.json"; import { getString, initLocale } from "./utils/locale"; import { registerPrefsScripts } from "./modules/preferenceScript"; import { createZToolkit } from "./utils/ztoolkit"; +import { TLDRFetcher, TLDRFieldKey } from "./modules/tldrFetcher"; +import { Data, DataStorage } from "./modules/dataStorage"; async function onStartup() { await Promise.all([ @@ -18,9 +17,11 @@ async function onStartup() { ]); initLocale(); - BasicExampleFactory.registerPrefs(); + await DataStorage.instance(TLDRFieldKey).getAsync(); // 加载TLDR数据 - BasicExampleFactory.registerNotifier(); + RegisterFactory.registerPrefs(); + + RegisterFactory.registerNotifier(); await onMainWindowLoad(window); } @@ -29,60 +30,15 @@ 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(); - - KeyExampleFactory.registerShortcuts(); - - await Zotero.Promise.delay(1000); - popupWin.changeLine({ - progress: 30, - text: `[30%] ${getString("startup-begin")}`, - }); - - UIExampleFactory.registerStyleSheet(); - - UIExampleFactory.registerRightClickMenuItem(); - - UIExampleFactory.registerRightClickMenuPopup(); - - UIExampleFactory.registerWindowMenuWithSeparator(); - - await UIExampleFactory.registerExtraColumn(); - - await UIExampleFactory.registerExtraColumnWithCustomCell(); - - await UIExampleFactory.registerCustomCellRenderer(); - - await UIExampleFactory.registerCustomItemBoxRow(); - - UIExampleFactory.registerLibraryTabPanel(); - - await UIExampleFactory.registerReaderTabPanel(); - - PromptExampleFactory.registerNormalCommandExample(); - - PromptExampleFactory.registerAnonymousCommandExample(); - - PromptExampleFactory.registerConditionalCommandExample(); - await Zotero.Promise.delay(1000); - popupWin.changeLine({ - progress: 100, - text: `[100%] ${getString("startup-finish")}`, - }); - popupWin.startCloseTimer(5000); + UIFactory.registerRightClickMenuItem(); - addon.hooks.onDialogEvents("dialogExample"); + UIFactory.registerRightClickCollectionMenuItem(); + + await UIFactory.registerTLDRItemBoxRow(); + + onLoad(); } async function onMainWindowUnload(win: Window): Promise { @@ -108,16 +64,9 @@ async function onNotify( ids: Array, extraData: { [key: string]: any }, ) { - // You can add your code to the corresponding notify type - ztoolkit.log("notify", event, type, ids, extraData); - if ( - event == "select" && - type == "tab" && - extraData[ids[0]].type == "reader" - ) { - BasicExampleFactory.exampleNotifierCallback(); - } else { - return; + Zotero.log(`${event} ${type} ${ids}, ${extraData}`); + if (event == "add" && type == "item" && ids.length > 0) { + onNotifyAddItems(ids); } } @@ -137,42 +86,73 @@ async function onPrefsEvent(type: string, data: { [key: string]: any }) { } } -function onShortcuts(type: string) { - switch (type) { - case "larger": - KeyExampleFactory.exampleShortcutLargerCallback(); - break; - case "smaller": - KeyExampleFactory.exampleShortcutSmallerCallback(); - break; - case "confliction": - KeyExampleFactory.exampleShortcutConflictingCallback(); - break; - default: - break; - } +function onLoad() { + (async () => { + let needFetchItems: Zotero.Item[] = []; + for (const lib of Zotero.Libraries.getAll()) { + needFetchItems = needFetchItems.concat((await Zotero.Items.getAll(lib.id)).filter((item: Zotero.Item) => { + return item.isRegularItem() && !item.isCollection(); + })); + } + onUpdateItems(needFetchItems, false); + })(); } -function onDialogEvents(type: string) { - switch (type) { - case "dialogExample": - HelperExampleFactory.dialogExample(); - break; - case "clipboardExample": - HelperExampleFactory.clipboardExample(); - break; - case "filePickerExample": - HelperExampleFactory.filePickerExample(); - break; - case "progressWindowExample": - HelperExampleFactory.progressWindowExample(); - break; - case "vtableExample": - HelperExampleFactory.vtableExample(); - break; - default: - break; +function onNotifyAddItems(ids: (string | number)[]) { + const addedRegularItems: Zotero.Item[] = []; + for (const id of ids) { + const item = Zotero.Items.get(id); + if (item.isRegularItem()) { + addedRegularItems.push(item); + } } + (async function () { + await Zotero.Promise.delay(3000); + onUpdateItems(addedRegularItems, false); + })(); +} + +function onUpdateItems(items: Zotero.Item[], forceFetch: boolean = false) { + items = items.filter((item: Zotero.Item) => { + if (!item.getField('title')) { return false; } + if (!forceFetch) { return DataStorage.instance(TLDRFieldKey).get()[item.id] === undefined; } + return true; + }); + if (items.length <= 0) { return; } + const newPopWin = (closeOnClick = false) => { + return new ztoolkit.ProgressWindow(config.addonName, { + closeOnClick: closeOnClick, + }).createLine({ + text: `Waiting: ${items.length}, succeed: 0, failed: 0`, + type: "default", + progress: 0, + }); + } + const popupWin = newPopWin().show(-1); + (async function () { + const count = items.length; + const failedItems: Zotero.Item[] = []; + const succeedItems: Zotero.Item[] = []; + await (async function () { + for (const [index, item] of items.entries()) { + await new TLDRFetcher(item).fetchTLDR() ? succeedItems.push(item) : failedItems.push(item); + ztoolkit.ItemBox.refresh(); + popupWin.changeLine({ + progress: index * 100 / count, + text: `Waiting: ${count - index - 1}, succeed: ${succeedItems.length}, failed: ${failedItems.length}`, + }); + } + })(); + + await (async function () { + popupWin.changeLine({ + type: "success", + progress: 100, + text: `Success: ${succeedItems.length}\nFailed: ${failedItems.length}`, + }); + popupWin.startCloseTimer(3000); + })(); + })(); } // Add your hooks here. For element click, etc. @@ -186,6 +166,5 @@ export default { onMainWindowUnload, onNotify, onPrefsEvent, - onShortcuts, - onDialogEvents, + onUpdateItems, }; diff --git a/src/modules/Common.ts b/src/modules/Common.ts new file mode 100644 index 0000000..565f675 --- /dev/null +++ b/src/modules/Common.ts @@ -0,0 +1,121 @@ +import { config } from "../../package.json"; +import { getString } from "../utils/locale"; +import { DataStorage } from "./dataStorage"; +import { TLDRFieldKey, TLDRItemNotFound, TLDRUnrelated } from "./tldrFetcher"; + +export class RegisterFactory { + // 注册zotero的通知 + static registerNotifier() { + const callback = { + notify: async ( + event: string, + type: string, + ids: number[] | string[], + extraData: { [key: string]: any } + ) => { + if (!addon?.data.alive) { + this.unregisterNotifier(notifierID); + return; + } + addon.hooks.onNotify(event, type, ids, extraData); + }, + }; + + // Register the callback in Zotero as an item observer + const notifierID = Zotero.Notifier.registerObserver(callback, [ + "item", + ]); + + // Unregister callback when the window closes (important to avoid a memory leak) + window.addEventListener( + "unload", + (e: Event) => { + this.unregisterNotifier(notifierID); + }, + false + ); + } + + private static unregisterNotifier(notifierID: string) { + Zotero.Notifier.unregisterObserver(notifierID); + } + + + // 注册首选项配置 + static registerPrefs() { + // const prefOptions = { + // pluginID: config.addonID, + // src: rootURI + "chrome/content/preferences.xhtml", + // label: getString("prefs.title"), + // image: `chrome://${config.addonRef}/content/icons/favicon.png`, + // extraDTD: [`chrome://${config.addonRef}/locale/overlay.dtd`], + // defaultXUL: true, + // }; + // ztoolkit.PreferencePane.register(prefOptions); + } +} + +export class UIFactory { + // item右键菜单 + static registerRightClickMenuItem() { + const menuIcon = `chrome://${config.addonRef}/content/icons/favicon@0.5x.png`; + // item menuitem with icon + ztoolkit.Menu.register("item", { + tag: "menuitem", + id: "zotero-itemmenu-tldr", + label: getString("menuitem-updatetldrlabel"), + commandListener: (ev) => { + const selectedItems = ZoteroPane.getSelectedItems() ?? []; + addon.hooks.onUpdateItems(selectedItems, selectedItems.length <= 1); + }, + icon: menuIcon, + }); + } + + // collection右键菜单 + static registerRightClickCollectionMenuItem() { + const menuIcon = `chrome://${config.addonRef}/content/icons/favicon@0.5x.png`; + ztoolkit.Menu.register("collection", { + tag: "menuitem", + id: "zotero-collectionmenu-tldr", + label: getString("menucollection-updatetldrlabel"), + commandListener: (ev) => addon.hooks.onUpdateItems(ZoteroPane.getSelectedCollection()?.getChildItems() ?? [], false), + icon: menuIcon, + }) + } + + // tldr行 + static async registerTLDRItemBoxRow() { + await ztoolkit.ItemBox.register( + TLDRFieldKey, + getString("itembox-tldrlabel"), + (field, unformatted, includeBaseMapped, item, original) => { + const tldrInfo = DataStorage.instance(TLDRFieldKey).get()[item.id]; + if (tldrInfo === TLDRUnrelated) { + return getString(TLDRUnrelated); + } else if (tldrInfo === TLDRItemNotFound) { + return getString(TLDRItemNotFound); + } else if (tldrInfo) { + return tldrInfo; + } else { + return ""; + } + }, + { + editable: true, + setFieldHook: (field, value, loadIn, item, original) => { + (async () => { + await DataStorage.instance(TLDRFieldKey).modify((data: any) => { + data[item.id] = value; + return data; + }); + ztoolkit.ItemBox.refresh(); + })(); + return true; + }, + index: 2, + multiline: true, + }, + ); + } +} \ No newline at end of file diff --git a/src/modules/dataStorage.ts b/src/modules/dataStorage.ts new file mode 100644 index 0000000..de04411 --- /dev/null +++ b/src/modules/dataStorage.ts @@ -0,0 +1,80 @@ +import { config } from "../../package.json"; + +export class Data { + private filePath: string + private inited = false; + private _data: any + + constructor(filePath: string) { + this.filePath = filePath; + } + + async getAsync() { + await this.initDataIfNeed(); + return this.data; + } + + get() { + return this.data; + } + + async modify(action: (data: any) => Promise) { + await this.initDataIfNeed(); + const data = await this.data; + const newData = await action(data); + try { + await IOUtils.writeJSON(this.filePath, newData, {mode: 'overwrite', compress: false}); + this.data = newData; + return newData; + } catch (error) { + return data; + } + } + + async delete() { + try { + await IOUtils.remove(this.filePath); + this.data = {}; + return true; + } catch (error) { + return false; + } + } + + private get data() { + return this._data; + } + + private set data(value: any) { + this._data = value; + } + + private async initDataIfNeed() { + if (this.inited) { return; } + try { + this.data = await IOUtils.readJSON(this.filePath, {decompress: false}); + } catch (error) { + this.data = {}; + } + } +} + +export class DataStorage { + private readonly dataDir = PathUtils.join(PathUtils.profileDir, 'extensions', config.addonName); + private dataMap: { [key: string]: Data } = {}; + + private static shared = new DataStorage(); + + static instance(dataType: string) { + const path = PathUtils.join(this.shared.dataDir, dataType); + if (this.shared.dataMap[dataType] === undefined) { + const data = new Data(path); + this.shared.dataMap[dataType] = data; + return data; + } else { + return this.shared.dataMap[dataType]; + } + } + + private constructor() { } +} diff --git a/src/modules/examples.ts b/src/modules/examples.ts deleted file mode 100644 index 79ea9cd..0000000 --- a/src/modules/examples.ts +++ /dev/null @@ -1,979 +0,0 @@ -import { config } from "../../package.json"; -import { getString } from "../utils/locale"; - -function example( - target: any, - propertyKey: string | symbol, - descriptor: PropertyDescriptor, -) { - const original = descriptor.value; - descriptor.value = function (...args: any) { - try { - ztoolkit.log(`Calling example ${target.name}.${String(propertyKey)}`); - return original.apply(this, args); - } catch (e) { - ztoolkit.log(`Error in example ${target.name}.${String(propertyKey)}`, e); - throw e; - } - }; - return descriptor; -} - -export class BasicExampleFactory { - @example - static registerNotifier() { - const callback = { - notify: async ( - event: string, - type: string, - ids: number[] | string[], - extraData: { [key: string]: any }, - ) => { - if (!addon?.data.alive) { - this.unregisterNotifier(notifierID); - return; - } - addon.hooks.onNotify(event, type, ids, extraData); - }, - }; - - // Register the callback in Zotero as an item observer - const notifierID = Zotero.Notifier.registerObserver(callback, [ - "tab", - "item", - "file", - ]); - - // Unregister callback when the window closes (important to avoid a memory leak) - window.addEventListener( - "unload", - (e: Event) => { - this.unregisterNotifier(notifierID); - }, - false, - ); - } - - @example - static exampleNotifierCallback() { - new ztoolkit.ProgressWindow(config.addonName) - .createLine({ - text: "Open Tab Detected!", - type: "success", - progress: 100, - }) - .show(); - } - - @example - private static unregisterNotifier(notifierID: string) { - Zotero.Notifier.unregisterObserver(notifierID); - } - - @example - static registerPrefs() { - const prefOptions = { - pluginID: config.addonID, - src: rootURI + "chrome/content/preferences.xhtml", - label: getString("prefs-title"), - image: `chrome://${config.addonRef}/content/icons/favicon.png`, - defaultXUL: true, - }; - ztoolkit.PreferencePane.register(prefOptions); - } -} - -export class KeyExampleFactory { - @example - static registerShortcuts() { - const keysetId = `${config.addonRef}-keyset`; - const cmdsetId = `${config.addonRef}-cmdset`; - const cmdSmallerId = `${config.addonRef}-cmd-smaller`; - // Register an event key for Alt+L - ztoolkit.Shortcut.register("event", { - id: `${config.addonRef}-key-larger`, - key: "L", - modifiers: "alt", - callback: (keyOptions) => { - addon.hooks.onShortcuts("larger"); - }, - }); - // Register an element key using for Alt+S - ztoolkit.Shortcut.register("element", { - id: `${config.addonRef}-key-smaller`, - key: "S", - modifiers: "alt", - xulData: { - document, - command: cmdSmallerId, - _parentId: keysetId, - _commandOptions: { - id: cmdSmallerId, - document, - _parentId: cmdsetId, - oncommand: `Zotero.${config.addonInstance}.hooks.onShortcuts('smaller')`, - }, - }, - }); - // Here we register an conflict key for Alt+S - // just to show how the confliction check works. - // This is something you should avoid in your plugin. - ztoolkit.Shortcut.register("event", { - id: `${config.addonRef}-key-smaller-conflict`, - key: "S", - modifiers: "alt", - callback: (keyOptions) => { - ztoolkit.getGlobal("alert")("Smaller! This is a conflict key."); - }, - }); - // Register an event key to check confliction - ztoolkit.Shortcut.register("event", { - id: `${config.addonRef}-key-check-conflict`, - key: "C", - modifiers: "alt", - callback: (keyOptions) => { - addon.hooks.onShortcuts("confliction"); - }, - }); - new ztoolkit.ProgressWindow(config.addonName) - .createLine({ - text: "Example Shortcuts: Alt+L/S/C", - type: "success", - }) - .show(); - } - - @example - static exampleShortcutLargerCallback() { - new ztoolkit.ProgressWindow(config.addonName) - .createLine({ - text: "Larger!", - type: "default", - }) - .show(); - } - - @example - static exampleShortcutSmallerCallback() { - new ztoolkit.ProgressWindow(config.addonName) - .createLine({ - text: "Smaller!", - type: "default", - }) - .show(); - } - - @example - static exampleShortcutConflictingCallback() { - const conflictingGroups = ztoolkit.Shortcut.checkAllKeyConflicting(); - new ztoolkit.ProgressWindow("Check Key Conflicting") - .createLine({ - text: `${conflictingGroups.length} groups of conflicting keys found. Details are in the debug output/console.`, - }) - .show(-1); - ztoolkit.log( - "Conflicting:", - conflictingGroups, - "All keys:", - ztoolkit.Shortcut.getAll(), - ); - } -} - -export class UIExampleFactory { - @example - static registerStyleSheet() { - const styles = ztoolkit.UI.createElement(document, "link", { - properties: { - type: "text/css", - rel: "stylesheet", - href: `chrome://${config.addonRef}/content/zoteroPane.css`, - }, - }); - document.documentElement.appendChild(styles); - document - .getElementById("zotero-item-pane-content") - ?.classList.add("makeItRed"); - } - - @example - static registerRightClickMenuItem() { - const menuIcon = `chrome://${config.addonRef}/content/icons/favicon@0.5x.png`; - // item menuitem with icon - ztoolkit.Menu.register("item", { - tag: "menuitem", - id: "zotero-itemmenu-addontemplate-test", - label: getString("menuitem-label"), - commandListener: (ev) => addon.hooks.onDialogEvents("dialogExample"), - icon: menuIcon, - }); - } - - @example - static registerRightClickMenuPopup() { - ztoolkit.Menu.register( - "item", - { - tag: "menu", - label: getString("menupopup-label"), - children: [ - { - tag: "menuitem", - label: getString("menuitem-submenulabel"), - oncommand: "alert('Hello World! Sub Menuitem.')", - }, - ], - }, - "before", - document.querySelector( - "#zotero-itemmenu-addontemplate-test", - ) as XUL.MenuItem, - ); - } - - @example - static registerWindowMenuWithSeparator() { - ztoolkit.Menu.register("menuFile", { - tag: "menuseparator", - }); - // menu->File menuitem - ztoolkit.Menu.register("menuFile", { - tag: "menuitem", - label: getString("menuitem-filemenulabel"), - oncommand: "alert('Hello World! File Menuitem.')", - }); - } - - @example - static async registerExtraColumn() { - await ztoolkit.ItemTree.register( - "test1", - "text column", - ( - field: string, - unformatted: boolean, - includeBaseMapped: boolean, - item: Zotero.Item, - ) => { - return field + String(item.id); - }, - { - iconPath: "chrome://zotero/skin/cross.png", - }, - ); - } - - @example - static async registerExtraColumnWithCustomCell() { - await ztoolkit.ItemTree.register( - "test2", - "custom column", - ( - field: string, - unformatted: boolean, - includeBaseMapped: boolean, - item: Zotero.Item, - ) => { - return String(item.id); - }, - { - renderCellHook(index, data, column) { - const span = document.createElementNS( - "http://www.w3.org/1999/xhtml", - "span", - ); - span.style.background = "#0dd068"; - span.innerText = "⭐" + data; - return span; - }, - }, - ); - } - - @example - static async registerCustomCellRenderer() { - await ztoolkit.ItemTree.addRenderCellHook( - "title", - // eslint-disable-next-line @typescript-eslint/ban-types - (index: number, data: string, column: any, original: Function) => { - const span = original(index, data, column) as HTMLSpanElement; - span.style.background = "rgb(30, 30, 30)"; - span.style.color = "rgb(156, 220, 240)"; - return span; - }, - ); - await ztoolkit.ItemTree.refresh(); - } - - @example - static async registerCustomItemBoxRow() { - await ztoolkit.ItemBox.register( - "itemBoxFieldEditable", - "Editable Custom Field", - (field, unformatted, includeBaseMapped, item, original) => { - return ( - ztoolkit.ExtraField.getExtraField(item, "itemBoxFieldEditable") || "" - ); - }, - { - editable: true, - setFieldHook: (field, value, loadIn, item, original) => { - window.alert("Custom itemBox value is changed and saved to extra!"); - ztoolkit.ExtraField.setExtraField( - item, - "itemBoxFieldEditable", - value, - ); - return true; - }, - index: 1, - }, - ); - - await ztoolkit.ItemBox.register( - "itemBoxFieldNonEditable", - "Non-Editable Custom Field", - (field, unformatted, includeBaseMapped, item, original) => { - return ( - "[CANNOT EDIT THIS]" + (item.getField("title") as string).slice(0, 10) - ); - }, - { - editable: false, - index: 2, - }, - ); - } - - @example - static registerLibraryTabPanel() { - const tabId = ztoolkit.LibraryTabPanel.register( - getString("tabpanel-lib-tab-label"), - (panel: XUL.Element, win: Window) => { - const elem = ztoolkit.UI.createElement(win.document, "vbox", { - children: [ - { - tag: "h2", - properties: { - innerText: "Hello World!", - }, - }, - { - tag: "div", - properties: { - innerText: "This is a library tab.", - }, - }, - { - tag: "button", - namespace: "html", - properties: { - innerText: "Unregister", - }, - listeners: [ - { - type: "click", - listener: () => { - ztoolkit.LibraryTabPanel.unregister(tabId); - }, - }, - ], - }, - ], - }); - panel.append(elem); - }, - { - targetIndex: 1, - }, - ); - } - - @example - static async registerReaderTabPanel() { - const tabId = await ztoolkit.ReaderTabPanel.register( - getString("tabpanel-reader-tab-label"), - ( - panel: XUL.TabPanel | undefined, - deck: XUL.Deck, - win: Window, - reader: _ZoteroTypes.ReaderInstance, - ) => { - if (!panel) { - ztoolkit.log( - "This reader do not have right-side bar. Adding reader tab skipped.", - ); - return; - } - ztoolkit.log(reader); - const elem = ztoolkit.UI.createElement(win.document, "vbox", { - id: `${config.addonRef}-${reader._instanceID}-extra-reader-tab-div`, - // This is important! Don't create content for multiple times - // ignoreIfExists: true, - removeIfExists: true, - children: [ - { - tag: "h2", - properties: { - innerText: "Hello World!", - }, - }, - { - tag: "div", - properties: { - innerText: "This is a reader tab.", - }, - }, - { - tag: "div", - properties: { - innerText: `Reader: ${reader._title.slice(0, 20)}`, - }, - }, - { - tag: "div", - properties: { - innerText: `itemID: ${reader.itemID}.`, - }, - }, - { - tag: "button", - namespace: "html", - properties: { - innerText: "Unregister", - }, - listeners: [ - { - type: "click", - listener: () => { - ztoolkit.ReaderTabPanel.unregister(tabId); - }, - }, - ], - }, - ], - }); - panel.append(elem); - }, - { - targetIndex: 1, - }, - ); - } -} - -export class PromptExampleFactory { - @example - static registerNormalCommandExample() { - ztoolkit.Prompt.register([ - { - name: "Normal Command Test", - label: "Plugin Template", - callback(prompt) { - ztoolkit.getGlobal("alert")("Command triggered!"); - }, - }, - ]); - } - - @example - static registerAnonymousCommandExample() { - ztoolkit.Prompt.register([ - { - id: "search", - callback: async (prompt) => { - // https://github.com/zotero/zotero/blob/7262465109c21919b56a7ab214f7c7a8e1e63909/chrome/content/zotero/integration/quickFormat.js#L589 - function getItemDescription(item: Zotero.Item) { - const nodes = []; - let str = ""; - let author, - authorDate = ""; - if (item.firstCreator) { - author = authorDate = item.firstCreator; - } - let date = item.getField("date", true, true) as string; - if (date && (date = date.substr(0, 4)) !== "0000") { - authorDate += " (" + parseInt(date) + ")"; - } - authorDate = authorDate.trim(); - if (authorDate) nodes.push(authorDate); - - const publicationTitle = item.getField( - "publicationTitle", - false, - true, - ); - if (publicationTitle) { - nodes.push(`${publicationTitle}`); - } - let volumeIssue = item.getField("volume"); - const issue = item.getField("issue"); - if (issue) volumeIssue += "(" + issue + ")"; - if (volumeIssue) nodes.push(volumeIssue); - - const publisherPlace = []; - let field; - if ((field = item.getField("publisher"))) - publisherPlace.push(field); - if ((field = item.getField("place"))) publisherPlace.push(field); - if (publisherPlace.length) nodes.push(publisherPlace.join(": ")); - - const pages = item.getField("pages"); - if (pages) nodes.push(pages); - - if (!nodes.length) { - const url = item.getField("url"); - if (url) nodes.push(url); - } - - // compile everything together - for (let i = 0, n = nodes.length; i < n; i++) { - const node = nodes[i]; - - if (i != 0) str += ", "; - - if (typeof node === "object") { - const label = document.createElement("label"); - label.setAttribute("value", str); - label.setAttribute("crop", "end"); - str = ""; - } else { - str += node; - } - } - str.length && (str += "."); - return str; - } - function filter(ids: number[]) { - ids = ids.filter(async (id) => { - const item = (await Zotero.Items.getAsync(id)) as Zotero.Item; - return item.isRegularItem() && !(item as any).isFeedItem; - }); - return ids; - } - const text = prompt.inputNode.value; - prompt.showTip("Searching..."); - const s = new Zotero.Search(); - s.addCondition("quicksearch-titleCreatorYear", "contains", text); - s.addCondition("itemType", "isNot", "attachment"); - let ids = await s.search(); - // prompt.exit will remove current container element. - // @ts-ignore ignore - prompt.exit(); - const container = prompt.createCommandsContainer(); - container.classList.add("suggestions"); - ids = filter(ids); - console.log(ids.length); - if (ids.length == 0) { - const s = new Zotero.Search(); - const operators = [ - "is", - "isNot", - "true", - "false", - "isInTheLast", - "isBefore", - "isAfter", - "contains", - "doesNotContain", - "beginsWith", - ]; - let hasValidCondition = false; - let joinMode = "all"; - if (/\s*\|\|\s*/.test(text)) { - joinMode = "any"; - } - text.split(/\s*(&&|\|\|)\s*/g).forEach((conditinString: string) => { - const conditions = conditinString.split(/\s+/g); - if ( - conditions.length == 3 && - operators.indexOf(conditions[1]) != -1 - ) { - hasValidCondition = true; - s.addCondition( - "joinMode", - joinMode as Zotero.Search.Operator, - "", - ); - s.addCondition( - conditions[0] as string, - conditions[1] as Zotero.Search.Operator, - conditions[2] as string, - ); - } - }); - if (hasValidCondition) { - ids = await s.search(); - } - } - ids = filter(ids); - console.log(ids.length); - if (ids.length > 0) { - ids.forEach((id: number) => { - const item = Zotero.Items.get(id); - const title = item.getField("title"); - const ele = ztoolkit.UI.createElement(document, "div", { - namespace: "html", - classList: ["command"], - listeners: [ - { - type: "mousemove", - listener: function () { - // @ts-ignore ignore - prompt.selectItem(this); - }, - }, - { - type: "click", - listener: () => { - prompt.promptNode.style.display = "none"; - Zotero_Tabs.select("zotero-pane"); - ZoteroPane.selectItem(item.id); - }, - }, - ], - styles: { - display: "flex", - flexDirection: "column", - justifyContent: "start", - }, - children: [ - { - tag: "span", - styles: { - fontWeight: "bold", - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - }, - properties: { - innerText: title, - }, - }, - { - tag: "span", - styles: { - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - }, - properties: { - innerHTML: getItemDescription(item), - }, - }, - ], - }); - container.appendChild(ele); - }); - } else { - // @ts-ignore ignore - prompt.exit(); - prompt.showTip("Not Found."); - } - }, - }, - ]); - } - - @example - static registerConditionalCommandExample() { - ztoolkit.Prompt.register([ - { - name: "Conditional Command Test", - label: "Plugin Template", - // The when function is executed when Prompt UI is woken up by `Shift + P`, and this command does not display when false is returned. - when: () => { - const items = ZoteroPane.getSelectedItems(); - return items.length > 0; - }, - callback(prompt) { - prompt.inputNode.placeholder = "Hello World!"; - const items = ZoteroPane.getSelectedItems(); - ztoolkit.getGlobal("alert")( - `You select ${items.length} items!\n\n${items - .map( - (item, index) => - String(index + 1) + ". " + item.getDisplayTitle(), - ) - .join("\n")}`, - ); - }, - }, - ]); - } -} - -export class HelperExampleFactory { - @example - static async dialogExample() { - const dialogData: { [key: string | number]: any } = { - inputValue: "test", - checkboxValue: true, - loadCallback: () => { - ztoolkit.log(dialogData, "Dialog Opened!"); - }, - unloadCallback: () => { - ztoolkit.log(dialogData, "Dialog closed!"); - }, - }; - const dialogHelper = new ztoolkit.Dialog(10, 2) - .addCell(0, 0, { - tag: "h1", - properties: { innerHTML: "Helper Examples" }, - }) - .addCell(1, 0, { - tag: "h2", - properties: { innerHTML: "Dialog Data Binding" }, - }) - .addCell(2, 0, { - tag: "p", - properties: { - innerHTML: - "Elements with attribute 'data-bind' are binded to the prop under 'dialogData' with the same name.", - }, - styles: { - width: "200px", - }, - }) - .addCell(3, 0, { - tag: "label", - namespace: "html", - attributes: { - for: "dialog-checkbox", - }, - properties: { innerHTML: "bind:checkbox" }, - }) - .addCell( - 3, - 1, - { - tag: "input", - namespace: "html", - id: "dialog-checkbox", - attributes: { - "data-bind": "checkboxValue", - "data-prop": "checked", - type: "checkbox", - }, - properties: { label: "Cell 1,0" }, - }, - false, - ) - .addCell(4, 0, { - tag: "label", - namespace: "html", - attributes: { - for: "dialog-input", - }, - properties: { innerHTML: "bind:input" }, - }) - .addCell( - 4, - 1, - { - tag: "input", - namespace: "html", - id: "dialog-input", - attributes: { - "data-bind": "inputValue", - "data-prop": "value", - type: "text", - }, - }, - false, - ) - .addCell(5, 0, { - tag: "h2", - properties: { innerHTML: "Toolkit Helper Examples" }, - }) - .addCell( - 6, - 0, - { - tag: "button", - namespace: "html", - attributes: { - type: "button", - }, - listeners: [ - { - type: "click", - listener: (e: Event) => { - addon.hooks.onDialogEvents("clipboardExample"); - }, - }, - ], - children: [ - { - tag: "div", - styles: { - padding: "2.5px 15px", - }, - properties: { - innerHTML: "example:clipboard", - }, - }, - ], - }, - false, - ) - .addCell( - 7, - 0, - { - tag: "button", - namespace: "html", - attributes: { - type: "button", - }, - listeners: [ - { - type: "click", - listener: (e: Event) => { - addon.hooks.onDialogEvents("filePickerExample"); - }, - }, - ], - children: [ - { - tag: "div", - styles: { - padding: "2.5px 15px", - }, - properties: { - innerHTML: "example:filepicker", - }, - }, - ], - }, - false, - ) - .addCell( - 8, - 0, - { - tag: "button", - namespace: "html", - attributes: { - type: "button", - }, - listeners: [ - { - type: "click", - listener: (e: Event) => { - addon.hooks.onDialogEvents("progressWindowExample"); - }, - }, - ], - children: [ - { - tag: "div", - styles: { - padding: "2.5px 15px", - }, - properties: { - innerHTML: "example:progressWindow", - }, - }, - ], - }, - false, - ) - .addCell( - 9, - 0, - { - tag: "button", - namespace: "html", - attributes: { - type: "button", - }, - listeners: [ - { - type: "click", - listener: (e: Event) => { - addon.hooks.onDialogEvents("vtableExample"); - }, - }, - ], - children: [ - { - tag: "div", - styles: { - padding: "2.5px 15px", - }, - properties: { - innerHTML: "example:virtualized-table", - }, - }, - ], - }, - false, - ) - .addButton("Confirm", "confirm") - .addButton("Cancel", "cancel") - .addButton("Help", "help", { - noClose: true, - callback: (e) => { - dialogHelper.window?.alert( - "Help Clicked! Dialog will not be closed.", - ); - }, - }) - .setDialogData(dialogData) - .open("Dialog Example"); - addon.data.dialog = dialogHelper; - await dialogData.unloadLock.promise; - addon.data.dialog = undefined; - addon.data.alive && - ztoolkit.getGlobal("alert")( - `Close dialog with ${dialogData._lastButtonId}.\nCheckbox: ${dialogData.checkboxValue}\nInput: ${dialogData.inputValue}.`, - ); - ztoolkit.log(dialogData); - } - - @example - static clipboardExample() { - new ztoolkit.Clipboard() - .addText( - "![Plugin Template](https://github.com/windingwind/zotero-plugin-template)", - "text/unicode", - ) - .addText( - 'Plugin Template', - "text/html", - ) - .copy(); - ztoolkit.getGlobal("alert")("Copied!"); - } - - @example - static async filePickerExample() { - const path = await new ztoolkit.FilePicker( - "Import File", - "open", - [ - ["PNG File(*.png)", "*.png"], - ["Any", "*.*"], - ], - "image.png", - ).open(); - ztoolkit.getGlobal("alert")(`Selected ${path}`); - } - - @example - static progressWindowExample() { - new ztoolkit.ProgressWindow(config.addonName) - .createLine({ - text: "ProgressWindow Example!", - type: "success", - progress: 100, - }) - .show(); - } - - @example - static vtableExample() { - ztoolkit.getGlobal("alert")("See src/modules/preferenceScript.ts"); - } -} diff --git a/src/modules/preferenceScript.ts b/src/modules/preferenceScript.ts index ef6e5b9..790be70 100644 --- a/src/modules/preferenceScript.ts +++ b/src/modules/preferenceScript.ts @@ -7,125 +7,12 @@ export async function registerPrefsScripts(_window: Window) { if (!addon.data.prefs) { addon.data.prefs = { window: _window, - columns: [ - { - dataKey: "title", - label: getString("prefs-table-title"), - fixedWidth: true, - width: 100, - }, - { - dataKey: "detail", - label: getString("prefs-table-detail"), - }, - ], - rows: [ - { - title: "Orange", - detail: "It's juicy", - }, - { - title: "Banana", - detail: "It's sweet", - }, - { - title: "Apple", - detail: "I mean the fruit APPLE", - }, - ], }; } else { addon.data.prefs.window = _window; } - updatePrefsUI(); bindPrefEvents(); } -async function updatePrefsUI() { - // You can initialize some UI elements on prefs window - // with addon.data.prefs.window.document - // Or bind some events to the elements - const renderLock = ztoolkit.getGlobal("Zotero").Promise.defer(); - if (addon.data.prefs?.window == undefined) return; - const tableHelper = new ztoolkit.VirtualizedTable(addon.data.prefs?.window) - .setContainerId(`${config.addonRef}-table-container`) - .setProp({ - id: `${config.addonRef}-prefs-table`, - // Do not use setLocale, as it modifies the Zotero.Intl.strings - // Set locales directly to columns - columns: addon.data.prefs?.columns, - showHeader: true, - multiSelect: true, - staticColumns: true, - disableFontSizeScaling: true, - }) - .setProp("getRowCount", () => addon.data.prefs?.rows.length || 0) - .setProp( - "getRowData", - (index) => - addon.data.prefs?.rows[index] || { - title: "no data", - detail: "no data", - }, - ) - // Show a progress window when selection changes - .setProp("onSelectionChange", (selection) => { - new ztoolkit.ProgressWindow(config.addonName) - .createLine({ - text: `Selected line: ${addon.data.prefs?.rows - .filter((v, i) => selection.isSelected(i)) - .map((row) => row.title) - .join(",")}`, - progress: 100, - }) - .show(); - }) - // When pressing delete, delete selected line and refresh table. - // Returning false to prevent default event. - .setProp("onKeyDown", (event: KeyboardEvent) => { - if (event.key == "Delete" || (Zotero.isMac && event.key == "Backspace")) { - addon.data.prefs!.rows = - addon.data.prefs?.rows.filter( - (v, i) => !tableHelper.treeInstance.selection.isSelected(i), - ) || []; - tableHelper.render(); - return false; - } - return true; - }) - // For find-as-you-type - .setProp( - "getRowString", - (index) => addon.data.prefs?.rows[index].title || "", - ) - // Render the table. - .render(-1, () => { - renderLock.resolve(); - }); - await renderLock.promise; - ztoolkit.log("Preference table rendered!"); -} - function bindPrefEvents() { - addon.data - .prefs!.window.document.querySelector( - `#zotero-prefpane-${config.addonRef}-enable`, - ) - ?.addEventListener("command", (e) => { - ztoolkit.log(e); - addon.data.prefs!.window.alert( - `Successfully changed to ${(e.target as XUL.Checkbox).checked}!`, - ); - }); - - addon.data - .prefs!.window.document.querySelector( - `#zotero-prefpane-${config.addonRef}-input`, - ) - ?.addEventListener("change", (e) => { - ztoolkit.log(e); - addon.data.prefs!.window.alert( - `Successfully changed to ${(e.target as HTMLInputElement).value}!`, - ); - }); } diff --git a/src/modules/tldrFetcher.ts b/src/modules/tldrFetcher.ts new file mode 100644 index 0000000..f523005 --- /dev/null +++ b/src/modules/tldrFetcher.ts @@ -0,0 +1,157 @@ +import { DataStorage } from "./dataStorage"; + +type SemanticScholarItemInfo = { + title?: string, + abstract?: string, + tldr?: string, +}; + +export const TLDRFieldKey = 'TLDR'; +export const TLDRUnrelated = 'tldr-unrelated' // semantic scholar 找到了该item,但是该item没有tldr +export const TLDRItemNotFound = 'tldr-itemnotfound' // semantic scholar 找不到该item, + +export class TLDRFetcher { + private readonly zoteroItem: Zotero.Item; + private readonly title?: string; + private readonly abstract?: string; + + constructor(item: Zotero.Item) { + this.zoteroItem = item; + if (item.isRegularItem() && !item.isCollection()) { + this.title = item.getField('title') as string; + this.abstract = item.getField('abstractNote') as string; + } + } + + async fetchTLDR() { + if (!this.title || this.title.length <= 0) { return false; } + try { + const infos = await this.fetchRelevanceItemInfos(this.title); + for (const info of infos) { + let match = false; + if (info.title && this.title && this.checkLCS(info.title, this.title)) { + match = true; + } else if (info.abstract && this.abstract && this.checkLCS(info.abstract, this.abstract)) { + match = true; + } + if (match) { + const result = info.tldr ?? TLDRUnrelated; + DataStorage.instance(TLDRFieldKey).modify((data: any) => { + data[this.zoteroItem.id] = result; + return data; + }); + return true; + } + } + DataStorage.instance(TLDRFieldKey).modify((data: any) => { + data[this.zoteroItem.id] = TLDRItemNotFound; + return data; + }); + return false; + } catch (error) { + Zotero.log(`post semantic scholar request error: ${error}`); + return false; + } + } + + private async fetchRelevanceItemInfos(title: string): Promise { + const semanticScholarURL = 'https://www.semanticscholar.org/api/1/search'; + const params = { + queryString: title, + page: 1, + pageSize: 10, + sort: 'relevance', + authors: [], + coAuthors: [], + venues: [], + performTitleMatch: true, + requireViewablePdf: false, + includeTldrs: true, + }; + const resp = await Zotero.HTTP.request("POST", semanticScholarURL, { + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(params), + }); + if (resp.status === 200) { + const results = JSON.parse(resp.response).results; + return results.map((item: any) => { + const result = { + title: item.title.text, + abstract: item.paperAbstract.text, + tldr: undefined, + }; + if (item.tldr) { + result.tldr = item.tldr.text; + } + return result; + }); + } + return []; + } + + private checkLCS(pattern: string, content: string): boolean { + const LCS = StringMatchUtils.longestCommonSubsequence(pattern, content); + return LCS.length >= Math.max(pattern.length, content.length) * 0.9 + } +} + + +class StringMatchUtils { + static longestCommonSubsequence(text1: string, text2: string): string { + const m = text1.length; + const n = text2.length; + + const dp: number[][] = new Array(m + 1); + for (let i = 0; i <= m; i++) { + dp[i] = new Array(n + 1).fill(0); + } + + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + if (text1[i - 1] === text2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + let i = m, j = n; + const lcs: string[] = []; + while (i > 0 && j > 0) { + if (text1[i - 1] === text2[j - 1]) { + lcs.unshift(text1[i - 1]); + i--; + j--; + } else if (dp[i - 1][j] > dp[i][j - 1]) { + i--; + } else { + j--; + } + } + + return lcs.join(''); + } + + // static minWindow(s: string, t: string): [number, number] | null { + // const m = s.length, n = t.length + // let start = -1, minLen = Number.MAX_SAFE_INTEGER, i = 0, j = 0, end; + // while (i < m) { + // if (s[i] == t[j]) { + // if (++j == n) { + // end = i + 1; + // while (--j >= 0) { + // while (s[i--] != t[j]); + // } + // ++i; ++j; + // if (end - i < minLen) { + // minLen = end - i; + // start = i; + // } + // } + // } + // ++i; + // } + // return start == -1 ? null : [start, minLen]; + // } +} \ No newline at end of file