summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefanVukovic99 <stefanvukovic44@gmail.com>2024-05-21 18:55:02 +0200
committerGitHub <noreply@github.com>2024-05-21 16:55:02 +0000
commitba9fa339a43a5f494785877018b10f111ccafff5 (patch)
tree1a4a0b141e08cfe767170425d5afda253bf92990
parent737e7eab8182fc4e083f7fd6df840327ab83287c (diff)
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
-rw-r--r--ext/css/display.css7
-rw-r--r--ext/data/schemas/options-schema.json2
-rw-r--r--ext/images/add-duplicate-term-kana.svg8
-rw-r--r--ext/images/add-duplicate-term-kanji.svg8
-rw-r--r--ext/images/overwrite-term-kana.svg30
-rw-r--r--ext/images/overwrite-term-kanji.svg31
-rw-r--r--ext/js/background/backend.js10
-rw-r--r--ext/js/comm/anki-connect.js15
-rw-r--r--ext/js/comm/api.js8
-rw-r--r--ext/js/data/anki-util.js2
-rw-r--r--ext/js/display/display-anki.js195
-rw-r--r--ext/js/pages/settings/anki-controller.js24
-rw-r--r--ext/settings.html10
-rw-r--r--ext/templates-display.html8
-rw-r--r--types/ext/anki.d.ts2
-rw-r--r--types/ext/api.d.ts6
-rw-r--r--types/ext/display-anki.d.ts3
-rw-r--r--types/ext/settings.d.ts2
18 files changed, 274 insertions, 97 deletions
diff --git a/ext/css/display.css b/ext/css/display.css
index 198ff662..9b4e0022 100644
--- a/ext/css/display.css
+++ b/ext/css/display.css
@@ -669,8 +669,13 @@ 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-kanji] { background-image: url('/images/add-term-kanji.svg'); }
+.icon[data-icon=overwrite-term-kanji] { background-image: url('/images/overwrite-term-kanji.svg'); }
+.icon[data-icon=overwrite-term-kana] { background-image: url('/images/overwrite-term-kana.svg'); }
+.icon[data-icon=overwrite-kanji] { background-image: url('/images/overwrite-term-kanji.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=add-duplicate-kanji] { background-image: url('/images/add-duplicate-term-kanji.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'); }
@@ -1920,7 +1925,7 @@ button.footer-notification-close-button {
display: none;
}
:root[data-anki-enabled=false] .action-button[data-action=view-note],
-:root[data-anki-enabled=false] .action-button[data-action=add-note] {
+:root[data-anki-enabled=false] .action-button[data-action=save-note] {
display: none;
}
:root[data-audio-enabled=false] .action-button[data-action=play-audio] {
diff --git a/ext/data/schemas/options-schema.json b/ext/data/schemas/options-schema.json
index 0cb6db0a..5db4c087 100644
--- a/ext/data/schemas/options-schema.json
+++ b/ext/data/schemas/options-schema.json
@@ -990,7 +990,7 @@
},
"duplicateBehavior": {
"type": "string",
- "enum": ["prevent", "new"],
+ "enum": ["prevent", "overwrite", "new"],
"default": "prevent"
},
"fieldTemplates": {
diff --git a/ext/images/add-duplicate-term-kana.svg b/ext/images/add-duplicate-term-kana.svg
index 267c55ec..6bc430e6 100644
--- a/ext/images/add-duplicate-term-kana.svg
+++ b/ext/images/add-duplicate-term-kana.svg
@@ -13,8 +13,8 @@
<stop stop-opacity=".28986" offset="0"/>
<stop stop-opacity="0" offset="1"/>
</radialGradient>
- <filter id="green-to-red" x="-.083333" y="-.083333" width="1.1667" height="1.1667" color-interpolation-filters="sRGB">
- <feColorMatrix result="color1" type="hueRotate" values="250"/>
+ <filter id="green-to-purple" x="-.083333" y="-.083333" width="1.1667" height="1.1667" color-interpolation-filters="sRGB">
+ <feColorMatrix result="color1" type="hueRotate" values="180"/>
<feColorMatrix result="color2" type="saturate" values="1"/>
</filter>
<symbol id="plus-in-circle" viewBox="0 0 16 16">
@@ -24,10 +24,10 @@
<path d="m6 8h1v-1h1v1h1v1h-1v1h-1v-1h-1v-1" fill="#fff"/>
</symbol>
</defs>
- <g transform="translate(1,-1)" filter="url(#green-to-red)">
+ <g transform="translate(1,-1)" filter="url(#green-to-purple)">
<use xlink:href="#plus-in-circle"/>
</g>
- <g transform="translate(-1,1)" filter="url(#green-to-red)">
+ <g transform="translate(-1,1)" filter="url(#green-to-purple)">
<use xlink:href="#plus-in-circle"/>
</g>
</svg>
diff --git a/ext/images/add-duplicate-term-kanji.svg b/ext/images/add-duplicate-term-kanji.svg
index fc3279d9..da39eb1c 100644
--- a/ext/images/add-duplicate-term-kanji.svg
+++ b/ext/images/add-duplicate-term-kanji.svg
@@ -13,8 +13,8 @@
<stop stop-opacity=".28986" offset="0"/>
<stop stop-opacity="0" offset="1"/>
</radialGradient>
- <filter id="green-to-red" x="-.038462" y="-.038462" width="1.0769" height="1.0769" color-interpolation-filters="sRGB">
- <feColorMatrix type="hueRotate" values="250"/>
+ <filter id="green-to-purple" x="-.038462" y="-.038462" width="1.0769" height="1.0769" color-interpolation-filters="sRGB">
+ <feColorMatrix type="hueRotate" values="180"/>
<feColorMatrix type="saturate" values="1"/>
<feColorMatrix type="hueRotate" values="0"/>
</filter>
@@ -28,10 +28,10 @@
</symbol>
</defs>
- <g transform="matrix(.91504 0 0 .91504 2.2745 -.91504)" filter="url(#green-to-red)">
+ <g transform="matrix(.91504 0 0 .91504 2.2745 -.91504)" filter="url(#green-to-purple)">
<use xlink:href="#plus-in-circle"/>
</g>
- <g transform="matrix(.91504 0 0 .91504 -.91504 2.2745)" filter="url(#green-to-red)">
+ <g transform="matrix(.91504 0 0 .91504 -.91504 2.2745)" filter="url(#green-to-purple)">
<use xlink:href="#plus-in-circle"/>
</g>
</svg>
diff --git a/ext/images/overwrite-term-kana.svg b/ext/images/overwrite-term-kana.svg
new file mode 100644
index 00000000..7bcb1926
--- /dev/null
+++ b/ext/images/overwrite-term-kana.svg
@@ -0,0 +1,30 @@
+<?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="inner-fill" 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="#6fb558" offset="0"/>
+ <stop stop-color="#a5db9b" offset="1"/>
+ </linearGradient>
+ <linearGradient id="outer-rim" x1="2.1167" x2="1.4552" y1="2.3812" y2="1.7198" gradientTransform="scale(3.7795 3.7795)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#34812c" offset="0"/>
+ <stop stop-color="#87b870" offset="1"/>
+ </linearGradient>
+ <radialGradient id="center-shadow" 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>
+ <filter id="green-to-orange" x="-.083333" y="-.083333" width="1.1667" height="1.1667" color-interpolation-filters="sRGB">
+ <feColorMatrix result="color1" type="hueRotate" values="280"/>
+ <feColorMatrix result="color2" type="saturate" values="2"/>
+ </filter>
+ </defs>
+ <g filter="url(#green-to-orange)">
+ <circle cx="7.5" cy="8.5" r="3" fill="url(#inner-fill)"/>
+ <circle cx="7.5" cy="8.5" r="3" fill="none" stroke="url(#outer-rim)"/>
+ <path
+ transform="matrix(.49952 0 0 .49952 .84885 3.6971)"
+ d="m15.631 9.9045 0.66159 0.08268c-0.18327 1.4812-1.4463 2.6278-2.9771 2.6278-0.94244 0-1.7834-0.43457-2.3334-1.1143l3.1e-5 1.1143h-0.66667v-2.3333h2.3333v0.66667l-1.2482 3.45e-4c0.42167 0.60433 1.1221 0.99971 1.9149 0.99971 1.1906 0 2.173-0.89179 2.3156-2.0439zm0.68445-3.2895v2.3333h-2.3333v-0.66666l1.2484-5.09e-5c-0.42165-0.60446-1.1222-0.99995-1.9151-0.99995-1.1907 0-2.173 0.8918-2.3156 2.0439l-0.66159-0.082684c0.18327-1.4812 1.4463-2.6279 2.9771-2.6279 0.94244 0 1.7834 0.43457 2.3334 1.1143l-2.9e-5 -1.1143z"
+ fill="#fff"
+ style="stroke:#ffffff;stroke-opacity:1;stroke-width:0.40038437;stroke-dasharray:none" />
+ </g>
+</svg>
diff --git a/ext/images/overwrite-term-kanji.svg b/ext/images/overwrite-term-kanji.svg
new file mode 100644
index 00000000..d7d48ad8
--- /dev/null
+++ b/ext/images/overwrite-term-kanji.svg
@@ -0,0 +1,31 @@
+<?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="inner-fill" 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="outer-rim" 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="center-shadow" 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>
+ <filter id="green-to-orange" x="-.083333" y="-.083333" width="1.1667" height="1.1667" color-interpolation-filters="sRGB">
+ <feColorMatrix result="color1" type="hueRotate" values="280"/>
+ <feColorMatrix result="color2" type="saturate" values="2"/>
+ </filter>
+ </defs>
+ <g filter="url(#green-to-orange)">
+ <circle cx="8" cy="8" r="6.5" fill="url(#inner-fill)"/>
+ <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(#outer-rim)"/>
+ <path
+ d="m10.309 8.2795 0.66159 0.082684c-0.18327 1.4812-1.4463 2.6278-2.9771 2.6278-0.94244 0-1.7834-0.43457-2.3334-1.1143l3.11e-5 1.1143h-0.66667v-2.3333h2.3333v0.66667l-1.2482 3.453e-4c0.42167 0.60433 1.1221 0.99971 1.9149 0.99971 1.1906 0 2.173-0.89179 2.3156-2.0439zm0.68445-3.2895v2.3333h-2.3333v-0.66666l1.2484-5.09e-5c-0.42165-0.60446-1.1222-0.99995-1.9151-0.99995-1.1907 0-2.173 0.8918-2.3156 2.0439l-0.66159-0.082684c0.18327-1.4813 1.4463-2.6279 2.9771-2.6279 0.94244 0 1.7834 0.43457 2.3334 1.1143l-2.9e-5 -1.1143z"
+ fill="#fff"
+ style="stroke:#ffffff;stroke-opacity:1;stroke-width:0.4;stroke-dasharray:none"
+ />
+ </g>
+</svg>
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]
*/
diff --git a/ext/settings.html b/ext/settings.html
index 525e99b1..12281764 100644
--- a/ext/settings.html
+++ b/ext/settings.html
@@ -1709,10 +1709,20 @@
<div class="settings-item-right">
<select data-setting="anki.duplicateBehavior">
<option value="prevent">Prevent adding</option>
+ <option value="overwrite">Allow overwriting</option>
<option value="new">Allow adding</option>
</select>
</div>
</div>
+ <div class="settings-item-children" id="anki-overwrite-warning" hidden>
+ <p class="danger-text">
+ Overwriting a card can result in the loss of data.
+ </p>
+ <p class="warning-text">
+ Duplicate cards of a different note type cannot be overwritten.
+ If there are multiple duplicate cards, the first card found will be overwritten.
+ </p>
+ </div>
</div>
</div>
</div>
diff --git a/ext/templates-display.html b/ext/templates-display.html
index 944a10ab..a948d4c3 100644
--- a/ext/templates-display.html
+++ b/ext/templates-display.html
@@ -12,10 +12,10 @@
<span class="action-icon icon color-icon" data-icon="view-note"></span>
<span class="action-button-badge icon" hidden></span>
</button>
- <button type="button" class="action-button" data-action="add-note" hidden disabled data-mode="term-kanji" title="Add expression" data-hotkey='["addNoteTermKanji","title","Add expression ({0})"]'>
+ <button type="button" class="action-button" data-action="save-note" hidden disabled data-mode="term-kanji" title="Add expression" data-hotkey='["addNoteTermKanji","title","Add expression ({0})"]'>
<span class="action-icon icon color-icon" data-icon="add-term-kanji"></span>
</button>
- <button type="button" class="action-button" data-action="add-note" hidden disabled data-mode="term-kana" title="Add reading" data-hotkey='["addNoteTermKana","title","Add reading ({0})"]'>
+ <button type="button" class="action-button" data-action="save-note" hidden disabled data-mode="term-kana" title="Add reading" data-hotkey='["addNoteTermKana","title","Add reading ({0})"]'>
<span class="action-icon icon color-icon" data-icon="add-term-kana"></span>
</button>
<button type="button" class="action-button" data-action="play-audio" title="Play audio" data-title-default="Play audio" data-hotkey='["playAudio",["title","data-title-default"],"Play audio ({0})"]' data-menu-position="left below h-cover v-cover">
@@ -114,8 +114,8 @@
<button type="button" class="action-button" data-action="view-note" hidden disabled title="View added note" data-hotkey='["viewNotes","title","View added note ({0})"]'>
<span class="action-icon icon color-icon" data-icon="view-note"></span>
</button>
- <button type="button" class="action-button" data-action="add-note" hidden disabled data-mode="kanji" title="Add kanji" data-hotkey='["addNoteKanji","title","Add kanji ({0})"]'>
- <span class="action-icon icon color-icon" data-icon="add-term-kanji"></span>
+ <button type="button" class="action-button" data-action="save-note" hidden disabled data-mode="kanji" title="Add kanji" data-hotkey='["addNoteKanji","title","Add kanji ({0})"]'>
+ <span class="action-icon icon color-icon" data-icon="add-kanji"></span>
</button>
<span class="entry-current-indicator-icon" title="Current entry">
<span class="icon color-icon" data-icon="entry-current"></span>
diff --git a/types/ext/anki.d.ts b/types/ext/anki.d.ts
index 4a903c52..8ad0890f 100644
--- a/types/ext/anki.d.ts
+++ b/types/ext/anki.d.ts
@@ -22,6 +22,8 @@ export type NoteId = number;
export type CardId = number;
+export type NoteWithId = Note & {id: NoteId};
+
export type Note = {
fields: NoteFields;
tags: string[];
diff --git a/types/ext/api.d.ts b/types/ext/api.d.ts
index 9a922fb0..3a2fc0b8 100644
--- a/types/ext/api.d.ts
+++ b/types/ext/api.d.ts
@@ -169,6 +169,12 @@ type ApiSurface = {
};
return: Anki.NoteId | null;
};
+ updateAnkiNote: {
+ params: {
+ noteWithId: Anki.NoteWithId;
+ };
+ return: null;
+ };
getAnkiNoteInfo: {
params: {
notes: Anki.Note[];
diff --git a/types/ext/display-anki.d.ts b/types/ext/display-anki.d.ts
index 1b0dd4ff..4b6c6e37 100644
--- a/types/ext/display-anki.d.ts
+++ b/types/ext/display-anki.d.ts
@@ -46,6 +46,9 @@ export type DictionaryEntryModeDetails = {
requirements: AnkiNoteBuilder.Requirement[];
canAdd: boolean;
valid: boolean;
+ /**
+ * Anki IDs of duplicate notes. May contain INVALID_NOTE_ID for notes whose ID could not be found.
+ */
noteIds: Anki.NoteId[] | null;
noteInfos?: (Anki.NoteInfo | null)[];
ankiError: Error | null;
diff --git a/types/ext/settings.d.ts b/types/ext/settings.d.ts
index 1fce0f59..48a66728 100644
--- a/types/ext/settings.d.ts
+++ b/types/ext/settings.d.ts
@@ -395,7 +395,7 @@ export type AnkiScreenshotFormat = 'png' | 'jpeg';
export type AnkiDuplicateScope = 'collection' | 'deck' | 'deck-root';
-export type AnkiDuplicateBehavior = 'prevent' | 'new';
+export type AnkiDuplicateBehavior = 'prevent' | 'overwrite' | 'new';
export type AnkiDisplayTags = 'never' | 'always' | 'non-standard';