From ba9fa339a43a5f494785877018b10f111ccafff5 Mon Sep 17 00:00:00 2001 From: StefanVukovic99 Date: Tue, 21 May 2024 18:55:02 +0200 Subject: add support for overwriting existing card (#859) * frontend * wip * minimum viable * minor simplification * simplify gradients in duplicate svg * simplify add-duplicate svg * colors good * arrows shape * disable overwrite if no valid duplicate IDs * add warning --- ext/js/display/display-anki.js | 195 ++++++++++++++++++++++++----------------- 1 file changed, 116 insertions(+), 79 deletions(-) (limited to 'ext/js/display') diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js index 442319f7..6ac84ede 100644 --- a/ext/js/display/display-anki.js +++ b/ext/js/display/display-anki.js @@ -22,7 +22,7 @@ import {toError} from '../core/to-error.js'; import {deferPromise} from '../core/utilities.js'; import {AnkiNoteBuilder} from '../data/anki-note-builder.js'; import {getDynamicTemplates} from '../data/anki-template-util.js'; -import {invalidNoteId, isNoteDataValid} from '../data/anki-util.js'; +import {INVALID_NOTE_ID, isNoteDataValid} from '../data/anki-util.js'; import {PopupMenu} from '../dom/popup-menu.js'; import {querySelectorNotNull} from '../dom/query-selector.js'; import {TemplateRendererProxy} from '../templates/template-renderer-proxy.js'; @@ -50,7 +50,7 @@ export class DisplayAnki { /** @type {?import('./display-notification.js').DisplayNotification} */ this._tagsNotification = null; /** @type {?Promise} */ - this._updateAdderButtonsPromise = null; + this._updateSaveButtonsPromise = null; /** @type {?import('core').TokenObject} */ this._updateDictionaryEntryDetailsToken = null; /** @type {EventListenerCollection} */ @@ -101,7 +101,7 @@ export class DisplayAnki { /** @type {(event: MouseEvent) => void} */ this._onShowTagsBind = this._onShowTags.bind(this); /** @type {(event: MouseEvent) => void} */ - this._onNoteAddBind = this._onNoteAdd.bind(this); + this._onNoteSaveBind = this._onNoteSave.bind(this); /** @type {(event: MouseEvent) => void} */ this._onViewNotesButtonClickBind = this._onViewNotesButtonClick.bind(this); /** @type {(event: MouseEvent) => void} */ @@ -115,9 +115,9 @@ export class DisplayAnki { this._noteContext = this._getNoteContext(); /* eslint-disable @stylistic/no-multi-spaces */ this._display.hotkeyHandler.registerActions([ - ['addNoteKanji', () => { this._tryAddAnkiNoteForSelectedEntry('kanji'); }], - ['addNoteTermKanji', () => { this._tryAddAnkiNoteForSelectedEntry('term-kanji'); }], - ['addNoteTermKana', () => { this._tryAddAnkiNoteForSelectedEntry('term-kana'); }], + ['addNoteKanji', () => { this._hotkeySaveAnkiNoteForSelectedEntry('kanji'); }], + ['addNoteTermKanji', () => { this._hotkeySaveAnkiNoteForSelectedEntry('term-kanji'); }], + ['addNoteTermKana', () => { this._hotkeySaveAnkiNoteForSelectedEntry('term-kana'); }], ['viewNotes', this._viewNotesForSelectedEntry.bind(this)] ]); /* eslint-enable @stylistic/no-multi-spaces */ @@ -251,8 +251,8 @@ export class DisplayAnki { for (const node of element.querySelectorAll('.action-button[data-action=view-tags]')) { eventListeners.addEventListener(node, 'click', this._onShowTagsBind); } - for (const node of element.querySelectorAll('.action-button[data-action=add-note]')) { - eventListeners.addEventListener(node, 'click', this._onNoteAddBind); + for (const node of element.querySelectorAll('.action-button[data-action=save-note]')) { + eventListeners.addEventListener(node, 'click', this._onNoteSaveBind); } for (const node of element.querySelectorAll('.action-button[data-action=view-note]')) { eventListeners.addEventListener(node, 'click', this._onViewNotesButtonClickBind); @@ -276,13 +276,13 @@ export class DisplayAnki { /** * @param {MouseEvent} e */ - _onNoteAdd(e) { + _onNoteSave(e) { e.preventDefault(); const element = /** @type {HTMLElement} */ (e.currentTarget); const mode = this._getValidCreateMode(element.dataset.mode); if (mode === null) { return; } const index = this._display.getElementDictionaryEntryIndex(element); - void this._addAnkiNote(index, mode); + void this._saveAnkiNote(index, mode); } /** @@ -300,9 +300,9 @@ export class DisplayAnki { * @param {import('display-anki').CreateMode} mode * @returns {?HTMLButtonElement} */ - _adderButtonFind(index, mode) { + _saveButtonFind(index, mode) { const entry = this._getEntry(index); - return entry !== null ? entry.querySelector(`.action-button[data-action=add-note][data-mode="${mode}"]`) : null; + return entry !== null ? entry.querySelector(`.action-button[data-action=save-note][data-mode="${mode}"]`) : null; } /** @@ -356,66 +356,76 @@ export class DisplayAnki { /** @type {?import('core').TokenObject} */ const token = {}; this._updateDictionaryEntryDetailsToken = token; - if (this._updateAdderButtonsPromise !== null) { - await this._updateAdderButtonsPromise; + if (this._updateSaveButtonsPromise !== null) { + await this._updateSaveButtonsPromise; } if (this._updateDictionaryEntryDetailsToken !== token) { return; } const {promise, resolve} = /** @type {import('core').DeferredPromiseDetails} */ (deferPromise()); try { - this._updateAdderButtonsPromise = promise; + this._updateSaveButtonsPromise = promise; const dictionaryEntryDetails = await this._getDictionaryEntryDetails(dictionaryEntries); if (this._updateDictionaryEntryDetailsToken !== token) { return; } this._dictionaryEntryDetails = dictionaryEntryDetails; - this._updateAdderButtons(dictionaryEntryDetails); + this._updateSaveButtons(dictionaryEntryDetails); } finally { resolve(); - if (this._updateAdderButtonsPromise === promise) { - this._updateAdderButtonsPromise = null; + if (this._updateSaveButtonsPromise === promise) { + this._updateSaveButtonsPromise = null; } } } /** * @param {HTMLButtonElement} button + * @param {number[]} noteIds */ - _showDuplicateAddButton(button) { - const isKanjiAdd = button.dataset.mode === 'term-kanji'; + _updateSaveButtonForDuplicateBehavior(button, noteIds) { + const behavior = this._duplicateBehavior; + if (behavior === 'prevent') { + button.disabled = true; + return; + } - const title = button.getAttribute('title'); - if (title) { - button.setAttribute('title', title.replace(/Add (?!duplicate)/, 'Add duplicate ')); + const mode = button.dataset.mode; + const verb = behavior === 'overwrite' ? 'Overwrite' : 'Add duplicate'; + const iconPrefix = behavior === 'overwrite' ? 'overwrite' : 'add-duplicate'; + const target = mode === 'term-kanji' ? 'expression' : 'reading'; + + if (behavior === 'overwrite') { + button.dataset.overwrite = 'true'; + if (!noteIds.some((id) => id !== INVALID_NOTE_ID)) { + button.disabled = true; + } + } else { + delete button.dataset.overwrite; } + button.setAttribute('title', `${verb} ${target}`); + // eslint-disable-next-line no-underscore-dangle const hotkeyLabel = this._display._hotkeyHelpController.getHotkeyLabel(button); - if (hotkeyLabel) { - if (hotkeyLabel === 'Add expression ({0})') { - // eslint-disable-next-line no-underscore-dangle - this._display._hotkeyHelpController.setHotkeyLabel(button, 'Add duplicate expression ({0})'); - } else if (hotkeyLabel === 'Add reading ({0})') { - // eslint-disable-next-line no-underscore-dangle - this._display._hotkeyHelpController.setHotkeyLabel(button, 'Add duplicate reading ({0})'); - } + // eslint-disable-next-line no-underscore-dangle + this._display._hotkeyHelpController.setHotkeyLabel(button, `${verb} ${target} ({0})`); } const actionIcon = button.querySelector('.action-icon'); if (actionIcon instanceof HTMLElement) { - actionIcon.dataset.icon = isKanjiAdd ? 'add-duplicate-term-kanji' : 'add-duplicate-term-kana'; + actionIcon.dataset.icon = `${iconPrefix}-${mode}`; } } /** * @param {import('display-anki').DictionaryEntryDetails[]} dictionaryEntryDetails */ - _updateAdderButtons(dictionaryEntryDetails) { + _updateSaveButtons(dictionaryEntryDetails) { const displayTags = this._displayTags; for (let i = 0, ii = dictionaryEntryDetails.length; i < ii; ++i) { /** @type {?Set} */ let allNoteIds = null; for (const {mode, canAdd, noteIds, noteInfos, ankiError} of dictionaryEntryDetails[i].modeMap.values()) { - const button = this._adderButtonFind(i, mode); + const button = this._saveButtonFind(i, mode); if (button !== null) { button.disabled = !canAdd; button.hidden = (ankiError !== null); @@ -425,14 +435,14 @@ export class DisplayAnki { // If entry has noteIds, show the "add duplicate" button. if (Array.isArray(noteIds) && noteIds.length > 0) { - this._updateButtonForDuplicate(button); + this._updateSaveButtonForDuplicateBehavior(button, noteIds); } } if (Array.isArray(noteIds) && noteIds.length > 0) { if (allNoteIds === null) { allNoteIds = new Set(); } for (const noteId of noteIds) { - if (noteId !== invalidNoteId) { + if (noteId !== INVALID_NOTE_ID) { allNoteIds.add(noteId); } } @@ -447,17 +457,6 @@ export class DisplayAnki { } } - /** - * @param {HTMLButtonElement} button - */ - _updateButtonForDuplicate(button) { - if (this._duplicateBehavior === 'prevent') { - button.disabled = true; - } else { - this._showDuplicateAddButton(button); - } - } - /** * @param {number} i * @param {(?import('anki').NoteInfo)[]} noteInfos @@ -503,16 +502,16 @@ export class DisplayAnki { /** * @param {import('display-anki').CreateMode} mode */ - _tryAddAnkiNoteForSelectedEntry(mode) { + _hotkeySaveAnkiNoteForSelectedEntry(mode) { const index = this._display.selectedIndex; - void this._addAnkiNote(index, mode); + void this._saveAnkiNote(index, mode); } /** * @param {number} dictionaryEntryIndex * @param {import('display-anki').CreateMode} mode */ - async _addAnkiNote(dictionaryEntryIndex, mode) { + async _saveAnkiNote(dictionaryEntryIndex, mode) { const dictionaryEntries = this._display.dictionaryEntries; const dictionaryEntryDetails = this._dictionaryEntryDetails; if (!( @@ -529,7 +528,7 @@ export class DisplayAnki { const {requirements} = details; - const button = this._adderButtonFind(dictionaryEntryIndex, mode); + const button = this._saveButtonFind(dictionaryEntryIndex, mode); if (button === null || button.disabled) { return; } this._hideErrorNotification(true); @@ -544,34 +543,9 @@ export class DisplayAnki { const error = this._getAddNoteRequirementsError(requirements, outputRequirements); if (error !== null) { allErrors.push(error); } - - let noteId = null; - let addNoteOkay = false; - try { - noteId = await this._display.application.api.addAnkiNote(note); - addNoteOkay = true; - } catch (e) { - allErrors.length = 0; - allErrors.push(toError(e)); - } - - if (addNoteOkay) { - if (noteId === null) { - allErrors.push(new Error('Note could not be added')); - } else { - if (this._suspendNewCards) { - try { - await this._display.application.api.suspendAnkiCardsForNote(noteId); - } catch (e) { - allErrors.push(toError(e)); - } - } - // Now that this dictionary entry has a duplicate in Anki, show the "add duplicate" buttons. - this._updateButtonForDuplicate(button); - - this._updateViewNoteButton(dictionaryEntryIndex, [noteId], true); - } - } + await (button.dataset.overwrite ? + this._updateAnkiNote(note, allErrors, button, dictionaryEntryIndex) : + this._addNewAnkiNote(note, allErrors, button, dictionaryEntryIndex)); } catch (e) { allErrors.push(toError(e)); } finally { @@ -585,6 +559,69 @@ export class DisplayAnki { } } + /** + * @param {import('anki').Note} note + * @param {Error[]} allErrors + * @param {HTMLButtonElement} button + * @param {number} dictionaryEntryIndex + */ + async _addNewAnkiNote(note, allErrors, button, dictionaryEntryIndex) { + let noteId = null; + let addNoteOkay = false; + try { + noteId = await this._display.application.api.addAnkiNote(note); + addNoteOkay = true; + } catch (e) { + allErrors.length = 0; + allErrors.push(toError(e)); + } + + if (addNoteOkay) { + if (noteId === null) { + allErrors.push(new Error('Note could not be added')); + } else { + if (this._suspendNewCards) { + try { + await this._display.application.api.suspendAnkiCardsForNote(noteId); + } catch (e) { + allErrors.push(toError(e)); + } + } + this._updateSaveButtonForDuplicateBehavior(button, [noteId]); + + this._updateViewNoteButton(dictionaryEntryIndex, [noteId], true); + } + } + } + + /** + * @param {import('anki').Note} note + * @param {Error[]} allErrors + * @param {HTMLButtonElement} button + * @param {number} dictionaryEntryIndex + */ + async _updateAnkiNote(note, allErrors, button, dictionaryEntryIndex) { + const dictionaryEntries = this._display.dictionaryEntries; + const allEntryDetails = await this._getDictionaryEntryDetails(dictionaryEntries); + const relevantEntryDetails = allEntryDetails[dictionaryEntryIndex]; + const mode = this._getValidCreateMode(button.dataset.mode); + if (mode === null) { return; } + const relevantModeDetails = relevantEntryDetails.modeMap.get(mode); + if (typeof relevantModeDetails === 'undefined') { return; } + const {noteIds} = relevantModeDetails; + if (noteIds === null) { return; } + const overwriteTarget = noteIds.find((id) => id !== INVALID_NOTE_ID); + if (typeof overwriteTarget === 'undefined') { return; } + + try { + const noteWithId = {...note, id: overwriteTarget}; + await this._display.application.api.updateAnkiNote(noteWithId); + } catch (e) { + allErrors.length = 0; + allErrors.push(toError(e)); + } + } + /** * @param {import('anki-note-builder').Requirement[]} requirements * @param {import('anki-note-builder').Requirement[]} outputRequirements -- cgit v1.2.3