diff options
Diffstat (limited to 'ext')
| -rw-r--r-- | ext/css/display.css | 2 | ||||
| -rw-r--r-- | ext/images/add-duplicate-term-kana.svg | 40 | ||||
| -rw-r--r-- | ext/images/add-duplicate-term-kanji.svg | 84 | ||||
| -rw-r--r-- | ext/js/background/backend.js | 87 | ||||
| -rw-r--r-- | ext/js/comm/anki-connect.js | 57 | ||||
| -rw-r--r-- | ext/js/data/anki-note-builder.js | 3 | ||||
| -rw-r--r-- | ext/js/display/display-anki.js | 55 | ||||
| -rw-r--r-- | ext/js/input/hotkey-help-controller.js | 32 | 
8 files changed, 338 insertions, 22 deletions
| diff --git a/ext/css/display.css b/ext/css/display.css index ca91a470..a148af13 100644 --- a/ext/css/display.css +++ b/ext/css/display.css @@ -669,6 +669,8 @@ button.action-button:active {  .icon[data-icon=view-note]      { background-image: url('/images/view-note.svg'); }  .icon[data-icon=add-term-kanji] { background-image: url('/images/add-term-kanji.svg'); }  .icon[data-icon=add-term-kana]  { background-image: url('/images/add-term-kana.svg'); } +.icon[data-icon=add-duplicate-term-kanji] { background-image: url('/images/add-duplicate-term-kanji.svg'); } +.icon[data-icon=add-duplicate-term-kana]  { background-image: url('/images/add-duplicate-term-kana.svg'); }  .icon[data-icon=play-audio]     { background-image: url('/images/play-audio.svg'); }  .icon[data-icon=source-term]    { background-image: url('/images/source-term.svg'); }  .icon[data-icon=entry-current]  { background-image: url('/images/entry-current.svg'); } diff --git a/ext/images/add-duplicate-term-kana.svg b/ext/images/add-duplicate-term-kana.svg new file mode 100644 index 00000000..9b574aee --- /dev/null +++ b/ext/images/add-duplicate-term-kana.svg @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> +  <linearGradient id="linearGradient4719" x1="-1.7198" x2="-1.7198" y1="2.7781" y2="1.4552" gradientTransform="matrix(3.7795 0 0 3.7795 14 .5)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#8dba64" offset="0"/> +   <stop stop-color="#b4d495" offset="1"/> +  </linearGradient> +  <linearGradient id="linearGradient4745-5" x1="2.1167" x2="1.4552" y1="2.3812" y2="1.7198" gradientTransform="scale(3.7795)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#376b19" offset="0"/> +   <stop stop-color="#81ab61" offset="1"/> +  </linearGradient> +  <radialGradient id="radialGradient4770-4" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(2.2677 -7.9311e-7 7.9312e-7 2.2677 2.7 3.7)" gradientUnits="userSpaceOnUse"> +   <stop stop-opacity=".28986" offset="0"/> +   <stop stop-opacity="0" offset="1"/> +  </radialGradient> +  <linearGradient id="linearGradient1389" x1="-1.7198" x2="-1.7198" y1="2.7781" y2="1.4552" gradientTransform="matrix(3.7795 0 0 3.7795 14 .5)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient4719"/> +  <linearGradient id="linearGradient1391" x1="2.1167" x2="1.4552" y1="2.3812" y2="1.7198" gradientTransform="scale(3.7795)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient4745-5"/> +  <radialGradient id="radialGradient1393" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(2.2677,-7.9311e-7,7.9312e-7,2.2677,2.7,3.7)" gradientUnits="userSpaceOnUse" xlink:href="#radialGradient4770-4"/> +  <filter id="filter1916" x="-.083333" y="-.083333" width="1.1667" height="1.1667" color-interpolation-filters="sRGB"> +   <feColorMatrix result="color1" type="hueRotate" values="260"/> +   <feColorMatrix result="color2" type="saturate" values="1"/> +  </filter> +  <filter id="filter1922" x="-.083333" y="-.083333" width="1.1667" height="1.1667" color-interpolation-filters="sRGB"> +   <feColorMatrix result="color1" type="hueRotate" values="260"/> +   <feColorMatrix result="color2" type="saturate" values="1"/> +  </filter> + </defs> + <g transform="translate(2,-2)" filter="url(#filter1916)"> +  <circle cx="7.5" cy="8.5" r="3" fill="url(#linearGradient1389)"/> +  <circle cx="7.5" cy="8.5" r="3" fill="none" stroke="url(#linearGradient1391)"/> +  <circle cx="7.5" cy="8.5" r="1.5" fill="url(#radialGradient1393)"/> +  <path d="m6 8h1v-1h1v1h1v1h-1v1h-1v-1h-1v-1" fill="#fff"/> + </g> + <g filter="url(#filter1922)"> +  <circle cx="7.5" cy="8.5" r="3" fill="url(#linearGradient4719)"/> +  <circle cx="7.5" cy="8.5" r="3" fill="none" stroke="url(#linearGradient4745-5)"/> +  <circle cx="7.5" cy="8.5" r="1.5" fill="url(#radialGradient4770-4)"/> +  <path d="m6 8h1v-1h1v1h1v1h-1v1h-1v-1h-1v-1" fill="#fff"/> + </g> +</svg> diff --git a/ext/images/add-duplicate-term-kanji.svg b/ext/images/add-duplicate-term-kanji.svg new file mode 100644 index 00000000..809d0159 --- /dev/null +++ b/ext/images/add-duplicate-term-kanji.svg @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> +  <linearGradient id="linearGradient4582" x1="-1.7198" x2="-1.7198" y1="3.5719" y2=".79375" gradientTransform="matrix(3.7795 0 0 3.7795 14.5 -6.308e-7)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#6fb558" offset="0"/> +   <stop stop-color="#a5db9b" offset="1"/> +  </linearGradient> +  <linearGradient id="linearGradient4758-7" x1="7.5406" x2="5.1594" y1="3.3073" y2=".92604" gradientTransform="matrix(3.7795 0 0 3.7795 -16 -6e-7)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#34812c" offset="0"/> +   <stop stop-color="#87b870" offset="1"/> +  </linearGradient> +  <radialGradient id="radialGradient4683-3" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(4.5354 8.0301e-7 -8.0301e-7 4.5354 -1.6 -1.6)" gradientUnits="userSpaceOnUse"> +   <stop stop-opacity=".28986" offset="0"/> +   <stop stop-opacity="0" offset="1"/> +  </radialGradient> +  <linearGradient id="linearGradient328" x1="-1.7198" x2="-1.7198" y1="3.5719" y2=".79375" gradientTransform="matrix(3.7795,0,0,3.7795,14.5,-6.308e-7)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient4582"/> +  <linearGradient id="linearGradient330" x1="7.5406" x2="5.1594" y1="3.3073" y2=".92604" gradientTransform="matrix(3.7795,0,0,3.7795,-16,-6e-7)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient4758-7"/> +  <radialGradient id="radialGradient332" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(4.5354,8.0301e-7,-8.0301e-7,4.5354,-1.6,-1.6)" gradientUnits="userSpaceOnUse" xlink:href="#radialGradient4683-3"/> +  <filter id="filter554" x="-.038462" y="-.038462" width="1.0769" height="1.0769" color-interpolation-filters="sRGB"> +   <feColorMatrix result="color1" type="hueRotate" values="250"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="color2" type="saturate" values="1"/> +  </filter> +  <filter id="filter795" x="-.038462" y="-.038462" width="1.0769" height="1.0769" color-interpolation-filters="sRGB"> +   <feColorMatrix result="color1" type="hueRotate" values="250"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="fbSourceGraphic" type="saturate" values="1"/> +   <feColorMatrix in="fbSourceGraphic" result="fbSourceGraphicAlpha" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/> +   <feColorMatrix in="fbSourceGraphic" result="color1" type="hueRotate" values="0"/> +   <feColorMatrix result="color2" type="saturate" values="1"/> +  </filter> + </defs> + <g transform="matrix(.91504 0 0 .91504 2.2745 -.91504)" filter="url(#filter795)"> +  <circle cx="8" cy="8" r="6.5" fill="url(#linearGradient328)"/> +  <circle cx="8" cy="8" r="5.75" fill="none" stroke="#fff" stroke-opacity=".50196" stroke-width="1.5"/> +  <circle cx="8" cy="8" r="6.5" fill="none" stroke="url(#linearGradient330)"/> +  <circle cx="8" cy="8" r="3" fill="url(#radialGradient332)"/> +  <path d="m5 7h2v-2h2v2h2v2h-2v2h-2v-2h-2v-2" fill="#fff"/> + </g> + <g transform="matrix(.91504 0 0 .91504 -.91504 2.2745)" filter="url(#filter554)"> +  <circle cx="8" cy="8" r="6.5" fill="url(#linearGradient328)"/> +  <circle cx="8" cy="8" r="5.75" fill="none" stroke="#fff" stroke-opacity=".50196" stroke-width="1.5"/> +  <circle cx="8" cy="8" r="6.5" fill="none" stroke="url(#linearGradient330)"/> +  <circle cx="8" cy="8" r="3" fill="url(#radialGradient332)"/> +  <path d="m5 7h2v-2h2v2h2v2h-2v2h-2v-2h-2v-2" fill="#fff"/> + </g> +</svg> diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 182f11aa..cd44a07f 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -544,22 +544,89 @@ export class Backend {          return await this._anki.addNote(note);      } +    /** +     * @param {import('anki').Note[]} notes +     * @returns {Promise<({ canAdd: true; } | { canAdd: false; error: string; })[]>} +     */ +    async detectDuplicateNotes(notes) { +        // `allowDuplicate` is on for all notes by default, so we temporarily set it to false +        // to check which notes are duplicates. +        const notesNoDuplicatesAllowed = notes.map((note) => ({...note, options: {...note.options, allowDuplicate: false}})); + +        return await this._anki.canAddNotesWithErrorDetail(notesNoDuplicatesAllowed); +    } + +    /** +     * Partitions notes between those that can / cannot be added. +     * It further sets the `isDuplicate` strings for notes that have a duplicate. +     * @param {import('anki').Note[]} notes +     * @returns {Promise<import('backend').CanAddResults>} +     */ +    async partitionAddibleNotes(notes) { +        const canAddResults = await this.detectDuplicateNotes(notes); + +        /** @type {{ note: import('anki').Note, isDuplicate: boolean }[]} */ +        const canAddArray = []; + +        /** @type {import('anki').Note[]} */ +        const cannotAddArray = []; + +        for (let i = 0; i < canAddResults.length; i++) { +            const result = canAddResults[i]; + +            // If the note is a duplicate, the error is "cannot create note because it is a duplicate". +            if (result.canAdd) { +                canAddArray.push({note: notes[i], isDuplicate: false}); +            } else if (result.error.endsWith('duplicate')) { +                canAddArray.push({note: notes[i], isDuplicate: true}); +            } else { +                cannotAddArray.push(notes[i]); +            } +        } + +        return {canAddArray, cannotAddArray}; +    } +      /** @type {import('api').ApiHandler<'getAnkiNoteInfo'>} */      async _onApiGetAnkiNoteInfo({notes, fetchAdditionalInfo}) { -        /** @type {import('anki').NoteInfoWrapper[]} */ -        const results = []; +        const {canAddArray, cannotAddArray} = await this.partitionAddibleNotes(notes); +          /** @type {{note: import('anki').Note, info: import('anki').NoteInfoWrapper}[]} */ -        const cannotAdd = []; -        const canAddArray = await this._anki.canAddNotes(notes); +        const cannotAdd = cannotAddArray.filter((note) => isNoteDataValid(note)).map((note) => ({note, info: {canAdd: false, valid: false, noteIds: null}})); + +        /** @type {import('anki').NoteInfoWrapper[]} */ +        const results = cannotAdd.map(({info}) => info); + +        /** @type {import('anki').Note[]} */ +        const duplicateNotes = []; + +        /** @type {number[]} */ +        const originalIndices = []; + +        for (let i = 0; i < canAddArray.length; i++) { +            if (canAddArray[i].isDuplicate) { +                duplicateNotes.push(canAddArray[i].note); +                // Keep original indices to locate duplicate inside `duplicateNoteIds` +                originalIndices.push(i); +            } +        } + +        const duplicateNoteIds = await this._anki.findNoteIds(duplicateNotes); + +        for (let i = 0; i < canAddArray.length; ++i) { +            const {note, isDuplicate} = canAddArray[i]; -        for (let i = 0; i < notes.length; ++i) { -            const note = notes[i]; -            let canAdd = canAddArray[i];              const valid = isNoteDataValid(note); -            if (!valid) { canAdd = false; } -            const info = {canAdd, valid, noteIds: null}; + +            const info = { +                canAdd: valid, +                valid, +                noteIds: isDuplicate ? duplicateNoteIds[originalIndices.indexOf(i)] : null +            }; +              results.push(info); -            if (!canAdd && valid) { + +            if (!valid) {                  cannotAdd.push({note, info});              }          } diff --git a/ext/js/comm/anki-connect.js b/ext/js/comm/anki-connect.js index 0bf38bda..a763f394 100644 --- a/ext/js/comm/anki-connect.js +++ b/ext/js/comm/anki-connect.js @@ -140,6 +140,17 @@ export class AnkiConnect {      }      /** +     * @param {import('anki').Note[]} notes +     * @returns {Promise<({ canAdd: true } | { canAdd: false, error: string })[]>} +     */ +    async canAddNotesWithErrorDetail(notes) { +        if (!this._enabled) { return []; } +        await this._checkVersion(); +        const result = await this._invoke('canAddNotesWithErrorDetail', {notes}); +        return this._normalizeCanAddNotesWithErrorDetailArray(result, notes.length); +    } + +    /**       * @param {import('anki').NoteId[]} noteIds       * @returns {Promise<(?import('anki').NoteInfo)[]>}       */ @@ -581,6 +592,52 @@ export class AnkiConnect {      /**       * @param {unknown} result +     * @param {number} expectedCount +     * @returns {import('anki-connect.js').CanAddResult[]} +     * @throws {Error} +     */ +    _normalizeCanAddNotesWithErrorDetailArray(result, expectedCount) { +        if (!Array.isArray(result)) { +            throw this._createUnexpectedResultError('array', result, ''); +        } +        if (expectedCount !== result.length) { +            throw this._createError(`Unexpected result array size: expected ${expectedCount}, received ${result.length}`, result); +        } +        /** @type {import('anki-connect.js').CanAddResult[]} */ +        const result2 = []; +        for (let i = 0; i < expectedCount; ++i) { +            const item = /** @type {unknown} */ (result[i]); +            if (item === null || typeof item !== 'object') { +                throw this._createError(`Unexpected result type at index ${i}: expected object, received ${this._getTypeName(item)}`, result); +            } + +            const {canAdd, error} = /** @type {{[key: string]: unknown}} */ (item); +            if (typeof canAdd !== 'boolean') { +                throw this._createError(`Unexpected result type at index ${i}, field canAdd: expected boolean, received ${this._getTypeName(canAdd)}`, result); +            } + +            if (canAdd && typeof error !== 'undefined') { +                throw this._createError(`Unexpected result type at index ${i}, field error: expected undefined, received ${this._getTypeName(error)}`, result); +            } +            if (!canAdd && typeof error !== 'string') { +                throw this._createError(`Unexpected result type at index ${i}, field error: expected string, received ${this._getTypeName(error)}`, result); +            } + +            if (canAdd) { +                result2.push({canAdd}); +            } else if (typeof error === 'string') { +                const item2 = { +                    canAdd, +                    error +                }; +                result2.push(item2); +            } +        } +        return result2; +    } + +    /** +     * @param {unknown} result       * @returns {(?import('anki').NoteInfo)[]}       * @throws {Error}       */ diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js index e156103a..6a6a6177 100644 --- a/ext/js/data/anki-note-builder.js +++ b/ext/js/data/anki-note-builder.js @@ -54,7 +54,6 @@ export class AnkiNoteBuilder {          fields,          tags = [],          requirements = [], -        checkForDuplicates = true,          duplicateScope = 'collection',          duplicateScopeCheckAllModels = false,          resultOutputMode = 'split', @@ -111,7 +110,7 @@ export class AnkiNoteBuilder {              deckName,              modelName,              options: { -                allowDuplicate: !checkForDuplicates, +                allowDuplicate: true,                  duplicateScope,                  duplicateScopeOptions: {                      deckName: duplicateScopeDeckName, diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js index 9a6b96c7..b7d118da 100644 --- a/ext/js/display/display-anki.js +++ b/ext/js/display/display-anki.js @@ -371,6 +371,36 @@ export class DisplayAnki {      }      /** +     * @param {HTMLButtonElement} button +     */ +    _showDuplicateAddButton(button) { +        const isKanjiAdd = button.dataset.mode === 'term-kanji'; + +        const title = button.getAttribute('title'); +        if (title) { +            button.setAttribute('title', title.replace(/Add (?!duplicate)/, 'Add duplicate ')); +        } + +        // 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})'); +            } +        } + +        const actionIcon = button.querySelector('.action-icon'); +        if (actionIcon instanceof HTMLElement) { +            actionIcon.dataset.icon = isKanjiAdd ? 'add-duplicate-term-kanji' : 'add-duplicate-term-kana'; +        } +    } + +    /**       * @param {import('display-anki').DictionaryEntryDetails[]} dictionaryEntryDetails       */      _updateAdderButtons(dictionaryEntryDetails) { @@ -383,6 +413,11 @@ export class DisplayAnki {                  if (button !== null) {                      button.disabled = !canAdd;                      button.hidden = (ankiError !== null); + +                    // If entry has noteIds, show the "add duplicate" button. +                    if (Array.isArray(noteIds) && noteIds.length > 0) { +                        this._showDuplicateAddButton(button); +                    }                  }                  if (Array.isArray(noteIds) && noteIds.length > 0) { @@ -394,6 +429,7 @@ export class DisplayAnki {                      this._setupTagsIndicator(i, noteInfos);                  }              } +              this._updateViewNoteButton(i, allNoteIds !== null ? [...allNoteIds] : [], false);          }      } @@ -506,7 +542,9 @@ export class DisplayAnki {                              allErrors.push(toError(e));                          }                      } -                    button.disabled = true; +                    // Now that this dictionary entry has a duplicate in Anki, show the "add duplicate" buttons. +                    this._showDuplicateAddButton(button); +                      this._updateViewNoteButton(dictionaryEntryIndex, [noteId], true);                  }              } @@ -615,7 +653,6 @@ export class DisplayAnki {       * @returns {Promise<import('display-anki').DictionaryEntryDetails[]>}       */      async _getDictionaryEntryDetails(dictionaryEntries) { -        const forceCanAddValue = (this._checkForDuplicates ? null : true);          const fetchAdditionalInfo = (this._displayTags !== 'never');          const notePromises = []; @@ -638,14 +675,13 @@ export class DisplayAnki {          let infos;          let ankiError = null;          try { -            if (forceCanAddValue !== null) { -                if (!await this._display.application.api.isAnkiConnected()) { -                    throw new Error('Anki not connected'); -                } -                infos = this._getAnkiNoteInfoForceValue(notes, forceCanAddValue); -            } else { -                infos = await this._display.application.api.getAnkiNoteInfo(notes, fetchAdditionalInfo); +            if (!await this._display.application.api.isAnkiConnected()) { +                throw new Error('Anki not connected');              } + +            infos = this._checkForDuplicates ? +                await this._display.application.api.getAnkiNoteInfo(notes, fetchAdditionalInfo) : +                this._getAnkiNoteInfoForceValue(notes, true);          } catch (e) {              infos = this._getAnkiNoteInfoForceValue(notes, false);              ankiError = toError(e); @@ -711,7 +747,6 @@ export class DisplayAnki {              modelName,              fields,              tags: this._noteTags, -            checkForDuplicates: this._checkForDuplicates,              duplicateScope: this._duplicateScope,              duplicateScopeCheckAllModels: this._duplicateScopeCheckAllModels,              resultOutputMode: this._resultOutputMode, diff --git a/ext/js/input/hotkey-help-controller.js b/ext/js/input/hotkey-help-controller.js index 16f3d26f..b495365d 100644 --- a/ext/js/input/hotkey-help-controller.js +++ b/ext/js/input/hotkey-help-controller.js @@ -181,4 +181,36 @@ export class HotkeyHelpController {              defaultAttributeValues          };      } + +    /** +     * @param {HTMLElement} node +     * @returns {?string} +     */ +    getHotkeyLabel(node) { +        const {hotkey} = node.dataset; +        if (typeof hotkey !== 'string') { return null; } + +        const data = /** @type {unknown} */ (parseJson(hotkey)); +        if (!Array.isArray(data)) { return null; } + +        const values = /** @type {unknown[]} */ (data)[2]; +        if (typeof values !== 'string') { return null; } + +        return values; +    } + +    /** +     * @param {HTMLElement} node +     * @param {string} label +     */ +    setHotkeyLabel(node, label) { +        const {hotkey} = node.dataset; +        if (typeof hotkey !== 'string') { return; } + +        const data = /** @type {unknown} */ (parseJson(hotkey)); +        if (!Array.isArray(data)) { return; } + +        data[2] = label; +        node.dataset.hotkey = JSON.stringify(data); +    }  } |