diff options
| -rw-r--r-- | ext/js/display/display-anki.js | 582 | ||||
| -rw-r--r-- | ext/js/display/display.js | 526 | ||||
| -rw-r--r-- | ext/popup.html | 1 | ||||
| -rw-r--r-- | ext/search.html | 1 | 
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> |