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 ''; - } -} |