init proj

This commit is contained in:
shenyutao 2023-08-17 14:38:20 +08:00
parent 03fadd7b25
commit 033bafa5bc
12 changed files with 452 additions and 1262 deletions

View File

@ -1,36 +1 @@
<linkset>
<html:link rel="localization" href="__addonRef__-preferences.ftl" />
</linkset>
<vbox
id="zotero-prefpane-__addonRef__"
onload="Zotero.__addonInstance__.hooks.onPrefsEvent('load', {window})"
>
<groupbox>
<label><html:h2 data-l10n-id="pref-title"></html:h2></label>
<checkbox
id="zotero-prefpane-__addonRef__-enable"
preference="extensions.zotero.__addonRef__.enable"
data-l10n-id="pref-enable"
/>
<hbox>
<html:label
for="zotero-prefpane-__addonRef__-input"
data-l10n-id="pref-input"
></html:label>
<html:input
type="text"
id="zotero-prefpane-__addonRef__-input"
preference="extensions.zotero.__addonRef__.input"
></html:input>
</hbox>
<hbox class="virtualized-table-container" flex="1" height="300px">
<html:div id="__addonRef__-table-container" />
</hbox>
</groupbox>
</vbox>
<vbox>
<html:label
data-l10n-id="pref-help"
data-l10n-args='{"time": "__buildTime__","name": "__addonName__","version":"__buildVersion__"}'
></html:label>
</vbox>

View File

@ -1,3 +0,0 @@
.makeItRed {
background-color: tomato;
}

View File

@ -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
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

View File

@ -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 = 阅读器标签
menuitem-updatetldrlabel = 更新TLDR
menucollection-updatetldrlabel = 批量更新TLDR
itembox-tldrlabel = TLDR
prefs-title = 插件模板

View File

@ -1,3 +1 @@
/* eslint-disable no-undef */
pref("__prefsPrefix__.enable", true);
pref("__prefsPrefix__.input", "This is input");
/* eslint-disable no-undef */

View File

@ -14,8 +14,6 @@ class Addon {
};
prefs?: {
window: Window;
columns: Array<ColumnOptions>;
rows: Array<{ [dataKey: string]: string }>;
};
dialog?: DialogHelper;
};

View File

@ -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<void> {
// Create ztoolkit for every window
addon.data.ztoolkit = createZToolkit();
const popupWin = new ztoolkit.ProgressWindow(config.addonName, {
closeOnClick: true,
closeTime: -1,
})
.createLine({
text: getString("startup-begin"),
type: "default",
progress: 0,
})
.show();
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<void> {
@ -108,16 +64,9 @@ async function onNotify(
ids: Array<string | number>,
extraData: { [key: string]: any },
) {
// You can add your code to the corresponding notify type
ztoolkit.log("notify", event, type, ids, extraData);
if (
event == "select" &&
type == "tab" &&
extraData[ids[0]].type == "reader"
) {
BasicExampleFactory.exampleNotifierCallback();
} else {
return;
Zotero.log(`${event} ${type} ${ids}, ${extraData}`);
if (event == "add" && type == "item" && ids.length > 0) {
onNotifyAddItems(ids);
}
}
@ -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,
};

121
src/modules/Common.ts Normal file
View File

@ -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,
},
);
}
}

View File

@ -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<any>) {
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() { }
}

View File

