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 . + */ + +/* 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 @@ + 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 @@ +