aboutsummaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
Diffstat (limited to 'ext')
-rw-r--r--ext/css/display.css2
-rw-r--r--ext/images/add-duplicate-term-kana.svg40
-rw-r--r--ext/images/add-duplicate-term-kanji.svg84
-rw-r--r--ext/js/background/backend.js87
-rw-r--r--ext/js/comm/anki-connect.js57
-rw-r--r--ext/js/data/anki-note-builder.js3
-rw-r--r--ext/js/display/display-anki.js55
-rw-r--r--ext/js/input/hotkey-help-controller.js32
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);
+ }
}