@ -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 <key> 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(`<i>${publicationTitle}</i>`);
}
let volumeIssue = item.getField("volume");
const issue = item.getField("issue");
if (issue) volumeIssue += "(" + issue + ")";
if (volumeIssue) nodes.push(volumeIssue);
const publisherPlace = [];
let field;
if ((field = item.getField("publisher")))
publisherPlace.push(field);
if ((field = item.getField("place"))) publisherPlace.push(field);
if (publisherPlace.length) nodes.push(publisherPlace.join(": "));
const pages = item.getField("pages");
if (pages) nodes.push(pages);
if (!nodes.length) {
const url = item.getField("url");
if (url) nodes.push(url);
}
// compile everything together
for (let i = 0, n = nodes.length; i < n; i++) {
const node = nodes[i];
if (i != 0) str += ", ";
if (typeof node === "object") {
const label = document.createElement("label");
label.setAttribute("value", str);
label.setAttribute("crop", "end");
str = "";
} else {
str += node;
}
}
str.length && (str += ".");
return str;
}
function filter(ids: number[]) {
ids = ids.filter(async (id) => {
const item = (await Zotero.Items.getAsync(id)) as Zotero.Item;
return item.isRegularItem() && !(item as any).isFeedItem;
});
return ids;
}
const text = prompt.inputNode.value;
prompt.showTip("Searching...");
const s = new Zotero.Search();
s.addCondition("quicksearch-titleCreatorYear", "contains", text);
s.addCondition("itemType", "isNot", "attachment");
let ids = await s.search();
// prompt.exit will remove current container element.
// @ts-ignore ignore
prompt.exit();
const container = prompt.createCommandsContainer();
container.classList.add("suggestions");
ids = filter(ids);
console.log(ids.length);
if (ids.length == 0) {
const s = new Zotero.Search();
const operators = [
"is",
"isNot",
"true",
"false",
"isInTheLast",
"isBefore",
"isAfter",
"contains",
"doesNotContain",
"beginsWith",
];
let hasValidCondition = false;
let joinMode = "all";
if (/\s*\|\|\s*/.test(text)) {
joinMode = "any";
}
text.split(/\s*(&&|\|\|)\s*/g).forEach((conditinString: string) => {
const conditions = conditinString.split(/\s+/g);
if (
conditions.length == 3 &&
operators.indexOf(conditions[1]) != -1
) {
hasValidCondition = true;
s.addCondition(
"joinMode",
joinMode as Zotero.Search.Operator,
"",
);
s.addCondition(
conditions[0] as string,
conditions[1] as Zotero.Search.Operator,
conditions[2] as string,
);
}
});
if (hasValidCondition) {
ids = await s.search();
}
}
ids = filter(ids);
console.log(ids.length);
if (ids.length > 0) {
ids.forEach((id: number) => {
const item = Zotero.Items.get(id);
const title = item.getField("title");
const ele = ztoolkit.UI.createElement(document, "div", {
namespace: "html",
classList: ["command"],
listeners: [
{
type: "mousemove",
listener: function () {
// @ts-ignore ignore
prompt.selectItem(this);
},
},
{
type: "click",
listener: () => {
prompt.promptNode.style.display = "none";
Zotero_Tabs.select("zotero-pane");
ZoteroPane.selectItem(item.id);
},
},
],
styles: {
display: "flex",
flexDirection: "column",
justifyContent: "start",
},
children: [
{
tag: "span",
styles: {
fontWeight: "bold",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
properties: {
innerText: title,
},
},
{
tag: "span",
styles: {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
properties: {
innerHTML: getItemDescription(item),
},
},
],
});
container.appendChild(ele);
});
} else {
// @ts-ignore ignore
prompt.exit();
prompt.showTip("Not Found.");
}
},
},
]);
}
@example
static registerConditionalCommandExample() {
ztoolkit.Prompt.register([
{
name: "Conditional Command Test",
label: "Plugin Template",
// The when function is executed when Prompt UI is woken up by `Shift + P`, and this command does not display when false is returned.
when: () => {
const items = ZoteroPane.getSelectedItems();
return items.length > 0;
},
callback(prompt) {
prompt.inputNode.placeholder = "Hello World!";
const items = ZoteroPane.getSelectedItems();
ztoolkit.getGlobal("alert")(
`You select ${items.length} items!\n\n${items
.map(
(item, index) =>
String(index + 1) + ". " + item.getDisplayTitle(),
)
.join("\n")}`,
);
},
},
]);
}
}
export class HelperExampleFactory {
@example
static async dialogExample() {
const dialogData: { [key: string | number]: any } = {
inputValue: "test",
checkboxValue: true,
loadCallback: () => {
ztoolkit.log(dialogData, "Dialog Opened!");
},
unloadCallback: () => {
ztoolkit.log(dialogData, "Dialog closed!");
},
};
const dialogHelper = new ztoolkit.Dialog(10, 2)
.addCell(0, 0, {
tag: "h1",
properties: { innerHTML: "Helper Examples" },
})
.addCell(1, 0, {
tag: "h2",
properties: { innerHTML: "Dialog Data Binding" },
})
.addCell(2, 0, {
tag: "p",
properties: {
innerHTML:
"Elements with attribute 'data-bind' are binded to the prop under 'dialogData' with the same name.",
},
styles: {
width: "200px",
},
})
.addCell(3, 0, {
tag: "label",
namespace: "html",
attributes: {
for: "dialog-checkbox",
},
properties: { innerHTML: "bind:checkbox" },
})
.addCell(
3,
1,
{
tag: "input",
namespace: "html",
id: "dialog-checkbox",
attributes: {
"data-bind": "checkboxValue",
"data-prop": "checked",
type: "checkbox",
},
properties: { label: "Cell 1,0" },
},
false,
)
.addCell(4, 0, {
tag: "label",
namespace: "html",
attributes: {
for: "dialog-input",
},
properties: { innerHTML: "bind:input" },
})
.addCell(
4,
1,
{
tag: "input",
namespace: "html",
id: "dialog-input",
attributes: {
"data-bind": "inputValue",
"data-prop": "value",
type: "text",
},
},
false,
)
.addCell(5, 0, {
tag: "h2",
properties: { innerHTML: "Toolkit Helper Examples" },
})
.addCell(
6,
0,
{
tag: "button",
namespace: "html",
attributes: {
type: "button",
},
listeners: [
{
type: "click",
listener: (e: Event) => {
addon.hooks.onDialogEvents("clipboardExample");
},
},
],
children: [
{
tag: "div",
styles: {
padding: "2.5px 15px",
},
properties: {
innerHTML: "example:clipboard",
},
},
],
},
false,
)
.addCell(
7,
0,
{
tag: "button",
namespace: "html",
attributes: {
type: "button",
},
listeners: [
{
type: "click",
listener: (e: Event) => {
addon.hooks.onDialogEvents("filePickerExample");
},
},
],
children: [
{
tag: "div",
styles: {
padding: "2.5px 15px",
},
properties: {
innerHTML: "example:filepicker",
},
},
],
},
false,
)
.addCell(
8,
0,
{
tag: "button",
namespace: "html",
attributes: {
type: "button",
},
listeners: [
{
type: "click",
listener: (e: Event) => {
addon.hooks.onDialogEvents("progressWindowExample");
},
},
],
children: [
{
tag: "div",
styles: {
padding: "2.5px 15px",
},
properties: {
innerHTML: "example:progressWindow",
},
},
],
},
false,
)
.addCell(
9,
0,
{
tag: "button",
namespace: "html",
attributes: {
type: "button",
},
listeners: [
{
type: "click",
listener: (e: Event) => {
addon.hooks.onDialogEvents("vtableExample");
},
},
],
children: [
{
tag: "div",
styles: {
padding: "2.5px 15px",
},
properties: {
innerHTML: "example:virtualized-table",
},
},
],
},
false,
)
.addButton("Confirm", "confirm")
.addButton("Cancel", "cancel")
.addButton("Help", "help", {
noClose: true,
callback: (e) => {
dialogHelper.window?.alert(
"Help Clicked! Dialog will not be closed.",
);
},
})
.setDialogData(dialogData)
.open("Dialog Example");
addon.data.dialog = dialogHelper;
await dialogData.unloadLock.promise;
addon.data.dialog = undefined;
addon.data.alive &&
ztoolkit.getGlobal("alert")(
`Close dialog with ${dialogData._lastButtonId}.\nCheckbox: ${dialogData.checkboxValue}\nInput: ${dialogData.inputValue}.`,
);
ztoolkit.log(dialogData);
}
@example
static clipboardExample() {
new ztoolkit.Clipboard()
.addText(
"![Plugin Template](https://github.com/windingwind/zotero-plugin-template)",
"text/unicode",
)
.addText(
'<a href="https://github.com/windingwind/zotero-plugin-template">Plugin Template</a>',
"text/html",
)
.copy();
ztoolkit.getGlobal("alert")("Copied!");
}
@example
static async filePickerExample() {
const path = await new ztoolkit.FilePicker(
"Import File",
"open",
[
["PNG File(*.png)", "*.png"],
["Any", "*.*"],
],
"image.png",
).open();
ztoolkit.getGlobal("alert")(`Selected ${path}`);
}
@example
static progressWindowExample() {
new ztoolkit.ProgressWindow(config.addonName)
.createLine({
text: "ProgressWindow Example!",
type: "success",
progress: 100,
})
.show();
}
@example
static vtableExample() {
ztoolkit.getGlobal("alert")("See src/modules/preferenceScript.ts");
}
}

