diff options
Diffstat (limited to 'ext/js/settings/anki-controller.js')
| -rw-r--r-- | ext/js/settings/anki-controller.js | 729 | 
1 files changed, 0 insertions, 729 deletions
| diff --git a/ext/js/settings/anki-controller.js b/ext/js/settings/anki-controller.js deleted file mode 100644 index 26cab68f..00000000 --- a/ext/js/settings/anki-controller.js +++ /dev/null @@ -1,729 +0,0 @@ -/* - * Copyright (C) 2019-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 - * AnkiConnect - * AnkiNoteBuilder - * ObjectPropertyAccessor - * SelectorObserver - */ - -class AnkiController { -    constructor(settingsController) { -        this._settingsController = settingsController; -        this._ankiConnect = new AnkiConnect(); -        this._ankiNoteBuilder = new AnkiNoteBuilder(false); -        this._selectorObserver = new SelectorObserver({ -            selector: '.anki-card', -            ignoreSelector: null, -            onAdded: this._createCardController.bind(this), -            onRemoved: this._removeCardController.bind(this), -            isStale: this._isCardControllerStale.bind(this) -        }); -        this._stringComparer = new Intl.Collator(); // Locale does not matter -        this._getAnkiDataPromise = null; -        this._ankiErrorContainer = null; -        this._ankiErrorMessageNode = null; -        this._ankiErrorMessageNodeDefaultContent = ''; -        this._ankiErrorMessageDetailsNode = null; -        this._ankiErrorMessageDetailsContainer = null; -        this._ankiErrorMessageDetailsToggle = null; -        this._ankiErrorInvalidResponseInfo = null; -        this._ankiCardPrimary = null; -        this._ankiCardPrimaryType = null; -        this._validateFieldsToken = null; -    } - -    get settingsController() { -        return this._settingsController; -    } - -    async prepare() { -        this._ankiErrorContainer = document.querySelector('#anki-error'); -        this._ankiErrorMessageNode = document.querySelector('#anki-error-message'); -        this._ankiErrorMessageNodeDefaultContent = this._ankiErrorMessageNode.textContent; -        this._ankiErrorMessageDetailsNode = document.querySelector('#anki-error-message-details'); -        this._ankiErrorMessageDetailsContainer = document.querySelector('#anki-error-message-details-container'); -        this._ankiErrorMessageDetailsToggle = document.querySelector('#anki-error-message-details-toggle'); -        this._ankiErrorInvalidResponseInfo = document.querySelector('#anki-error-invalid-response-info'); -        this._ankiEnableCheckbox = document.querySelector('[data-setting="anki.enable"]'); -        this._ankiCardPrimary = document.querySelector('#anki-card-primary'); -        this._ankiCardPrimaryType = document.querySelector('#anki-card-primary-type'); - -        this._setupFieldMenus(); - -        this._ankiErrorMessageDetailsToggle.addEventListener('click', this._onAnkiErrorMessageDetailsToggleClick.bind(this), false); -        if (this._ankiEnableCheckbox !== null) { this._ankiEnableCheckbox.addEventListener('settingChanged', this._onAnkiEnableChanged.bind(this), false); } -        if (this._ankiCardPrimaryType !== null) { this._ankiCardPrimaryType.addEventListener('change', this._onAnkiCardPrimaryTypeChange.bind(this), false); } - -        const options = await this._settingsController.getOptions(); -        this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); -        this._onOptionsChanged({options}); -    } - -    getFieldMarkers(type) { -        switch (type) { -            case 'terms': -                return [ -                    'audio', -                    'clipboard-image', -                    'clipboard-text', -                    'cloze-body', -                    'cloze-prefix', -                    'cloze-suffix', -                    'conjugation', -                    'dictionary', -                    'document-title', -                    'expression', -                    'frequencies', -                    'furigana', -                    'furigana-plain', -                    'glossary', -                    'glossary-brief', -                    'glossary-no-dictionary', -                    'pitch-accents', -                    'pitch-accent-graphs', -                    'pitch-accent-positions', -                    'reading', -                    'screenshot', -                    'sentence', -                    'tags', -                    'url' -                ]; -            case 'kanji': -                return [ -                    'character', -                    'clipboard-image', -                    'clipboard-text', -                    'cloze-body', -                    'cloze-prefix', -                    'cloze-suffix', -                    'dictionary', -                    'document-title', -                    'glossary', -                    'kunyomi', -                    'onyomi', -                    'screenshot', -                    'sentence', -                    'stroke-count', -                    'tags', -                    'url' -                ]; -            default: -                return []; -        } -    } - -    getFieldMarkersHtml(markers) { -        const fragment = document.createDocumentFragment(); -        for (const marker of markers) { -            const markerNode = this._settingsController.instantiateTemplate('anki-card-field-marker'); -            markerNode.querySelector('.marker-link').textContent = marker; -            fragment.appendChild(markerNode); -        } -        return fragment; -    } - -    async getAnkiData() { -        let promise = this._getAnkiDataPromise; -        if (promise === null) { -            promise = this._getAnkiData(); -            this._getAnkiDataPromise = promise; -            promise.finally(() => { this._getAnkiDataPromise = null; }); -        } -        return promise; -    } - -    async getModelFieldNames(model) { -        return await this._ankiConnect.getModelFieldNames(model); -    } - -    getRequiredPermissions(fieldValue) { -        return this._settingsController.permissionsUtil.getRequiredPermissionsForAnkiFieldValue(fieldValue); -    } - -    containsAnyMarker(field) { -        return this._ankiNoteBuilder.containsAnyMarker(field); -    } - -    // Private - -    async _onOptionsChanged({options: {anki}}) { -        this._ankiConnect.server = anki.server; -        this._ankiConnect.enabled = anki.enable; - -        this._selectorObserver.disconnect(); -        this._selectorObserver.observe(document.documentElement, true); -    } - -    _onAnkiErrorMessageDetailsToggleClick() { -        const node = this._ankiErrorMessageDetailsContainer; -        node.hidden = !node.hidden; -    } - -    _onAnkiEnableChanged({detail: {value}}) { -        if (this._ankiConnect.server === null) { return; } -        this._ankiConnect.enabled = value; - -        for (const cardController of this._selectorObserver.datas()) { -            cardController.updateAnkiState(); -        } -    } - -    _onAnkiCardPrimaryTypeChange(e) { -        if (this._ankiCardPrimary === null) { return; } -        const node = e.currentTarget; -        let ankiCardMenu; -        if (node.selectedIndex >= 0) { -            const option = node.options[node.selectedIndex]; -            ankiCardMenu = option.dataset.ankiCardMenu; -        } - -        this._ankiCardPrimary.dataset.ankiCardType = node.value; -        if (typeof ankiCardMenu !== 'undefined') { -            this._ankiCardPrimary.dataset.ankiCardMenu = ankiCardMenu; -        } else { -            delete this._ankiCardPrimary.dataset.ankiCardMenu; -        } -    } - -    _createCardController(node) { -        const cardController = new AnkiCardController(this._settingsController, this, node); -        cardController.prepare(); -        return cardController; -    } - -    _removeCardController(node, cardController) { -        cardController.cleanup(); -    } - -    _isCardControllerStale(node, cardController) { -        return cardController.isStale(); -    } - -    _setupFieldMenus() { -        const fieldMenuTargets = [ -            [['terms'], '#anki-card-terms-field-menu-template'], -            [['kanji'], '#anki-card-kanji-field-menu-template'], -            [['terms', 'kanji'], '#anki-card-all-field-menu-template'] -        ]; -        for (const [types, selector] of fieldMenuTargets) { -            const element = document.querySelector(selector); -            if (element === null) { continue; } - -            let markers = []; -            for (const type of types) { -                markers.push(...this.getFieldMarkers(type)); -            } -            markers = [...new Set(markers)]; - -            const container = element.content.querySelector('.popup-menu-body'); -            if (container === null) { return; } - -            const fragment = document.createDocumentFragment(); -            for (const marker of markers) { -                const option = document.createElement('button'); -                option.textContent = marker; -                option.className = 'popup-menu-item'; -                option.dataset.menuAction = 'setFieldMarker'; -                option.dataset.marker = marker; -                fragment.appendChild(option); -            } -            container.appendChild(fragment); -        } -    } - -    async _getAnkiData() { -        this._setAnkiStatusChanging(); -        const [ -            [deckNames, error1], -            [modelNames, error2] -        ] = await Promise.all([ -            this._getDeckNames(), -            this._getModelNames() -        ]); - -        if (error1 !== null) { -            this._showAnkiError(error1); -        } else if (error2 !== null) { -            this._showAnkiError(error2); -        } else { -            this._hideAnkiError(); -        } - -        return {deckNames, modelNames}; -    } - -    async _getDeckNames() { -        try { -            const result = await this._ankiConnect.getDeckNames(); -            this._sortStringArray(result); -            return [result, null]; -        } catch (e) { -            return [[], e]; -        } -    } - -    async _getModelNames() { -        try { -            const result = await this._ankiConnect.getModelNames(); -            this._sortStringArray(result); -            return [result, null]; -        } catch (e) { -            return [[], e]; -        } -    } - -    _setAnkiStatusChanging() { -        this._ankiErrorMessageNode.textContent = this._ankiErrorMessageNodeDefaultContent; -        this._ankiErrorMessageNode.classList.remove('danger-text'); -    } - -    _hideAnkiError() { -        if (this._ankiErrorContainer !== null) { -            this._ankiErrorContainer.hidden = true; -        } -        this._ankiErrorMessageDetailsContainer.hidden = true; -        this._ankiErrorMessageDetailsToggle.hidden = true; -        this._ankiErrorInvalidResponseInfo.hidden = true; -        this._ankiErrorMessageNode.textContent = (this._ankiConnect.enabled ? 'Connected' : 'Not enabled'); -        this._ankiErrorMessageNode.classList.remove('danger-text'); -        this._ankiErrorMessageDetailsNode.textContent = ''; -    } - -    _showAnkiError(error) { -        let errorString = typeof error === 'object' && error !== null ? error.message : null; -        if (!errorString) { errorString = `${error}`; } -        if (!/[.!?]$/.test(errorString)) { errorString += '.'; } -        this._ankiErrorMessageNode.textContent = errorString; -        this._ankiErrorMessageNode.classList.add('danger-text'); - -        const data = error.data; -        let details = ''; -        if (typeof data !== 'undefined') { -            details += `${JSON.stringify(data, null, 4)}\n\n`; -        } -        details += `${error.stack}`.trimRight(); -        this._ankiErrorMessageDetailsNode.textContent = details; - -        if (this._ankiErrorContainer !== null) { -            this._ankiErrorContainer.hidden = false; -        } -        this._ankiErrorMessageDetailsContainer.hidden = true; -        this._ankiErrorInvalidResponseInfo.hidden = (errorString.indexOf('Invalid response') < 0); -        this._ankiErrorMessageDetailsToggle.hidden = false; -    } - -    _sortStringArray(array) { -        const stringComparer = this._stringComparer; -        array.sort((a, b) => stringComparer.compare(a, b)); -    } -} - -class AnkiCardController { -    constructor(settingsController, ankiController, node) { -        this._settingsController = settingsController; -        this._ankiController = ankiController; -        this._node = node; -        this._cardType = node.dataset.ankiCardType; -        this._cardMenu = node.dataset.ankiCardMenu; -        this._eventListeners = new EventListenerCollection(); -        this._fieldEventListeners = new EventListenerCollection(); -        this._deck = null; -        this._model = null; -        this._fields = null; -        this._modelChangingTo = null; -        this._ankiCardDeckSelect = null; -        this._ankiCardModelSelect = null; -        this._ankiCardFieldsContainer = null; -        this._cleaned = false; -        this._fieldEntries = []; -    } - -    async prepare() { -        const options = await this._settingsController.getOptions(); -        const ankiOptions = options.anki; -        if (this._cleaned) { return; } - -        const cardOptions = this._getCardOptions(ankiOptions, this._cardType); -        if (cardOptions === null) { return; } -        const {deck, model, fields} = cardOptions; -        this._deck = deck; -        this._model = model; -        this._fields = fields; - -        this._ankiCardDeckSelect = this._node.querySelector('.anki-card-deck'); -        this._ankiCardModelSelect = this._node.querySelector('.anki-card-model'); -        this._ankiCardFieldsContainer = this._node.querySelector('.anki-card-fields'); - -        this._setupSelects([], []); -        this._setupFields(); - -        this._eventListeners.addEventListener(this._ankiCardDeckSelect, 'change', this._onCardDeckChange.bind(this), false); -        this._eventListeners.addEventListener(this._ankiCardModelSelect, 'change', this._onCardModelChange.bind(this), false); -        this._eventListeners.on(this._settingsController, 'permissionsChanged', this._onPermissionsChanged.bind(this)); - -        await this.updateAnkiState(); -    } - -    cleanup() { -        this._cleaned = true; -        this._fieldEntries = []; -        this._eventListeners.removeAllEventListeners(); -    } - -    async updateAnkiState() { -        if (this._fields === null) { return; } -        const {deckNames, modelNames} = await this._ankiController.getAnkiData(); -        if (this._cleaned) { return; } -        this._setupSelects(deckNames, modelNames); -    } - -    isStale() { -        return (this._cardType !== this._node.dataset.ankiCardType); -    } - -    // Private - -    _onCardDeckChange(e) { -        this._setDeck(e.currentTarget.value); -    } - -    _onCardModelChange(e) { -        this._setModel(e.currentTarget.value); -    } - -    _onFieldChange(index, e) { -        const node = e.currentTarget; -        this._validateFieldPermissions(node, index, true); -        this._validateField(node, index); -    } - -    _onFieldInput(index, e) { -        const node = e.currentTarget; -        this._validateField(node, index); -    } - -    _onFieldSettingChanged(index, e) { -        const node = e.currentTarget; -        this._validateFieldPermissions(node, index, false); -    } - -    _onFieldMenuClose({currentTarget: button, detail: {action, item}}) { -        switch (action) { -            case 'setFieldMarker': -                this._setFieldMarker(button, item.dataset.marker); -                break; -        } -    } - -    _onFieldMarkerLinkClick(e) { -        e.preventDefault(); -        const link = e.currentTarget; -        this._setFieldMarker(link, link.textContent); -    } - -    _validateField(node, index) { -        let valid = (node.dataset.hasPermissions !== 'false'); -        if (valid && index === 0 && !this._ankiController.containsAnyMarker(node.value)) { -            valid = false; -        } -        node.dataset.invalid = `${!valid}`; -    } - -    _setFieldMarker(element, marker) { -        const input = element.closest('.anki-card-field-value-container').querySelector('.anki-card-field-value'); -        input.value = `{${marker}}`; -        input.dispatchEvent(new Event('change')); -    } - -    _getCardOptions(ankiOptions, cardType) { -        switch (cardType) { -            case 'terms': return ankiOptions.terms; -            case 'kanji': return ankiOptions.kanji; -            default: return null; -        } -    } - -    _setupSelects(deckNames, modelNames) { -        const deck = this._deck; -        const model = this._model; -        if (!deckNames.includes(deck)) { deckNames = [...deckNames, deck]; } -        if (!modelNames.includes(model)) { modelNames = [...modelNames, model]; } - -        this._setSelectOptions(this._ankiCardDeckSelect, deckNames); -        this._ankiCardDeckSelect.value = deck; - -        this._setSelectOptions(this._ankiCardModelSelect, modelNames); -        this._ankiCardModelSelect.value = model; -    } - -    _setSelectOptions(select, optionValues) { -        const fragment = document.createDocumentFragment(); -        for (const optionValue of optionValues) { -            const option = document.createElement('option'); -            option.value = optionValue; -            option.textContent = optionValue; -            fragment.appendChild(option); -        } -        select.textContent = ''; -        select.appendChild(fragment); -    } - -    _setupFields() { -        this._fieldEventListeners.removeAllEventListeners(); - -        const markers = this._ankiController.getFieldMarkers(this._cardType); -        const totalFragment = document.createDocumentFragment(); -        this._fieldEntries = []; -        let index = 0; -        for (const [fieldName, fieldValue] of Object.entries(this._fields)) { -            const content = this._settingsController.instantiateTemplateFragment('anki-card-field'); - -            const fieldNameContainerNode = content.querySelector('.anki-card-field-name-container'); -            fieldNameContainerNode.dataset.index = `${index}`; -            const fieldNameNode = content.querySelector('.anki-card-field-name'); -            fieldNameNode.textContent = fieldName; - -            const valueContainer = content.querySelector('.anki-card-field-value-container'); -            valueContainer.dataset.index = `${index}`; - -            const inputField = content.querySelector('.anki-card-field-value'); -            inputField.value = fieldValue; -            inputField.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields', fieldName]); -            this._validateFieldPermissions(inputField, index, false); - -            this._fieldEventListeners.addEventListener(inputField, 'change', this._onFieldChange.bind(this, index), false); -            this._fieldEventListeners.addEventListener(inputField, 'input', this._onFieldInput.bind(this, index), false); -            this._fieldEventListeners.addEventListener(inputField, 'settingChanged', this._onFieldSettingChanged.bind(this, index), false); -            this._validateField(inputField, index); - -            const markerList = content.querySelector('.anki-card-field-marker-list'); -            if (markerList !== null) { -                const markersFragment = this._ankiController.getFieldMarkersHtml(markers); -                for (const element of markersFragment.querySelectorAll('.marker-link')) { -                    this._fieldEventListeners.addEventListener(element, 'click', this._onFieldMarkerLinkClick.bind(this), false); -                } -                markerList.appendChild(markersFragment); -            } - -            const menuButton = content.querySelector('.anki-card-field-value-menu-button'); -            if (menuButton !== null) { -                if (typeof this._cardMenu !== 'undefined') { -                    menuButton.dataset.menu = this._cardMenu; -                } else { -                    delete menuButton.dataset.menu; -                } -                this._fieldEventListeners.addEventListener(menuButton, 'menuClose', this._onFieldMenuClose.bind(this), false); -            } - -            totalFragment.appendChild(content); -            this._fieldEntries.push({fieldName, inputField, fieldNameContainerNode}); - -            ++index; -        } - -        const ELEMENT_NODE = Node.ELEMENT_NODE; -        const container = this._ankiCardFieldsContainer; -        for (const node of [...container.childNodes]) { -            if (node.nodeType === ELEMENT_NODE && node.dataset.persistent === 'true') { continue; } -            container.removeChild(node); -        } -        container.appendChild(totalFragment); - -        this._validateFields(); -    } - -    async _validateFields() { -        const token = {}; -        this._validateFieldsToken = token; - -        let fieldNames; -        try { -            fieldNames = await this._ankiController.getModelFieldNames(this._model); -        } catch (e) { -            return; -        } - -        if (token !== this._validateFieldsToken) { return; } - -        const fieldNamesSet = new Set(fieldNames); -        let index = 0; -        for (const {fieldName, fieldNameContainerNode} of this._fieldEntries) { -            fieldNameContainerNode.dataset.invalid = `${!fieldNamesSet.has(fieldName)}`; -            fieldNameContainerNode.dataset.orderMatches = `${index < fieldNames.length && fieldName === fieldNames[index]}`; -            ++index; -        } -    } - -    async _setDeck(value) { -        if (this._deck === value) { return; } -        this._deck = value; - -        await this._settingsController.modifyProfileSettings([{ -            action: 'set', -            path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'deck']), -            value -        }]); -    } - -    async _setModel(value) { -        if (this._modelChangingTo !== null) { -            // Revert -            this._ankiCardModelSelect.value = this._modelChangingTo; -            return; -        } -        if (this._model === value) { return; } - -        let fieldNames; -        let options; -        try { -            this._modelChangingTo = value; -            fieldNames = await this._ankiController.getModelFieldNames(value); -            options = await this._ankiController.settingsController.getOptions(); -        } catch (e) { -            // Revert -            this._ankiCardModelSelect.value = this._model; -            return; -        } finally { -            this._modelChangingTo = null; -        } - -        const cardType = this._cardType; -        const cardOptions = this._getCardOptions(options.anki, cardType); -        const oldFields = cardOptions !== null ? cardOptions.fields : null; - -        const fields = {}; -        for (let i = 0, ii = fieldNames.length; i < ii; ++i) { -            const fieldName = fieldNames[i]; -            fields[fieldName] = this._getDefaultFieldValue(fieldName, i, cardType, oldFields); -        } - -        const targets = [ -            { -                action: 'set', -                path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'model']), -                value -            }, -            { -                action: 'set', -                path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields']), -                value: fields -            } -        ]; - -        this._model = value; -        this._fields = fields; - -        await this._settingsController.modifyProfileSettings(targets); - -        this._setupFields(); -    } - -    async _requestPermissions(permissions) { -        try { -            await this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true); -        } catch (e) { -            log.error(e); -        } -    } - -    async _validateFieldPermissions(node, index, request) { -        const fieldValue = node.value; -        const permissions = this._ankiController.getRequiredPermissions(fieldValue); -        if (permissions.length > 0) { -            node.dataset.requiredPermission = permissions.join(' '); -            const hasPermissions = await ( -                request ? -                this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true) : -                this._settingsController.permissionsUtil.hasPermissions({permissions}) -            ); -            node.dataset.hasPermissions = `${hasPermissions}`; -        } else { -            delete node.dataset.requiredPermission; -            delete node.dataset.hasPermissions; -        } - -        this._validateField(node, index); -    } - -    _onPermissionsChanged({permissions: {permissions}}) { -        const permissionsSet = new Set(permissions); -        for (let i = 0, ii = this._fieldEntries.length; i < ii; ++i) { -            const {inputField} = this._fieldEntries[i]; -            let {requiredPermission} = inputField.dataset; -            if (typeof requiredPermission !== 'string') { continue; } -            requiredPermission = (requiredPermission.length === 0 ? [] : requiredPermission.split(' ')); - -            let hasPermissions = true; -            for (const permission of requiredPermission) { -                if (!permissionsSet.has(permission)) { -                    hasPermissions = false; -                    break; -                } -            } - -            inputField.dataset.hasPermissions = `${hasPermissions}`; -            this._validateField(inputField, i); -        } -    } - -    _getDefaultFieldValue(fieldName, index, cardType, oldFields) { -        if ( -            typeof oldFields === 'object' && -            oldFields !== null && -            Object.prototype.hasOwnProperty.call(oldFields, fieldName) -        ) { -            return oldFields[fieldName]; -        } - -        if (index === 0) { -            return (cardType === 'kanji' ? '{character}' : '{expression}'); -        } - -        const markers = this._ankiController.getFieldMarkers(cardType); -        const markerAliases = new Map([ -            ['glossary', ['definition', 'meaning']], -            ['audio', ['sound']], -            ['dictionary', ['dict']] -        ]); - -        const hyphenPattern = /-/g; -        for (const marker of markers) { -            const names = [marker]; -            const aliases = markerAliases.get(marker); -            if (typeof aliases !== 'undefined') { -                names.push(...aliases); -            } - -            let pattern = '^(?:'; -            for (let i = 0, ii = names.length; i < ii; ++i) { -                const name = names[i]; -                if (i > 0) { pattern += '|'; } -                pattern += name.replace(hyphenPattern, '[-_ ]*'); -            } -            pattern += ')$'; -            pattern = new RegExp(pattern, 'i'); - -            if (pattern.test(fieldName)) { -                return `{${marker}}`; -            } -        } - -        return ''; -    } -} |