init proj
This commit is contained in:
parent
03fadd7b25
commit
033bafa5bc
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
.makeItRed {
|
||||
background-color: tomato;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 = 插件模板
|
||||
|
|
@ -1,3 +1 @@
|
|||
/* eslint-disable no-undef */
|
||||
pref("__prefsPrefix__.enable", true);
|
||||
pref("__prefsPrefix__.input", "This is input");
|
||||
/* eslint-disable no-undef */
|
||||
|
|
@ -14,8 +14,6 @@ class Addon {
|
|||
};
|
||||
prefs?: {
|
||||
window: Window;
|
||||
columns: Array<ColumnOptions>;
|
||||
rows: Array<{ [dataKey: string]: string }>;
|
||||
};
|
||||
dialog?: DialogHelper;
|
||||
};
|
||||
|
|
|
|||
187
src/hooks.ts
187
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<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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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() { }
|
||||
}
|
||||
|
|
@ -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(
|
||||
"",
|
||||
"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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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}!`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
// }
|
||||
}
|
||||
Loading…
Reference in New Issue