diff --git a/.idea/icon.svg b/.idea/icon.svg index 1ed1d8d..9e6c109 100644 --- a/.idea/icon.svg +++ b/.idea/icon.svg @@ -1,4 +1,7 @@ - - - + + + + diff --git a/README.md b/README.md index 067d5bf..2a23664 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,18 @@ A plugin that does one thing only: **Detect** and **Manage** duplicate items in # Changelog +## v3.0.1 + +
+
Click here to show more. + +In this version, we have made the following changes: + +1. 🧬 **CHANGE!**: We have updated the UI of buttons in the duplicate pane. +2. 🐛 **FIX!**: We have optimized the performance of duplicate search and detection. + + + ## v3.0.0
diff --git a/addon/chrome/content/zoplicate.css b/addon/chrome/content/zoplicate.css index bb9ec84..3b4bdc8 100644 --- a/addon/chrome/content/zoplicate.css +++ b/addon/chrome/content/zoplicate.css @@ -1,30 +1,21 @@ -.duplicate-box-button { - -moz-appearance: none; - border-radius: 12px; - border: 1px solid rgba(0, 0, 0, .65); - background: -moz-linear-gradient(rgba(110, 110, 110, .9), rgba(70, 70, 70, .9) 49%, rgba(50, 50, 50, .9) 51%, rgba(40, 40, 40, .9)); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .2), inset 0 0 1px rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); - background-clip: padding-box; - background-origin: padding-box; - padding: 2px 9px; - margin: 6px; - min-height: 22px; +.duplicate-custom-head { + display: flex; + flex-direction: row; + align-self: stretch; + gap: 6px; + padding: 6px 8px; + background: var(--material-toolbar); + border-bottom: var(--material-panedivider); + height: 28px; } -.duplicate-box-button[disabled="false"]:-moz-focusring { - box-shadow: 0 0 1px -moz-mac-focusring inset, 0 0 4px 1px -moz-mac-focusring, 0 0 2px 1px -moz-mac-focusring; -} - -.duplicate-box-button[disabled="false"]:hover{ - background: white; +.duplicate-custom-head button { + height: 26px; + margin: 0; + flex-grow: 1; cursor: pointer; } -.duplicate-box-button[disabled="false"]:hover:active { - background: -moz-linear-gradient(rgba(83, 128, 232, .9), rgba(91, 118, 255, .9)); - box-shadow: inset 0 0 3px rgba(0, 0, 0, .2), inset 0 1px 7px rgba(0, 0, 0, .4), 0 1px 0 rgba(255, 255, 255, .1); -} - -.duplicate-box-button[disabled="true"] { - filter: grayscale(100%); +.duplicate-custom-head:empty { + display: none; } diff --git a/package.json b/package.json index ea53264..a86a37e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zoplicate", - "version": "3.0.0", + "version": "3.0.1", "description": "Detect and manage duplicate items in Zotero.", "config": { "addonName": "Zoplicate", diff --git a/src/addon.ts b/src/addon.ts index f07bba3..e092284 100644 --- a/src/addon.ts +++ b/src/addon.ts @@ -27,6 +27,7 @@ class Addon { duplicateSearchObj: { [libraryID: number]: Zotero.Search }; duplicateCounts: { [libraryID: number]: { total: number; unique: number } }; duplicateSets: { [libraryID: number]: typeof Zotero.DisjointSetForest }; + nonDuplicateSectionID: string | false; }; // Lifecycle hooks public hooks: typeof hooks; @@ -44,6 +45,7 @@ class Addon { duplicateSearchObj: {}, duplicateCounts: {}, duplicateSets: {}, + nonDuplicateSectionID: false, }; this.hooks = hooks; this.api = {}; @@ -61,6 +63,7 @@ class Addon { duplicateSearchObj: {}, duplicateCounts: {}, duplicateSets: {}, + nonDuplicateSectionID: false, }; this.hooks = hooks; this.api = {}; diff --git a/src/hooks.ts b/src/hooks.ts index 475fcf3..bb30fd3 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -9,12 +9,15 @@ import { fetchDuplicates, registerButtonsInDuplicatePane } from "./modules/dupli import menus from "./modules/menus"; // import "./modules/zduplicates.js"; import database from "./modules/db"; -import { NonDuplicates, registerNonDuplicatesSection } from "./modules/nonDuplicates"; -import { patchGetSearchObject, patchItemSaveData } from "./modules/patcher"; +import { registerNonDuplicatesSection } from "./modules/nonDuplicates"; +import { + patchFindDuplicates, + patchGetSearchObject, + patchItemSaveData +} from "./modules/patcher"; import { containsRegularItem, isInDuplicatesPane, refreshItemTree } from "./utils/zotero"; import { registerDuplicateStats } from "./modules/duplicateStats"; import { waitUtilAsync } from "./utils/wait"; -import Dexie from "dexie"; async function onStartup() { await Promise.all([Zotero.initializationPromise, Zotero.unlockPromise, Zotero.uiReadyPromise]); @@ -29,46 +32,33 @@ async function onStartup() { await onMainWindowLoad(window); } -function handleError(e:any) { - switch (e.name) { - case "AbortError": - if (e.inner) { - return handleError(e.inner); - } - ztoolkit.log("Abort error " + e.message); - break; - case "QuotaExceededError": - ztoolkit.log("QuotaExceededError " + e.message); - break; - default: - ztoolkit.log(e); - break; - } -} - async function onMainWindowLoad(win: Window): Promise { - await waitUtilAsync(() => document.readyState === "complete"); - // Dexie.dependencies.indexedDB = window.indexedDB; - // Dexie.dependencies.IDBKeyRange = window.IDBKeyRange; + // await waitUtilAsync(() => document.readyState === "complete"); + + // create ztoolkit addon.data.ztoolkit = createZToolkit(); + + // init database const db = database.getDatabase(); - ztoolkit.log("onMainWindowLoad before db init", window.IDBKeyRange); await db.init(); - db.insertNonDuplicatePair(1, 2, 1);//.catch((e) => handleError(e)); - ztoolkit.log("insert done"); - NonDuplicates.getInstance().init(db); - registerNonDuplicatesSection(db); + // register stylesheets and preferences registerStyleSheets(); registerPrefs(); + + // patch Zotero duplicate search object and events Notifier.registerNotifier(); - BulkDuplicates.getInstance().registerUIElements(win); - menus.registerMenus(win); + patchFindDuplicates(db); patchGetSearchObject(); patchItemSaveData(); + + // register duplicate UI elements await registerDuplicateStats(); - registerButtonsInDuplicatePane(win); - ztoolkit.log("onMainWindowLoad done"); + await registerButtonsInDuplicatePane(win); + BulkDuplicates.getInstance().registerUIElements(win); + registerNonDuplicatesSection(db); + + menus.registerMenus(win); } async function onMainWindowUnload(win: Window): Promise { diff --git a/src/modules/bulkDuplicates.ts b/src/modules/bulkDuplicates.ts index 59e1f9c..fa23151 100644 --- a/src/modules/bulkDuplicates.ts +++ b/src/modules/bulkDuplicates.ts @@ -3,10 +3,9 @@ import { getString } from "../utils/locale"; import { config } from "../../package.json"; import { getPref, MasterItem } from "../utils/prefs"; import { truncateString } from "../utils/utils"; -import { fetchDuplicates } from "./duplicates"; +import { fetchDuplicates, updateDuplicateButtonsVisibilities } from "./duplicates"; import { merge } from "./merger"; import { isInDuplicatesPane, refreshItemTree } from "../utils/zotero"; -import { updateButtonDisabled } from "../utils/view"; import { DuplicateItems } from "./duplicateItems"; export class BulkDuplicates { @@ -45,18 +44,21 @@ export class BulkDuplicates { } private getBulkMergeButtons(win: Window) { - return [win.document.getElementById(BulkDuplicates.innerButtonID), win.document.getElementById(BulkDuplicates.externalButtonID)]; + return [ + win.document.getElementById(BulkDuplicates.innerButtonID), + win.document.getElementById(BulkDuplicates.externalButtonID), + ]; } - public createBulkMergeButton(win: Window, id: string): TagElementProps { + public createBulkMergeButton(win: Window, id: string, showing = true): TagElementProps { return { tag: "button", id: id, attributes: { label: getString("bulk-merge-title"), image: `chrome://${config.addonRef}/content/icons/merge.svg`, + hidden: !showing, }, - classList: ["duplicate-box-button"], namespace: "xul", listeners: [ { @@ -175,47 +177,30 @@ export class BulkDuplicates { registerUIElements(win: Window): void { this.win = win; - const msgID = "zoplicate-bulk-merge-message"; - const msgVBox: TagElementProps = { - tag: "vbox", - id: msgID, - properties: { - textContent: getString("duplicate-panel-message"), - }, - ignoreIfExists: true, - }; - ZoteroPane.collectionsView && ZoteroPane.collectionsView.onSelect.addListener(async () => { - ztoolkit.log(`Main window loaded`, win.indexedDB); - - const groupBox = win.document.getElementById("zotero-item-pane-groupbox") as Element; - if (isInDuplicatesPane()) { - ztoolkit.UI.appendElement(msgVBox, groupBox); - ztoolkit.UI.appendElement(this.createBulkMergeButton(win, BulkDuplicates.externalButtonID), groupBox); - if (this._isRunning && ZoteroPane.itemsView) { - await ZoteroPane.itemsView.waitForLoad(); - ZoteroPane.itemsView.selection.clearSelection(); - } - } else { - const externalButton = win.document.getElementById(BulkDuplicates.externalButtonID); - if (externalButton) { - groupBox.removeChild(win.document.getElementById(msgID)!); - groupBox.removeChild(externalButton); - } + ztoolkit.log("collectionsView onSelect"); + const inDuplicatePane = isInDuplicatesPane(); + if (ZoteroPane.itemsView && inDuplicatePane && this._isRunning) { + await ZoteroPane.itemsView.waitForLoad(); + ZoteroPane.itemsView.selection.clearSelection(); } }); ZoteroPane.itemsView && - ZoteroPane.itemsView.onRefresh.addListener(() => { - // ztoolkit.log("refresh"); - if (isInDuplicatesPane() && ZoteroPane.itemsView) { - const disabled = ZoteroPane.itemsView.rowCount <= 0; - updateButtonDisabled(win!, disabled, BulkDuplicates.innerButtonID, BulkDuplicates.externalButtonID); - if (this._isRunning) { - ZoteroPane.itemsView.selection.clearSelection(); - } + ZoteroPane.itemsView.onRefresh.addListener(async () => { + ztoolkit.log("refresh"); + const precondition = isInDuplicatesPane(); + if (precondition && ZoteroPane.itemsView && this._isRunning) { + ZoteroPane.itemsView.selection.clearSelection(); } + await updateDuplicateButtonsVisibilities(); }); + + ZoteroPane.itemsView && + ZoteroPane.itemsView.onSelect.addListener(async () => { + ztoolkit.log("itemsView.onSelect"); + await updateDuplicateButtonsVisibilities(); + }); } } diff --git a/src/modules/duplicates.ts b/src/modules/duplicates.ts index 2e01b99..e42ab67 100644 --- a/src/modules/duplicates.ts +++ b/src/modules/duplicates.ts @@ -3,23 +3,55 @@ import { DialogHelper } from "zotero-plugin-toolkit/dist/helpers/dialog"; import { TagElementProps } from "zotero-plugin-toolkit/dist/tools/ui"; import { getPref, setPref, Action, MasterItem } from "../utils/prefs"; import { merge } from "./merger"; -import { goToDuplicatesPane } from "../utils/zotero"; +import { goToDuplicatesPane, isInDuplicatesPane } from "../utils/zotero"; import { DuplicateItems } from "./duplicateItems"; -import { createNonDuplicateButton } from "./nonDuplicates"; +import { createNonDuplicateButton, NonDuplicates } from "./nonDuplicates"; import { BulkDuplicates } from "./bulkDuplicates"; +import { toggleButtonHidden } from "../utils/view"; + +function addButtonsInDuplicatePanes(innerButton: boolean, siblingElement: Element) { + const mergeButtonID = innerButton ? BulkDuplicates.innerButtonID : BulkDuplicates.externalButtonID; + const nonDuplicateButtonID = innerButton ? NonDuplicates.innerButtonID : NonDuplicates.externalButtonID; + ztoolkit.UI.insertElementBefore( + { + tag: "div", + namespace: "html", + classList: ["duplicate-custom-head", "empty"], + children: [ + BulkDuplicates.getInstance().createBulkMergeButton(siblingElement.ownerDocument.defaultView!, mergeButtonID), + createNonDuplicateButton(nonDuplicateButtonID), + ], + }, + siblingElement, + ); +} -export function registerButtonsInDuplicatePane(win: Window) { - const mergeButton = win.document.getElementById("zotero-duplicates-merge-button") as Element; +export async function registerButtonsInDuplicatePane(win: Window) { + // const duplicatePane = win.document.getElementById("zotero-duplicates-merge-pane"); + // 1. when selecting items in duplicatePane + const mergeButton = win.document.getElementById("zotero-duplicates-merge-button"); if (mergeButton) { - ztoolkit.UI.insertElementBefore(createNonDuplicateButton(), mergeButton); - ztoolkit.UI.insertElementBefore( - BulkDuplicates.getInstance().createBulkMergeButton(win, BulkDuplicates.innerButtonID), - mergeButton, - ); + const groupBox = mergeButton.parentElement as Element; + addButtonsInDuplicatePanes(true, groupBox); } + // 2. when not selecting items, i.e., in itemMessagePane + const customHead = win.document.querySelector("item-message-pane .custom-head"); + if (customHead) { + addButtonsInDuplicatePanes(false, customHead); + } + + await updateDuplicateButtonsVisibilities(); +} + +export async function updateDuplicateButtonsVisibilities() { + const inDuplicatePane = isInDuplicatesPane(); + const showBulkMergeButton = inDuplicatePane && ZoteroPane.itemsView && ZoteroPane.itemsView.rowCount > 0; + const showNonDuplicateButton = inDuplicatePane && (await areDuplicates()); + toggleButtonHidden(window, !showBulkMergeButton, BulkDuplicates.innerButtonID, BulkDuplicates.externalButtonID); + toggleButtonHidden(window, !showNonDuplicateButton, NonDuplicates.innerButtonID, NonDuplicates.externalButtonID); } -export async function areDuplicates(items: number[] | Zotero.Item[]) { +export async function areDuplicates(items: number[] | Zotero.Item[] = ZoteroPane.getSelectedItems()) { if (items.length < 2) return false; const libraryIDs = new Set( items.map((item) => (typeof item === "number" ? Zotero.Items.get(item).libraryID : item.libraryID)), diff --git a/src/modules/nonDuplicates.ts b/src/modules/nonDuplicates.ts index 54cacd1..d095421 100644 --- a/src/modules/nonDuplicates.ts +++ b/src/modules/nonDuplicates.ts @@ -1,5 +1,4 @@ import database, { IDatabase } from "./db"; -import { patchFindDuplicates } from "./patcher"; import { config } from "../../package.json"; import { isInDuplicatesPane, refreshItemTree } from "../utils/zotero"; import { TagElementProps } from "zotero-plugin-toolkit/dist/tools/ui"; @@ -7,7 +6,7 @@ import { getString } from "../utils/locale"; import { areDuplicates, fetchDuplicates } from "./duplicates"; export function registerNonDuplicatesSection(db: IDatabase) { - const key = Zotero.ItemPaneManager.registerSection({ + addon.data.nonDuplicateSectionID = Zotero.ItemPaneManager.registerSection({ paneID: `sec-non-duplicates`, pluginID: config.addonID, header: { @@ -36,11 +35,13 @@ export function registerNonDuplicatesSection(db: IDatabase) { dataOut: null | number[]; deferred: any; itemTreeID: string; + filterLibraryIDs: number[]; } = { dataIn: null, dataOut: null, deferred: Zotero.Promise.defer(), itemTreeID: "non-duplicate-box-select-item-dialog", + filterLibraryIDs: [item.libraryID], }; window.openDialog( "chrome://zotero/content/selectItemsDialog.xhtml", @@ -56,7 +57,7 @@ export function registerNonDuplicatesSection(db: IDatabase) { } const itemIDs = [...io.dataOut, item.id]; - if(new Set(itemIDs).size < 2) { + if (new Set(itemIDs).size < 2) { return; } @@ -67,8 +68,7 @@ export function registerNonDuplicatesSection(db: IDatabase) { message = "add-not-duplicates-alert-error-diff-library"; } else if (await db.existsNonDuplicates(itemIDs)) { message = "add-not-duplicates-alert-error-exist"; - } - else if (!(await areDuplicates(itemIDs))) { + } else if (!(await areDuplicates(itemIDs))) { message = "add-not-duplicates-alert-error-duplicates"; } @@ -129,14 +129,7 @@ export function registerNonDuplicatesSection(db: IDatabase) { } }, onItemChange: ({ body, item, setEnabled }) => { - // ztoolkit.log("debug flag onItemChange non duplicates", item.getDisplayTitle(), body); body.dataset.itemID = String(item.id); - // if (body.closest("bn-workspace") as HTMLElement | undefined) { - // setEnabled(true); - // body.dataset.itemID = String(item.id); - // return; - // } - // setEnabled(false); }, onRender: () => {}, onAsyncRender: async ({ body, item, editable }) => { @@ -210,16 +203,15 @@ export async function toggleNonDuplicates(action: "mark" | "unmark", items?: num ); } -export function createNonDuplicateButton(): TagElementProps { +export function createNonDuplicateButton(id: string, showing = true): TagElementProps { return { tag: "button", - id: "non-duplicates-button", + id: id, attributes: { label: getString("menuitem-not-duplicate"), image: `chrome://${config.addonRef}/content/icons/non-duplicate.svg`, - disabled: false, + hidden: !showing, }, - classList: ["duplicate-box-button"], namespace: "xul", listeners: [ { @@ -237,6 +229,9 @@ export class NonDuplicates { private static _instance: NonDuplicates; public allNonDuplicates: Set = new Set(); + public static readonly nonDuplicateButtonID = "non-duplicates-button"; + public static readonly innerButtonID = this.nonDuplicateButtonID + "-inner"; + public static readonly externalButtonID = this.nonDuplicateButtonID + "-external"; private constructor() {} @@ -246,8 +241,4 @@ export class NonDuplicates { } return NonDuplicates._instance; } - - init(db: IDatabase) { - patchFindDuplicates(db); - } } diff --git a/src/modules/patcher.ts b/src/modules/patcher.ts index 2a0bfe8..38d26e3 100644 --- a/src/modules/patcher.ts +++ b/src/modules/patcher.ts @@ -15,7 +15,7 @@ export function patchFindDuplicates(db: IDatabase) { target: Zotero.Duplicates.prototype, funcSign: "_findDuplicates", enabled: true, - patcher: (original) => + patcher: (original: any) => async function (this: any) { const duplicateSets = await db.getNonDuplicates({ libraryID: this.libraryID }); NonDuplicates.getInstance().allNonDuplicates = new Set( diff --git a/src/utils/view.ts b/src/utils/view.ts index f11a5cb..aea5e51 100644 --- a/src/utils/view.ts +++ b/src/utils/view.ts @@ -14,12 +14,18 @@ function updateButtonAttribute(win: Window, attribute: string, value: { toString const button = win.document.getElementById(id); if (button) { button.setAttribute(attribute, value.toString()); + } else { + ztoolkit.log(`Element with id ${id} not found`); } }); } -function updateButtonDisabled(win: Window, disabled: boolean, ...ids: string[]) { +function toggleButtonDisabled(win: Window, disabled: boolean, ...ids: string[]) { updateButtonAttribute(win, "disabled", disabled, ...ids); } -export { removeSiblings, updateButtonAttribute, updateButtonDisabled }; +function toggleButtonHidden(win: Window, hidden: boolean, ...ids: string[]) { + updateButtonAttribute(win, "hidden", hidden, ...ids); +} + +export { removeSiblings, updateButtonAttribute, toggleButtonDisabled, toggleButtonHidden }; diff --git a/src/utils/ztoolkit.ts b/src/utils/ztoolkit.ts index 3d73fed..736b058 100644 --- a/src/utils/ztoolkit.ts +++ b/src/utils/ztoolkit.ts @@ -50,5 +50,8 @@ class MyToolkit extends BasicTool { unregisterAll() { unregister(this); + if (addon.data.nonDuplicateSectionID) { + Zotero.ItemPaneManager.unregisterSection(addon.data.nonDuplicateSectionID); + } } } diff --git a/update-beta.json b/update-beta.json index 88732a3..7d89d97 100644 --- a/update-beta.json +++ b/update-beta.json @@ -3,7 +3,7 @@ "zoplicate@chenglongma.com": { "updates": [ { - "version": "3.0.0", + "version": "3.0.1", "update_link": "https://github.com/ChenglongMa/zoplicate/releases/latest/download/zoplicate.xpi", "applications": { "zotero": { diff --git a/update.json b/update.json index 88732a3..7d89d97 100644 --- a/update.json +++ b/update.json @@ -3,7 +3,7 @@ "zoplicate@chenglongma.com": { "updates": [ { - "version": "3.0.0", + "version": "3.0.1", "update_link": "https://github.com/ChenglongMa/zoplicate/releases/latest/download/zoplicate.xpi", "applications": { "zotero": {