aboutsummaryrefslogtreecommitdiff
path: root/ext/js/display/display-anki.js
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 /ext/js/display/display-anki.js
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
Diffstat (limited to 'ext/js/display/display-anki.js')
-rw-r--r--ext/js/display/display-anki.js195
1 files changed, 116 insertions, 79 deletions
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}