diff options
Diffstat (limited to 'ext/js')
| -rw-r--r-- | ext/js/comm/anki-connect.js | 13 | ||||
| -rw-r--r-- | ext/js/pages/settings/anki-controller.js | 16 | ||||
| -rw-r--r-- | ext/js/pages/settings/anki-deck-generator-controller.js | 587 | ||||
| -rw-r--r-- | ext/js/pages/settings/settings-main.js | 4 | 
4 files changed, 620 insertions, 0 deletions
| diff --git a/ext/js/comm/anki-connect.js b/ext/js/comm/anki-connect.js index 23183e79..446b2139 100644 --- a/ext/js/comm/anki-connect.js +++ b/ext/js/comm/anki-connect.js @@ -129,6 +129,19 @@ export class AnkiConnect {          return result;      } +    /** +     * @param {import('anki').Note[]} notes +     * @returns {Promise<?((number | null)[] | null)>} +     */ +    async addNotes(notes) { +        if (!this._enabled) { return null; } +        await this._checkVersion(); +        const result = await this._invoke('addNotes', {notes}); +        if (result !== null && !Array.isArray(result)) { +            throw this._createUnexpectedResultError('(number | null)[] | null', result); +        } +        return result; +    }      /**       * @param {import('anki').Note[]} notes diff --git a/ext/js/pages/settings/anki-controller.js b/ext/js/pages/settings/anki-controller.js index 9640ed62..7eb09c85 100644 --- a/ext/js/pages/settings/anki-controller.js +++ b/ext/js/pages/settings/anki-controller.js @@ -505,6 +505,22 @@ export class AnkiController {                  return null;          }      } + +    /** +     * @param {import('anki').Note[]} notes +     * @returns {Promise<?((number | null)[] | null)>} +     */ +    async addNotes(notes) { +        return await this._ankiConnect.addNotes(notes); +    } + +    /** +     * @param {import('anki').Note[]} notes +     * @returns {Promise<boolean[]>} +     */ +    async canAddNotes(notes) { +        return await this._ankiConnect.canAddNotes(notes); +    }  }  class AnkiCardController { diff --git a/ext/js/pages/settings/anki-deck-generator-controller.js b/ext/js/pages/settings/anki-deck-generator-controller.js new file mode 100644 index 00000000..c8b17742 --- /dev/null +++ b/ext/js/pages/settings/anki-deck-generator-controller.js @@ -0,0 +1,587 @@ +/* + * Copyright (C) 2023-2024  Yomitan 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/>. + */ + +import {ExtensionError} from '../../core/extension-error.js'; +import {log} from '../../core/log.js'; +import {toError} from '../../core/to-error.js'; +import {AnkiNoteBuilder} from '../../data/anki-note-builder.js'; +import {getDynamicTemplates} from '../../data/anki-template-util.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; +import {TemplateRendererProxy} from '../../templates/template-renderer-proxy.js'; + +export class AnkiDeckGeneratorController { +    /** +     * @param {import('../../application.js').Application} application +     * @param {import('./settings-controller.js').SettingsController} settingsController +     * @param {import('./modal-controller.js').ModalController} modalController +     * @param {import('./anki-controller.js').AnkiController} ankiController +     */ +    constructor(application, settingsController, modalController, ankiController) { +        /** @type {import('../../application.js').Application} */ +        this._application = application; +        /** @type {import('./settings-controller.js').SettingsController} */ +        this._settingsController = settingsController; +        /** @type {import('./modal-controller.js').ModalController} */ +        this._modalController = modalController; +        /** @type {import('./anki-controller.js').AnkiController} */ +        this._ankiController = ankiController; +        /** @type {?string} */ +        this._defaultFieldTemplates = null; +        /** @type {HTMLTextAreaElement} */ +        this._mainSettingsEntry = querySelectorNotNull(document, '#generate-anki-notes-main-settings-entry'); +        /** @type {HTMLTextAreaElement} */ +        this._wordInputTextarea = querySelectorNotNull(document, '#generate-anki-notes-textarea'); +        /** @type {HTMLInputElement} */ +        this._renderTextInput = querySelectorNotNull(document, '#generate-anki-notes-test-text-input'); +        /** @type {HTMLElement} */ +        this._renderResult = querySelectorNotNull(document, '#generate-anki-notes-render-result'); +        /** @type {HTMLElement} */ +        this._activeModelText = querySelectorNotNull(document, '#generate-anki-notes-active-model'); +        /** @type {HTMLElement} */ +        this._activeDeckText = querySelectorNotNull(document, '#generate-anki-notes-active-deck'); +        /** @type {HTMLInputElement} */ +        this._addMediaCheckbox = querySelectorNotNull(document, '#generate-anki-notes-add-media'); +        /** @type {HTMLInputElement} */ +        this._disallowDuplicatesCheckbox = querySelectorNotNull(document, '#generate-anki-notes-disallow-duplicates'); +        /** @type {string} */ +        this._activeNoteType = ''; +        /** @type {string} */ +        this._activeAnkiDeck = ''; +        /** @type {HTMLSpanElement} */ +        this._sendWordcount = querySelectorNotNull(document, '#generate-anki-notes-send-wordcount'); +        /** @type {HTMLSpanElement} */ +        this._exportWordcount = querySelectorNotNull(document, '#generate-anki-notes-export-wordcount'); +        /** @type {HTMLButtonElement} */ +        this._sendToAnkiButtonConfirmButton = querySelectorNotNull(document, '#generate-anki-notes-send-button-confirm'); +        /** @type {HTMLButtonElement} */ +        this._exportButtonConfirmButton = querySelectorNotNull(document, '#generate-anki-notes-export-button-confirm'); +        /** @type {NodeListOf<HTMLElement>} */ +        this._progressContainers = (document.querySelectorAll('.generate-anki-notes-progress')); +        /** @type {?import('./modal.js').Modal} */ +        this._sendToAnkiConfirmModal = null; +        /** @type {?import('./modal.js').Modal} */ +        this._exportConfirmModal = null; +        /** @type {boolean} */ +        this._cancel = false; +        /** @type {boolean} */ +        this._inProgress = false; +        /** @type {AnkiNoteBuilder} */ +        this._ankiNoteBuilder = new AnkiNoteBuilder(settingsController.application.api, new TemplateRendererProxy()); +    } + +    /** */ +    async prepare() { +        this._defaultFieldTemplates = await this._settingsController.application.api.getDefaultAnkiFieldTemplates(); + +        /** @type {HTMLButtonElement} */ +        const testRenderButton = querySelectorNotNull(document, '#generate-anki-notes-test-render-button'); +        /** @type {HTMLButtonElement} */ +        const sendToAnkiButton = querySelectorNotNull(document, '#generate-anki-notes-send-to-anki-button'); +        /** @type {HTMLButtonElement} */ +        const sendToAnkiCancelButton = querySelectorNotNull(document, '#generate-anki-notes-send-to-anki-cancel-button'); +        /** @type {HTMLButtonElement} */ +        const exportButton = querySelectorNotNull(document, '#generate-anki-notes-export-button'); +        /** @type {HTMLButtonElement} */ +        const exportCancelButton = querySelectorNotNull(document, '#generate-anki-notes-export-cancel-button'); +        /** @type {HTMLButtonElement} */ +        const generateButton = querySelectorNotNull(document, '#generate-anki-notes-export-button'); + +        this._sendToAnkiConfirmModal = this._modalController.getModal('generate-anki-notes-send-to-anki'); +        this._exportConfirmModal = this._modalController.getModal('generate-anki-notes-export'); + +        testRenderButton.addEventListener('click', this._onRender.bind(this), false); +        sendToAnkiButton.addEventListener('click', this._onSendToAnki.bind(this), false); +        this._sendToAnkiButtonConfirmButton.addEventListener('click', this._onSendToAnkiConfirm.bind(this), false); +        sendToAnkiCancelButton.addEventListener('click', (() => { this._cancel = true; }).bind(this), false); +        exportButton.addEventListener('click', this._onExport.bind(this), false); +        this._exportButtonConfirmButton.addEventListener('click', this._onExportConfirm.bind(this), false); +        exportCancelButton.addEventListener('click', (() => { this._cancel = true; }).bind(this), false); +        generateButton.addEventListener('click', this._onExport.bind(this), false); + +        void this._updateExampleText(); +        this._mainSettingsEntry.addEventListener('click', this._updateExampleText.bind(this), false); + +        void this._updateActiveModel(); +        this._mainSettingsEntry.addEventListener('click', this._updateActiveModel.bind(this), false); +    } + +    // Private + +    /** */ +    async _updateActiveModel() { +        const activeModelText = /** @type {HTMLElement} */ (this._activeModelText); +        const activeDeckText = /** @type {HTMLElement} */ (this._activeDeckText); +        const activeDeckTextConfirm = querySelectorNotNull(document, '#generate-anki-notes-active-deck-confirm'); +        const options = await this._settingsController.getOptions(); + +        this._activeNoteType = options.anki.terms.model; +        this._activeAnkiDeck = options.anki.terms.deck; +        activeModelText.textContent = this._activeNoteType; +        activeDeckText.textContent = this._activeAnkiDeck; +        activeDeckTextConfirm.textContent = this._activeAnkiDeck; +    } + +    /** */ +    async _resetState() { +        this._updateProgressBar(true, '', 0, 1, false); +        this._cancel = false; + +        this._exportButtonConfirmButton.disabled = false; +        this._exportWordcount.textContent = /** @type {HTMLTextAreaElement} */ (this._wordInputTextarea).value.split('\n').filter(Boolean).length.toString(); + +        this._sendToAnkiButtonConfirmButton.disabled = false; +        this._addMediaCheckbox.disabled = false; +        this._disallowDuplicatesCheckbox.disabled = false; +        this._sendWordcount.textContent = /** @type {HTMLTextAreaElement} */ (this._wordInputTextarea).value.split('\n').filter(Boolean).length.toString(); +    } + +    /** */ +    async _startGenerationState() { +        this._inProgress = true; + +        this._exportButtonConfirmButton.disabled = true; + +        this._sendToAnkiButtonConfirmButton.disabled = true; +        this._addMediaCheckbox.disabled = true; +        this._disallowDuplicatesCheckbox.disabled = true; +    } + +    /** */ +    async _endGenerationState() { +        this._inProgress = false; + +        if (this._exportConfirmModal !== null) { +            this._exportConfirmModal.setVisible(false); +        } + +        if (this._sendToAnkiConfirmModal !== null) { +            this._sendToAnkiConfirmModal.setVisible(false); +        } + +        this._updateProgressBar(false, '', 1, 1, false); +    } + +    /** */ +    async _endGenerationStateError() { +        this._inProgress = false; +    } + +    /** +     * @param {MouseEvent} e +     */ +    _onExport(e) { +        e.preventDefault(); +        if (this._exportConfirmModal !== null) { +            this._exportConfirmModal.setVisible(true); +            if (this._inProgress) { return; } +            void this._resetState(); +        } +    } + +    /** +     * @param {MouseEvent} e +     */ +    async _onExportConfirm(e) { +        e.preventDefault(); +        void this._startGenerationState(); +        const terms = /** @type {HTMLTextAreaElement} */ (this._wordInputTextarea).value.split('\n'); +        let ankiTSV = '#separator:tab\n#html:true\n#notetype column:1\n#deck column:2\n#tags column:3\n'; +        let index = 0; +        requestAnimationFrame(() => { +            this._updateProgressBar(true, 'Exporting to File...', 0, terms.length, true); +            setTimeout(async () => { +                for (const value of terms) { +                    if (!value) { continue; } +                    if (this._cancel) { +                        void this._endGenerationState(); +                        return; +                    } +                    const noteData = await this._generateNoteData(value, 'term-kanji', false); +                    if (noteData !== null) { +                        const fieldsTSV = this._fieldsToTSV(noteData.fields); +                        if (fieldsTSV) { +                            ankiTSV += this._activeNoteType + '\t'; +                            ankiTSV += this._activeAnkiDeck + '\t'; +                            ankiTSV += noteData.tags.join(' ') + '\t'; +                            ankiTSV += fieldsTSV; +                            ankiTSV += '\n'; +                        } +                    } +                    index++; +                    this._updateProgressBar(false, '', index, terms.length, true); +                } +                const today = new Date(); +                const fileName = 'anki-deck-' + today.toISOString().split('.')[0].replaceAll(/(T|:)/g, '-') + '.txt'; +                const blob = new Blob([ankiTSV], {type: 'application/octet-stream'}); +                this._saveBlob(blob, fileName); + +                void this._endGenerationState(); +            }, 1); +        }); +    } + +    /** +     * @param {MouseEvent} e +     */ +    _onSendToAnki(e) { +        e.preventDefault(); +        if (this._sendToAnkiConfirmModal !== null) { +            this._sendToAnkiConfirmModal.setVisible(true); +            if (this._inProgress) { return; } +            void this._resetState(); +        } +    } + +    /** +     * @param {MouseEvent} e +     */ +    async _onSendToAnkiConfirm(e) { +        e.preventDefault(); +        void this._startGenerationState(); +        const terms = /** @type {HTMLTextAreaElement} */ (this._wordInputTextarea).value.split('\n'); +        const addMedia = this._addMediaCheckbox.checked; +        const disallowDuplicates = this._disallowDuplicatesCheckbox.checked; +        /** @type {import("anki.js").Note[]} */ +        let notes = []; +        let index = 0; +        requestAnimationFrame(() => { +            this._updateProgressBar(true, 'Sending to Anki...', 0, terms.length, true); +            setTimeout(async () => { +                for (const value of terms) { +                    if (!value) { continue; } +                    if (this._cancel) { +                        void this._endGenerationState(); +                        return; +                    } +                    const noteData = await this._generateNoteData(value, 'term-kanji', addMedia); +                    if (noteData) { +                        notes.push(noteData); +                    } +                    if (notes.length >= 100) { +                        const sendNotesResult = await this._sendNotes(notes, disallowDuplicates); +                        if (sendNotesResult === false) { +                            void this._endGenerationStateError(); +                            return; +                        } +                        notes = []; +                    } +                    index++; +                    this._updateProgressBar(false, '', index, terms.length, true); +                } +                if (notes.length > 0) { +                    const sendNotesResult = await this._sendNotes(notes, disallowDuplicates); +                    if (sendNotesResult === false) { +                        void this._endGenerationStateError(); +                        return; +                    } +                } + +                void this._endGenerationState(); +            }, 1); +        }); +    } + +    /** +     * @param {import("anki.js").Note[]} notes +     * @param {boolean} disallowDuplicates +     * @returns {Promise<boolean>} +     */ +    async _sendNotes(notes, disallowDuplicates) { +        try { +            if (disallowDuplicates) { +                const duplicateNotes = await this._ankiController.canAddNotes(notes.map((note) => ({...note, options: {...note.options, allowDuplicate: false}}))); +                notes = notes.filter((_, i) => duplicateNotes[i]); +            } +            const addNotesResult = await this._ankiController.addNotes(notes); +            if (addNotesResult === null || addNotesResult.includes(null)) { +                this._updateProgressBarError('Ankiconnect error: Failed to add cards'); +                return false; +            } +        } catch (error) { +            if (error instanceof Error) { +                this._updateProgressBarError('Ankiconnect error: ' + error.message + ''); +                log.error(error); +                return false; +            } +        } +        return true; +    } + +    /** +     * @param {boolean} init +     * @param {string} text +     * @param {number} current +     * @param {number} end +     * @param {boolean} visible +     */ +    _updateProgressBar(init, text, current, end, visible) { +        if (!visible) { +            for (const progress of this._progressContainers) { progress.hidden = true; } +            return; +        } +        if (init) { +            for (const progress of this._progressContainers) { +                progress.hidden = false; +                for (const infoLabel of progress.querySelectorAll('.progress-info')) { +                    infoLabel.textContent = text; +                    infoLabel.classList.remove('danger-text'); +                } +            } +        } +        for (const progress of this._progressContainers) { +            /** @type {NodeListOf<HTMLElement>} */ +            const statusLabels = progress.querySelectorAll('.progress-status'); +            for (const statusLabel of statusLabels) { statusLabel.textContent = ((current / end) * 100).toFixed(0).toString() + '%'; } +            /** @type {NodeListOf<HTMLElement>} */ +            const progressBars = progress.querySelectorAll('.progress-bar'); +            for (const progressBar of progressBars) { progressBar.style.width = ((current / end) * 100).toString() + '%'; } +        } +    } + +    /** +     * @param {string} text +     */ +    _updateProgressBarError(text) { +        for (const progress of this._progressContainers) { +            progress.hidden = false; +            for (const infoLabel of progress.querySelectorAll('.progress-info')) { +                infoLabel.textContent = text; +                infoLabel.classList.add('danger-text'); +            } +        } +    } + +    /** +     * @param {HTMLElement} infoNode +     * @param {import('anki-templates-internal').CreateModeNoTest} mode +     * @param {boolean} showSuccessResult +     */ +    async _testNoteData(infoNode, mode, showSuccessResult) { +        /** @type {Error[]} */ +        const allErrors = []; +        const text = /** @type {HTMLInputElement} */ (this._renderTextInput).value; +        let result; +        try { +            const noteData = await this._generateNoteData(text, mode, false); +            result = noteData ? this._fieldsToTSV(noteData.fields) : `No definition found for ${text}`; +        } catch (e) { +            allErrors.push(toError(e)); +        } + +        /** +         * @param {Error} e +         * @returns {string} +         */ +        const errorToMessageString = (e) => { +            if (e instanceof ExtensionError) { +                const v = e.data; +                if (typeof v === 'object' && v !== null) { +                    const v2 = /** @type {import('core').UnknownObject} */ (v).error; +                    if (v2 instanceof Error) { +                        return v2.message; +                    } +                } +            } +            return e.message; +        }; + +        const hasError = allErrors.length > 0; +        infoNode.hidden = !(showSuccessResult || hasError); +        if (hasError || !result) { +            infoNode.textContent = allErrors.map(errorToMessageString).join('\n'); +        } else { +            infoNode.textContent = showSuccessResult ? result : ''; +        } +        infoNode.classList.toggle('text-danger', hasError); +    } + +    /** +     * @param {string} word +     * @param {import('anki-templates-internal').CreateModeNoTest} mode +     * @param {boolean} addMedia +     * @returns {Promise<?import('anki.js').Note>} +     */ +    async _generateNoteData(word, mode, addMedia) { +        const optionsContext = this._settingsController.getOptionsContext(); +        const data = await this._getDictionaryEntry(word, optionsContext); +        if (data === null) { +            return null; +        } +        const {dictionaryEntry, text: sentenceText} = data; +        const options = await this._settingsController.getOptions(); +        const context = { +            url: window.location.href, +            sentence: { +                text: sentenceText, +                offset: 0 +            }, +            documentTitle: document.title, +            query: sentenceText, +            fullQuery: sentenceText +        }; +        const template = this._getAnkiTemplate(options); +        const deckOptionsFields = options.anki.terms.fields; +        const {general: {resultOutputMode, glossaryLayoutMode, compactTags}} = options; +        const fields = []; +        for (const deckField in deckOptionsFields) { +            if (Object.prototype.hasOwnProperty.call(deckOptionsFields, deckField)) { +                fields.push([deckField, deckOptionsFields[deckField]]); +            } +        } +        const idleTimeout = (Number.isFinite(options.anki.downloadTimeout) && options.anki.downloadTimeout > 0 ? options.anki.downloadTimeout : null); +        const mediaOptions = addMedia ? {audio: {sources: options.audio.sources, preferredAudioIndex: null, idleTimeout: idleTimeout}} : null; +        const requirements = addMedia ? [...this._getDictionaryEntryMedia(dictionaryEntry), {type: 'audio'}] : []; +        const {note} = await this._ankiNoteBuilder.createNote(/** @type {import('anki-note-builder').CreateNoteDetails} */ ({ +            dictionaryEntry, +            mode, +            context, +            template, +            deckName: this._activeAnkiDeck, +            modelName: this._activeNoteType, +            fields: fields, +            resultOutputMode, +            glossaryLayoutMode, +            compactTags, +            tags: options.anki.tags, +            mediaOptions: mediaOptions, +            requirements: requirements, +            duplicateScope: options.anki.duplicateScope, +            duplicateScopeCheckAllModels: options.anki.duplicateScopeCheckAllModels +        })); +        return note; +    } + +    /** +     * @param {string} text +     * @param {import('settings').OptionsContext} optionsContext +     * @returns {Promise<?{dictionaryEntry: import('dictionary').TermDictionaryEntry, text: string}>} +     */ +    async _getDictionaryEntry(text, optionsContext) { +        const {dictionaryEntries} = await this._settingsController.application.api.termsFind(text, {}, optionsContext); +        if (dictionaryEntries.length === 0) { return null; } + +        return { +            dictionaryEntry: /** @type {import('dictionary').TermDictionaryEntry} */ (dictionaryEntries[0]), +            text: text +        }; +    } + +    /** +     * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry +     * @returns {Array<object>} +     */ +    _getDictionaryEntryMedia(dictionaryEntry) { +        const media = []; +        const definitions = dictionaryEntry.definitions; +        for (const definition of definitions) { +            const paths = this._findAllPaths(definition); +            for (const path of paths) { +                media.push({dictionary: definition.dictionary, path: path, type: 'dictionaryMedia'}); +            } +        } +        return media; +    } + +    /** +     * @param {object} obj +     * @returns {Array<string>} +     */ +    _findAllPaths(obj) { +        // @ts-expect-error - Recursive function to find object keys deeply nested in objects and arrays. Essentially impossible to type correctly. +        // eslint-disable-next-line unicorn/no-array-reduce, @typescript-eslint/no-unsafe-argument +        return Object.entries(obj).reduce((acc, [key, value]) => (key === 'path' ? [...acc, value] : (typeof value === 'object' ? [...acc, ...this._findAllPaths(value)] : acc)), []); +    } + +    /** +     * @param {import('settings').ProfileOptions} options +     * @returns {string} +     */ +    _getAnkiTemplate(options) { +        let staticTemplates = options.anki.fieldTemplates; +        if (typeof staticTemplates !== 'string') { staticTemplates = this._defaultFieldTemplates; } +        const dynamicTemplates = getDynamicTemplates(options); +        return staticTemplates + '\n' + dynamicTemplates; +    } + +    /** +     * @param {Event} e +     */ +    _onRender(e) { +        e.preventDefault(); + +        const infoNode = /** @type {HTMLElement} */ (this._renderResult); +        infoNode.hidden = true; +        void this._testNoteData(infoNode, 'term-kanji', true); +    } + +    /** */ +    async _updateExampleText() { +        this._languageSummaries = await this._application.api.getLanguageSummaries(); +        const options = await this._settingsController.getOptions(); +        const activeLanguage = /** @type {import('language').LanguageSummary} */ (this._languageSummaries.find(({iso}) => iso === options.general.language)); +        this._renderTextInput.lang = options.general.language; +        this._renderTextInput.value = activeLanguage.exampleText; +    } + +    /** +     * @param {import('anki.js').NoteFields} noteFields +     * @returns {string} +     */ +    _fieldsToTSV(noteFields) { +        let tsv = ''; +        for (const key in noteFields) { +            if (Object.prototype.hasOwnProperty.call(noteFields, key)) { +                tsv += noteFields[key].replaceAll('\t', '   ') + '\t'; +            } +        } +        return tsv; +    } + +    /** +     * @param {Blob} blob +     * @param {string} fileName +     */ +    _saveBlob(blob, fileName) { +        if ( +            typeof navigator === 'object' && navigator !== null && +            // @ts-expect-error - call for legacy Edge +            typeof navigator.msSaveBlob === 'function' && +            // @ts-expect-error - call for legacy Edge +            navigator.msSaveBlob(blob) +        ) { +            return; +        } + +        const blobUrl = URL.createObjectURL(blob); + +        const a = document.createElement('a'); +        a.href = blobUrl; +        a.download = fileName; +        a.rel = 'noopener'; +        a.target = '_blank'; + +        const revoke = () => { +            URL.revokeObjectURL(blobUrl); +            a.href = ''; +            this._settingsExportRevoke = null; +        }; +        this._settingsExportRevoke = revoke; + +        a.dispatchEvent(new MouseEvent('click')); +        setTimeout(revoke, 60000); +    } +} diff --git a/ext/js/pages/settings/settings-main.js b/ext/js/pages/settings/settings-main.js index df43df9b..3d3e2352 100644 --- a/ext/js/pages/settings/settings-main.js +++ b/ext/js/pages/settings/settings-main.js @@ -21,6 +21,7 @@ import {DocumentFocusController} from '../../dom/document-focus-controller.js';  import {querySelectorNotNull} from '../../dom/query-selector.js';  import {ExtensionContentController} from '../common/extension-content-controller.js';  import {AnkiController} from './anki-controller.js'; +import {AnkiDeckGeneratorController} from './anki-deck-generator-controller.js';  import {AnkiTemplatesController} from './anki-templates-controller.js';  import {AudioController} from './audio-controller.js';  import {BackupController} from './backup-controller.js'; @@ -117,6 +118,9 @@ await Application.main(true, async (application) => {      const ankiController = new AnkiController(settingsController);      preparePromises.push(ankiController.prepare()); +    const ankiDeckGeneratorController = new AnkiDeckGeneratorController(application, settingsController, modalController, ankiController); +    preparePromises.push(ankiDeckGeneratorController.prepare()); +      const ankiTemplatesController = new AnkiTemplatesController(settingsController, modalController, ankiController);      preparePromises.push(ankiTemplatesController.prepare()); |