diff options
Diffstat (limited to 'ext/js')
| -rw-r--r-- | ext/js/background/backend.js | 10 | ||||
| -rw-r--r-- | ext/js/comm/anki-connect.js | 15 | ||||
| -rw-r--r-- | ext/js/comm/api.js | 8 | ||||
| -rw-r--r-- | ext/js/data/anki-util.js | 2 | ||||
| -rw-r--r-- | ext/js/display/display-anki.js | 195 | ||||
| -rw-r--r-- | ext/js/pages/settings/anki-controller.js | 24 | 
6 files changed, 172 insertions, 82 deletions
| diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index dfb85e05..b9e23cbb 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -28,7 +28,7 @@ import {logErrorLevelToNumber} from '../core/log-utilities.js';  import {log} from '../core/log.js';  import {isObjectNotArray} from '../core/object-utilities.js';  import {clone, deferPromise, promiseTimeout} from '../core/utilities.js'; -import {invalidNoteId, isNoteDataValid} from '../data/anki-util.js'; +import {INVALID_NOTE_ID, isNoteDataValid} from '../data/anki-util.js';  import {arrayBufferToBase64} from '../data/array-buffer-util.js';  import {OptionsUtil} from '../data/options-util.js';  import {getAllPermissions, hasPermissions, hasRequiredPermissionsForOptions} from '../data/permissions-util.js'; @@ -153,6 +153,7 @@ export class Backend {              ['getAnkiConnectVersion',        this._onApiGetAnkiConnectVersion.bind(this)],              ['isAnkiConnected',              this._onApiIsAnkiConnected.bind(this)],              ['addAnkiNote',                  this._onApiAddAnkiNote.bind(this)], +            ['updateAnkiNote',               this._onApiUpdateAnkiNote.bind(this)],              ['getAnkiNoteInfo',              this._onApiGetAnkiNoteInfo.bind(this)],              ['injectAnkiNoteMedia',          this._onApiInjectAnkiNoteMedia.bind(this)],              ['viewNotes',                    this._onApiViewNotes.bind(this)], @@ -539,6 +540,11 @@ export class Backend {          return await this._anki.addNote(note);      } +    /** @type {import('api').ApiHandler<'updateAnkiNote'>} */ +    async _onApiUpdateAnkiNote({noteWithId}) { +        return await this._anki.updateNoteFields(noteWithId); +    } +      /**       * @param {import('anki').Note[]} notes       * @returns {Promise<import('backend').CanAddResults>} @@ -600,7 +606,7 @@ export class Backend {              const valid = isNoteDataValid(note);              if (isDuplicate && duplicateNoteIds[originalIndices.indexOf(i)].length === 0) { -                duplicateNoteIds[originalIndices.indexOf(i)] = [invalidNoteId]; +                duplicateNoteIds[originalIndices.indexOf(i)] = [INVALID_NOTE_ID];              }              const noteIds = isDuplicate ? duplicateNoteIds[originalIndices.indexOf(i)] : null; diff --git a/ext/js/comm/anki-connect.js b/ext/js/comm/anki-connect.js index 446b2139..8c5e2c85 100644 --- a/ext/js/comm/anki-connect.js +++ b/ext/js/comm/anki-connect.js @@ -144,6 +144,21 @@ export class AnkiConnect {      }      /** +     * @param {import('anki').Note} noteWithId +     * @returns {Promise<null>} +     */ +    async updateNoteFields(noteWithId) { +        if (!this._enabled) { return null; } +        await this._checkVersion(); +        const result = await this._invoke('updateNoteFields', {note: noteWithId}); +        if (result !== null) { +            throw this._createUnexpectedResultError('null', result); +        } +        return result; +    } + + +    /**       * @param {import('anki').Note[]} notes       * @returns {Promise<boolean[]>}       */ diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index e8db7846..b044a783 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -96,6 +96,14 @@ export class API {      }      /** +     * @param {import('api').ApiParam<'updateAnkiNote', 'noteWithId'>} noteWithId +     * @returns {Promise<import('api').ApiReturn<'updateAnkiNote'>>} +     */ +    updateAnkiNote(noteWithId) { +        return this._invoke('updateAnkiNote', {noteWithId}); +    } + +    /**       * @param {import('api').ApiParam<'getAnkiNoteInfo', 'notes'>} notes       * @param {import('api').ApiParam<'getAnkiNoteInfo', 'fetchAdditionalInfo'>} fetchAdditionalInfo       * @returns {Promise<import('api').ApiReturn<'getAnkiNoteInfo'>>} diff --git a/ext/js/data/anki-util.js b/ext/js/data/anki-util.js index c076c482..88b3c251 100644 --- a/ext/js/data/anki-util.js +++ b/ext/js/data/anki-util.js @@ -83,4 +83,4 @@ export function isNoteDataValid(note) {      );  } -export const invalidNoteId = -1; +export const INVALID_NOTE_ID = -1; 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<void>} */ -        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<void>} */ (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<number>} */              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);                          }                      } @@ -448,17 +458,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 { @@ -586,6 +560,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       * @returns {?DisplayAnkiError} diff --git a/ext/js/pages/settings/anki-controller.js b/ext/js/pages/settings/anki-controller.js index 2d461336..de8b2c39 100644 --- a/ext/js/pages/settings/anki-controller.js +++ b/ext/js/pages/settings/anki-controller.js @@ -63,6 +63,10 @@ export class AnkiController {          /** @type {HTMLElement} */          this._ankiErrorInvalidResponseInfo = querySelectorNotNull(document, '#anki-error-invalid-response-info');          /** @type {HTMLElement} */ +        this._duplicateBehaviorSelect = querySelectorNotNull(document, '[data-setting="anki.duplicateBehavior"]'); +        /** @type {HTMLElement} */ +        this._duplicateOverwriteWarning = querySelectorNotNull(document, '#anki-overwrite-warning'); +        /** @type {HTMLElement} */          this._ankiCardPrimary = querySelectorNotNull(document, '#anki-card-primary');          /** @type {?Error} */          this._ankiError = null; @@ -109,6 +113,8 @@ export class AnkiController {          ankiApiKeyInput.addEventListener('focus', this._onApiKeyInputFocus.bind(this));          ankiApiKeyInput.addEventListener('blur', this._onApiKeyInputBlur.bind(this)); +        this._duplicateBehaviorSelect.addEventListener('change', this._onDuplicateBehaviorSelectChange.bind(this)); +          await this._updateOptions();          this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); @@ -171,6 +177,8 @@ export class AnkiController {          this._selectorObserver.disconnect();          this._selectorObserver.observe(document.documentElement, true); +        this._updateDuplicateOverwriteWarning(anki.duplicateBehavior); +          this._setupFieldMenus(dictionaries);      } @@ -240,6 +248,22 @@ export class AnkiController {      }      /** +     * @param {Event} e +     */ +    _onDuplicateBehaviorSelectChange(e) { +        const node = /** @type {HTMLSelectElement} */ (e.currentTarget); +        const behavior = node.value; +        this._updateDuplicateOverwriteWarning(behavior); +    } + +    /** +     * @param {string} behavior +     */ +    _updateDuplicateOverwriteWarning(behavior) { +        this._duplicateOverwriteWarning.hidden = behavior !== 'overwrite'; +    } + +    /**       * @param {string} ankiCardType       * @param {string} [ankiCardMenu]       */ |