aboutsummaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2021-07-03 19:32:11 -0400
committerGitHub <noreply@github.com>2021-07-03 19:32:11 -0400
commitea47cb82487ac12379b2bdf89a3a9155608f45f7 (patch)
treed66cec528be57b9e2de2e1576a758ec4b68f0ca3 /ext
parent82b7ebfa58c1d68916d66ffb49a0407a7dfc7aba (diff)
DisplayAnki (#1791)
* Remove anki functionality from display * Rename function * Display API updates * Use DisplayAnki * TODO
Diffstat (limited to 'ext')
-rw-r--r--ext/js/display/display-anki.js582
-rw-r--r--ext/js/display/display.js526
-rw-r--r--ext/popup.html1
-rw-r--r--ext/search.html1
4 files changed, 621 insertions, 489 deletions
diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js
new file mode 100644
index 00000000..8dd94214
--- /dev/null
+++ b/ext/js/display/display-anki.js
@@ -0,0 +1,582 @@
+/*
+ * Copyright (C) 2021 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/* global
+ * AnkiNoteBuilder
+ * AnkiUtil
+ * DisplayNotification
+ */
+
+class DisplayAnki {
+ constructor(display) {
+ this._display = display;
+ this._ankiFieldTemplates = null;
+ this._ankiFieldTemplatesDefault = null;
+ this._ankiNoteBuilder = new AnkiNoteBuilder();
+ this._ankiNoteNotification = null;
+ this._ankiNoteNotificationEventListeners = null;
+ this._ankiTagNotification = null;
+ this._updateAdderButtonsPromise = Promise.resolve();
+ this._updateAdderButtonsToken = null;
+ this._eventListeners = new EventListenerCollection();
+ this._checkForDuplicates = false;
+ this._suspendNewCards = false;
+ this._compactTags = false;
+ this._resultOutputMode = 'split';
+ this._glossaryLayoutMode = 'default';
+ this._displayTags = 'never';
+ this._duplicateScope = 'collection';
+ this._screenshotFormat = 'png';
+ this._screenshotQuality = 100;
+ this._noteTags = [];
+ this._modeOptions = new Map();
+ this._onShowTagsBind = this._onShowTags.bind(this);
+ this._onNoteAddBind = this._onNoteAdd.bind(this);
+ this._onNoteViewBind = this._onNoteView.bind(this);
+ }
+
+ prepare() {
+ this._display.hotkeyHandler.registerActions([
+ ['addNoteKanji', () => { this._tryAddAnkiNoteForSelectedEntry('kanji'); }],
+ ['addNoteTermKanji', () => { this._tryAddAnkiNoteForSelectedEntry('term-kanji'); }],
+ ['addNoteTermKana', () => { this._tryAddAnkiNoteForSelectedEntry('term-kana'); }],
+ ['viewNote', () => { this._tryViewAnkiNoteForSelectedEntry(); }]
+ ]);
+ this._display.on('optionsUpdated', this._onOptionsUpdated.bind(this));
+ }
+
+ cleanupEntries() {
+ this._updateAdderButtonsToken = null;
+ this._hideAnkiNoteErrors(false);
+ }
+
+ setupEntry(entry) {
+ this._addMultipleEventListeners(entry, '.action-view-tags', 'click', this._onShowTagsBind);
+ this._addMultipleEventListeners(entry, '.action-add-note', 'click', this._onNoteAddBind);
+ this._addMultipleEventListeners(entry, '.action-view-note', 'click', this._onNoteViewBind);
+ }
+
+ setupEntriesComplete(isTerms, dictionaryEntries) { // TODO : Don't pass (isTerms, dictionaryEntries)
+ this._updateAdderButtons(isTerms, dictionaryEntries);
+ }
+
+ async getLogData(dictionaryEntry) {
+ const result = {};
+
+ // Anki note data
+ let ankiNoteData;
+ let ankiNoteDataException;
+ try {
+ const context = this._getNoteContext();
+ ankiNoteData = await this._ankiNoteBuilder.getRenderingData({
+ dictionaryEntry,
+ mode: 'test',
+ context,
+ resultOutputMode: this.resultOutputMode,
+ glossaryLayoutMode: this._glossaryLayoutMode,
+ compactTags: this._compactTags,
+ injectedMedia: null,
+ marker: 'test'
+ });
+ } catch (e) {
+ ankiNoteDataException = e;
+ }
+ result.ankiNoteData = ankiNoteData;
+ if (typeof ankiNoteDataException !== 'undefined') {
+ result.ankiNoteDataException = ankiNoteDataException;
+ }
+
+ // Anki notes
+ const ankiNotes = [];
+ const modes = this._getModes(dictionaryEntry.type === 'term');
+ for (const mode of modes) {
+ let note;
+ let errors;
+ try {
+ const noteContext = this._getNoteContext();
+ ({note: note, errors} = await this._createNote(dictionaryEntry, mode, noteContext, false));
+ } catch (e) {
+ errors = [e];
+ }
+ const entry = {mode, note};
+ if (Array.isArray(errors) && errors.length > 0) {
+ entry.errors = errors;
+ }
+ ankiNotes.push(entry);
+ }
+ result.ankiNotes = ankiNotes;
+
+ return result;
+ }
+
+ // Private
+
+ _onOptionsUpdated({options}) {
+ const {
+ general: {resultOutputMode, glossaryLayoutMode, compactTags},
+ anki: {tags, duplicateScope, suspendNewCards, checkForDuplicates, displayTags, kanji, terms, screenshot: {format, quality}}
+ } = options;
+
+ this._checkForDuplicates = checkForDuplicates;
+ this._suspendNewCards = suspendNewCards;
+ this._compactTags = compactTags;
+ this._resultOutputMode = resultOutputMode;
+ this._glossaryLayoutMode = glossaryLayoutMode;
+ this._displayTags = displayTags;
+ this._duplicateScope = duplicateScope;
+ this._screenshotFormat = format;
+ this._screenshotQuality = quality;
+ this._noteTags = [...tags];
+ this._modeOptions.clear();
+ this._modeOptions.set('kanji', kanji);
+ this._modeOptions.set('term-kanji', terms);
+ this._modeOptions.set('term-kana', terms);
+
+ this._updateAnkiFieldTemplates(options);
+ }
+
+ _onNoteAdd(e) {
+ e.preventDefault();
+ const node = e.currentTarget;
+ const index = this._display.getElementDictionaryEntryIndex(node);
+ this._addAnkiNote(index, node.dataset.mode);
+ }
+
+ _onShowTags(e) {
+ e.preventDefault();
+ const tags = e.currentTarget.title;
+ this._showAnkiTagsNotification(tags);
+ }
+
+ _onNoteView(e) {
+ e.preventDefault();
+ const link = e.currentTarget;
+ yomichan.api.noteView(link.dataset.noteId);
+ }
+
+ _addMultipleEventListeners(container, selector, ...args) {
+ for (const node of container.querySelectorAll(selector)) {
+ this._eventListeners.addEventListener(node, ...args);
+ }
+ }
+
+ _adderButtonFind(index, mode) {
+ const entry = this._getEntry(index);
+ return entry !== null ? entry.querySelector(`.action-add-note[data-mode="${mode}"]`) : null;
+ }
+
+ _tagsIndicatorFind(index) {
+ const entry = this._getEntry(index);
+ return entry !== null ? entry.querySelector('.action-view-tags') : null;
+ }
+
+ _viewerButtonFind(index) {
+ const entry = this._getEntry(index);
+ return entry !== null ? entry.querySelector('.action-view-note') : null;
+ }
+
+ _getEntry(index) {
+ const entries = this._display.dictionaryEntryNodes;
+ return index >= 0 && index < entries.length ? entries[index] : null;
+ }
+
+ _viewerButtonShow(index, noteId) {
+ const viewerButton = this._viewerButtonFind(index);
+ if (viewerButton === null) {
+ return;
+ }
+ viewerButton.disabled = false;
+ viewerButton.hidden = false;
+ viewerButton.dataset.noteId = noteId;
+ }
+
+ _getNoteContext() {
+ const {state} = this._display.history;
+ let {documentTitle, url, sentence} = (isObject(state) ? state : {});
+ if (typeof documentTitle !== 'string') {
+ documentTitle = document.title;
+ }
+ if (typeof url !== 'string') {
+ url = window.location.href;
+ }
+ sentence = this._getValidSentenceData(sentence);
+ return {
+ url,
+ sentence,
+ documentTitle,
+ query: this._display.query,
+ fullQuery: this._display.fullQuery
+ };
+ }
+
+ _getDictionaryEntryDetailsForNote(dictionaryEntry) {
+ const {type} = dictionaryEntry;
+ if (type === 'kanji') {
+ const {character} = dictionaryEntry;
+ return {type, character};
+ }
+
+ const {headwords} = dictionaryEntry;
+ let bestIndex = -1;
+ for (let i = 0, ii = headwords.length; i < ii; ++i) {
+ const {term, reading, sources} = headwords[i];
+ for (const {deinflectedText} of sources) {
+ if (term === deinflectedText) {
+ bestIndex = i;
+ i = ii;
+ break;
+ } else if (reading === deinflectedText && bestIndex < 0) {
+ bestIndex = i;
+ break;
+ }
+ }
+ }
+
+ const {term, reading} = headwords[Math.max(0, bestIndex)];
+ return {type, term, reading};
+ }
+
+ async _updateAdderButtons(isTerms, dictionaryEntries) {
+ const token = {};
+ this._updateAdderButtonsToken = token;
+ await this._updateAdderButtonsPromise;
+ if (this._updateAdderButtonsToken !== token) { return; }
+
+ const {promise, resolve} = deferPromise();
+ try {
+ this._updateAdderButtonsPromise = promise;
+
+ const modes = this._getModes(isTerms);
+ let states;
+ try {
+ const noteContext = this._getNoteContext();
+ states = await this._areDictionaryEntriesAddable(
+ dictionaryEntries,
+ modes,
+ noteContext,
+ this._checkForDuplicates ? null : true,
+ this._displayTags !== 'never'
+ );
+ } catch (e) {
+ return;
+ }
+
+ if (this._updateAdderButtonsToken !== token) { return; }
+
+ this._updateAdderButtons2(states, modes);
+ } finally {
+ resolve();
+ }
+ }
+
+ _updateAdderButtons2(states, modes) {
+ const displayTags = this._displayTags;
+ for (let i = 0, ii = states.length; i < ii; ++i) {
+ const infos = states[i];
+ let noteId = null;
+ for (let j = 0, jj = infos.length; j < jj; ++j) {
+ const {canAdd, noteIds, noteInfos} = infos[j];
+ const mode = modes[j];
+ const button = this._adderButtonFind(i, mode);
+ if (button === null) {
+ continue;
+ }
+
+ if (Array.isArray(noteIds) && noteIds.length > 0) {
+ noteId = noteIds[0];
+ }
+ button.disabled = !canAdd;
+ button.hidden = false;
+
+ if (displayTags !== 'never' && Array.isArray(noteInfos)) {
+ this._setupTagsIndicator(i, noteInfos);
+ }
+ }
+ if (noteId !== null) {
+ this._viewerButtonShow(i, noteId);
+ }
+ }
+ }
+
+ _setupTagsIndicator(i, noteInfos) {
+ const tagsIndicator = this._tagsIndicatorFind(i);
+ if (tagsIndicator === null) {
+ return;
+ }
+
+ const displayTags = new Set();
+ for (const {tags} of noteInfos) {
+ for (const tag of tags) {
+ displayTags.add(tag);
+ }
+ }
+ if (this._displayTags === 'non-standard') {
+ for (const tag of this._noteTags) {
+ displayTags.delete(tag);
+ }
+ }
+
+ if (displayTags.size > 0) {
+ tagsIndicator.disabled = false;
+ tagsIndicator.hidden = false;
+ tagsIndicator.title = `Card tags: ${[...displayTags].join(', ')}`;
+ }
+ }
+
+ _showAnkiTagsNotification(message) {
+ if (this._ankiTagNotification === null) {
+ const node = this._display.displayGenerator.createEmptyFooterNotification();
+ node.classList.add('click-scannable');
+ this._ankiTagNotification = new DisplayNotification(this._display.notificationContainer, node);
+ }
+
+ this._ankiTagNotification.setContent(message);
+ this._ankiTagNotification.open();
+ }
+
+
+ _tryAddAnkiNoteForSelectedEntry(mode) {
+ const index = this._display.selectedIndex;
+ this._addAnkiNote(index, mode);
+ }
+
+ _tryViewAnkiNoteForSelectedEntry() {
+ const index = this._display.selectedIndex;
+ const button = this._viewerButtonFind(index);
+ if (button !== null && !button.disabled) {
+ yomichan.api.noteView(button.dataset.noteId);
+ }
+ }
+
+ async _addAnkiNote(dictionaryEntryIndex, mode) {
+ const dictionaryEntries = this._display.dictionaryEntries;
+ if (dictionaryEntryIndex < 0 || dictionaryEntryIndex >= dictionaryEntries.length) { return; }
+ const dictionaryEntry = dictionaryEntries[dictionaryEntryIndex];
+
+ const button = this._adderButtonFind(dictionaryEntryIndex, mode);
+ if (button === null || button.disabled) { return; }
+
+ this._hideAnkiNoteErrors(true);
+
+ const allErrors = [];
+ const progressIndicatorVisible = this._display.progressIndicatorVisible;
+ const overrideToken = progressIndicatorVisible.setOverride(true);
+ try {
+ const noteContext = this._getNoteContext();
+ const {note, errors} = await this._createNote(dictionaryEntry, mode, noteContext, true);
+ allErrors.push(...errors);
+
+ let noteId = null;
+ let addNoteOkay = false;
+ try {
+ noteId = await yomichan.api.addAnkiNote(note);
+ addNoteOkay = true;
+ } catch (e) {
+ allErrors.length = 0;
+ allErrors.push(e);
+ }
+
+ if (addNoteOkay) {
+ if (noteId === null) {
+ allErrors.push(new Error('Note could not be added'));
+ } else {
+ if (this._suspendNewCards) {
+ try {
+ await yomichan.api.suspendAnkiCardsForNote(noteId);
+ } catch (e) {
+ allErrors.push(e);
+ }
+ }
+ button.disabled = true;
+ this._viewerButtonShow(dictionaryEntryIndex, noteId);
+ }
+ }
+ } catch (e) {
+ allErrors.push(e);
+ } finally {
+ progressIndicatorVisible.clearOverride(overrideToken);
+ }
+
+ if (allErrors.length > 0) {
+ this._showAnkiNoteErrors(allErrors);
+ } else {
+ this._hideAnkiNoteErrors(true);
+ }
+ }
+
+ _showAnkiNoteErrors(errors) {
+ if (this._ankiNoteNotificationEventListeners !== null) {
+ this._ankiNoteNotificationEventListeners.removeAllEventListeners();
+ }
+
+ if (this._ankiNoteNotification === null) {
+ const node = this._display.displayGenerator.createEmptyFooterNotification();
+ this._ankiNoteNotification = new DisplayNotification(this._display.notificationContainer, node);
+ this._ankiNoteNotificationEventListeners = new EventListenerCollection();
+ }
+
+ const content = this._display.displayGenerator.createAnkiNoteErrorsNotificationContent(errors);
+ for (const node of content.querySelectorAll('.anki-note-error-log-link')) {
+ this._ankiNoteNotificationEventListeners.addEventListener(node, 'click', () => {
+ console.log({ankiNoteErrors: errors});
+ }, false);
+ }
+
+ this._ankiNoteNotification.setContent(content);
+ this._ankiNoteNotification.open();
+ }
+
+ _hideAnkiNoteErrors(animate) {
+ if (this._ankiNoteNotification === null) { return; }
+ this._ankiNoteNotification.close(animate);
+ this._ankiNoteNotificationEventListeners.removeAllEventListeners();
+ }
+
+ async _updateAnkiFieldTemplates(options) {
+ this._ankiFieldTemplates = await this._getAnkiFieldTemplates(options);
+ }
+
+ async _getAnkiFieldTemplates(options) {
+ let templates = options.anki.fieldTemplates;
+ if (typeof templates === 'string') { return templates; }
+
+ templates = this._ankiFieldTemplatesDefault;
+ if (typeof templates === 'string') { return templates; }
+
+ templates = await yomichan.api.getDefaultAnkiFieldTemplates();
+ this._ankiFieldTemplatesDefault = templates;
+ return templates;
+ }
+
+ async _areDictionaryEntriesAddable(dictionaryEntries, modes, context, forceCanAddValue, fetchAdditionalInfo) {
+ const modeCount = modes.length;
+ const notePromises = [];
+ for (const dictionaryEntry of dictionaryEntries) {
+ for (const mode of modes) {
+ const notePromise = this._createNote(dictionaryEntry, mode, context, false);
+ notePromises.push(notePromise);
+ }
+ }
+ const notes = (await Promise.all(notePromises)).map(({note}) => note);
+
+ let infos;
+ if (forceCanAddValue !== null) {
+ if (!await yomichan.api.isAnkiConnected()) {
+ throw new Error('Anki not connected');
+ }
+ infos = this._getAnkiNoteInfoForceValue(notes, forceCanAddValue);
+ } else {
+ infos = await yomichan.api.getAnkiNoteInfo(notes, fetchAdditionalInfo);
+ }
+
+ const results = [];
+ for (let i = 0, ii = infos.length; i < ii; i += modeCount) {
+ results.push(infos.slice(i, i + modeCount));
+ }
+ return results;
+ }
+
+ _getAnkiNoteInfoForceValue(notes, canAdd) {
+ const results = [];
+ for (const note of notes) {
+ const valid = AnkiUtil.isNoteDataValid(note);
+ results.push({canAdd, valid, noteIds: null});
+ }
+ return results;
+ }
+
+ async _createNote(dictionaryEntry, mode, context, injectMedia) {
+ const modeOptions = this._modeOptions.get(mode);
+ if (typeof modeOptions === 'undefined') { throw new Error(`Unsupported note type: ${mode}`); }
+ const template = this._ankiFieldTemplates;
+ const {deck: deckName, model: modelName} = modeOptions;
+ const fields = Object.entries(modeOptions.fields);
+
+ const errors = [];
+ let injectedMedia = null;
+ if (injectMedia) {
+ let errors2;
+ ({result: injectedMedia, errors: errors2} = await this._injectAnkiNoteMedia(dictionaryEntry, fields));
+ for (const error of errors2) {
+ errors.push(deserializeError(error));
+ }
+ }
+
+ const {note, errors: createNoteErrors} = await this._ankiNoteBuilder.createNote({
+ dictionaryEntry,
+ mode,
+ context,
+ template,
+ deckName,
+ modelName,
+ fields,
+ tags: this._noteTags,
+ checkForDuplicates: this._checkForDuplicates,
+ duplicateScope: this._duplicateScope,
+ resultOutputMode: this.resultOutputMode,
+ glossaryLayoutMode: this._glossaryLayoutMode,
+ compactTags: this._compactTags,
+ injectedMedia,
+ errors
+ });
+ errors.push(...createNoteErrors);
+ return {note, errors};
+ }
+
+ async _injectAnkiNoteMedia(dictionaryEntry, fields) {
+ const timestamp = Date.now();
+
+ const dictionaryEntryDetails = this._getDictionaryEntryDetailsForNote(dictionaryEntry);
+
+ const audioDetails = (
+ dictionaryEntryDetails.type !== 'kanji' && AnkiUtil.fieldsObjectContainsMarker(fields, 'audio') ?
+ this._display.getAnkiNoteMediaAudioDetails(dictionaryEntryDetails.term, dictionaryEntryDetails.reading) :
+ null
+ );
+
+ const {tabId, frameId} = this._display.getContentOrigin();
+ const screenshotDetails = (
+ AnkiUtil.fieldsObjectContainsMarker(fields, 'screenshot') && typeof tabId === 'number' ?
+ {tabId, frameId, format: this._screenshotFormat, quality: this._screenshotQuality} :
+ null
+ );
+
+ const clipboardDetails = {
+ image: AnkiUtil.fieldsObjectContainsMarker(fields, 'clipboard-image'),
+ text: AnkiUtil.fieldsObjectContainsMarker(fields, 'clipboard-text')
+ };
+
+ return await yomichan.api.injectAnkiNoteMedia(
+ timestamp,
+ dictionaryEntryDetails,
+ audioDetails,
+ screenshotDetails,
+ clipboardDetails
+ );
+ }
+
+ _getModes(isTerms) {
+ return isTerms ? ['term-kanji', 'term-kana'] : ['kanji'];
+ }
+
+ _getValidSentenceData(sentence) {
+ let {text, offset} = (isObject(sentence) ? sentence : {});
+ if (typeof text !== 'string') { text = ''; }
+ if (typeof offset !== 'number') { offset = 0; }
+ return {text, offset};
+ }
+}
diff --git a/ext/js/display/display.js b/ext/js/display/display.js
index bb7ced66..8387ae4f 100644
--- a/ext/js/display/display.js
+++ b/ext/js/display/display.js
@@ -16,8 +16,7 @@
*/
/* global
- * AnkiNoteBuilder
- * AnkiUtil
+ * DisplayAnki
* DisplayAudio
* DisplayGenerator
* DisplayHistory
@@ -86,10 +85,6 @@ class Display extends EventDispatcher {
getSearchContext: this._getSearchContext.bind(this),
documentUtil: this._documentUtil
});
- this._ankiFieldTemplates = null;
- this._ankiFieldTemplatesDefault = null;
- this._ankiNoteBuilder = new AnkiNoteBuilder();
- this._updateAdderButtonsPromise = Promise.resolve();
this._contentScrollElement = document.querySelector('#content-scroll');
this._contentScrollBodyElement = document.querySelector('#content-body');
this._windowScroll = new ScrollElement(this._contentScrollElement);
@@ -111,12 +106,10 @@ class Display extends EventDispatcher {
this._tagNotification = null;
this._footerNotificationContainer = document.querySelector('#content-footer');
this._displayAudio = new DisplayAudio(this);
- this._ankiNoteNotification = null;
- this._ankiNoteNotificationEventListeners = null;
- this._ankiTagNotification = null;
this._queryPostProcessor = null;
this._optionToggleHotkeyHandler = new OptionToggleHotkeyHandler(this);
this._elementOverflowController = new ElementOverflowController();
+ this._displayAnki = new DisplayAnki(this);
this._hotkeyHandler.registerActions([
['close', () => { this._onHotkeyClose(); }],
@@ -126,10 +119,6 @@ class Display extends EventDispatcher {
['firstEntry', () => { this._focusEntry(0, true); }],
['historyBackward', () => { this._sourceTermView(); }],
['historyForward', () => { this._nextTermView(); }],
- ['addNoteKanji', () => { this._tryAddAnkiNoteForSelectedEntry('kanji'); }],
- ['addNoteTermKanji', () => { this._tryAddAnkiNoteForSelectedEntry('term-kanji'); }],
- ['addNoteTermKana', () => { this._tryAddAnkiNoteForSelectedEntry('term-kana'); }],
- ['viewNote', () => { this._tryViewAnkiNoteForSelectedEntry(); }],
['playAudio', () => { this._playAudioCurrent(); }],
['playAudioFromSource', this._onHotkeyActionPlayAudioFromSource.bind(this)],
['copyHostSelection', () => this._copyHostSelection()],
@@ -202,6 +191,22 @@ class Display extends EventDispatcher {
return this._footerNotificationContainer;
}
+ get selectedIndex() {
+ return this._index;
+ }
+
+ get history() {
+ return this._history;
+ }
+
+ get query() {
+ return this._query;
+ }
+
+ get fullQuery() {
+ return this._fullQuery;
+ }
+
async prepare() {
// State setup
const {documentElement} = document;
@@ -216,6 +221,7 @@ class Display extends EventDispatcher {
await this._hotkeyHelpController.prepare();
await this._displayGenerator.prepare();
this._displayAudio.prepare();
+ this._displayAnki.prepare();
this._queryParser.prepare();
this._history.prepare();
this._optionToggleHotkeyHandler.prepare();
@@ -291,10 +297,8 @@ class Display extends EventDispatcher {
async updateOptions() {
const options = await yomichan.api.optionsGet(this.getOptionsContext());
- const templates = await this._getAnkiFieldTemplates(options);
const {scanning: scanningOptions, sentenceParsing: sentenceParsingOptions} = options;
this._options = options;
- this._ankiFieldTemplates = templates;
this._updateHotkeys(options);
this._updateDocumentOptions(options);
@@ -441,6 +445,17 @@ class Display extends EventDispatcher {
return await yomichan.crossFrame.invoke(this._parentFrameId, action, params);
}
+ getElementDictionaryEntryIndex(element) {
+ const node = element.closest('.entry');
+ if (node === null) { return -1; }
+ const index = parseInt(node.dataset.index, 10);
+ return Number.isFinite(index) ? index : -1;
+ }
+
+ getAnkiNoteMediaAudioDetails(term, reading) {
+ return this._displayAudio.getAnkiNoteMediaAudioDetails(term, reading);
+ }
+
// Message handlers
_onDirectMessage(data) {
@@ -530,8 +545,8 @@ class Display extends EventDispatcher {
this._eventListeners.removeAllEventListeners();
this._mediaLoader.unloadAll();
this._displayAudio.cleanupEntries();
+ this._displayAnki.cleanupEntries();
this._hideTagNotification(false);
- this._hideAnkiNoteErrors(false);
this._dictionaryEntries = [];
this._dictionaryEntryNodes = [];
this._elementOverflowController.clearElements();
@@ -712,19 +727,6 @@ class Display extends EventDispatcher {
}
}
- _onNoteAdd(e) {
- e.preventDefault();
- const link = e.currentTarget;
- const index = this._getClosestDictionaryEntryIndex(link);
- this._addAnkiNote(index, link.dataset.mode);
- }
-
- _onNoteView(e) {
- e.preventDefault();
- const link = e.currentTarget;
- yomichan.api.noteView(link.dataset.noteId);
- }
-
_onWheel(e) {
if (e.altKey) {
if (e.deltaY !== 0) {
@@ -752,7 +754,7 @@ class Display extends EventDispatcher {
_onDebugLogClick(e) {
const link = e.currentTarget;
- const index = this._getClosestDictionaryEntryIndex(link);
+ const index = this.getElementDictionaryEntryIndex(link);
this._logDictionaryEntryData(index);
}
@@ -810,7 +812,7 @@ class Display extends EventDispatcher {
this._tagNotification = new DisplayNotification(this._footerNotificationContainer, node);
}
- const index = this._getClosestDictionaryEntryIndex(tagNode);
+ const index = this.getElementDictionaryEntryIndex(tagNode);
const dictionaryEntry = (index >= 0 && index < this._dictionaryEntries.length ? this._dictionaryEntries[index] : null);
const content = this._displayGenerator.createTagFooterNotificationDetails(tagNode, dictionaryEntry);
@@ -960,6 +962,7 @@ class Display extends EventDispatcher {
this._dictionaryEntryNodes.push(entry);
this._addEntryEventListeners(entry);
this._displayAudio.setupEntry(entry, i);
+ this._displayAnki.setupEntry(entry, i);
container.appendChild(entry);
if (focusEntry === i) {
this._focusEntry(i, false);
@@ -977,8 +980,7 @@ class Display extends EventDispatcher {
}
this._displayAudio.setupEntriesComplete();
-
- this._updateAdderButtons(token, isTerms, dictionaryEntries);
+ this._displayAnki.setupEntriesComplete(isTerms, dictionaryEntries);
}
_setContentExtensionUnloaded() {
@@ -1065,104 +1067,6 @@ class Display extends EventDispatcher {
}
}
- async _updateAdderButtons(token, isTerms, dictionaryEntries) {
- await this._updateAdderButtonsPromise;
- if (this._setContentToken !== token) { return; }
-
- const {promise, resolve} = deferPromise();
- try {
- this._updateAdderButtonsPromise = promise;
-
- const modes = this._getModes(isTerms);
- let states;
- try {
- const noteContext = this._getNoteContext();
- const {checkForDuplicates, displayTags} = this._options.anki;
- states = await this._areDictionaryEntriesAddable(dictionaryEntries, modes, noteContext, checkForDuplicates ? null : true, displayTags !== 'never');
- } catch (e) {
- return;
- }
-
- if (this._setContentToken !== token) { return; }
-
- this._updateAdderButtons2(states, modes);
- } finally {
- resolve();
- }
- }
-
- _updateAdderButtons2(states, modes) {
- const {displayTags} = this._options.anki;
- for (let i = 0, ii = states.length; i < ii; ++i) {
- const infos = states[i];
- let noteId = null;
- for (let j = 0, jj = infos.length; j < jj; ++j) {
- const {canAdd, noteIds, noteInfos} = infos[j];
- const mode = modes[j];
- const button = this._adderButtonFind(i, mode);
- if (button === null) {
- continue;
- }
-
- if (Array.isArray(noteIds) && noteIds.length > 0) {
- noteId = noteIds[0];
- }
- button.disabled = !canAdd;
- button.hidden = false;
-
- if (displayTags !== 'never' && Array.isArray(noteInfos)) {
- this._setupTagsIndicator(i, noteInfos);
- }
- }
- if (noteId !== null) {
- this._viewerButtonShow(i, noteId);
- }
- }
- }
-
- _setupTagsIndicator(i, noteInfos) {
- const tagsIndicator = this._tagsIndicatorFind(i);
- if (tagsIndicator === null) {
- return;
- }
-
- const {tags: optionTags, displayTags} = this._options.anki;
- const noteTags = new Set();
- for (const {tags} of noteInfos) {
- for (const tag of tags) {
- noteTags.add(tag);
- }
- }
- if (displayTags === 'non-standard') {
- for (const tag of optionTags) {
- noteTags.delete(tag);
- }
- }
-
- if (noteTags.size > 0) {
- tagsIndicator.disabled = false;
- tagsIndicator.hidden = false;
- tagsIndicator.title = `Card tags: ${[...noteTags].join(', ')}`;
- }
- }
-
- _onShowTags(e) {
- e.preventDefault();
- const tags = e.currentTarget.title;
- this._showAnkiTagsNotification(tags);
- }
-
- _showAnkiTagsNotification(message) {
- if (this._ankiTagNotification === null) {
- const node = this._displayGenerator.createEmptyFooterNotification();
- node.classList.add('click-scannable');
- this._ankiTagNotification = new DisplayNotification(this._footerNotificationContainer, node);
- }
-
- this._ankiTagNotification.setContent(message);
- this._ankiTagNotification.open();
- }
-
_entrySetCurrent(index) {
const entryPre = this._getEntry(this._index);
if (entryPre !== null) {
@@ -1239,100 +1143,6 @@ class Display extends EventDispatcher {
}
}
- _tryAddAnkiNoteForSelectedEntry(mode) {
- this._addAnkiNote(this._index, mode);
- }
-
- _tryViewAnkiNoteForSelectedEntry() {
- const button = this._viewerButtonFind(this._index);
- if (button !== null && !button.disabled) {
- yomichan.api.noteView(button.dataset.noteId);
- }
- }
-
- async _addAnkiNote(dictionaryEntryIndex, mode) {
- if (dictionaryEntryIndex < 0 || dictionaryEntryIndex >= this._dictionaryEntries.length) { return; }
- const dictionaryEntry = this._dictionaryEntries[dictionaryEntryIndex];
-
- const button = this._adderButtonFind(dictionaryEntryIndex, mode);
- if (button === null || button.disabled) { return; }
-
- this._hideAnkiNoteErrors(true);
-
- const allErrors = [];
- const overrideToken = this._progressIndicatorVisible.setOverride(true);
- try {
- const {anki: {suspendNewCards}} = this._options;
- const noteContext = this._getNoteContext();
- const {note, errors} = await this._createNote(dictionaryEntry, mode, noteContext, true);
- allErrors.push(...errors);
-
- let noteId = null;
- let addNoteOkay = false;
- try {
- noteId = await yomichan.api.addAnkiNote(note);
- addNoteOkay = true;
- } catch (e) {
- allErrors.length = 0;
- allErrors.push(e);
- }
-
- if (addNoteOkay) {
- if (noteId === null) {
- allErrors.push(new Error('Note could not be added'));
- } else {
- if (suspendNewCards) {
- try {
- await yomichan.api.suspendAnkiCardsForNote(noteId);
- } catch (e) {
- allErrors.push(e);
- }
- }
- button.disabled = true;
- this._viewerButtonShow(dictionaryEntryIndex, noteId);
- }
- }
- } catch (e) {
- allErrors.push(e);
- } finally {
- this._progressIndicatorVisible.clearOverride(overrideToken);
- }
-
- if (allErrors.length > 0) {
- this._showAnkiNoteErrors(allErrors);
- } else {
- this._hideAnkiNoteErrors(true);
- }
- }
-
- _showAnkiNoteErrors(errors) {
- if (this._ankiNoteNotificationEventListeners !== null) {
- this._ankiNoteNotificationEventListeners.removeAllEventListeners();
- }
-
- if (this._ankiNoteNotification === null) {
- const node = this._displayGenerator.createEmptyFooterNotification();
- this._ankiNoteNotification = new DisplayNotification(this._footerNotificationContainer, node);
- this._ankiNoteNotificationEventListeners = new EventListenerCollection();
- }
-
- const content = this._displayGenerator.createAnkiNoteErrorsNotificationContent(errors);
- for (const node of content.querySelectorAll('.anki-note-error-log-link')) {
- this._ankiNoteNotificationEventListeners.addEventListener(node, 'click', () => {
- console.log({ankiNoteErrors: errors});
- }, false);
- }
-
- this._ankiNoteNotification.setContent(content);
- this._ankiNoteNotification.open();
- }
-
- _hideAnkiNoteErrors(animate) {
- if (this._ankiNoteNotification === null) { return; }
- this._ankiNoteNotification.close(animate);
- this._ankiNoteNotificationEventListeners.removeAllEventListeners();
- }
-
async _playAudioCurrent() {
await this._displayAudio.playAudio(this._index, 0);
}
@@ -1342,74 +1152,12 @@ class Display extends EventDispatcher {
return index >= 0 && index < entries.length ? entries[index] : null;
}
- _getValidSentenceData(sentence) {
- let {text, offset} = (isObject(sentence) ? sentence : {});
- if (typeof text !== 'string') { text = ''; }
- if (typeof offset !== 'number') { offset = 0; }
- return {text, offset};
- }
-
- _getClosestDictionaryEntryIndex(element) {
- return this._getClosestIndex(element, '.entry');
- }
-
- _getClosestIndex(element, selector) {
- const node = element.closest(selector);
- if (node === null) { return -1; }
- const index = parseInt(node.dataset.index, 10);
- return Number.isFinite(index) ? index : -1;
- }
-
- _adderButtonFind(index, mode) {
- const entry = this._getEntry(index);
- return entry !== null ? entry.querySelector(`.action-add-note[data-mode="${mode}"]`) : null;
- }
-
- _tagsIndicatorFind(index) {
- const entry = this._getEntry(index);
- return entry !== null ? entry.querySelector('.action-view-tags') : null;
- }
-
- _viewerButtonFind(index) {
- const entry = this._getEntry(index);
- return entry !== null ? entry.querySelector('.action-view-note') : null;
- }
-
- _viewerButtonShow(index, noteId) {
- const viewerButton = this._viewerButtonFind(index);
- if (viewerButton === null) {
- return;
- }
- viewerButton.disabled = false;
- viewerButton.hidden = false;
- viewerButton.dataset.noteId = noteId;
- }
-
_getElementTop(element) {
const elementRect = element.getBoundingClientRect();
const documentRect = this._contentScrollBodyElement.getBoundingClientRect();
return elementRect.top - documentRect.top;
}
- _getNoteContext() {
- const {state} = this._history;
- let {documentTitle, url, sentence} = (isObject(state) ? state : {});
- if (typeof documentTitle !== 'string') {
- documentTitle = document.title;
- }
- if (typeof url !== 'string') {
- url = window.location.href;
- }
- sentence = this._getValidSentenceData(sentence);
- return {
- url,
- sentence,
- documentTitle,
- query: this._query,
- fullQuery: this._fullQuery
- };
- }
-
_historyHasState() {
return isObject(this._history.state);
}
@@ -1464,158 +1212,6 @@ class Display extends EventDispatcher {
yomichan.trigger('closePopups');
}
- async _getAnkiFieldTemplates(options) {
- let templates = options.anki.fieldTemplates;
- if (typeof templates === 'string') { return templates; }
-
- templates = this._ankiFieldTemplatesDefault;
- if (typeof templates === 'string') { return templates; }
-
- templates = await yomichan.api.getDefaultAnkiFieldTemplates();
- this._ankiFieldTemplatesDefault = templates;
- return templates;
- }
-
- async _areDictionaryEntriesAddable(dictionaryEntries, modes, context, forceCanAddValue, fetchAdditionalInfo) {
- const modeCount = modes.length;
- const notePromises = [];
- for (const dictionaryEntry of dictionaryEntries) {
- for (const mode of modes) {
- const notePromise = this._createNote(dictionaryEntry, mode, context, false);
- notePromises.push(notePromise);
- }
- }
- const notes = (await Promise.all(notePromises)).map(({note}) => note);
-
- let infos;
- if (forceCanAddValue !== null) {
- if (!await yomichan.api.isAnkiConnected()) {
- throw new Error('Anki not connected');
- }
- infos = this._getAnkiNoteInfoForceValue(notes, forceCanAddValue);
- } else {
- infos = await yomichan.api.getAnkiNoteInfo(notes, fetchAdditionalInfo);
- }
-
- const results = [];
- for (let i = 0, ii = infos.length; i < ii; i += modeCount) {
- results.push(infos.slice(i, i + modeCount));
- }
- return results;
- }
-
- _getAnkiNoteInfoForceValue(notes, canAdd) {
- const results = [];
- for (const note of notes) {
- const valid = AnkiUtil.isNoteDataValid(note);
- results.push({canAdd, valid, noteIds: null});
- }
- return results;
- }
-
- async _createNote(dictionaryEntry, mode, context, injectMedia) {
- const options = this._options;
- const template = this._ankiFieldTemplates;
- const {
- general: {resultOutputMode, glossaryLayoutMode, compactTags},
- anki: ankiOptions
- } = options;
- const {tags, checkForDuplicates, duplicateScope} = ankiOptions;
- const modeOptions = (mode === 'kanji') ? ankiOptions.kanji : ankiOptions.terms;
- const {deck: deckName, model: modelName} = modeOptions;
- const fields = Object.entries(modeOptions.fields);
-
- const errors = [];
- let injectedMedia = null;
- if (injectMedia) {
- let errors2;
- ({result: injectedMedia, errors: errors2} = await this._injectAnkiNoteMedia(dictionaryEntry, options, fields));
- for (const error of errors2) {
- errors.push(deserializeError(error));
- }
- }
-
- const {note, errors: createNoteErrors} = await this._ankiNoteBuilder.createNote({
- dictionaryEntry,
- mode,
- context,
- template,
- deckName,
- modelName,
- fields,
- tags,
- checkForDuplicates,
- duplicateScope,
- resultOutputMode,
- glossaryLayoutMode,
- compactTags,
- injectedMedia,
- errors
- });
- errors.push(...createNoteErrors);
- return {note, errors};
- }
-
- async _injectAnkiNoteMedia(dictionaryEntry, options, fields) {
- const {anki: {screenshot: {format, quality}}} = options;
-
- const timestamp = Date.now();
-
- const dictionaryEntryDetails = this._getDictionaryEntryDetailsForNote(dictionaryEntry);
-
- const audioDetails = (
- dictionaryEntryDetails.type !== 'kanji' && AnkiUtil.fieldsObjectContainsMarker(fields, 'audio') ?
- this._displayAudio.getAnkiNoteMediaAudioDetails(dictionaryEntryDetails.term, dictionaryEntryDetails.reading) :
- null
- );
-
- const screenshotDetails = (
- AnkiUtil.fieldsObjectContainsMarker(fields, 'screenshot') && typeof this._contentOriginTabId === 'number' ?
- {tabId: this._contentOriginTabId, frameId: this._contentOriginFrameId, format, quality} :
- null
- );
-
- const clipboardDetails = {
- image: AnkiUtil.fieldsObjectContainsMarker(fields, 'clipboard-image'),
- text: AnkiUtil.fieldsObjectContainsMarker(fields, 'clipboard-text')
- };
-
- return await yomichan.api.injectAnkiNoteMedia(
- timestamp,
- dictionaryEntryDetails,
- audioDetails,
- screenshotDetails,
- clipboardDetails
- );
- }
-
- _getDictionaryEntryDetailsForNote(dictionaryEntry) {
- const {type} = dictionaryEntry;
- if (type === 'kanji') {
- const {character} = dictionaryEntry;
- return {type, character};
- }
-
- const {headwords} = dictionaryEntry;
- let bestIndex = -1;
- for (let i = 0, ii = headwords.length; i < ii; ++i) {
- const {term, reading, sources} = headwords[i];
- for (const {deinflectedText} of sources) {
- if (term === deinflectedText) {
- bestIndex = i;
- i = ii;
- break;
- } else if (reading === deinflectedText && bestIndex < 0) {
- bestIndex = i;
- break;
- }
- }
- }
-
- const {term, reading} = headwords[Math.max(0, bestIndex)];
- return {type, term, reading};
- }
-
async _setOptionsContextIfDifferent(optionsContext) {
if (deepEqual(this._optionsContext, optionsContext)) { return; }
await this.setOptionsContext(optionsContext);
@@ -1755,9 +1351,6 @@ class Display extends EventDispatcher {
_addEntryEventListeners(entry) {
this._eventListeners.addEventListener(entry, 'click', this._onEntryClick.bind(this));
- this._addMultipleEventListeners(entry, '.action-view-tags', 'click', this._onShowTags.bind(this));
- this._addMultipleEventListeners(entry, '.action-add-note', 'click', this._onNoteAdd.bind(this));
- this._addMultipleEventListeners(entry, '.action-view-note', 'click', this._onNoteView.bind(this));
this._addMultipleEventListeners(entry, '.headword-kanji-link', 'click', this._onKanjiLookup.bind(this));
this._addMultipleEventListeners(entry, '.debug-log-link', 'click', this._onDebugLogClick.bind(this));
this._addMultipleEventListeners(entry, '.tag-label', 'click', this._onTagClick.bind(this));
@@ -1924,58 +1517,13 @@ class Display extends EventDispatcher {
return typeof queryPostProcessor === 'function' ? queryPostProcessor(query) : query;
}
- _getModes(isTerms) {
- return isTerms ? ['term-kanji', 'term-kana'] : ['kanji'];
- }
-
async _logDictionaryEntryData(index) {
if (index < 0 || index >= this._dictionaryEntries.length) { return; }
const dictionaryEntry = this._dictionaryEntries[index];
const result = {dictionaryEntry};
- // Anki note data
- let ankiNoteData;
- let ankiNoteDataException;
- try {
- const context = this._getNoteContext();
- const {general: {resultOutputMode, glossaryLayoutMode, compactTags}} = this._options;
- ankiNoteData = await this._ankiNoteBuilder.getRenderingData({
- dictionaryEntry,
- mode: 'test',
- context,
- resultOutputMode,
- glossaryLayoutMode,
- compactTags,
- injectedMedia: null,
- marker: 'test'
- });
- } catch (e) {
- ankiNoteDataException = e;
- }
- result.ankiNoteData = ankiNoteData;
- if (typeof ankiNoteDataException !== 'undefined') {
- result.ankiNoteDataException = ankiNoteDataException;
- }
-
- // Anki notes
- const ankiNotes = [];
- const modes = this._getModes(dictionaryEntry.type === 'term');
- for (const mode of modes) {
- let note;
- let errors;
- try {
- const noteContext = this._getNoteContext();
- ({note: note, errors} = await this._createNote(dictionaryEntry, mode, noteContext, false));
- } catch (e) {
- errors = [e];
- }
- const entry = {mode, note};
- if (Array.isArray(errors) && errors.length > 0) {
- entry.errors = errors;
- }
- ankiNotes.push(entry);
- }
- result.ankiNotes = ankiNotes;
+ const result2 = await this._displayAnki.getLogData(dictionaryEntry);
+ Object.assign(result, result2);
console.log(result);
}
diff --git a/ext/popup.html b/ext/popup.html
index 3018f8bf..ae5685f5 100644
--- a/ext/popup.html
+++ b/ext/popup.html
@@ -100,6 +100,7 @@
<script src="/js/data/anki-note-builder.js"></script>
<script src="/js/data/anki-util.js"></script>
<script src="/js/display/display.js"></script>
+<script src="/js/display/display-anki.js"></script>
<script src="/js/display/display-audio.js"></script>
<script src="/js/display/display-generator.js"></script>
<script src="/js/display/display-history.js"></script>
diff --git a/ext/search.html b/ext/search.html
index 2e25620e..44d3c680 100644
--- a/ext/search.html
+++ b/ext/search.html
@@ -86,6 +86,7 @@
<script src="/js/data/anki-note-builder.js"></script>
<script src="/js/data/anki-util.js"></script>
<script src="/js/display/display.js"></script>
+<script src="/js/display/display-anki.js"></script>
<script src="/js/display/display-audio.js"></script>
<script src="/js/display/display-generator.js"></script>
<script src="/js/display/display-history.js"></script>