View File

@ -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}!`,
);
});
}

157
src/modules/tldrFetcher.ts Normal file
View File

@ -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<SemanticScholarItemInfo[]> {
const semanticScholarURL = 'https://www.semanticscholar.org/api/1/search';
const params = {
queryString: title,
page: 1,
pageSize: 10,
sort: 'relevance',
authors: [],
coAuthors: [],
venues: [],
performTitleMatch: true,
requireViewablePdf: false,
includeTldrs: true,
};
const resp = await Zotero.HTTP.request("POST", semanticScholarURL, {
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(params),
});
if (resp.status === 200) {
const results = JSON.parse(resp.response).results;
return results.map((item: any) => {
const result = {
title: item.title.text,
abstract: item.paperAbstract.text,
tldr: undefined,
};
if (item.tldr) {
result.tldr = item.tldr.text;
}
return result;
});
}
return [];
}
private checkLCS(pattern: string, content: string): boolean {
const LCS = StringMatchUtils.longestCommonSubsequence(pattern, content);
return LCS.length >= Math.max(pattern.length, content.length) * 0.9
}
}
class StringMatchUtils {
static longestCommonSubsequence(text1: string, text2: string): string {
const m = text1.length;
const n = text2.length;
const dp: number[][] = new Array(m + 1);
for (let i = 0; i <= m; i++) {
dp[i] = new Array(n + 1).fill(0);
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (text1[i - 1] === text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
let i = m, j = n;
const lcs: string[] = [];
while (i > 0 && j > 0) {
if (text1[i - 1] === text2[j - 1]) {
lcs.unshift(text1[i - 1]);
i--;
j--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
i--;
} else {
j--;
}
}
return lcs.join('');
}
// static minWindow(s: string, t: string): [number, number] | null {
// const m = s.length, n = t.length
// let start = -1, minLen = Number.MAX_SAFE_INTEGER, i = 0, j = 0, end;
// while (i < m) {
// if (s[i] == t[j]) {
// if (++j == n) {
// end = i + 1;
// while (--j >= 0) {
// while (s[i--] != t[j]);
// }
// ++i; ++j;
// if (end - i < minLen) {
// minLen = end - i;
// start = i;
// }
// }
// }
// ++i;
// }
// return start == -1 ? null : [start, minLen];
// }
}