diff options
Diffstat (limited to 'ext/bg/js/settings')
24 files changed, 0 insertions, 6327 deletions
diff --git a/ext/bg/js/settings/anki-controller.js b/ext/bg/js/settings/anki-controller.js deleted file mode 100644 index db3e3c14..00000000 --- a/ext/bg/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) { - yomichan.logError(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 ''; - } -} diff --git a/ext/bg/js/settings/anki-templates-controller.js b/ext/bg/js/settings/anki-templates-controller.js deleted file mode 100644 index 31bd1e92..00000000 --- a/ext/bg/js/settings/anki-templates-controller.js +++ /dev/null @@ -1,228 +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 - * AnkiNoteBuilder - * api - */ - -class AnkiTemplatesController { - constructor(settingsController, modalController, ankiController) { - this._settingsController = settingsController; - this._modalController = modalController; - this._ankiController = ankiController; - this._cachedDefinitionValue = null; - this._cachedDefinitionText = null; - this._defaultFieldTemplates = null; - this._fieldTemplatesTextarea = null; - this._compileResultInfo = null; - this._renderFieldInput = null; - this._renderResult = null; - this._fieldTemplateResetModal = null; - this._ankiNoteBuilder = new AnkiNoteBuilder(true); - } - - async prepare() { - this._defaultFieldTemplates = await api.getDefaultAnkiFieldTemplates(); - - this._fieldTemplatesTextarea = document.querySelector('#anki-card-templates-textarea'); - this._compileResultInfo = document.querySelector('#anki-card-templates-compile-result'); - this._renderFieldInput = document.querySelector('#anki-card-templates-test-field-input'); - this._renderTextInput = document.querySelector('#anki-card-templates-test-text-input'); - this._renderResult = document.querySelector('#anki-card-templates-render-result'); - const menuButton = document.querySelector('#anki-card-templates-test-field-menu-button'); - const testRenderButton = document.querySelector('#anki-card-templates-test-render-button'); - const resetButton = document.querySelector('#anki-card-templates-reset-button'); - const resetConfirmButton = document.querySelector('#anki-card-templates-reset-button-confirm'); - const fieldList = document.querySelector('#anki-card-templates-field-list'); - this._fieldTemplateResetModal = this._modalController.getModal('anki-card-templates-reset'); - - const markers = new Set([ - ...this._ankiController.getFieldMarkers('terms'), - ...this._ankiController.getFieldMarkers('kanji') - ]); - - if (fieldList !== null) { - const fragment = this._ankiController.getFieldMarkersHtml(markers); - fieldList.appendChild(fragment); - for (const node of fieldList.querySelectorAll('.marker-link')) { - node.addEventListener('click', this._onMarkerClicked.bind(this), false); - } - } - - this._fieldTemplatesTextarea.addEventListener('change', this._onChanged.bind(this), false); - testRenderButton.addEventListener('click', this._onRender.bind(this), false); - resetButton.addEventListener('click', this._onReset.bind(this), false); - resetConfirmButton.addEventListener('click', this._onResetConfirm.bind(this), false); - if (menuButton !== null) { - menuButton.addEventListener('menuClose', this._onFieldMenuClose.bind(this), false); - } - - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - - const options = await this._settingsController.getOptions(); - this._onOptionsChanged({options}); - } - - // Private - - _onOptionsChanged({options}) { - let templates = options.anki.fieldTemplates; - if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; } - this._fieldTemplatesTextarea.value = templates; - - this._onValidateCompile(); - } - - _onReset(e) { - e.preventDefault(); - this._fieldTemplateResetModal.setVisible(true); - } - - _onResetConfirm(e) { - e.preventDefault(); - - this._fieldTemplateResetModal.setVisible(false); - - const value = this._defaultFieldTemplates; - - this._fieldTemplatesTextarea.value = value; - this._fieldTemplatesTextarea.dispatchEvent(new Event('change')); - } - - async _onChanged(e) { - // Get value - let templates = e.currentTarget.value; - if (templates === this._defaultFieldTemplates) { - // Default - templates = null; - } - - // Overwrite - await this._settingsController.setProfileSetting('anki.fieldTemplates', templates); - - // Compile - this._onValidateCompile(); - } - - _onValidateCompile() { - this._validate(this._compileResultInfo, '{expression}', 'term-kanji', false, true); - } - - _onMarkerClicked(e) { - e.preventDefault(); - this._renderFieldInput.value = `{${e.target.textContent}}`; - } - - _onRender(e) { - e.preventDefault(); - - const field = this._renderFieldInput.value; - const infoNode = this._renderResult; - infoNode.hidden = true; - this._cachedDefinitionText = null; - this._validate(infoNode, field, 'term-kanji', true, false); - } - - _onFieldMenuClose({currentTarget: button, detail: {action, item}}) { - switch (action) { - case 'setFieldMarker': - this._setFieldMarker(button, item.dataset.marker); - break; - } - } - - _setFieldMarker(element, marker) { - const input = this._renderFieldInput; - input.value = `{${marker}}`; - input.dispatchEvent(new Event('change')); - } - - async _getDefinition(text, optionsContext) { - if (this._cachedDefinitionText !== text) { - const {definitions} = await api.termsFind(text, {}, optionsContext); - if (definitions.length === 0) { return null; } - - this._cachedDefinitionValue = definitions[0]; - this._cachedDefinitionText = text; - } - return this._cachedDefinitionValue; - } - - async _validate(infoNode, field, mode, showSuccessResult, invalidateInput) { - const text = this._renderTextInput.value || ''; - const errors = []; - let result = `No definition found for ${text}`; - try { - const optionsContext = this._settingsController.getOptionsContext(); - const definition = await this._getDefinition(text, optionsContext); - if (definition !== null) { - const options = await this._settingsController.getOptions(); - const context = { - url: window.location.href, - sentence: {text: definition.rawSource, offset: 0}, - documentTitle: document.title - }; - let templates = options.anki.fieldTemplates; - if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; } - const {general: {resultOutputMode, glossaryLayoutMode, compactTags}} = options; - const note = await this._ankiNoteBuilder.createNote({ - definition, - mode, - context, - templates, - deckName: '', - modelName: '', - fields: [ - ['field', field] - ], - resultOutputMode, - glossaryLayoutMode, - compactTags, - errors - }); - result = note.fields.field; - } - } catch (e) { - errors.push(e); - } - - const errorToMessageString = (e) => { - if (isObject(e)) { - let v = e.data; - if (isObject(v)) { - v = v.error; - if (isObject(v)) { - e = v; - } - } - - v = e.message; - if (typeof v === 'string') { return v; } - } - return `${e}`; - }; - - const hasError = errors.length > 0; - infoNode.hidden = !(showSuccessResult || hasError); - infoNode.textContent = hasError ? errors.map(errorToMessageString).join('\n') : (showSuccessResult ? result : ''); - infoNode.classList.toggle('text-danger', hasError); - if (invalidateInput) { - this._fieldTemplatesTextarea.dataset.invalid = `${hasError}`; - } - } -} diff --git a/ext/bg/js/settings/audio-controller.js b/ext/bg/js/settings/audio-controller.js deleted file mode 100644 index e62383a8..00000000 --- a/ext/bg/js/settings/audio-controller.js +++ /dev/null @@ -1,234 +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 - * AudioSystem - */ - -class AudioController { - constructor(settingsController) { - this._settingsController = settingsController; - this._audioSystem = new AudioSystem(); - this._audioSourceContainer = null; - this._audioSourceAddButton = null; - this._audioSourceEntries = []; - this._ttsVoiceTestTextInput = null; - } - - async prepare() { - this._audioSystem.prepare(); - - this._ttsVoiceTestTextInput = document.querySelector('#text-to-speech-voice-test-text'); - this._audioSourceContainer = document.querySelector('#audio-source-list'); - this._audioSourceAddButton = document.querySelector('#audio-source-add'); - this._audioSourceContainer.textContent = ''; - - this._audioSourceAddButton.addEventListener('click', this._onAddAudioSource.bind(this), false); - - if (typeof speechSynthesis !== 'undefined') { - speechSynthesis.addEventListener('voiceschanged', this._updateTextToSpeechVoices.bind(this), false); - } - this._updateTextToSpeechVoices(); - - document.querySelector('#text-to-speech-voice-test').addEventListener('click', this._onTestTextToSpeech.bind(this), false); - - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - - const options = await this._settingsController.getOptions(); - this._onOptionsChanged({options}); - } - - // Private - - _onOptionsChanged({options}) { - for (let i = this._audioSourceEntries.length - 1; i >= 0; --i) { - this._cleanupAudioSourceEntry(i); - } - - for (const audioSource of options.audio.sources) { - this._createAudioSourceEntry(audioSource); - } - } - - _onTestTextToSpeech() { - try { - const text = this._ttsVoiceTestTextInput.value || ''; - const voiceUri = document.querySelector('[data-setting="audio.textToSpeechVoice"]').value; - - const audio = this._audioSystem.createTextToSpeechAudio(text, voiceUri); - audio.volume = 1.0; - audio.play(); - } catch (e) { - // NOP - } - } - - _updateTextToSpeechVoices() { - const voices = ( - typeof speechSynthesis !== 'undefined' ? - [...speechSynthesis.getVoices()].map((voice, index) => ({ - voice, - isJapanese: this._languageTagIsJapanese(voice.lang), - index - })) : - [] - ); - voices.sort(this._textToSpeechVoiceCompare.bind(this)); - - for (const select of document.querySelectorAll('[data-setting="audio.textToSpeechVoice"]')) { - const fragment = document.createDocumentFragment(); - - let option = document.createElement('option'); - option.value = ''; - option.textContent = 'None'; - fragment.appendChild(option); - - for (const {voice} of voices) { - option = document.createElement('option'); - option.value = voice.voiceURI; - option.textContent = `${voice.name} (${voice.lang})`; - fragment.appendChild(option); - } - - select.textContent = ''; - select.appendChild(fragment); - } - } - - _textToSpeechVoiceCompare(a, b) { - if (a.isJapanese) { - if (!b.isJapanese) { return -1; } - } else { - if (b.isJapanese) { return 1; } - } - - if (a.voice.default) { - if (!b.voice.default) { return -1; } - } else { - if (b.voice.default) { return 1; } - } - - return a.index - b.index; - } - - _languageTagIsJapanese(languageTag) { - return ( - languageTag.startsWith('ja_') || - languageTag.startsWith('ja-') || - languageTag.startsWith('jpn-') - ); - } - - _getUnusedAudioSource() { - const audioSourcesAvailable = [ - 'jpod101', - 'jpod101-alternate', - 'jisho', - 'custom' - ]; - for (const source of audioSourcesAvailable) { - if (!this._audioSourceEntries.some((metadata) => metadata.value === source)) { - return source; - } - } - return audioSourcesAvailable[0]; - } - - _createAudioSourceEntry(value) { - const eventListeners = new EventListenerCollection(); - const container = this._settingsController.instantiateTemplate('audio-source'); - const select = container.querySelector('.audio-source-select'); - const removeButton = container.querySelector('.audio-source-remove'); - const menuButton = container.querySelector('.audio-source-menu-button'); - - select.value = value; - - const entry = { - container, - eventListeners, - value - }; - - eventListeners.addEventListener(select, 'change', this._onAudioSourceSelectChange.bind(this, entry), false); - if (removeButton !== null) { - eventListeners.addEventListener(removeButton, 'click', this._onAudioSourceRemoveClicked.bind(this, entry), false); - } - if (menuButton !== null) { - eventListeners.addEventListener(menuButton, 'menuClose', this._onMenuClose.bind(this, entry), false); - } - - this._audioSourceContainer.appendChild(container); - this._audioSourceEntries.push(entry); - } - - async _removeAudioSourceEntry(entry) { - const index = this._audioSourceEntries.indexOf(entry); - if (index < 0) { return; } - - this._cleanupAudioSourceEntry(index); - await this._settingsController.modifyProfileSettings([{ - action: 'splice', - path: 'audio.sources', - start: index, - deleteCount: 1, - items: [] - }]); - } - - _cleanupAudioSourceEntry(index) { - const {container, eventListeners} = this._audioSourceEntries[index]; - if (container.parentNode !== null) { - container.parentNode.removeChild(container); - } - eventListeners.removeAllEventListeners(); - this._audioSourceEntries.splice(index, 1); - } - - async _onAddAudioSource() { - const audioSource = this._getUnusedAudioSource(); - const index = this._audioSourceEntries.length; - this._createAudioSourceEntry(audioSource); - await this._settingsController.modifyProfileSettings([{ - action: 'splice', - path: 'audio.sources', - start: index, - deleteCount: 0, - items: [audioSource] - }]); - } - - async _onAudioSourceSelectChange(entry, event) { - const index = this._audioSourceEntries.indexOf(entry); - if (index < 0) { return; } - - const value = event.currentTarget.value; - entry.value = value; - await this._settingsController.setProfileSetting(`audio.sources[${index}]`, value); - } - - _onAudioSourceRemoveClicked(entry) { - this._removeAudioSourceEntry(entry); - } - - _onMenuClose(entry, e) { - switch (e.detail.action) { - case 'remove': - this._removeAudioSourceEntry(entry); - break; - } - } -} diff --git a/ext/bg/js/settings/backup-controller.js b/ext/bg/js/settings/backup-controller.js deleted file mode 100644 index 8837b927..00000000 --- a/ext/bg/js/settings/backup-controller.js +++ /dev/null @@ -1,419 +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. 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 - * DictionaryController - * OptionsUtil - * api - */ - -class BackupController { - constructor(settingsController, modalController) { - this._settingsController = settingsController; - this._modalController = modalController; - this._settingsExportToken = null; - this._settingsExportRevoke = null; - this._currentVersion = 0; - this._settingsResetModal = null; - this._settingsImportErrorModal = null; - this._settingsImportWarningModal = null; - this._optionsUtil = null; - try { - this._optionsUtil = new OptionsUtil(); - } catch (e) { - // NOP - } - } - - async prepare() { - if (this._optionsUtil !== null) { - await this._optionsUtil.prepare(); - } - - if (this._modalController !== null) { - this._settingsResetModal = this._modalController.getModal('settings-reset'); - this._settingsImportErrorModal = this._modalController.getModal('settings-import-error'); - this._settingsImportWarningModal = this._modalController.getModal('settings-import-warning'); - } - - this._addNodeEventListener('#settings-export-button', 'click', this._onSettingsExportClick.bind(this), false); - this._addNodeEventListener('#settings-import-button', 'click', this._onSettingsImportClick.bind(this), false); - this._addNodeEventListener('#settings-import-file', 'change', this._onSettingsImportFileChange.bind(this), false); - this._addNodeEventListener('#settings-reset-button', 'click', this._onSettingsResetClick.bind(this), false); - this._addNodeEventListener('#settings-reset-confirm-button', 'click', this._onSettingsResetConfirmClick.bind(this), false); - } - - // Private - - _addNodeEventListener(selector, ...args) { - const node = document.querySelector(selector); - if (node === null) { return; } - - node.addEventListener(...args); - } - - _getSettingsExportDateString(date, dateSeparator, dateTimeSeparator, timeSeparator, resolution) { - const values = [ - date.getUTCFullYear().toString(), - dateSeparator, - (date.getUTCMonth() + 1).toString().padStart(2, '0'), - dateSeparator, - date.getUTCDate().toString().padStart(2, '0'), - dateTimeSeparator, - date.getUTCHours().toString().padStart(2, '0'), - timeSeparator, - date.getUTCMinutes().toString().padStart(2, '0'), - timeSeparator, - date.getUTCSeconds().toString().padStart(2, '0') - ]; - return values.slice(0, resolution * 2 - 1).join(''); - } - - async _getSettingsExportData(date) { - const optionsFull = await this._settingsController.getOptionsFull(); - const environment = await api.getEnvironmentInfo(); - const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates(); - const permissions = await this._settingsController.permissionsUtil.getAllPermissions(); - - // Format options - for (const {options} of optionsFull.profiles) { - if (options.anki.fieldTemplates === fieldTemplatesDefault || !options.anki.fieldTemplates) { - options.anki.fieldTemplates = null; - } - } - - const data = { - version: this._currentVersion, - date: this._getSettingsExportDateString(date, '-', ' ', ':', 6), - url: chrome.runtime.getURL('/'), - manifest: chrome.runtime.getManifest(), - environment, - userAgent: navigator.userAgent, - permissions, - options: optionsFull - }; - - return data; - } - - _saveBlob(blob, fileName) { - if (typeof navigator === 'object' && typeof navigator.msSaveBlob === 'function') { - if (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); - } - - async _onSettingsExportClick() { - if (this._settingsExportRevoke !== null) { - this._settingsExportRevoke(); - this._settingsExportRevoke = null; - } - - const date = new Date(Date.now()); - - const token = {}; - this._settingsExportToken = token; - const data = await this._getSettingsExportData(date); - if (this._settingsExportToken !== token) { - // A new export has been started - return; - } - this._settingsExportToken = null; - - const fileName = `yomichan-settings-${this._getSettingsExportDateString(date, '-', '-', '-', 6)}.json`; - const blob = new Blob([JSON.stringify(data, null, 4)], {type: 'application/json'}); - this._saveBlob(blob, fileName); - } - - _readFileArrayBuffer(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result); - reader.onerror = () => reject(reader.error); - reader.readAsArrayBuffer(file); - }); - } - - // Importing - - async _settingsImportSetOptionsFull(optionsFull) { - await this._settingsController.setAllSettings(optionsFull); - } - - _showSettingsImportError(error) { - yomichan.logError(error); - document.querySelector('#settings-import-error-message').textContent = `${error}`; - this._settingsImportErrorModal.setVisible(true); - } - - async _showSettingsImportWarnings(warnings) { - const modal = this._settingsImportWarningModal; - const buttons = document.querySelectorAll('.settings-import-warning-import-button'); - const messageContainer = document.querySelector('#settings-import-warning-message'); - if (buttons.length === 0 || messageContainer === null) { - return {result: false}; - } - - // Set message - const fragment = document.createDocumentFragment(); - for (const warning of warnings) { - const node = document.createElement('li'); - node.textContent = `${warning}`; - fragment.appendChild(node); - } - messageContainer.textContent = ''; - messageContainer.appendChild(fragment); - - // Show modal - modal.setVisible(true); - - // Wait for modal to close - return new Promise((resolve) => { - const onButtonClick = (e) => { - e.preventDefault(); - complete({ - result: true, - sanitize: e.currentTarget.dataset.importSanitize === 'true' - }); - modal.setVisible(false); - }; - const onModalVisibilityChanged = ({visible}) => { - if (visible) { return; } - complete({result: false}); - }; - - let completed = false; - const complete = (result) => { - if (completed) { return; } - completed = true; - - modal.off('visibilityChanged', onModalVisibilityChanged); - for (const button of buttons) { - button.removeEventListener('click', onButtonClick, false); - } - - resolve(result); - }; - - // Hook events - modal.on('visibilityChanged', onModalVisibilityChanged); - for (const button of buttons) { - button.addEventListener('click', onButtonClick, false); - } - }); - } - - _isLocalhostUrl(urlString) { - try { - const url = new URL(urlString); - switch (url.hostname.toLowerCase()) { - case 'localhost': - case '127.0.0.1': - case '[::1]': - switch (url.protocol.toLowerCase()) { - case 'http:': - case 'https:': - return true; - } - break; - } - } catch (e) { - // NOP - } - return false; - } - - _settingsImportSanitizeProfileOptions(options, dryRun) { - const warnings = []; - - const anki = options.anki; - if (isObject(anki)) { - const fieldTemplates = anki.fieldTemplates; - if (typeof fieldTemplates === 'string') { - warnings.push('anki.fieldTemplates contains a non-default value'); - if (!dryRun) { - anki.fieldTemplates = null; - } - } - const server = anki.server; - if (typeof server === 'string' && server.length > 0 && !this._isLocalhostUrl(server)) { - warnings.push('anki.server uses a non-localhost URL'); - if (!dryRun) { - anki.server = 'http://127.0.0.1:8765'; - } - } - } - - const audio = options.audio; - if (isObject(audio)) { - const customSourceUrl = audio.customSourceUrl; - if (typeof customSourceUrl === 'string' && customSourceUrl.length > 0 && !this._isLocalhostUrl(customSourceUrl)) { - warnings.push('audio.customSourceUrl uses a non-localhost URL'); - if (!dryRun) { - audio.customSourceUrl = ''; - } - } - } - - return warnings; - } - - _settingsImportSanitizeOptions(optionsFull, dryRun) { - const warnings = new Set(); - - const profiles = optionsFull.profiles; - if (Array.isArray(profiles)) { - for (const profile of profiles) { - if (!isObject(profile)) { continue; } - const options = profile.options; - if (!isObject(options)) { continue; } - - const warnings2 = this._settingsImportSanitizeProfileOptions(options, dryRun); - for (const warning of warnings2) { - warnings.add(warning); - } - } - } - - return warnings; - } - - _utf8Decode(arrayBuffer) { - try { - return new TextDecoder('utf-8').decode(arrayBuffer); - } catch (e) { - const binaryString = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)); - return decodeURIComponent(escape(binaryString)); - } - } - - async _importSettingsFile(file) { - const dataString = this._utf8Decode(await this._readFileArrayBuffer(file)); - const data = JSON.parse(dataString); - - // Type check - if (!isObject(data)) { - throw new Error(`Invalid data type: ${typeof data}`); - } - - // Version check - const version = data.version; - if (!( - typeof version === 'number' && - Number.isFinite(version) && - version === Math.floor(version) - )) { - throw new Error(`Invalid version: ${version}`); - } - - if (!( - version >= 0 && - version <= this._currentVersion - )) { - throw new Error(`Unsupported version: ${version}`); - } - - // Verify options exists - let optionsFull = data.options; - if (!isObject(optionsFull)) { - throw new Error(`Invalid options type: ${typeof optionsFull}`); - } - - // Upgrade options - optionsFull = await this._optionsUtil.update(optionsFull); - - // Check for warnings - const sanitizationWarnings = this._settingsImportSanitizeOptions(optionsFull, true); - - // Show sanitization warnings - if (sanitizationWarnings.size > 0) { - const {result, sanitize} = await this._showSettingsImportWarnings(sanitizationWarnings); - if (!result) { return; } - - if (sanitize !== false) { - this._settingsImportSanitizeOptions(optionsFull, false); - } - } - - // Assign options - await this._settingsImportSetOptionsFull(optionsFull); - } - - _onSettingsImportClick() { - document.querySelector('#settings-import-file').click(); - } - - async _onSettingsImportFileChange(e) { - const files = e.target.files; - if (files.length === 0) { return; } - - const file = files[0]; - e.target.value = null; - try { - await this._importSettingsFile(file); - } catch (error) { - this._showSettingsImportError(error); - } - } - - // Resetting - - _onSettingsResetClick() { - this._settingsResetModal.setVisible(true); - } - - async _onSettingsResetConfirmClick() { - this._settingsResetModal.setVisible(false); - - // Get default options - const optionsFull = this._optionsUtil.getDefault(); - - // Update dictionaries - const dictionaries = await this._settingsController.getDictionaryInfo(); - for (const {options: {dictionaries: optionsDictionaries}} of optionsFull.profiles) { - for (const {title} of dictionaries) { - optionsDictionaries[title] = DictionaryController.createDefaultDictionarySettings(); - } - } - - // Assign options - try { - await this._settingsImportSetOptionsFull(optionsFull); - } catch (e) { - yomichan.logError(e); - } - } -} diff --git a/ext/bg/js/settings/dictionary-controller.js b/ext/bg/js/settings/dictionary-controller.js deleted file mode 100644 index ea9f7503..00000000 --- a/ext/bg/js/settings/dictionary-controller.js +++ /dev/null @@ -1,558 +0,0 @@ -/* - * Copyright (C) 2020-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 - * DictionaryDatabase - * ObjectPropertyAccessor - * api - */ - -class DictionaryEntry { - constructor(dictionaryController, node, dictionaryInfo) { - this._dictionaryController = dictionaryController; - this._node = node; - this._dictionaryInfo = dictionaryInfo; - this._eventListeners = new EventListenerCollection(); - this._detailsContainer = null; - this._hasDetails = false; - this._hasCounts = false; - } - - get node() { - return this._node; - } - - get dictionaryTitle() { - return this._dictionaryInfo.title; - } - - prepare() { - const node = this._node; - const {title, revision, prefixWildcardsSupported, version} = this._dictionaryInfo; - - this._detailsContainer = node.querySelector('.dictionary-details'); - - const enabledCheckbox = node.querySelector('.dictionary-enabled'); - const allowSecondarySearchesCheckbox = node.querySelector('.dictionary-allow-secondary-searches'); - const priorityInput = node.querySelector('.dictionary-priority'); - const deleteButton = node.querySelector('.dictionary-delete-button'); - const menuButton = node.querySelector('.dictionary-menu-button'); - const detailsTable = node.querySelector('.dictionary-details-table'); - const detailsToggleLink = node.querySelector('.dictionary-details-toggle-link'); - const outdatedContainer = node.querySelector('.dictionary-outdated-notification'); - const titleNode = node.querySelector('.dictionary-title'); - const versionNode = node.querySelector('.dictionary-version'); - const wildcardSupportedCheckbox = node.querySelector('.dictionary-prefix-wildcard-searches-supported'); - - const hasDetails = (detailsTable !== null && this._setupDetails(detailsTable)); - this._hasDetails = hasDetails; - - titleNode.textContent = title; - versionNode.textContent = `rev.${revision}`; - if (wildcardSupportedCheckbox !== null) { - wildcardSupportedCheckbox.checked = !!prefixWildcardsSupported; - } - if (outdatedContainer !== null) { - outdatedContainer.hidden = (version >= 3); - } - if (detailsToggleLink !== null) { - detailsToggleLink.hidden = !hasDetails; - } - if (enabledCheckbox !== null) { - enabledCheckbox.dataset.setting = ObjectPropertyAccessor.getPathString(['dictionaries', title, 'enabled']); - this._eventListeners.addEventListener(enabledCheckbox, 'settingChanged', this._onEnabledChanged.bind(this), false); - } - if (priorityInput !== null) { - priorityInput.dataset.setting = ObjectPropertyAccessor.getPathString(['dictionaries', title, 'priority']); - } - if (allowSecondarySearchesCheckbox !== null) { - allowSecondarySearchesCheckbox.dataset.setting = ObjectPropertyAccessor.getPathString(['dictionaries', title, 'allowSecondarySearches']); - } - if (deleteButton !== null) { - this._eventListeners.addEventListener(deleteButton, 'click', this._onDeleteButtonClicked.bind(this), false); - } - if (menuButton !== null) { - this._eventListeners.addEventListener(menuButton, 'menuOpen', this._onMenuOpen.bind(this), false); - this._eventListeners.addEventListener(menuButton, 'menuClose', this._onMenuClose.bind(this), false); - } - if (detailsToggleLink !== null && this._detailsContainer !== null) { - this._eventListeners.addEventListener(detailsToggleLink, 'click', this._onDetailsToggleLinkClicked.bind(this), false); - } - } - - cleanup() { - this._eventListeners.removeAllEventListeners(); - const node = this._node; - if (node.parentNode !== null) { - node.parentNode.removeChild(node); - } - } - - setCounts(counts) { - const node = this._node.querySelector('.dictionary-counts'); - node.textContent = JSON.stringify({info: this._dictionaryInfo, counts}, null, 4); - node.hidden = false; - this._hasCounts = true; - } - - // Private - - _onDeleteButtonClicked(e) { - e.preventDefault(); - this._delete(); - } - - _onMenuOpen(e) { - const bodyNode = e.detail.menu.bodyNode; - const showDetails = bodyNode.querySelector('.popup-menu-item[data-menu-action="showDetails"]'); - const hideDetails = bodyNode.querySelector('.popup-menu-item[data-menu-action="hideDetails"]'); - const hasDetails = (this._detailsContainer !== null); - const detailsVisible = (hasDetails && !this._detailsContainer.hidden); - if (showDetails !== null) { - showDetails.hidden = detailsVisible; - showDetails.disabled = !hasDetails; - } - if (hideDetails !== null) { - hideDetails.hidden = !detailsVisible; - hideDetails.disabled = !hasDetails; - } - } - - _onMenuClose(e) { - switch (e.detail.action) { - case 'delete': - this._delete(); - break; - case 'showDetails': - if (this._detailsContainer !== null) { this._detailsContainer.hidden = false; } - break; - case 'hideDetails': - if (this._detailsContainer !== null) { this._detailsContainer.hidden = true; } - break; - } - } - - _onDetailsToggleLinkClicked(e) { - e.preventDefault(); - this._detailsContainer.hidden = !this._detailsContainer.hidden; - } - - _onEnabledChanged(e) { - const {detail: {value}} = e; - this._node.dataset.enabled = `${value}`; - this._dictionaryController.updateDictionariesEnabled(); - } - - _setupDetails(detailsTable) { - const targets = [ - ['Author', 'author'], - ['URL', 'url'], - ['Description', 'description'], - ['Attribution', 'attribution'] - ]; - - const dictionaryInfo = this._dictionaryInfo; - const fragment = document.createDocumentFragment(); - let any = false; - for (const [label, key] of targets) { - const info = dictionaryInfo[key]; - if (typeof info !== 'string') { continue; } - - const details = this._dictionaryController.instantiateTemplate('dictionary-details-entry'); - details.dataset.type = key; - details.querySelector('.dictionary-details-entry-label').textContent = `${label}:`; - details.querySelector('.dictionary-details-entry-info').textContent = info; - fragment.appendChild(details); - - any = true; - } - - detailsTable.appendChild(fragment); - return any; - } - - _delete() { - this._dictionaryController.deleteDictionary(this.dictionaryTitle); - } -} - -class DictionaryController { - constructor(settingsController, modalController, storageController, statusFooter) { - this._settingsController = settingsController; - this._modalController = modalController; - this._storageController = storageController; - this._statusFooter = statusFooter; - this._dictionaries = null; - this._dictionaryEntries = []; - this._databaseStateToken = null; - this._checkingIntegrity = false; - this._checkIntegrityButton = null; - this._dictionaryEntryContainer = null; - this._integrityExtraInfoContainer = null; - this._dictionaryInstallCountNode = null; - this._dictionaryEnabledCountNode = null; - this._noDictionariesInstalledWarnings = null; - this._noDictionariesEnabledWarnings = null; - this._deleteDictionaryModal = null; - this._integrityExtraInfoNode = null; - this._isDeleting = false; - } - - async prepare() { - this._checkIntegrityButton = document.querySelector('#dictionary-check-integrity'); - this._dictionaryEntryContainer = document.querySelector('#dictionary-list'); - this._integrityExtraInfoContainer = document.querySelector('#dictionary-list-extra'); - this._dictionaryInstallCountNode = document.querySelector('#dictionary-install-count'); - this._dictionaryEnabledCountNode = document.querySelector('#dictionary-enabled-count'); - this._noDictionariesInstalledWarnings = document.querySelectorAll('.no-dictionaries-installed-warning'); - this._noDictionariesEnabledWarnings = document.querySelectorAll('.no-dictionaries-enabled-warning'); - this._deleteDictionaryModal = this._modalController.getModal('dictionary-confirm-delete'); - - yomichan.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - - document.querySelector('#dictionary-confirm-delete-button').addEventListener('click', this._onDictionaryConfirmDelete.bind(this), false); - if (this._checkIntegrityButton !== null) { - this._checkIntegrityButton.addEventListener('click', this._onCheckIntegrityButtonClick.bind(this), false); - } - - await this._onDatabaseUpdated(); - } - - deleteDictionary(dictionaryTitle) { - if (this._isDeleting) { return; } - const modal = this._deleteDictionaryModal; - modal.node.dataset.dictionaryTitle = dictionaryTitle; - modal.node.querySelector('#dictionary-confirm-delete-name').textContent = dictionaryTitle; - modal.setVisible(true); - } - - instantiateTemplate(name) { - return this._settingsController.instantiateTemplate(name); - } - - async updateDictionariesEnabled() { - const options = await this._settingsController.getOptions(); - this._updateDictionariesEnabledWarnings(options); - } - - // Private - - _onOptionsChanged({options}) { - this._updateDictionariesEnabledWarnings(options); - } - - async _onDatabaseUpdated() { - const token = {}; - this._databaseStateToken = token; - this._dictionaries = null; - const dictionaries = await this._settingsController.getDictionaryInfo(); - const options = await this._settingsController.getOptions(); - if (this._databaseStateToken !== token) { return; } - this._dictionaries = dictionaries; - - this._updateMainDictionarySelectOptions(dictionaries); - - for (const entry of this._dictionaryEntries) { - entry.cleanup(); - } - this._dictionaryEntries = []; - - if (this._dictionaryInstallCountNode !== null) { - this._dictionaryInstallCountNode.textContent = `${dictionaries.length}`; - } - - const hasDictionary = (dictionaries.length > 0); - for (const node of this._noDictionariesInstalledWarnings) { - node.hidden = hasDictionary; - } - - this._updateDictionariesEnabledWarnings(options); - - await this._ensureDictionarySettings(dictionaries); - for (const dictionary of dictionaries) { - this._createDictionaryEntry(dictionary); - } - } - - _updateDictionariesEnabledWarnings(options) { - let enabledCount = 0; - if (this._dictionaries !== null) { - for (const {title} of this._dictionaries) { - if (Object.prototype.hasOwnProperty.call(options.dictionaries, title)) { - const {enabled} = options.dictionaries[title]; - if (enabled) { - ++enabledCount; - } - } - } - } - - const hasEnabledDictionary = (enabledCount > 0); - for (const node of this._noDictionariesEnabledWarnings) { - node.hidden = hasEnabledDictionary; - } - - if (this._dictionaryEnabledCountNode !== null) { - this._dictionaryEnabledCountNode.textContent = `${enabledCount}`; - } - } - - _onDictionaryConfirmDelete(e) { - e.preventDefault(); - - const modal = this._deleteDictionaryModal; - modal.setVisible(false); - - const title = modal.node.dataset.dictionaryTitle; - if (typeof title !== 'string') { return; } - delete modal.node.dataset.dictionaryTitle; - - this._deleteDictionary(title); - } - - _onCheckIntegrityButtonClick(e) { - e.preventDefault(); - this._checkIntegrity(); - } - - _updateMainDictionarySelectOptions(dictionaries) { - for (const select of document.querySelectorAll('[data-setting="general.mainDictionary"]')) { - const fragment = document.createDocumentFragment(); - - let option = document.createElement('option'); - option.className = 'text-muted'; - option.value = ''; - option.textContent = 'Not selected'; - fragment.appendChild(option); - - for (const {title, sequenced} of dictionaries) { - if (!sequenced) { continue; } - option = document.createElement('option'); - option.value = title; - option.textContent = title; - fragment.appendChild(option); - } - - select.textContent = ''; // Empty - select.appendChild(fragment); - } - } - - async _checkIntegrity() { - if (this._dictionaries === null || this._checkingIntegrity || this._isDeleting) { return; } - - try { - this._checkingIntegrity = true; - this._setButtonsEnabled(false); - - const token = this._databaseStateToken; - const dictionaryTitles = this._dictionaries.map(({title}) => title); - const {counts, total} = await api.getDictionaryCounts(dictionaryTitles, true); - if (this._databaseStateToken !== token) { return; } - - for (let i = 0, ii = Math.min(counts.length, this._dictionaryEntries.length); i < ii; ++i) { - const entry = this._dictionaryEntries[i]; - entry.setCounts(counts[i]); - } - - this._setCounts(counts, total); - } finally { - this._setButtonsEnabled(true); - this._checkingIntegrity = false; - } - } - - _setCounts(dictionaryCounts, totalCounts) { - const remainders = Object.assign({}, totalCounts); - const keys = Object.keys(remainders); - - for (const counts of dictionaryCounts) { - for (const key of keys) { - remainders[key] -= counts[key]; - } - } - - let totalRemainder = 0; - for (const key of keys) { - totalRemainder += remainders[key]; - } - - this._cleanupExtra(); - if (totalRemainder > 0) { - this.extra = this._createExtra(totalCounts, remainders, totalRemainder); - } - } - - _createExtra(totalCounts, remainders, totalRemainder) { - const node = this.instantiateTemplate('dictionary-extra'); - this._integrityExtraInfoNode = node; - - node.querySelector('.dictionary-total-count').textContent = `${totalRemainder} item${totalRemainder !== 1 ? 's' : ''}`; - - const n = node.querySelector('.dictionary-counts'); - n.textContent = JSON.stringify({counts: totalCounts, remainders}, null, 4); - n.hidden = false; - - this._integrityExtraInfoContainer.appendChild(node); - } - - _cleanupExtra() { - const node = this._integrityExtraInfoNode; - if (node === null) { return; } - this._integrityExtraInfoNode = null; - - const parent = node.parentNode; - if (parent === null) { return; } - - parent.removeChild(node); - } - - _createDictionaryEntry(dictionary) { - const node = this.instantiateTemplate('dictionary'); - this._dictionaryEntryContainer.appendChild(node); - - const entry = new DictionaryEntry(this, node, dictionary); - this._dictionaryEntries.push(entry); - entry.prepare(); - } - - async _deleteDictionary(dictionaryTitle) { - if (this._isDeleting || this._checkingIntegrity) { return; } - - const index = this._dictionaryEntries.findIndex((entry) => entry.dictionaryTitle === dictionaryTitle); - if (index < 0) { return; } - - const storageController = this._storageController; - const statusFooter = this._statusFooter; - const {node} = this._dictionaryEntries[index]; - const progressSelector = '.dictionary-delete-progress'; - const progressContainers = [ - ...node.querySelectorAll('.progress-container'), - ...document.querySelectorAll(`#dictionaries-modal ${progressSelector}`) - ]; - const progressBars = [ - ...node.querySelectorAll('.progress-bar'), - ...document.querySelectorAll(`${progressSelector} .progress-bar`) - ]; - const infoLabels = document.querySelectorAll(`${progressSelector} .progress-info`); - const statusLabels = document.querySelectorAll(`${progressSelector} .progress-status`); - const prevention = this._settingsController.preventPageExit(); - try { - this._isDeleting = true; - this._setButtonsEnabled(false); - - const onProgress = ({processed, count, storeCount, storesProcesed}) => { - const percent = ( - (count > 0 && storesProcesed > 0) ? - (processed / count) * (storesProcesed / storeCount) * 100.0 : - 0.0 - ); - const cssString = `${percent}%`; - const statusString = `${percent.toFixed(0)}%`; - for (const progressBar of progressBars) { progressBar.style.width = cssString; } - for (const label of statusLabels) { label.textContent = statusString; } - }; - - onProgress({processed: 0, count: 1, storeCount: 1, storesProcesed: 0}); - - for (const progress of progressContainers) { progress.hidden = false; } - for (const label of infoLabels) { label.textContent = 'Deleting dictionary...'; } - if (statusFooter !== null) { statusFooter.setTaskActive(progressSelector, true); } - - await this._deleteDictionaryInternal(dictionaryTitle, onProgress); - await this._deleteDictionarySettings(dictionaryTitle); - } catch (e) { - yomichan.logError(e); - } finally { - prevention.end(); - for (const progress of progressContainers) { progress.hidden = true; } - if (statusFooter !== null) { statusFooter.setTaskActive(progressSelector, false); } - this._setButtonsEnabled(true); - this._isDeleting = false; - if (storageController !== null) { storageController.updateStats(); } - } - } - - _setButtonsEnabled(value) { - value = !value; - for (const node of document.querySelectorAll('.dictionary-database-mutating-input')) { - node.disabled = value; - } - } - - async _deleteDictionaryInternal(dictionaryTitle, onProgress) { - const dictionaryDatabase = await this._getPreparedDictionaryDatabase(); - try { - await dictionaryDatabase.deleteDictionary(dictionaryTitle, {rate: 1000}, onProgress); - api.triggerDatabaseUpdated('dictionary', 'delete'); - } finally { - dictionaryDatabase.close(); - } - } - - async _getPreparedDictionaryDatabase() { - const dictionaryDatabase = new DictionaryDatabase(); - await dictionaryDatabase.prepare(); - return dictionaryDatabase; - } - - async _deleteDictionarySettings(dictionaryTitle) { - const optionsFull = await this._settingsController.getOptionsFull(); - const {profiles} = optionsFull; - const targets = []; - for (let i = 0, ii = profiles.length; i < ii; ++i) { - const {options: {dictionaries}} = profiles[i]; - if (Object.prototype.hasOwnProperty.call(dictionaries, dictionaryTitle)) { - const path = ObjectPropertyAccessor.getPathString(['profiles', i, 'options', 'dictionaries', dictionaryTitle]); - targets.push({action: 'delete', path}); - } - } - await this._settingsController.modifyGlobalSettings(targets); - } - - async _ensureDictionarySettings(dictionaries2) { - const optionsFull = await this._settingsController.getOptionsFull(); - const {profiles} = optionsFull; - const targets = []; - for (const {title} of dictionaries2) { - for (let i = 0, ii = profiles.length; i < ii; ++i) { - const {options: {dictionaries: dictionaryOptions}} = profiles[i]; - if (Object.prototype.hasOwnProperty.call(dictionaryOptions, title)) { continue; } - - const path = ObjectPropertyAccessor.getPathString(['profiles', i, 'options', 'dictionaries', title]); - targets.push({ - action: 'set', - path, - value: DictionaryController.createDefaultDictionarySettings() - }); - } - } - - if (targets.length > 0) { - await this._settingsController.modifyGlobalSettings(targets); - } - } - - static createDefaultDictionarySettings() { - return { - enabled: false, - allowSecondarySearches: false, - priority: 0 - }; - } -} diff --git a/ext/bg/js/settings/dictionary-import-controller.js b/ext/bg/js/settings/dictionary-import-controller.js deleted file mode 100644 index c4ad9e59..00000000 --- a/ext/bg/js/settings/dictionary-import-controller.js +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright (C) 2020-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 - * DictionaryDatabase - * DictionaryImporter - * ObjectPropertyAccessor - * api - */ - -class DictionaryImportController { - constructor(settingsController, modalController, storageController, statusFooter) { - this._settingsController = settingsController; - this._modalController = modalController; - this._storageController = storageController; - this._statusFooter = statusFooter; - this._modifying = false; - this._purgeButton = null; - this._purgeConfirmButton = null; - this._importFileButton = null; - this._importFileInput = null; - this._purgeConfirmModal = null; - this._errorContainer = null; - this._spinner = null; - this._purgeNotification = null; - this._errorToStringOverrides = [ - [ - 'A mutation operation was attempted on a database that did not allow mutations.', - 'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.' - ], - [ - 'The operation failed for reasons unrelated to the database itself and not covered by any other error code.', - 'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.' - ] - ]; - } - - async prepare() { - this._purgeButton = document.querySelector('#dictionary-delete-all-button'); - this._purgeConfirmButton = document.querySelector('#dictionary-confirm-delete-all-button'); - this._importFileButton = document.querySelector('#dictionary-import-file-button'); - this._importFileInput = document.querySelector('#dictionary-import-file-input'); - this._purgeConfirmModal = this._modalController.getModal('dictionary-confirm-delete-all'); - this._errorContainer = document.querySelector('#dictionary-error'); - this._spinner = document.querySelector('#dictionary-spinner'); - this._purgeNotification = document.querySelector('#dictionary-delete-all-status'); - - this._purgeButton.addEventListener('click', this._onPurgeButtonClick.bind(this), false); - this._purgeConfirmButton.addEventListener('click', this._onPurgeConfirmButtonClick.bind(this), false); - this._importFileButton.addEventListener('click', this._onImportButtonClick.bind(this), false); - this._importFileInput.addEventListener('change', this._onImportFileChange.bind(this), false); - } - - // Private - - _onImportButtonClick() { - this._importFileInput.click(); - } - - _onPurgeButtonClick(e) { - e.preventDefault(); - this._purgeConfirmModal.setVisible(true); - } - - _onPurgeConfirmButtonClick(e) { - e.preventDefault(); - this._purgeConfirmModal.setVisible(false); - this._purgeDatabase(); - } - - _onImportFileChange(e) { - const node = e.currentTarget; - const files = [...node.files]; - node.value = null; - this._importDictionaries(files); - } - - async _purgeDatabase() { - if (this._modifying) { return; } - - const purgeNotification = this._purgeNotification; - const storageController = this._storageController; - const prevention = this._preventPageExit(); - - try { - this._setModifying(true); - this._hideErrors(); - this._setSpinnerVisible(true); - if (purgeNotification !== null) { purgeNotification.hidden = false; } - - await api.purgeDatabase(); - const errors = await this._clearDictionarySettings(); - - if (errors.length > 0) { - this._showErrors(errors); - } - } catch (error) { - this._showErrors([error]); - } finally { - prevention.end(); - if (purgeNotification !== null) { purgeNotification.hidden = true; } - this._setSpinnerVisible(false); - this._setModifying(false); - if (storageController !== null) { storageController.updateStats(); } - } - } - - async _importDictionaries(files) { - if (this._modifying) { return; } - - const statusFooter = this._statusFooter; - const storageController = this._storageController; - const importInfo = document.querySelector('#dictionary-import-info'); - const progressSelector = '.dictionary-import-progress'; - const progressContainers = [ - ...document.querySelectorAll('#dictionary-import-progress-container'), - ...document.querySelectorAll(`#dictionaries-modal ${progressSelector}`) - ]; - const progressBars = [ - ...document.querySelectorAll('#dictionary-import-progress-container .progress-bar'), - ...document.querySelectorAll(`${progressSelector} .progress-bar`) - ]; - const infoLabels = document.querySelectorAll(`${progressSelector} .progress-info`); - const statusLabels = document.querySelectorAll(`${progressSelector} .progress-status`); - - const prevention = this._preventPageExit(); - - try { - this._setModifying(true); - this._hideErrors(); - this._setSpinnerVisible(true); - - for (const progress of progressContainers) { progress.hidden = false; } - - const optionsFull = await this._settingsController.getOptionsFull(); - const importDetails = { - prefixWildcardsSupported: optionsFull.global.database.prefixWildcardsSupported - }; - - const onProgress = (total, current) => { - const percent = (current / total * 100.0); - const cssString = `${percent}%`; - const statusString = `${percent.toFixed(0)}%`; - for (const progressBar of progressBars) { progressBar.style.width = cssString; } - for (const label of statusLabels) { label.textContent = statusString; } - if (storageController !== null) { storageController.updateStats(); } - }; - - const fileCount = files.length; - for (let i = 0; i < fileCount; ++i) { - if (importInfo !== null && fileCount > 1) { - importInfo.hidden = false; - importInfo.textContent = `(${i + 1} of ${fileCount})`; - } - - onProgress(1, 0); - - const labelText = `Importing dictionary${fileCount > 1 ? ` (${i + 1} of ${fileCount})` : ''}...`; - for (const label of infoLabels) { label.textContent = labelText; } - if (statusFooter !== null) { statusFooter.setTaskActive(progressSelector, true); } - - await this._importDictionary(files[i], importDetails, onProgress); - } - } catch (err) { - this._showErrors([err]); - } finally { - prevention.end(); - for (const progress of progressContainers) { progress.hidden = true; } - if (statusFooter !== null) { statusFooter.setTaskActive(progressSelector, false); } - if (importInfo !== null) { - importInfo.textContent = ''; - importInfo.hidden = true; - } - this._setSpinnerVisible(false); - this._setModifying(false); - if (storageController !== null) { storageController.updateStats(); } - } - } - - async _importDictionary(file, importDetails, onProgress) { - const dictionaryDatabase = await this._getPreparedDictionaryDatabase(); - try { - const dictionaryImporter = new DictionaryImporter(); - const archiveContent = await this._readFile(file); - const {result, errors} = await dictionaryImporter.importDictionary(dictionaryDatabase, archiveContent, importDetails, onProgress); - api.triggerDatabaseUpdated('dictionary', 'import'); - const errors2 = await this._addDictionarySettings(result.sequenced, result.title); - - if (errors.length > 0) { - const allErrors = [...errors, ...errors2]; - allErrors.push(new Error(`Dictionary may not have been imported properly: ${allErrors.length} error${allErrors.length === 1 ? '' : 's'} reported.`)); - this._showErrors(allErrors); - } - } finally { - dictionaryDatabase.close(); - } - } - - async _addDictionarySettings(sequenced, title) { - const optionsFull = await this._settingsController.getOptionsFull(); - const targets = []; - const profileCount = optionsFull.profiles.length; - for (let i = 0; i < profileCount; ++i) { - const {options} = optionsFull.profiles[i]; - const value = this._createDictionaryOptions(); - const path1 = ObjectPropertyAccessor.getPathString(['profiles', i, 'options', 'dictionaries', title]); - targets.push({action: 'set', path: path1, value}); - - if (sequenced && options.general.mainDictionary === '') { - const path2 = ObjectPropertyAccessor.getPathString(['profiles', i, 'options', 'general', 'mainDictionary']); - targets.push({action: 'set', path: path2, value: title}); - } - } - return await this._modifyGlobalSettings(targets); - } - - async _clearDictionarySettings() { - const optionsFull = await this._settingsController.getOptionsFull(); - const targets = []; - const profileCount = optionsFull.profiles.length; - for (let i = 0; i < profileCount; ++i) { - const path1 = ObjectPropertyAccessor.getPathString(['profiles', i, 'options', 'dictionaries']); - targets.push({action: 'set', path: path1, value: {}}); - const path2 = ObjectPropertyAccessor.getPathString(['profiles', i, 'options', 'general', 'mainDictionary']); - targets.push({action: 'set', path: path2, value: ''}); - } - return await this._modifyGlobalSettings(targets); - } - - _setSpinnerVisible(visible) { - if (this._spinner !== null) { - this._spinner.hidden = !visible; - } - } - - _preventPageExit() { - return this._settingsController.preventPageExit(); - } - - _showErrors(errors) { - const uniqueErrors = new Map(); - for (const error of errors) { - yomichan.logError(error); - const errorString = this._errorToString(error); - let count = uniqueErrors.get(errorString); - if (typeof count === 'undefined') { - count = 0; - } - uniqueErrors.set(errorString, count + 1); - } - - const fragment = document.createDocumentFragment(); - for (const [e, count] of uniqueErrors.entries()) { - const div = document.createElement('p'); - if (count > 1) { - div.textContent = `${e} `; - const em = document.createElement('em'); - em.textContent = `(${count})`; - div.appendChild(em); - } else { - div.textContent = `${e}`; - } - fragment.appendChild(div); - } - - this._errorContainer.appendChild(fragment); - this._errorContainer.hidden = false; - } - - _hideErrors() { - this._errorContainer.textContent = ''; - this._errorContainer.hidden = true; - } - - _readFile(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result); - reader.onerror = () => reject(reader.error); - reader.readAsBinaryString(file); - }); - } - - _createDictionaryOptions() { - return { - priority: 0, - enabled: true, - allowSecondarySearches: false - }; - } - - _errorToString(error) { - error = (typeof error.toString === 'function' ? error.toString() : `${error}`); - - for (const [match, newErrorString] of this._errorToStringOverrides) { - if (error.includes(match)) { - return newErrorString; - } - } - - return error; - } - - _setModifying(value) { - this._modifying = value; - this._setButtonsEnabled(!value); - } - - _setButtonsEnabled(value) { - value = !value; - for (const node of document.querySelectorAll('.dictionary-database-mutating-input')) { - node.disabled = value; - } - } - - async _getPreparedDictionaryDatabase() { - const dictionaryDatabase = new DictionaryDatabase(); - await dictionaryDatabase.prepare(); - return dictionaryDatabase; - } - - async _modifyGlobalSettings(targets) { - const results = await this._settingsController.modifyGlobalSettings(targets); - const errors = []; - for (const {error} of results) { - if (typeof error !== 'undefined') { - errors.push(deserializeError(error)); - } - } - return errors; - } -} diff --git a/ext/bg/js/settings/generic-setting-controller.js b/ext/bg/js/settings/generic-setting-controller.js deleted file mode 100644 index 7d6fc2e6..00000000 --- a/ext/bg/js/settings/generic-setting-controller.js +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright (C) 2020-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/>. - */ - -/* globals - * DOMDataBinder - */ - -class GenericSettingController { - constructor(settingsController) { - this._settingsController = settingsController; - this._defaultScope = 'profile'; - this._dataBinder = new DOMDataBinder({ - selector: '[data-setting]', - createElementMetadata: this._createElementMetadata.bind(this), - compareElementMetadata: this._compareElementMetadata.bind(this), - getValues: this._getValues.bind(this), - setValues: this._setValues.bind(this) - }); - this._transforms = new Map([ - ['setAttribute', this._setAttribute.bind(this)], - ['setVisibility', this._setVisibility.bind(this)], - ['splitTags', this._splitTags.bind(this)], - ['joinTags', this._joinTags.bind(this)], - ['toNumber', this._toNumber.bind(this)], - ['toBoolean', this._toBoolean.bind(this)], - ['toString', this._toString.bind(this)], - ['conditionalConvert', this._conditionalConvert.bind(this)] - ]); - } - - async prepare() { - this._dataBinder.observe(document.body); - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - } - - async refresh() { - await this._dataBinder.refresh(); - } - - // Private - - _onOptionsChanged() { - this._dataBinder.refresh(); - } - - _createElementMetadata(element) { - const {dataset: {setting: path, scope, transform: transformRaw}} = element; - let transforms; - if (typeof transformRaw === 'string') { - transforms = JSON.parse(transformRaw); - if (!Array.isArray(transforms)) { transforms = [transforms]; } - } else { - transforms = []; - } - return { - path, - scope, - transforms, - transformRaw - }; - } - - _compareElementMetadata(metadata1, metadata2) { - return ( - metadata1.path === metadata2.path && - metadata1.scope === metadata2.scope && - metadata1.transformRaw === metadata2.transformRaw - ); - } - - async _getValues(targets) { - const defaultScope = this._defaultScope; - const settingsTargets = []; - for (const {metadata: {path, scope}} of targets) { - const target = { - path, - scope: scope || defaultScope - }; - settingsTargets.push(target); - } - return this._transformResults(await this._settingsController.getSettings(settingsTargets), targets); - } - - async _setValues(targets) { - const defaultScope = this._defaultScope; - const settingsTargets = []; - for (const {metadata: {path, scope, transforms}, value, element} of targets) { - const transformedValue = this._applyTransforms(value, transforms, 'pre', element); - const target = { - path, - scope: scope || defaultScope, - action: 'set', - value: transformedValue - }; - settingsTargets.push(target); - } - return this._transformResults(await this._settingsController.modifySettings(settingsTargets), targets); - } - - _transformResults(values, targets) { - return values.map((value, i) => { - const error = value.error; - if (error) { return deserializeError(error); } - const {metadata: {transforms}, element} = targets[i]; - const result = this._applyTransforms(value.result, transforms, 'post', element); - return {result}; - }); - } - - _applyTransforms(value, transforms, step, element) { - for (const transform of transforms) { - const transformStep = transform.step; - if (typeof transformStep !== 'undefined' && transformStep !== step) { continue; } - - const transformFunction = this._transforms.get(transform.type); - if (typeof transformFunction === 'undefined') { continue; } - - value = transformFunction(value, transform, element); - } - return value; - } - - _getAncestor(node, ancestorDistance) { - if (ancestorDistance < 0) { - return document.documentElement; - } - for (let i = 0; i < ancestorDistance && node !== null; ++i) { - node = node.parentNode; - } - return node; - } - - _getRelativeElement(node, ancestorDistance, selector) { - const selectorRoot = ( - typeof ancestorDistance === 'number' ? - this._getAncestor(node, ancestorDistance) : - document - ); - if (selectorRoot === null) { return null; } - - return ( - typeof selector === 'string' ? - selectorRoot.querySelector(selector) : - (selectorRoot === document ? document.documentElement : selectorRoot) - ); - } - - _evaluateSimpleOperation(operation, lhs, rhs) { - switch (operation) { - case '!': return !lhs; - case '!!': return !!lhs; - case '===': return lhs === rhs; - case '!==': return lhs !== rhs; - case '>=': return lhs >= rhs; - case '<=': return lhs <= rhs; - case '>': return lhs > rhs; - case '<': return lhs < rhs; - default: return false; - } - } - - // Transforms - - _setAttribute(value, data, element) { - const {ancestorDistance, selector, attribute} = data; - const relativeElement = this._getRelativeElement(element, ancestorDistance, selector); - if (relativeElement !== null) { - relativeElement.setAttribute(attribute, `${value}`); - } - return value; - } - - _setVisibility(value, data, element) { - const {ancestorDistance, selector, condition} = data; - const relativeElement = this._getRelativeElement(element, ancestorDistance, selector); - if (relativeElement !== null) { - relativeElement.hidden = !this._evaluateSimpleOperation(condition.op, value, condition.value); - } - return value; - } - - _splitTags(value) { - return `${value}`.split(/[,; ]+/).filter((v) => !!v); - } - - _joinTags(value) { - return value.join(' '); - } - - _toNumber(value, data) { - let {constraints} = data; - if (!isObject(constraints)) { constraints = {}; } - return DOMDataBinder.convertToNumber(value, constraints); - } - - _toBoolean(value) { - return (value === 'true'); - } - - _toString(value) { - return `${value}`; - } - - _conditionalConvert(value, data) { - const {cases} = data; - if (Array.isArray(cases)) { - for (const {op, value: value2, default: isDefault, result} of cases) { - if (isDefault === true) { - value = result; - } else if (this._evaluateSimpleOperation(op, value, value2)) { - value = result; - break; - } - } - } - return value; - } -} diff --git a/ext/bg/js/settings/keyboard-mouse-input-field.js b/ext/bg/js/settings/keyboard-mouse-input-field.js deleted file mode 100644 index 09477519..00000000 --- a/ext/bg/js/settings/keyboard-mouse-input-field.js +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright (C) 2020-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 - * DocumentUtil - * HotkeyUtil - */ - -class KeyboardMouseInputField extends EventDispatcher { - constructor(inputNode, mouseButton, os, isPointerTypeSupported=null) { - super(); - this._inputNode = inputNode; - this._mouseButton = mouseButton; - this._isPointerTypeSupported = isPointerTypeSupported; - this._hotkeyUtil = new HotkeyUtil(os); - this._eventListeners = new EventListenerCollection(); - this._key = null; - this._modifiers = []; - this._penPointerIds = new Set(); - this._mouseModifiersSupported = false; - this._keySupported = false; - } - - get modifiers() { - return this._modifiers; - } - - prepare(key, modifiers, mouseModifiersSupported=false, keySupported=false) { - this.cleanup(); - - this._mouseModifiersSupported = mouseModifiersSupported; - this._keySupported = keySupported; - this.setInput(key, modifiers); - const events = [ - [this._inputNode, 'keydown', this._onModifierKeyDown.bind(this), false], - [this._inputNode, 'keyup', this._onModifierKeyUp.bind(this), false] - ]; - if (mouseModifiersSupported && this._mouseButton !== null) { - events.push( - [this._mouseButton, 'mousedown', this._onMouseButtonMouseDown.bind(this), false], - [this._mouseButton, 'pointerdown', this._onMouseButtonPointerDown.bind(this), false], - [this._mouseButton, 'pointerover', this._onMouseButtonPointerOver.bind(this), false], - [this._mouseButton, 'pointerout', this._onMouseButtonPointerOut.bind(this), false], - [this._mouseButton, 'pointercancel', this._onMouseButtonPointerCancel.bind(this), false], - [this._mouseButton, 'mouseup', this._onMouseButtonMouseUp.bind(this), false], - [this._mouseButton, 'contextmenu', this._onMouseButtonContextMenu.bind(this), false] - ); - } - for (const args of events) { - this._eventListeners.addEventListener(...args); - } - } - - setInput(key, modifiers) { - this._key = key; - this._modifiers = this._sortModifiers(modifiers); - this._updateDisplayString(); - } - - cleanup() { - this._eventListeners.removeAllEventListeners(); - this._modifiers = []; - this._key = null; - this._mouseModifiersSupported = false; - this._keySupported = false; - this._penPointerIds.clear(); - } - - clearInputs() { - this._updateModifiers([], null); - } - - // Private - - _sortModifiers(modifiers) { - return this._hotkeyUtil.sortModifiers(modifiers); - } - - _updateDisplayString() { - const displayValue = this._hotkeyUtil.getInputDisplayValue(this._key, this._modifiers); - this._inputNode.value = displayValue; - } - - _getModifierKeys(e) { - const modifiers = new Set(DocumentUtil.getActiveModifiers(e)); - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey - // https://askubuntu.com/questions/567731/why-is-shift-alt-being-mapped-to-meta - // It works with mouse events on some platforms, so try to determine if metaKey is pressed. - // This is a hack and only works when both Shift and Alt are not pressed. - if ( - !modifiers.has('meta') && - e.key === 'Meta' && - !( - modifiers.size === 2 && - modifiers.has('shift') && - modifiers.has('alt') - ) - ) { - modifiers.add('meta'); - } - return modifiers; - } - - _isModifierKey(keyName) { - switch (keyName) { - case 'AltLeft': - case 'AltRight': - case 'ControlLeft': - case 'ControlRight': - case 'MetaLeft': - case 'MetaRight': - case 'ShiftLeft': - case 'ShiftRight': - case 'OSLeft': - case 'OSRight': - return true; - default: - return false; - } - } - - _onModifierKeyDown(e) { - e.preventDefault(); - - let key = e.code; - if (key === 'Unidentified' || key === '') { key = void 0; } - if (this._keySupported) { - this._updateModifiers([...this._getModifierKeys(e)], this._isModifierKey(key) ? void 0 : key); - } else { - switch (key) { - case 'Escape': - case 'Backspace': - this.clearInputs(); - break; - default: - this._addModifiers(this._getModifierKeys(e)); - break; - } - } - } - - _onModifierKeyUp(e) { - e.preventDefault(); - } - - _onMouseButtonMouseDown(e) { - e.preventDefault(); - this._addModifiers(DocumentUtil.getActiveButtons(e)); - } - - _onMouseButtonPointerDown(e) { - if (!e.isPrimary) { return; } - - let {pointerType, pointerId} = e; - // Workaround for Firefox bug not detecting certain 'touch' events as 'pen' events. - if (this._penPointerIds.has(pointerId)) { pointerType = 'pen'; } - - if ( - typeof this._isPointerTypeSupported !== 'function' || - !this._isPointerTypeSupported(pointerType) - ) { - return; - } - e.preventDefault(); - this._addModifiers(DocumentUtil.getActiveButtons(e)); - } - - _onMouseButtonPointerOver(e) { - const {pointerType, pointerId} = e; - if (pointerType === 'pen') { - this._penPointerIds.add(pointerId); - } - } - - _onMouseButtonPointerOut(e) { - const {pointerId} = e; - this._penPointerIds.delete(pointerId); - } - - _onMouseButtonPointerCancel(e) { - this._onMouseButtonPointerOut(e); - } - - _onMouseButtonMouseUp(e) { - e.preventDefault(); - } - - _onMouseButtonContextMenu(e) { - e.preventDefault(); - } - - _addModifiers(newModifiers, newKey) { - const modifiers = new Set(this._modifiers); - for (const modifier of newModifiers) { - modifiers.add(modifier); - } - this._updateModifiers([...modifiers], newKey); - } - - _updateModifiers(modifiers, newKey) { - modifiers = this._sortModifiers(modifiers); - - let changed = false; - if (typeof newKey !== 'undefined' && this._key !== newKey) { - this._key = newKey; - changed = true; - } - if (!this._areArraysEqual(this._modifiers, modifiers)) { - this._modifiers = modifiers; - changed = true; - } - - this._updateDisplayString(); - if (changed) { - this.trigger('change', {modifiers: this._modifiers, key: this._key}); - } - } - - _areArraysEqual(array1, array2) { - const length = array1.length; - if (length !== array2.length) { return false; } - - for (let i = 0; i < length; ++i) { - if (array1[i] !== array2[i]) { return false; } - } - - return true; - } -} diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js deleted file mode 100644 index 9786d196..00000000 --- a/ext/bg/js/settings/main.js +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2016-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 - * AnkiController - * AnkiTemplatesController - * AudioController - * BackupController - * DictionaryController - * DictionaryImportController - * GenericSettingController - * ModalController - * PermissionsToggleController - * PopupPreviewController - * ProfileController - * ScanInputsController - * ScanInputsSimpleController - * SettingsController - * StorageController - * api - */ - -function showExtensionInformation() { - const node = document.getElementById('extension-info'); - if (node === null) { return; } - - const manifest = chrome.runtime.getManifest(); - node.textContent = `${manifest.name} v${manifest.version}`; -} - -async function setupEnvironmentInfo() { - const {browser, platform} = await api.getEnvironmentInfo(); - document.documentElement.dataset.browser = browser; - document.documentElement.dataset.operatingSystem = platform.os; -} - - -(async () => { - try { - api.forwardLogsToBackend(); - await yomichan.backendReady(); - - setupEnvironmentInfo(); - showExtensionInformation(); - - const optionsFull = await api.optionsGetFull(); - - const modalController = new ModalController(); - modalController.prepare(); - - const settingsController = new SettingsController(optionsFull.profileCurrent); - settingsController.prepare(); - - const storageController = new StorageController(); - storageController.prepare(); - - const genericSettingController = new GenericSettingController(settingsController); - genericSettingController.prepare(); - - const permissionsToggleController = new PermissionsToggleController(settingsController); - permissionsToggleController.prepare(); - - const popupPreviewController = new PopupPreviewController(settingsController); - popupPreviewController.prepare(); - - const audioController = new AudioController(settingsController); - audioController.prepare(); - - const profileController = new ProfileController(settingsController, modalController); - profileController.prepare(); - - const dictionaryController = new DictionaryController(settingsController, modalController, storageController, null); - dictionaryController.prepare(); - - const dictionaryImportController = new DictionaryImportController(settingsController, modalController, storageController, null); - dictionaryImportController.prepare(); - - const ankiController = new AnkiController(settingsController); - ankiController.prepare(); - - const ankiTemplatesController = new AnkiTemplatesController(settingsController, modalController, ankiController); - ankiTemplatesController.prepare(); - - const settingsBackup = new BackupController(settingsController, modalController); - settingsBackup.prepare(); - - const scanInputsController = new ScanInputsController(settingsController); - scanInputsController.prepare(); - - const simpleScanningInputController = new ScanInputsSimpleController(settingsController); - simpleScanningInputController.prepare(); - - yomichan.ready(); - } catch (e) { - yomichan.logError(e); - } -})(); diff --git a/ext/bg/js/settings/modal-controller.js b/ext/bg/js/settings/modal-controller.js deleted file mode 100644 index fe4f911b..00000000 --- a/ext/bg/js/settings/modal-controller.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2020-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 - * Modal - */ - -class ModalController { - constructor() { - this._modals = []; - this._modalMap = new Map(); - } - - prepare() { - const idSuffix = '-modal'; - for (const node of document.querySelectorAll('.modal')) { - let {id} = node; - if (typeof id !== 'string') { continue; } - - if (id.endsWith(idSuffix)) { - id = id.substring(0, id.length - idSuffix.length); - } - - const modal = new Modal(node); - modal.prepare(); - this._modalMap.set(id, modal); - this._modalMap.set(node, modal); - this._modals.push(modal); - } - } - - getModal(nameOrNode) { - const modal = this._modalMap.get(nameOrNode); - return (typeof modal !== 'undefined' ? modal : null); - } - - getTopVisibleModal() { - for (let i = this._modals.length - 1; i >= 0; --i) { - const modal = this._modals[i]; - if (modal.isVisible()) { - return modal; - } - } - return null; - } -} diff --git a/ext/bg/js/settings/modal-jquery.js b/ext/bg/js/settings/modal-jquery.js deleted file mode 100644 index 8c69ae6d..00000000 --- a/ext/bg/js/settings/modal-jquery.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2020-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/>. - */ - -class Modal extends EventDispatcher { - constructor(node) { - super(); - this._node = node; - this._eventListeners = new EventListenerCollection(); - } - - get node() { - return this._node; - } - - prepare() { - // NOP - } - - isVisible() { - return !!(this._getWrappedNode().data('bs.modal') || {}).isShown; - } - - setVisible(value) { - this._getWrappedNode().modal(value ? 'show' : 'hide'); - } - - on(eventName, callback) { - if (eventName === 'visibilityChanged') { - if (this._eventListeners.size === 0) { - const wrappedNode = this._getWrappedNode(); - this._eventListeners.on(wrappedNode, 'hidden.bs.modal', this._onModalHide.bind(this)); - this._eventListeners.on(wrappedNode, 'shown.bs.modal', this._onModalShow.bind(this)); - } - } - return super.on(eventName, callback); - } - - off(eventName, callback) { - const result = super.off(eventName, callback); - if (eventName === 'visibilityChanged' && !this.hasListeners(eventName)) { - this._eventListeners.removeAllEventListeners(); - } - return result; - } - - // Private - - _onModalHide() { - this.trigger('visibilityChanged', {visible: false}); - } - - _onModalShow() { - this.trigger('visibilityChanged', {visible: true}); - } - - _getWrappedNode() { - return $(this._node); - } -} diff --git a/ext/bg/js/settings/modal.js b/ext/bg/js/settings/modal.js deleted file mode 100644 index 2ef49540..00000000 --- a/ext/bg/js/settings/modal.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2020-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 - * PanelElement - */ - -class Modal extends PanelElement { - constructor(node) { - super({ - node, - closingAnimationDuration: 375 // Milliseconds; includes buffer - }); - this._contentNode = null; - this._canCloseOnClick = false; - } - - prepare() { - const node = this.node; - this._contentNode = node.querySelector('.modal-content'); - let dimmerNode = node.querySelector('.modal-content-dimmer'); - if (dimmerNode === null) { dimmerNode = node; } - dimmerNode.addEventListener('mousedown', this._onModalContainerMouseDown.bind(this), false); - dimmerNode.addEventListener('mouseup', this._onModalContainerMouseUp.bind(this), false); - dimmerNode.addEventListener('click', this._onModalContainerClick.bind(this), false); - - for (const actionNode of node.querySelectorAll('[data-modal-action]')) { - actionNode.addEventListener('click', this._onActionNodeClick.bind(this), false); - } - } - - // Private - - _onModalContainerMouseDown(e) { - this._canCloseOnClick = (e.currentTarget === e.target); - } - - _onModalContainerMouseUp(e) { - if (!this._canCloseOnClick) { return; } - this._canCloseOnClick = (e.currentTarget === e.target); - } - - _onModalContainerClick(e) { - if (!this._canCloseOnClick) { return; } - this._canCloseOnClick = false; - if (e.currentTarget !== e.target) { return; } - this.setVisible(false); - } - - _onActionNodeClick(e) { - const {modalAction} = e.currentTarget.dataset; - switch (modalAction) { - case 'expand': - this._setExpanded(true); - break; - case 'collapse': - this._setExpanded(false); - break; - } - } - - _setExpanded(expanded) { - if (this._contentNode === null) { return; } - this._contentNode.classList.toggle('modal-content-full', expanded); - } -} diff --git a/ext/bg/js/settings/permissions-toggle-controller.js b/ext/bg/js/settings/permissions-toggle-controller.js deleted file mode 100644 index f80e7585..00000000 --- a/ext/bg/js/settings/permissions-toggle-controller.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (C) 2020-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 - * ObjectPropertyAccessor - */ - -class PermissionsToggleController { - constructor(settingsController) { - this._settingsController = settingsController; - this._toggles = null; - } - - async prepare() { - this._toggles = document.querySelectorAll('.permissions-toggle'); - - for (const toggle of this._toggles) { - toggle.addEventListener('change', this._onPermissionsToggleChange.bind(this), false); - } - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - this._settingsController.on('permissionsChanged', this._onPermissionsChanged.bind(this)); - - const options = await this._settingsController.getOptions(); - this._onOptionsChanged({options}); - } - - // Private - - _onOptionsChanged({options}) { - let accessor = null; - for (const toggle of this._toggles) { - const {permissionsSetting} = toggle.dataset; - if (typeof permissionsSetting !== 'string') { continue; } - - if (accessor === null) { - accessor = new ObjectPropertyAccessor(options); - } - - const path = ObjectPropertyAccessor.getPathArray(permissionsSetting); - let value; - try { - value = accessor.get(path, path.length); - } catch (e) { - continue; - } - toggle.checked = !!value; - } - this._updateValidity(); - } - - async _onPermissionsToggleChange(e) { - const toggle = e.currentTarget; - let value = toggle.checked; - const valuePre = !value; - const {permissionsSetting} = toggle.dataset; - const hasPermissionsSetting = typeof permissionsSetting === 'string'; - - if (value || !hasPermissionsSetting) { - toggle.checked = valuePre; - const permissions = this._getRequiredPermissions(toggle); - try { - value = await this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, value); - } catch (error) { - value = valuePre; - try { - value = await this._settingsController.permissionsUtil.hasPermissions({permissions}); - } catch (error2) { - // NOP - } - } - toggle.checked = value; - } - - if (hasPermissionsSetting) { - this._setToggleValid(toggle, true); - await this._settingsController.setProfileSetting(permissionsSetting, value); - } - } - - _onPermissionsChanged({permissions: {permissions}}) { - const permissionsSet = new Set(permissions); - for (const toggle of this._toggles) { - const {permissionsSetting} = toggle.dataset; - const hasPermissions = this._hasAll(permissionsSet, this._getRequiredPermissions(toggle)); - - if (typeof permissionsSetting === 'string') { - const valid = !toggle.checked || hasPermissions; - this._setToggleValid(toggle, valid); - } else { - toggle.checked = hasPermissions; - } - } - } - - _setToggleValid(toggle, valid) { - const relative = toggle.closest('.settings-item'); - if (relative === null) { return; } - relative.dataset.invalid = `${!valid}`; - } - - async _updateValidity() { - const permissions = await this._settingsController.permissionsUtil.getAllPermissions(); - this._onPermissionsChanged({permissions}); - } - - _hasAll(set, values) { - for (const value of values) { - if (!set.has(value)) { return false; } - } - return true; - } - - _getRequiredPermissions(toggle) { - const requiredPermissions = toggle.dataset.requiredPermissions; - return (typeof requiredPermissions === 'string' && requiredPermissions.length > 0 ? requiredPermissions.split(' ') : []); - } -} diff --git a/ext/bg/js/settings/pitch-accents-preview-main.js b/ext/bg/js/settings/pitch-accents-preview-main.js deleted file mode 100644 index f292170a..00000000 --- a/ext/bg/js/settings/pitch-accents-preview-main.js +++ /dev/null @@ -1,33 +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 - * DisplayGenerator - */ - -(async () => { - try { - const displayGenerator = new DisplayGenerator({ - japaneseUtil: null, - mediaLoader: null - }); - await displayGenerator.prepare(); - displayGenerator.preparePitchAccents(); - } catch (e) { - yomichan.logError(e); - } -})(); diff --git a/ext/bg/js/settings/popup-preview-controller.js b/ext/bg/js/settings/popup-preview-controller.js deleted file mode 100644 index f98b0679..00000000 --- a/ext/bg/js/settings/popup-preview-controller.js +++ /dev/null @@ -1,129 +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 - * wanakana - */ - -class PopupPreviewController { - constructor(settingsController) { - this._settingsController = settingsController; - this._previewVisible = false; - this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); - this._frame = null; - this._previewTextInput = null; - this._customCss = null; - this._customOuterCss = null; - this._previewFrameContainer = null; - } - - async prepare() { - const button = document.querySelector('#settings-popup-preview-button'); - if (button !== null) { - button.addEventListener('click', this._onShowPopupPreviewButtonClick.bind(this), false); - } else { - this._frame = document.querySelector('#popup-preview-frame'); - this._customCss = document.querySelector('#custom-popup-css'); - this._customOuterCss = document.querySelector('#custom-popup-outer-css'); - this._previewFrameContainer = document.querySelector('.preview-frame-container'); - - this._customCss.addEventListener('input', this._onCustomCssChange.bind(this), false); - this._customCss.addEventListener('settingChanged', this._onCustomCssChange.bind(this), false); - this._customOuterCss.addEventListener('input', this._onCustomOuterCssChange.bind(this), false); - this._customOuterCss.addEventListener('settingChanged', this._onCustomOuterCssChange.bind(this), false); - this._frame.addEventListener('load', this._onFrameLoad2.bind(this), false); - this._settingsController.on('optionsContextChanged', this._onOptionsContextChange.bind(this)); - } - } - - // Private - - _onShowPopupPreviewButtonClick() { - if (this._previewVisible) { return; } - this._showAppearancePreview(); - this._previewVisible = true; - } - - _showAppearancePreview() { - const container = document.querySelector('#settings-popup-preview-container'); - const buttonContainer = document.querySelector('#settings-popup-preview-button-container'); - const settings = document.querySelector('#settings-popup-preview-settings'); - const text = document.querySelector('#settings-popup-preview-text'); - const customCss = document.querySelector('#custom-popup-css'); - const customOuterCss = document.querySelector('#custom-popup-outer-css'); - const frame = document.createElement('iframe'); - - this._previewTextInput = text; - this._frame = frame; - this._customCss = customCss; - this._customOuterCss = customOuterCss; - - wanakana.bind(text); - - frame.addEventListener('load', this._onFrameLoad.bind(this), false); - text.addEventListener('input', this._onTextChange.bind(this), false); - customCss.addEventListener('input', this._onCustomCssChange.bind(this), false); - customOuterCss.addEventListener('input', this._onCustomOuterCssChange.bind(this), false); - this._settingsController.on('optionsContextChanged', this._onOptionsContextChange.bind(this)); - - frame.src = '/popup-preview.html'; - frame.id = 'settings-popup-preview-frame'; - - container.appendChild(frame); - if (buttonContainer.parentNode !== null) { - buttonContainer.parentNode.removeChild(buttonContainer); - } - settings.style.display = ''; - } - - _onFrameLoad() { - this._onOptionsContextChange(); - this._setText(this._previewTextInput.value); - } - - _onFrameLoad2() { - this._onOptionsContextChange(); - this._onCustomCssChange(); - this._onCustomOuterCssChange(); - } - - _onTextChange(e) { - this._setText(e.currentTarget.value); - } - - _onCustomCssChange() { - this._invoke('setCustomCss', {css: this._customCss.value}); - } - - _onCustomOuterCssChange() { - this._invoke('setCustomOuterCss', {css: this._customOuterCss.value}); - } - - _onOptionsContextChange() { - const optionsContext = this._settingsController.getOptionsContext(); - this._invoke('updateOptionsContext', {optionsContext}); - } - - _setText(text) { - this._invoke('setText', {text}); - } - - _invoke(action, params) { - if (this._frame === null || this._frame.contentWindow === null) { return; } - this._frame.contentWindow.postMessage({action, params}, this._targetOrigin); - } -} diff --git a/ext/bg/js/settings/popup-preview-frame-main.js b/ext/bg/js/settings/popup-preview-frame-main.js deleted file mode 100644 index dcbc0d96..00000000 --- a/ext/bg/js/settings/popup-preview-frame-main.js +++ /dev/null @@ -1,44 +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 - * HotkeyHandler - * PopupFactory - * PopupPreviewFrame - * api - */ - -(async () => { - try { - api.forwardLogsToBackend(); - - const {tabId, frameId} = await api.frameInformationGet(); - - const hotkeyHandler = new HotkeyHandler(); - hotkeyHandler.prepare(); - - const popupFactory = new PopupFactory(frameId); - popupFactory.prepare(); - - const preview = new PopupPreviewFrame(tabId, frameId, popupFactory, hotkeyHandler); - await preview.prepare(); - - document.documentElement.dataset.loaded = 'true'; - } catch (e) { - yomichan.logError(e); - } -})(); diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js deleted file mode 100644 index 56100fb3..00000000 --- a/ext/bg/js/settings/popup-preview-frame.js +++ /dev/null @@ -1,233 +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 - * Frontend - * TextSourceRange - * api - * wanakana - */ - -class PopupPreviewFrame { - constructor(tabId, frameId, popupFactory, hotkeyHandler) { - this._tabId = tabId; - this._frameId = frameId; - this._popupFactory = popupFactory; - this._hotkeyHandler = hotkeyHandler; - this._frontend = null; - this._apiOptionsGetOld = null; - this._popupShown = false; - this._themeChangeTimeout = null; - this._textSource = null; - this._optionsContext = null; - this._exampleText = null; - this._exampleTextInput = null; - this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); - - this._windowMessageHandlers = new Map([ - ['setText', this._onSetText.bind(this)], - ['setCustomCss', this._setCustomCss.bind(this)], - ['setCustomOuterCss', this._setCustomOuterCss.bind(this)], - ['updateOptionsContext', this._updateOptionsContext.bind(this)] - ]); - } - - async prepare() { - this._exampleText = document.querySelector('#example-text'); - this._exampleTextInput = document.querySelector('#example-text-input'); - - if (this._exampleTextInput !== null && typeof wanakana !== 'undefined') { - wanakana.bind(this._exampleTextInput); - } - - window.addEventListener('message', this._onMessage.bind(this), false); - - // Setup events - document.querySelector('#theme-dark-checkbox').addEventListener('change', this._onThemeDarkCheckboxChanged.bind(this), false); - this._exampleText.addEventListener('click', this._onExampleTextClick.bind(this), false); - this._exampleTextInput.addEventListener('blur', this._onExampleTextInputBlur.bind(this), false); - this._exampleTextInput.addEventListener('input', this._onExampleTextInputInput.bind(this), false); - - // Overwrite API functions - this._apiOptionsGetOld = api.optionsGet.bind(api); - api.optionsGet = this._apiOptionsGet.bind(this); - - // Overwrite frontend - this._frontend = new Frontend({ - tabId: this._tabId, - frameId: this._frameId, - popupFactory: this._popupFactory, - depth: 0, - parentPopupId: null, - parentFrameId: null, - useProxyPopup: false, - canUseWindowPopup: false, - pageType: 'web', - allowRootFramePopupProxy: false, - childrenSupported: false, - hotkeyHandler: this._hotkeyHandler - }); - this._frontend.setOptionsContextOverride(this._optionsContext); - await this._frontend.prepare(); - this._frontend.setDisabledOverride(true); - this._frontend.canClearSelection = false; - this._frontend.popup.on('customOuterCssChanged', this._onCustomOuterCssChanged.bind(this)); - - // Update search - this._updateSearch(); - } - - // Private - - async _apiOptionsGet(...args) { - const options = await this._apiOptionsGetOld(...args); - options.general.enable = true; - options.general.debugInfo = false; - options.general.popupWidth = 400; - options.general.popupHeight = 250; - options.general.popupHorizontalOffset = 0; - options.general.popupVerticalOffset = 10; - options.general.popupHorizontalOffset2 = 10; - options.general.popupVerticalOffset2 = 0; - options.general.popupHorizontalTextPosition = 'below'; - options.general.popupVerticalTextPosition = 'before'; - options.scanning.selectText = false; - return options; - } - - _onCustomOuterCssChanged({node, inShadow}) { - if (node === null || inShadow) { return; } - - const node2 = document.querySelector('#popup-outer-css'); - if (node2 === null) { return; } - - // This simulates the stylesheet priorities when injecting using the web extension API. - node2.parentNode.insertBefore(node, node2); - } - - _onMessage(e) { - if (e.origin !== this._targetOrigin) { return; } - - const {action, params} = e.data; - const handler = this._windowMessageHandlers.get(action); - if (typeof handler !== 'function') { return; } - - handler(params); - } - - _onThemeDarkCheckboxChanged(e) { - document.documentElement.classList.toggle('dark', e.target.checked); - if (this._themeChangeTimeout !== null) { - clearTimeout(this._themeChangeTimeout); - } - this._themeChangeTimeout = setTimeout(() => { - this._themeChangeTimeout = null; - const popup = this._frontend.popup; - if (popup === null) { return; } - popup.updateTheme(); - }, 300); - } - - _onExampleTextClick() { - if (this._exampleTextInput === null) { return; } - const visible = this._exampleTextInput.hidden; - this._exampleTextInput.hidden = !visible; - if (!visible) { return; } - this._exampleTextInput.focus(); - this._exampleTextInput.select(); - } - - _onExampleTextInputBlur() { - if (this._exampleTextInput === null) { return; } - this._exampleTextInput.hidden = true; - } - - _onExampleTextInputInput(e) { - this._setText(e.currentTarget.value); - } - - _onSetText({text}) { - this._setText(text, true); - } - - _setText(text, setInput) { - if (setInput && this._exampleTextInput !== null) { - this._exampleTextInput.value = text; - } - - if (this._exampleText === null) { return; } - - this._exampleText.textContent = text; - if (this._frontend === null) { return; } - this._updateSearch(); - } - - _setInfoVisible(visible) { - const node = document.querySelector('.placeholder-info'); - if (node === null) { return; } - - node.classList.toggle('placeholder-info-visible', visible); - } - - _setCustomCss({css}) { - if (this._frontend === null) { return; } - const popup = this._frontend.popup; - if (popup === null) { return; } - popup.setCustomCss(css); - } - - _setCustomOuterCss({css}) { - if (this._frontend === null) { return; } - const popup = this._frontend.popup; - if (popup === null) { return; } - popup.setCustomOuterCss(css, false); - } - - async _updateOptionsContext({optionsContext}) { - this._optionsContext = optionsContext; - if (this._frontend === null) { return; } - this._frontend.setOptionsContextOverride(optionsContext); - await this._frontend.updateOptions(); - await this._updateSearch(); - } - - async _updateSearch() { - if (this._exampleText === null) { return; } - - const textNode = this._exampleText.firstChild; - if (textNode === null) { return; } - - const range = document.createRange(); - range.selectNodeContents(textNode); - const source = new TextSourceRange(range, range.toString(), null, null); - - try { - await this._frontend.setTextSource(source); - } finally { - source.cleanup(); - } - this._textSource = source; - await this._frontend.showContentCompleted(); - - const popup = this._frontend.popup; - if (popup !== null && popup.isVisibleSync()) { - this._popupShown = true; - } - - this._setInfoVisible(!this._popupShown); - } -} diff --git a/ext/bg/js/settings/profile-conditions-ui.js b/ext/bg/js/settings/profile-conditions-ui.js deleted file mode 100644 index 5fda1dc0..00000000 --- a/ext/bg/js/settings/profile-conditions-ui.js +++ /dev/null @@ -1,712 +0,0 @@ -/* - * Copyright (C) 2020-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 - * KeyboardMouseInputField - */ - -class ProfileConditionsUI extends EventDispatcher { - constructor(settingsController) { - super(); - this._settingsController = settingsController; - this._os = null; - this._conditionGroupsContainer = null; - this._addConditionGroupButton = null; - this._children = []; - this._eventListeners = new EventListenerCollection(); - this._defaultType = 'popupLevel'; - this._profileIndex = 0; - this._descriptors = new Map([ - [ - 'popupLevel', - { - displayName: 'Popup Level', - defaultOperator: 'equal', - operators: new Map([ - ['equal', {displayName: '=', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], - ['notEqual', {displayName: '\u2260', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], - ['lessThan', {displayName: '<', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], - ['greaterThan', {displayName: '>', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], - ['lessThanOrEqual', {displayName: '\u2264', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], - ['greaterThanOrEqual', {displayName: '\u2265', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}] - ]) - } - ], - [ - 'url', - { - displayName: 'URL', - defaultOperator: 'matchDomain', - operators: new Map([ - ['matchDomain', {displayName: 'Matches Domain', type: 'string', defaultValue: 'example.com', resetDefaultOnChange: true, validate: this._validateDomains.bind(this), normalize: this._normalizeDomains.bind(this)}], - ['matchRegExp', {displayName: 'Matches RegExp', type: 'string', defaultValue: 'example\\.com', resetDefaultOnChange: true, validate: this._validateRegExp.bind(this)}] - ]) - } - ], - [ - 'modifierKeys', - { - displayName: 'Modifier Keys', - defaultOperator: 'are', - operators: new Map([ - ['are', {displayName: 'Are', type: 'modifierKeys', defaultValue: ''}], - ['areNot', {displayName: 'Are Not', type: 'modifierKeys', defaultValue: ''}], - ['include', {displayName: 'Include', type: 'modifierKeys', defaultValue: ''}], - ['notInclude', {displayName: 'Don\'t Include', type: 'modifierKeys', defaultValue: ''}] - ]) - } - ] - ]); - } - - get settingsController() { - return this._settingsController; - } - - get profileIndex() { - return this._profileIndex; - } - - get os() { - return this._os; - } - - set os(value) { - this._os = value; - } - - async prepare(profileIndex) { - const options = await this._settingsController.getOptionsFull(); - const {profiles} = options; - if (profileIndex < 0 || profileIndex >= profiles.length) { return; } - const {conditionGroups} = profiles[profileIndex]; - - this._profileIndex = profileIndex; - this._conditionGroupsContainer = document.querySelector('#profile-condition-groups'); - this._addConditionGroupButton = document.querySelector('#profile-add-condition-group'); - - for (let i = 0, ii = conditionGroups.length; i < ii; ++i) { - this._addConditionGroup(conditionGroups[i], i); - } - - this._eventListeners.addEventListener(this._addConditionGroupButton, 'click', this._onAddConditionGroupButtonClick.bind(this), false); - } - - cleanup() { - this._eventListeners.removeAllEventListeners(); - - for (const child of this._children) { - child.cleanup(); - } - this._children = []; - - this._conditionGroupsContainer = null; - this._addConditionGroupButton = null; - } - - instantiateTemplate(names) { - return this._settingsController.instantiateTemplate(names); - } - - getDescriptorTypes() { - const results = []; - for (const [name, {displayName}] of this._descriptors.entries()) { - results.push({name, displayName}); - } - return results; - } - - getDescriptorOperators(type) { - const info = this._descriptors.get(type); - const results = []; - if (typeof info !== 'undefined') { - for (const [name, {displayName}] of info.operators.entries()) { - results.push({name, displayName}); - } - } - return results; - } - - getDefaultType() { - return this._defaultType; - } - - getDefaultOperator(type) { - const info = this._descriptors.get(type); - return (typeof info !== 'undefined' ? info.defaultOperator : ''); - } - - getOperatorDetails(type, operator) { - const info = this._getOperatorDetails(type, operator); - - const { - displayName=operator, - type: type2='string', - defaultValue='', - resetDefaultOnChange=false, - validate=null, - normalize=null - } = (typeof info === 'undefined' ? {} : info); - - return { - displayName, - type: type2, - defaultValue, - resetDefaultOnChange, - validate, - normalize - }; - } - - getDefaultCondition() { - const type = this.getDefaultType(); - const operator = this.getDefaultOperator(type); - const {defaultValue: value} = this.getOperatorDetails(type, operator); - return {type, operator, value}; - } - - removeConditionGroup(child) { - const index = child.index; - if (index < 0 || index >= this._children.length) { return false; } - - const child2 = this._children[index]; - if (child !== child2) { return false; } - - this._children.splice(index, 1); - child.cleanup(); - - for (let i = index, ii = this._children.length; i < ii; ++i) { - this._children[i].index = i; - } - - this.settingsController.modifyGlobalSettings([{ - action: 'splice', - path: this.getPath('conditionGroups'), - start: index, - deleteCount: 1, - items: [] - }]); - - this._triggerConditionGroupCountChanged(this._children.length); - - return true; - } - - splitValue(value) { - return value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0); - } - - getPath(property) { - property = (typeof property === 'string' ? `.${property}` : ''); - return `profiles[${this.profileIndex}]${property}`; - } - - createKeyboardMouseInputField(inputNode, mouseButton) { - return new KeyboardMouseInputField(inputNode, mouseButton, this._os); - } - - // Private - - _onAddConditionGroupButtonClick() { - const conditionGroup = { - conditions: [this.getDefaultCondition()] - }; - const index = this._children.length; - - this._addConditionGroup(conditionGroup, index); - - this.settingsController.modifyGlobalSettings([{ - action: 'splice', - path: this.getPath('conditionGroups'), - start: index, - deleteCount: 0, - items: [conditionGroup] - }]); - - this._triggerConditionGroupCountChanged(this._children.length); - } - - _addConditionGroup(conditionGroup, index) { - const child = new ProfileConditionGroupUI(this, index); - child.prepare(conditionGroup); - this._children.push(child); - this._conditionGroupsContainer.appendChild(child.node); - return child; - } - - _getOperatorDetails(type, operator) { - const info = this._descriptors.get(type); - return (typeof info !== 'undefined' ? info.operators.get(operator) : void 0); - } - - _validateInteger(value) { - const number = Number.parseFloat(value); - return Number.isFinite(number) && Math.floor(number) === number; - } - - _validateDomains(value) { - return this.splitValue(value).length > 0; - } - - _validateRegExp(value) { - try { - new RegExp(value, 'i'); - return true; - } catch (e) { - return false; - } - } - - _normalizeInteger(value) { - const number = Number.parseFloat(value); - return `${number}`; - } - - _normalizeDomains(value) { - return this.splitValue(value).join(', '); - } - - _triggerConditionGroupCountChanged(count) { - this.trigger('conditionGroupCountChanged', {count, profileIndex: this._profileIndex}); - } -} - -class ProfileConditionGroupUI { - constructor(parent, index) { - this._parent = parent; - this._index = index; - this._node = null; - this._conditionContainer = null; - this._addConditionButton = null; - this._children = []; - this._eventListeners = new EventListenerCollection(); - } - - get settingsController() { - return this._parent.settingsController; - } - - get parent() { - return this._parent; - } - - get index() { - return this._index; - } - - set index(value) { - this._index = value; - } - - get node() { - return this._node; - } - - get childCount() { - return this._children.length; - } - - prepare(conditionGroup) { - this._node = this._parent.instantiateTemplate('profile-condition-group'); - this._conditionContainer = this._node.querySelector('.profile-condition-list'); - this._addConditionButton = this._node.querySelector('.profile-condition-add-button'); - - const conditions = conditionGroup.conditions; - for (let i = 0, ii = conditions.length; i < ii; ++i) { - this._addCondition(conditions[i], i); - } - - this._eventListeners.addEventListener(this._addConditionButton, 'click', this._onAddConditionButtonClick.bind(this), false); - } - - cleanup() { - this._eventListeners.removeAllEventListeners(); - - for (const child of this._children) { - child.cleanup(); - } - this._children = []; - - if (this._node === null) { return; } - - const node = this._node; - this._node = null; - this._conditionContainer = null; - this._addConditionButton = null; - - if (node.parentNode !== null) { - node.parentNode.removeChild(node); - } - } - - removeCondition(child) { - const index = child.index; - if (index < 0 || index >= this._children.length) { return false; } - - const child2 = this._children[index]; - if (child !== child2) { return false; } - - this._children.splice(index, 1); - child.cleanup(); - - for (let i = index, ii = this._children.length; i < ii; ++i) { - this._children[i].index = i; - } - - this.settingsController.modifyGlobalSettings([{ - action: 'splice', - path: this.getPath('conditions'), - start: index, - deleteCount: 1, - items: [] - }]); - - if (this._children.length === 0) { - this.removeSelf(); - } - - return true; - } - - getPath(property) { - property = (typeof property === 'string' ? `.${property}` : ''); - return this._parent.getPath(`conditionGroups[${this._index}]${property}`); - } - - removeSelf() { - this._parent.removeConditionGroup(this); - } - - // Private - - _onAddConditionButtonClick() { - const condition = this._parent.getDefaultCondition(); - const index = this._children.length; - - this._addCondition(condition, index); - - this.settingsController.modifyGlobalSettings([{ - action: 'splice', - path: this.getPath('conditions'), - start: index, - deleteCount: 0, - items: [condition] - }]); - } - - _addCondition(condition, index) { - const child = new ProfileConditionUI(this, index); - child.prepare(condition); - this._children.push(child); - this._conditionContainer.appendChild(child.node); - return child; - } -} - -class ProfileConditionUI { - constructor(parent, index) { - this._parent = parent; - this._index = index; - this._node = null; - this._typeInput = null; - this._operatorInput = null; - this._valueInputContainer = null; - this._removeButton = null; - this._mouseButton = null; - this._mouseButtonContainer = null; - this._menuButton = null; - this._value = ''; - this._kbmInputField = null; - this._eventListeners = new EventListenerCollection(); - this._inputEventListeners = new EventListenerCollection(); - } - - get settingsController() { - return this._parent.parent.settingsController; - } - - get parent() { - return this._parent; - } - - get index() { - return this._index; - } - - set index(value) { - this._index = value; - } - - get node() { - return this._node; - } - - prepare(condition) { - const {type, operator, value} = condition; - - this._node = this._parent.parent.instantiateTemplate('profile-condition'); - this._typeInput = this._node.querySelector('.profile-condition-type'); - this._typeOptionContainer = this._typeInput.querySelector('optgroup'); - this._operatorInput = this._node.querySelector('.profile-condition-operator'); - this._operatorOptionContainer = this._operatorInput.querySelector('optgroup'); - this._valueInput = this._node.querySelector('.profile-condition-input'); - this._removeButton = this._node.querySelector('.profile-condition-remove'); - this._mouseButton = this._node.querySelector('.mouse-button'); - this._mouseButtonContainer = this._node.querySelector('.mouse-button-container'); - this._menuButton = this._node.querySelector('.profile-condition-menu-button'); - - const operatorDetails = this._getOperatorDetails(type, operator); - this._updateTypes(type); - this._updateOperators(type, operator); - this._updateValueInput(value, operatorDetails); - - this._eventListeners.addEventListener(this._typeInput, 'change', this._onTypeChange.bind(this), false); - this._eventListeners.addEventListener(this._operatorInput, 'change', this._onOperatorChange.bind(this), false); - if (this._removeButton !== null) { this._eventListeners.addEventListener(this._removeButton, 'click', this._onRemoveButtonClick.bind(this), false); } - if (this._menuButton !== null) { - this._eventListeners.addEventListener(this._menuButton, 'menuOpen', this._onMenuOpen.bind(this), false); - this._eventListeners.addEventListener(this._menuButton, 'menuClose', this._onMenuClose.bind(this), false); - } - } - - cleanup() { - this._eventListeners.removeAllEventListeners(); - this._value = ''; - - if (this._node === null) { return; } - - const node = this._node; - this._node = null; - this._typeInput = null; - this._operatorInput = null; - this._valueInputContainer = null; - this._removeButton = null; - - if (node.parentNode !== null) { - node.parentNode.removeChild(node); - } - } - - getPath(property) { - property = (typeof property === 'string' ? `.${property}` : ''); - return this._parent.getPath(`conditions[${this._index}]${property}`); - } - - // Private - - _onTypeChange(e) { - const type = e.currentTarget.value; - this._setType(type); - } - - _onOperatorChange(e) { - const type = this._typeInput.value; - const operator = e.currentTarget.value; - this._setOperator(type, operator); - } - - _onValueInputChange({validate, normalize}, e) { - const node = e.currentTarget; - const value = node.value; - const okay = this._validateValue(value, validate); - this._value = value; - if (okay) { - const normalizedValue = this._normalizeValue(value, normalize); - node.value = normalizedValue; - this.settingsController.setGlobalSetting(this.getPath('value'), normalizedValue); - } - } - - _onModifierInputChange({validate, normalize}, {modifiers}) { - modifiers = this._joinModifiers(modifiers); - const okay = this._validateValue(modifiers, validate); - this._value = modifiers; - if (okay) { - const normalizedValue = this._normalizeValue(modifiers, normalize); - this.settingsController.setGlobalSetting(this.getPath('value'), normalizedValue); - } - } - - _onRemoveButtonClick() { - this._removeSelf(); - } - - _onMenuOpen(e) { - const bodyNode = e.detail.menu.bodyNode; - const deleteGroup = bodyNode.querySelector('.popup-menu-item[data-menu-action="deleteGroup"]'); - if (deleteGroup !== null) { - deleteGroup.hidden = (this._parent.childCount <= 1); - } - } - - _onMenuClose(e) { - switch (e.detail.action) { - case 'delete': - this._removeSelf(); - break; - case 'deleteGroup': - this._parent.removeSelf(); - break; - case 'resetValue': - this._resetValue(); - break; - } - } - - _getDescriptorTypes() { - return this._parent.parent.getDescriptorTypes(); - } - - _getDescriptorOperators(type) { - return this._parent.parent.getDescriptorOperators(type); - } - - _getOperatorDetails(type, operator) { - return this._parent.parent.getOperatorDetails(type, operator); - } - - _updateTypes(type) { - const types = this._getDescriptorTypes(); - this._updateSelect(this._typeInput, this._typeOptionContainer, types, type); - } - - _updateOperators(type, operator) { - const operators = this._getDescriptorOperators(type); - this._updateSelect(this._operatorInput, this._operatorOptionContainer, operators, operator); - } - - _updateSelect(select, optionContainer, values, value) { - optionContainer.textContent = ''; - for (const {name, displayName} of values) { - const option = document.createElement('option'); - option.value = name; - option.textContent = displayName; - optionContainer.appendChild(option); - } - select.value = value; - } - - _updateValueInput(value, {type, validate, normalize}) { - this._inputEventListeners.removeAllEventListeners(); - if (this._kbmInputField !== null) { - this._kbmInputField.cleanup(); - this._kbmInputField = null; - } - - let inputType = 'text'; - let inputValue = value; - let inputStep = null; - let showMouseButton = false; - const events = []; - const inputData = {validate, normalize}; - const node = this._valueInput; - - switch (type) { - case 'integer': - inputType = 'number'; - inputStep = '1'; - events.push(['addEventListener', node, 'change', this._onValueInputChange.bind(this, inputData), false]); - break; - case 'modifierKeys': - case 'modifierInputs': - inputValue = null; - showMouseButton = (type === 'modifierInputs'); - this._kbmInputField = this._parent.parent.createKeyboardMouseInputField(node, this._mouseButton); - this._kbmInputField.prepare(null, this._splitModifiers(value), showMouseButton, false); - events.push(['on', this._kbmInputField, 'change', this._onModifierInputChange.bind(this, inputData), false]); - break; - default: // 'string' - events.push(['addEventListener', node, 'change', this._onValueInputChange.bind(this, inputData), false]); - break; - } - - this._value = value; - delete node.dataset.invalid; - node.type = inputType; - if (inputValue !== null) { - node.value = inputValue; - } - if (typeof inputStep === 'string') { - node.step = inputStep; - } else { - node.removeAttribute('step'); - } - this._mouseButtonContainer.hidden = !showMouseButton; - for (const args of events) { - this._inputEventListeners.addGeneric(...args); - } - - this._validateValue(value, validate); - } - - _validateValue(value, validate) { - const okay = (validate === null || validate(value)); - this._valueInput.dataset.invalid = `${!okay}`; - return okay; - } - - _normalizeValue(value, normalize) { - return (normalize !== null ? normalize(value) : value); - } - - _removeSelf() { - this._parent.removeCondition(this); - } - - _splitModifiers(modifiersString) { - return modifiersString.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0); - } - - _joinModifiers(modifiersArray) { - return modifiersArray.join(', '); - } - - async _setType(type, operator) { - const operators = this._getDescriptorOperators(type); - if (typeof operator === 'undefined') { - operator = operators.length > 0 ? operators[0].name : ''; - } - const operatorDetails = this._getOperatorDetails(type, operator); - const {defaultValue} = operatorDetails; - this._updateSelect(this._operatorInput, this._operatorOptionContainer, operators, operator); - this._updateValueInput(defaultValue, operatorDetails); - await this.settingsController.modifyGlobalSettings([ - {action: 'set', path: this.getPath('type'), value: type}, - {action: 'set', path: this.getPath('operator'), value: operator}, - {action: 'set', path: this.getPath('value'), value: defaultValue} - ]); - } - - async _setOperator(type, operator) { - const operatorDetails = this._getOperatorDetails(type, operator); - const settingsModifications = [{action: 'set', path: this.getPath('operator'), value: operator}]; - if (operatorDetails.resetDefaultOnChange) { - const {defaultValue} = operatorDetails; - const okay = this._updateValueInput(defaultValue, operatorDetails); - if (okay) { - settingsModifications.push({action: 'set', path: this.getPath('value'), value: defaultValue}); - } - } - await this.settingsController.modifyGlobalSettings(settingsModifications); - } - - async _resetValue() { - const type = this._typeInput.value; - const operator = this._operatorInput.value; - await this._setType(type, operator); - } -} diff --git a/ext/bg/js/settings/profile-controller.js b/ext/bg/js/settings/profile-controller.js deleted file mode 100644 index 914fc679..00000000 --- a/ext/bg/js/settings/profile-controller.js +++ /dev/null @@ -1,698 +0,0 @@ -/* - * Copyright (C) 2020-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 - * ProfileConditionsUI - * api - */ - -class ProfileController { - constructor(settingsController, modalController) { - this._settingsController = settingsController; - this._modalController = modalController; - this._profileConditionsUI = new ProfileConditionsUI(settingsController); - this._profileConditionsIndex = null; - this._profileActiveSelect = null; - this._profileTargetSelect = null; - this._profileCopySourceSelect = null; - this._profileNameInput = null; - this._removeProfileNameElement = null; - this._profileAddButton = null; - this._profileRemoveButton = null; - this._profileRemoveConfirmButton = null; - this._profileCopyButton = null; - this._profileCopyConfirmButton = null; - this._profileMoveUpButton = null; - this._profileMoveDownButton = null; - this._profileEntryListContainer = null; - this._profileConditionsProfileName = null; - this._profileRemoveModal = null; - this._profileCopyModal = null; - this._profileConditionsModal = null; - this._profileEntriesSupported = false; - this._profileEntryList = []; - this._profiles = []; - this._profileCurrent = 0; - } - - get profileCount() { - return this._profiles.length; - } - - get profileCurrentIndex() { - return this._profileCurrent; - } - - async prepare() { - const {platform: {os}} = await api.getEnvironmentInfo(); - this._profileConditionsUI.os = os; - - this._profileActiveSelect = document.querySelector('#profile-active-select'); - this._profileTargetSelect = document.querySelector('#profile-target-select'); - this._profileCopySourceSelect = document.querySelector('#profile-copy-source-select'); - this._profileNameInput = document.querySelector('#profile-name-input'); - this._removeProfileNameElement = document.querySelector('#profile-remove-name'); - this._profileAddButton = document.querySelector('#profile-add-button'); - this._profileRemoveButton = document.querySelector('#profile-remove-button'); - this._profileRemoveConfirmButton = document.querySelector('#profile-remove-confirm-button'); - this._profileCopyButton = document.querySelector('#profile-copy-button'); - this._profileCopyConfirmButton = document.querySelector('#profile-copy-confirm-button'); - this._profileMoveUpButton = document.querySelector('#profile-move-up-button'); - this._profileMoveDownButton = document.querySelector('#profile-move-down-button'); - this._profileEntryListContainer = document.querySelector('#profile-entry-list'); - this._profileConditionsProfileName = document.querySelector('#profile-conditions-profile-name'); - this._profileRemoveModal = this._modalController.getModal('profile-remove'); - this._profileCopyModal = this._modalController.getModal('profile-copy'); - this._profileConditionsModal = this._modalController.getModal('profile-conditions'); - - this._profileEntriesSupported = (this._profileEntryListContainer !== null); - - if (this._profileActiveSelect !== null) { this._profileActiveSelect.addEventListener('change', this._onProfileActiveChange.bind(this), false); } - if (this._profileTargetSelect !== null) { this._profileTargetSelect.addEventListener('change', this._onProfileTargetChange.bind(this), false); } - if (this._profileNameInput !== null) { this._profileNameInput.addEventListener('change', this._onNameChanged.bind(this), false); } - if (this._profileAddButton !== null) { this._profileAddButton.addEventListener('click', this._onAdd.bind(this), false); } - if (this._profileRemoveButton !== null) { this._profileRemoveButton.addEventListener('click', this._onDelete.bind(this), false); } - if (this._profileRemoveConfirmButton !== null) { this._profileRemoveConfirmButton.addEventListener('click', this._onDeleteConfirm.bind(this), false); } - if (this._profileCopyButton !== null) { this._profileCopyButton.addEventListener('click', this._onCopy.bind(this), false); } - if (this._profileCopyConfirmButton !== null) { this._profileCopyConfirmButton.addEventListener('click', this._onCopyConfirm.bind(this), false); } - if (this._profileMoveUpButton !== null) { this._profileMoveUpButton.addEventListener('click', this._onMove.bind(this, -1), false); } - if (this._profileMoveDownButton !== null) { this._profileMoveDownButton.addEventListener('click', this._onMove.bind(this, 1), false); } - - this._profileConditionsUI.on('conditionGroupCountChanged', this._onConditionGroupCountChanged.bind(this)); - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - this._onOptionsChanged(); - } - - async moveProfile(profileIndex, offset) { - if (this._getProfile(profileIndex) === null) { return; } - - const profileIndexNew = Math.max(0, Math.min(this._profiles.length - 1, profileIndex + offset)); - if (profileIndex === profileIndexNew) { return; } - - await this.swapProfiles(profileIndex, profileIndexNew); - } - - async setProfileName(profileIndex, value) { - const profile = this._getProfile(profileIndex); - if (profile === null) { return; } - - profile.name = value; - this._updateSelectName(profileIndex, value); - - const profileEntry = this._getProfileEntry(profileIndex); - if (profileEntry !== null) { profileEntry.setName(value); } - - await this._settingsController.setGlobalSetting(`profiles[${profileIndex}].name`, value); - } - - async setDefaultProfile(profileIndex) { - const profile = this._getProfile(profileIndex); - if (profile === null) { return; } - - this._profileActiveSelect.value = `${profileIndex}`; - this._profileCurrent = profileIndex; - - const profileEntry = this._getProfileEntry(profileIndex); - if (profileEntry !== null) { profileEntry.setIsDefault(true); } - - await this._settingsController.setGlobalSetting('profileCurrent', profileIndex); - } - - async copyProfile(sourceProfileIndex, destinationProfileIndex) { - const sourceProfile = this._getProfile(sourceProfileIndex); - if (sourceProfile === null || !this._getProfile(destinationProfileIndex)) { return; } - - const options = clone(sourceProfile.options); - this._profiles[destinationProfileIndex].options = options; - - this._updateProfileSelectOptions(); - - const destinationProfileEntry = this._getProfileEntry(destinationProfileIndex); - if (destinationProfileEntry !== null) { - destinationProfileEntry.updateState(); - } - - await this._settingsController.modifyGlobalSettings([{ - action: 'set', - path: `profiles[${destinationProfileIndex}].options`, - value: options - }]); - - await this._settingsController.refresh(); - } - - async duplicateProfile(profileIndex) { - const profile = this._getProfile(profileIndex); - if (this.profile === null) { return; } - - // Create new profile - const newProfile = clone(profile); - newProfile.name = this._createCopyName(profile.name, this._profiles, 100); - - // Update state - const index = this._profiles.length; - this._profiles.push(newProfile); - if (this._profileEntriesSupported) { - this._addProfileEntry(index); - } - this._updateProfileSelectOptions(); - - // Modify settings - await this._settingsController.modifyGlobalSettings([{ - action: 'splice', - path: 'profiles', - start: index, - deleteCount: 0, - items: [newProfile] - }]); - - // Update profile index - this._settingsController.profileIndex = index; - } - - async deleteProfile(profileIndex) { - const profile = this._getProfile(profileIndex); - if (profile === null || this.profileCount <= 1) { return; } - - // Get indices - let profileCurrentNew = this._profileCurrent; - const settingsProfileIndex = this._settingsController.profileIndex; - - // Construct settings modifications - const modifications = [{ - action: 'splice', - path: 'profiles', - start: profileIndex, - deleteCount: 1, - items: [] - }]; - if (profileCurrentNew >= profileIndex) { - profileCurrentNew = Math.min(profileCurrentNew - 1, this._profiles.length - 1); - modifications.push({ - action: 'set', - path: 'profileCurrent', - value: profileCurrentNew - }); - } - - // Update state - this._profileCurrent = profileCurrentNew; - - this._profiles.splice(profileIndex, 1); - - if (profileIndex < this._profileEntryList.length) { - const profileEntry = this._profileEntryList[profileIndex]; - profileEntry.cleanup(); - this._profileEntryList.splice(profileIndex, 1); - - for (let i = profileIndex, ii = this._profileEntryList.length; i < ii; ++i) { - this._profileEntryList[i].index = i; - } - } - - const profileEntry2 = this._getProfileEntry(profileCurrentNew); - if (profileEntry2 !== null) { - profileEntry2.setIsDefault(true); - } - - this._updateProfileSelectOptions(); - - // Modify settings - await this._settingsController.modifyGlobalSettings(modifications); - - // Update profile index - if (settingsProfileIndex === profileIndex) { - this._settingsController.profileIndex = profileCurrentNew; - } - } - - async swapProfiles(index1, index2) { - const profile1 = this._getProfile(index1); - const profile2 = this._getProfile(index2); - if (profile1 === null || profile2 === null || index1 === index2) { return; } - - // Get swapped indices - const profileCurrent = this._profileCurrent; - const profileCurrentNew = this._getSwappedValue(profileCurrent, index1, index2); - - const settingsProfileIndex = this._settingsController.profileIndex; - const settingsProfileIndexNew = this._getSwappedValue(settingsProfileIndex, index1, index2); - - // Construct settings modifications - const modifications = [{ - action: 'swap', - path1: `profiles[${index1}]`, - path2: `profiles[${index2}]` - }]; - if (profileCurrentNew !== profileCurrent) { - modifications.push({ - action: 'set', - path: 'profileCurrent', - value: profileCurrentNew - }); - } - - // Update state - this._profileCurrent = profileCurrentNew; - - this._profiles[index1] = profile2; - this._profiles[index2] = profile1; - - const entry1 = this._getProfileEntry(index1); - const entry2 = this._getProfileEntry(index2); - if (entry1 !== null && entry2 !== null) { - entry1.index = index2; - entry2.index = index1; - this._swapDomNodes(entry1.node, entry2.node); - this._profileEntryList[index1] = entry2; - this._profileEntryList[index2] = entry1; - } - - this._updateProfileSelectOptions(); - - // Modify settings - await this._settingsController.modifyGlobalSettings(modifications); - - // Update profile index - if (settingsProfileIndex !== settingsProfileIndexNew) { - this._settingsController.profileIndex = settingsProfileIndexNew; - } - } - - openDeleteProfileModal(profileIndex) { - const profile = this._getProfile(profileIndex); - if (profile === null || this.profileCount <= 1) { return; } - - this._removeProfileNameElement.textContent = profile.name; - this._profileRemoveModal.node.dataset.profileIndex = `${profileIndex}`; - this._profileRemoveModal.setVisible(true); - } - - openCopyProfileModal(profileIndex) { - const profile = this._getProfile(profileIndex); - if (profile === null || this.profileCount <= 1) { return; } - - let copyFromIndex = this._profileCurrent; - if (copyFromIndex === profileIndex) { - if (profileIndex !== 0) { - copyFromIndex = 0; - } else if (this.profileCount > 1) { - copyFromIndex = 1; - } - } - - const profileIndexString = `${profileIndex}`; - for (const option of this._profileCopySourceSelect.querySelectorAll('option')) { - const {value} = option; - option.disabled = (value === profileIndexString); - } - this._profileCopySourceSelect.value = `${copyFromIndex}`; - - this._profileCopyModal.node.dataset.profileIndex = `${profileIndex}`; - this._profileCopyModal.setVisible(true); - } - - openProfileConditionsModal(profileIndex) { - const profile = this._getProfile(profileIndex); - if (profile === null) { return; } - - if (this._profileConditionsModal === null) { return; } - this._profileConditionsModal.setVisible(true); - - this._profileConditionsUI.cleanup(); - this._profileConditionsIndex = profileIndex; - this._profileConditionsUI.prepare(profileIndex); - if (this._profileConditionsProfileName !== null) { - this._profileConditionsProfileName.textContent = profile.name; - } - } - - // Private - - async _onOptionsChanged() { - // Update state - const {profiles, profileCurrent} = await this._settingsController.getOptionsFull(); - this._profiles = profiles; - this._profileCurrent = profileCurrent; - - const settingsProfileIndex = this._settingsController.profileIndex; - const settingsProfile = this._getProfile(settingsProfileIndex); - - // Udpate UI - this._updateProfileSelectOptions(); - - this._profileActiveSelect.value = `${profileCurrent}`; - this._profileTargetSelect.value = `${settingsProfileIndex}`; - - if (this._profileRemoveButton !== null) { this._profileRemoveButton.disabled = (profiles.length <= 1); } - if (this._profileCopyButton !== null) { this._profileCopyButton.disabled = (profiles.length <= 1); } - if (this._profileMoveUpButton !== null) { this._profileMoveUpButton.disabled = (settingsProfileIndex <= 0); } - if (this._profileMoveDownButton !== null) { this._profileMoveDownButton.disabled = (settingsProfileIndex >= profiles.length - 1); } - - if (this._profileNameInput !== null && settingsProfile !== null) { this._profileNameInput.value = settingsProfile.name; } - - // Update profile conditions - this._profileConditionsUI.cleanup(); - const conditionsProfile = this._getProfile(this._profileConditionsIndex !== null ? this._profileConditionsIndex : settingsProfileIndex); - if (conditionsProfile !== null) { - this._profileConditionsUI.prepare(settingsProfileIndex); - } - - // Udpate profile entries - for (const entry of this._profileEntryList) { - entry.cleanup(); - } - this._profileEntryList = []; - if (this._profileEntriesSupported) { - for (let i = 0, ii = profiles.length; i < ii; ++i) { - this._addProfileEntry(i); - } - } - } - - _onProfileActiveChange(e) { - const value = this._tryGetValidProfileIndex(e.currentTarget.value); - if (value === null) { return; } - this.setDefaultProfile(value); - } - - _onProfileTargetChange(e) { - const value = this._tryGetValidProfileIndex(e.currentTarget.value); - if (value === null) { return; } - this._settingsController.profileIndex = value; - } - - _onNameChanged(e) { - this.setProfileName(this._settingsController.profileIndex, e.currentTarget.value); - } - - _onAdd() { - this.duplicateProfile(this._settingsController.profileIndex); - } - - _onDelete(e) { - const profileIndex = this._settingsController.profileIndex; - if (e.shiftKey) { - this.deleteProfile(profileIndex); - } else { - this.openDeleteProfileModal(profileIndex); - } - } - - _onDeleteConfirm() { - const modal = this._profileRemoveModal; - modal.setVisible(false); - const {node} = modal; - let profileIndex = node.dataset.profileIndex; - delete node.dataset.profileIndex; - - profileIndex = this._tryGetValidProfileIndex(profileIndex); - if (profileIndex === null) { return; } - - this.deleteProfile(profileIndex); - } - - _onCopy() { - this.openCopyProfileModal(this._settingsController.profileIndex); - } - - _onCopyConfirm() { - const modal = this._profileCopyModal; - modal.setVisible(false); - const {node} = modal; - let destinationProfileIndex = node.dataset.profileIndex; - delete node.dataset.profileIndex; - - destinationProfileIndex = this._tryGetValidProfileIndex(destinationProfileIndex); - if (destinationProfileIndex === null) { return; } - - const sourceProfileIndex = this._tryGetValidProfileIndex(this._profileCopySourceSelect.value); - if (sourceProfileIndex === null) { return; } - - this.copyProfile(sourceProfileIndex, destinationProfileIndex); - } - - _onMove(offset) { - this.moveProfile(this._settingsController.profileIndex, offset); - } - - _onConditionGroupCountChanged({count, profileIndex}) { - if (profileIndex >= 0 && profileIndex < this._profileEntryList.length) { - const profileEntry = this._profileEntryList[profileIndex]; - profileEntry.setConditionGroupsCount(count); - } - } - - _addProfileEntry(profileIndex) { - const profile = this._profiles[profileIndex]; - const node = this._settingsController.instantiateTemplate('profile-entry'); - const entry = new ProfileEntry(this, node); - this._profileEntryList.push(entry); - entry.prepare(profile, profileIndex); - this._profileEntryListContainer.appendChild(node); - } - - _updateProfileSelectOptions() { - for (const select of this._getAllProfileSelects()) { - const fragment = document.createDocumentFragment(); - for (let i = 0; i < this._profiles.length; ++i) { - const profile = this._profiles[i]; - const option = document.createElement('option'); - option.value = `${i}`; - option.textContent = profile.name; - fragment.appendChild(option); - } - select.textContent = ''; - select.appendChild(fragment); - } - } - - _updateSelectName(index, name) { - const optionValue = `${index}`; - for (const select of this._getAllProfileSelects()) { - for (const option of select.querySelectorAll('option')) { - if (option.value === optionValue) { - option.textContent = name; - } - } - } - } - - _getAllProfileSelects() { - return [ - this._profileActiveSelect, - this._profileTargetSelect, - this._profileCopySourceSelect - ]; - } - - _tryGetValidProfileIndex(stringValue) { - if (typeof stringValue !== 'string') { return null; } - const intValue = parseInt(stringValue, 10); - return ( - Number.isFinite(intValue) && - intValue >= 0 && - intValue < this.profileCount ? - intValue : null - ); - } - - _createCopyName(name, profiles, maxUniqueAttempts) { - let space, index, prefix, suffix; - const match = /^([\w\W]*\(Copy)((\s+)(\d+))?(\)\s*)$/.exec(name); - if (match === null) { - prefix = `${name} (Copy`; - space = ''; - index = ''; - suffix = ')'; - } else { - prefix = match[1]; - suffix = match[5]; - if (typeof match[2] === 'string') { - space = match[3]; - index = parseInt(match[4], 10) + 1; - } else { - space = ' '; - index = 2; - } - } - - let i = 0; - while (true) { - const newName = `${prefix}${space}${index}${suffix}`; - if (i++ >= maxUniqueAttempts || profiles.findIndex((profile) => profile.name === newName) < 0) { - return newName; - } - if (typeof index !== 'number') { - index = 2; - space = ' '; - } else { - ++index; - } - } - } - - _getSwappedValue(currentValue, value1, value2) { - if (currentValue === value1) { return value2; } - if (currentValue === value2) { return value1; } - return currentValue; - } - - _getProfile(profileIndex) { - return (profileIndex >= 0 && profileIndex < this._profiles.length ? this._profiles[profileIndex] : null); - } - - _getProfileEntry(profileIndex) { - return (profileIndex >= 0 && profileIndex < this._profileEntryList.length ? this._profileEntryList[profileIndex] : null); - } - - _swapDomNodes(node1, node2) { - const parent1 = node1.parentNode; - const parent2 = node2.parentNode; - const next1 = node1.nextSibling; - const next2 = node2.nextSibling; - if (node2 !== next1) { parent1.insertBefore(node2, next1); } - if (node1 !== next2) { parent2.insertBefore(node1, next2); } - } -} - -class ProfileEntry { - constructor(profileController, node) { - this._profileController = profileController; - this._node = node; - this._profile = null; - this._index = 0; - this._isDefaultRadio = null; - this._nameInput = null; - this._countLink = null; - this._countText = null; - this._menuButton = null; - this._eventListeners = new EventListenerCollection(); - } - - get index() { - return this._index; - } - - set index(value) { - this._index = value; - } - - get node() { - return this._node; - } - - prepare(profile, index) { - this._profile = profile; - this._index = index; - - const node = this._node; - this._isDefaultRadio = node.querySelector('.profile-entry-is-default-radio'); - this._nameInput = node.querySelector('.profile-entry-name-input'); - this._countLink = node.querySelector('.profile-entry-condition-count-link'); - this._countText = node.querySelector('.profile-entry-condition-count'); - this._menuButton = node.querySelector('.profile-entry-menu-button'); - - this.updateState(); - - this._eventListeners.addEventListener(this._isDefaultRadio, 'change', this._onIsDefaultRadioChange.bind(this), false); - this._eventListeners.addEventListener(this._nameInput, 'input', this._onNameInputInput.bind(this), false); - this._eventListeners.addEventListener(this._countLink, 'click', this._onConditionsCountLinkClick.bind(this), false); - this._eventListeners.addEventListener(this._menuButton, 'menuOpen', this._onMenuOpen.bind(this), false); - this._eventListeners.addEventListener(this._menuButton, 'menuClose', this._onMenuClose.bind(this), false); - } - - cleanup() { - this._eventListeners.removeAllEventListeners(); - if (this._node.parentNode !== null) { - this._node.parentNode.removeChild(this._node); - } - } - - setName(value) { - if (this._nameInput.value === value) { return; } - this._nameInput.value = value; - } - - setIsDefault(value) { - this._isDefaultRadio.checked = value; - } - - updateState() { - this._nameInput.value = this._profile.name; - this._countText.textContent = `${this._profile.conditionGroups.length}`; - this._isDefaultRadio.checked = (this._index === this._profileController.profileCurrentIndex); - } - - setConditionGroupsCount(count) { - this._countText.textContent = `${count}`; - } - - // Private - - _onIsDefaultRadioChange(e) { - if (!e.currentTarget.checked) { return; } - this._profileController.setDefaultProfile(this._index); - } - - _onNameInputInput(e) { - const name = e.currentTarget.value; - this._profileController.setProfileName(this._index, name); - } - - _onConditionsCountLinkClick() { - this._profileController.openProfileConditionsModal(this._index); - } - - _onMenuOpen(e) { - const bodyNode = e.detail.menu.bodyNode; - const count = this._profileController.profileCount; - this._setMenuActionEnabled(bodyNode, 'moveUp', this._index > 0); - this._setMenuActionEnabled(bodyNode, 'moveDown', this._index < count - 1); - this._setMenuActionEnabled(bodyNode, 'copyFrom', count > 1); - this._setMenuActionEnabled(bodyNode, 'delete', count > 1); - } - - _onMenuClose(e) { - switch (e.detail.action) { - case 'moveUp': - this._profileController.moveProfile(this._index, -1); - break; - case 'moveDown': - this._profileController.moveProfile(this._index, 1); - break; - case 'copyFrom': - this._profileController.openCopyProfileModal(this._index); - break; - case 'editConditions': - this._profileController.openProfileConditionsModal(this._index); - break; - case 'duplicate': - this._profileController.duplicateProfile(this._index); - break; - case 'delete': - this._profileController.openDeleteProfileModal(this._index); - break; - } - } - - _setMenuActionEnabled(menu, action, enabled) { - const element = menu.querySelector(`[data-menu-action="${action}"]`); - if (element === null) { return; } - element.disabled = !enabled; - } -} diff --git a/ext/bg/js/settings/scan-inputs-controller.js b/ext/bg/js/settings/scan-inputs-controller.js deleted file mode 100644 index eb179c6a..00000000 --- a/ext/bg/js/settings/scan-inputs-controller.js +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright (C) 2020-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 - * KeyboardMouseInputField - * api - */ - -class ScanInputsController { - constructor(settingsController) { - this._settingsController = settingsController; - this._os = null; - this._container = null; - this._addButton = null; - this._scanningInputCountNodes = null; - this._entries = []; - } - - async prepare() { - const {platform: {os}} = await api.getEnvironmentInfo(); - this._os = os; - - this._container = document.querySelector('#scan-input-list'); - this._addButton = document.querySelector('#scan-input-add'); - this._scanningInputCountNodes = document.querySelectorAll('.scanning-input-count'); - - this._addButton.addEventListener('click', this._onAddButtonClick.bind(this), false); - this._settingsController.on('scanInputsChanged', this._onScanInputsChanged.bind(this)); - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - - this.refresh(); - } - - removeInput(index) { - if (index < 0 || index >= this._entries.length) { return false; } - const input = this._entries[index]; - input.cleanup(); - this._entries.splice(index, 1); - for (let i = index, ii = this._entries.length; i < ii; ++i) { - this._entries[i].index = i; - } - this._updateCounts(); - this._modifyProfileSettings([{ - action: 'splice', - path: 'scanning.inputs', - start: index, - deleteCount: 1, - items: [] - }]); - return true; - } - - async setProperty(index, property, value, event) { - const path = `scanning.inputs[${index}].${property}`; - await this._settingsController.setProfileSetting(path, value); - if (event) { - this._triggerScanInputsChanged(); - } - } - - instantiateTemplate(name) { - return this._settingsController.instantiateTemplate(name); - } - - async refresh() { - const options = await this._settingsController.getOptions(); - this._onOptionsChanged({options}); - } - - // Private - - _onScanInputsChanged({source}) { - if (source === this) { return; } - this.refresh(); - } - - _onOptionsChanged({options}) { - const {inputs} = options.scanning; - - for (let i = this._entries.length - 1; i >= 0; --i) { - this._entries[i].cleanup(); - } - this._entries.length = 0; - - for (let i = 0, ii = inputs.length; i < ii; ++i) { - this._addOption(i, inputs[i]); - } - - this._updateCounts(); - } - - _onAddButtonClick(e) { - e.preventDefault(); - - const index = this._entries.length; - const scanningInput = ScanInputsController.createDefaultMouseInput('', ''); - this._addOption(index, scanningInput); - this._updateCounts(); - this._modifyProfileSettings([{ - action: 'splice', - path: 'scanning.inputs', - start: index, - deleteCount: 0, - items: [scanningInput] - }]); - } - - _addOption(index, scanningInput) { - const field = new ScanInputField(this, index, this._os); - this._entries.push(field); - field.prepare(this._container, scanningInput); - } - - _updateCounts() { - const stringValue = `${this._entries.length}`; - for (const node of this._scanningInputCountNodes) { - node.textContent = stringValue; - } - } - - async _modifyProfileSettings(targets) { - await this._settingsController.modifyProfileSettings(targets); - this._triggerScanInputsChanged(); - } - - _triggerScanInputsChanged() { - this._settingsController.trigger('scanInputsChanged', {source: this}); - } - - static createDefaultMouseInput(include, exclude) { - return { - include, - exclude, - types: {mouse: true, touch: false, pen: false}, - options: { - showAdvanced: false, - searchTerms: true, - searchKanji: true, - scanOnTouchMove: true, - scanOnPenHover: true, - scanOnPenPress: true, - scanOnPenRelease: false, - preventTouchScrolling: true - } - }; - } -} - -class ScanInputField { - constructor(parent, index, os) { - this._parent = parent; - this._index = index; - this._os = os; - this._node = null; - this._includeInputField = null; - this._excludeInputField = null; - this._eventListeners = new EventListenerCollection(); - } - - get index() { - return this._index; - } - - set index(value) { - this._index = value; - this._updateDataSettingTargets(); - } - - prepare(container, scanningInput) { - const {include, exclude, options: {showAdvanced}} = scanningInput; - - const node = this._parent.instantiateTemplate('scan-input'); - const includeInputNode = node.querySelector('.scan-input-field[data-property=include]'); - const includeMouseButton = node.querySelector('.mouse-button[data-property=include]'); - const excludeInputNode = node.querySelector('.scan-input-field[data-property=exclude]'); - const excludeMouseButton = node.querySelector('.mouse-button[data-property=exclude]'); - const removeButton = node.querySelector('.scan-input-remove'); - const menuButton = node.querySelector('.scanning-input-menu-button'); - - node.dataset.showAdvanced = `${showAdvanced}`; - - this._node = node; - container.appendChild(node); - - const isPointerTypeSupported = this._isPointerTypeSupported.bind(this); - this._includeInputField = new KeyboardMouseInputField(includeInputNode, includeMouseButton, this._os, isPointerTypeSupported); - this._excludeInputField = new KeyboardMouseInputField(excludeInputNode, excludeMouseButton, this._os, isPointerTypeSupported); - this._includeInputField.prepare(null, this._splitModifiers(include), true, false); - this._excludeInputField.prepare(null, this._splitModifiers(exclude), true, false); - - this._eventListeners.on(this._includeInputField, 'change', this._onIncludeValueChange.bind(this)); - this._eventListeners.on(this._excludeInputField, 'change', this._onExcludeValueChange.bind(this)); - if (removeButton !== null) { - this._eventListeners.addEventListener(removeButton, 'click', this._onRemoveClick.bind(this)); - } - if (menuButton !== null) { - this._eventListeners.addEventListener(menuButton, 'menuOpen', this._onMenuOpen.bind(this)); - this._eventListeners.addEventListener(menuButton, 'menuClose', this._onMenuClose.bind(this)); - } - - this._updateDataSettingTargets(); - } - - cleanup() { - this._eventListeners.removeAllEventListeners(); - if (this._includeInputField !== null) { - this._includeInputField.cleanup(); - this._includeInputField = null; - } - if (this._node !== null) { - const parent = this._node.parentNode; - if (parent !== null) { parent.removeChild(this._node); } - this._node = null; - } - } - - // Private - - _onIncludeValueChange({modifiers}) { - modifiers = this._joinModifiers(modifiers); - this._parent.setProperty(this._index, 'include', modifiers, true); - } - - _onExcludeValueChange({modifiers}) { - modifiers = this._joinModifiers(modifiers); - this._parent.setProperty(this._index, 'exclude', modifiers, true); - } - - _onRemoveClick(e) { - e.preventDefault(); - this._removeSelf(); - } - - _onMenuOpen(e) { - const bodyNode = e.detail.menu.bodyNode; - const showAdvanced = bodyNode.querySelector('.popup-menu-item[data-menu-action="showAdvanced"]'); - const hideAdvanced = bodyNode.querySelector('.popup-menu-item[data-menu-action="hideAdvanced"]'); - const advancedVisible = (this._node.dataset.showAdvanced === 'true'); - if (showAdvanced !== null) { - showAdvanced.hidden = advancedVisible; - } - if (hideAdvanced !== null) { - hideAdvanced.hidden = !advancedVisible; - } - } - - _onMenuClose(e) { - switch (e.detail.action) { - case 'remove': - this._removeSelf(); - break; - case 'showAdvanced': - this._setAdvancedOptionsVisible(true); - break; - case 'hideAdvanced': - this._setAdvancedOptionsVisible(false); - break; - case 'clearInputs': - this._includeInputField.clearInputs(); - this._excludeInputField.clearInputs(); - break; - } - } - - _isPointerTypeSupported(pointerType) { - if (this._node === null) { return false; } - const node = this._node.querySelector(`input.scan-input-settings-checkbox[data-property="types.${pointerType}"]`); - return node !== null && node.checked; - } - - _updateDataSettingTargets() { - const index = this._index; - for (const typeCheckbox of this._node.querySelectorAll('.scan-input-settings-checkbox')) { - const {property} = typeCheckbox.dataset; - typeCheckbox.dataset.setting = `scanning.inputs[${index}].${property}`; - } - } - - _removeSelf() { - this._parent.removeInput(this._index); - } - - _setAdvancedOptionsVisible(showAdvanced) { - showAdvanced = !!showAdvanced; - this._node.dataset.showAdvanced = `${showAdvanced}`; - this._parent.setProperty(this._index, 'options.showAdvanced', showAdvanced, false); - } - - _splitModifiers(modifiersString) { - return modifiersString.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0); - } - - _joinModifiers(modifiersArray) { - return modifiersArray.join(', '); - } -} diff --git a/ext/bg/js/settings/scan-inputs-simple-controller.js b/ext/bg/js/settings/scan-inputs-simple-controller.js deleted file mode 100644 index 01f044c2..00000000 --- a/ext/bg/js/settings/scan-inputs-simple-controller.js +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (C) 2020-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 - * HotkeyUtil - * ScanInputsController - * api - */ - -class ScanInputsSimpleController { - constructor(settingsController) { - this._settingsController = settingsController; - this._middleMouseButtonScan = null; - this._mainScanModifierKeyInput = null; - this._mainScanModifierKeyInputHasOther = false; - this._hotkeyUtil = new HotkeyUtil(); - } - - async prepare() { - this._middleMouseButtonScan = document.querySelector('#middle-mouse-button-scan'); - this._mainScanModifierKeyInput = document.querySelector('#main-scan-modifier-key'); - - const {platform: {os}} = await api.getEnvironmentInfo(); - this._hotkeyUtil.os = os; - - this._mainScanModifierKeyInputHasOther = false; - this._populateSelect(this._mainScanModifierKeyInput, this._mainScanModifierKeyInputHasOther); - - const options = await this._settingsController.getOptions(); - - this._middleMouseButtonScan.addEventListener('change', this.onMiddleMouseButtonScanChange.bind(this), false); - this._mainScanModifierKeyInput.addEventListener('change', this._onMainScanModifierKeyInputChange.bind(this), false); - - this._settingsController.on('scanInputsChanged', this._onScanInputsChanged.bind(this)); - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - this._onOptionsChanged({options}); - } - - async refresh() { - const options = await this._settingsController.getOptions(); - this._onOptionsChanged({options}); - } - - // Private - - _onScanInputsChanged({source}) { - if (source === this) { return; } - this.refresh(); - } - - _onOptionsChanged({options}) { - const {scanning: {inputs}} = options; - const middleMouseSupportedIndex = this._getIndexOfMiddleMouseButtonScanInput(inputs); - const mainScanInputIndex = this._getIndexOfMainScanInput(inputs); - const hasMainScanInput = (mainScanInputIndex >= 0); - - let middleMouseSupported = false; - if (middleMouseSupportedIndex >= 0) { - const includeValues = this._splitValue(inputs[middleMouseSupportedIndex].include); - if (includeValues.includes('mouse2')) { - middleMouseSupported = true; - } - } - - let mainScanInput = 'none'; - if (hasMainScanInput) { - const includeValues = this._splitValue(inputs[mainScanInputIndex].include); - if (includeValues.length > 0) { - mainScanInput = includeValues[0]; - } - } else { - mainScanInput = 'other'; - } - - this._setHasMainScanInput(hasMainScanInput); - - this._middleMouseButtonScan.checked = middleMouseSupported; - this._mainScanModifierKeyInput.value = mainScanInput; - } - - onMiddleMouseButtonScanChange(e) { - const middleMouseSupported = e.currentTarget.checked; - this._setMiddleMouseSuppported(middleMouseSupported); - } - - _onMainScanModifierKeyInputChange(e) { - const mainScanKey = e.currentTarget.value; - if (mainScanKey === 'other') { return; } - const mainScanInputs = (mainScanKey === 'none' ? [] : [mainScanKey]); - this._setMainScanInputs(mainScanInputs); - } - - _populateSelect(select, hasOther) { - const modifierKeys = [ - {value: 'none', name: 'No key'} - ]; - for (const value of ['alt', 'ctrl', 'shift', 'meta']) { - const name = this._hotkeyUtil.getModifierDisplayValue(value); - modifierKeys.push({value, name}); - } - - if (hasOther) { - modifierKeys.push({value: 'other', name: 'Other'}); - } - - const fragment = document.createDocumentFragment(); - for (const {value, name} of modifierKeys) { - const option = document.createElement('option'); - option.value = value; - option.textContent = name; - fragment.appendChild(option); - } - select.textContent = ''; - select.appendChild(fragment); - } - - _splitValue(value) { - return value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0); - } - - async _setMiddleMouseSuppported(value) { - // Find target index - const options = await this._settingsController.getOptions(); - const {scanning: {inputs}} = options; - const index = this._getIndexOfMiddleMouseButtonScanInput(inputs); - - if (value) { - // Add new - if (index >= 0) { return; } - let insertionPosition = this._getIndexOfMainScanInput(inputs); - insertionPosition = (insertionPosition >= 0 ? insertionPosition + 1 : inputs.length); - const input = ScanInputsController.createDefaultMouseInput('mouse2', ''); - await this._modifyProfileSettings([{ - action: 'splice', - path: 'scanning.inputs', - start: insertionPosition, - deleteCount: 0, - items: [input] - }]); - } else { - // Modify existing - if (index < 0) { return; } - await this._modifyProfileSettings([{ - action: 'splice', - path: 'scanning.inputs', - start: index, - deleteCount: 1, - items: [] - }]); - } - } - - async _setMainScanInputs(value) { - value = value.join(', '); - - // Find target index - const options = await this._settingsController.getOptions(); - const {scanning: {inputs}} = options; - const index = this._getIndexOfMainScanInput(inputs); - - this._setHasMainScanInput(true); - - if (index < 0) { - // Add new - const input = ScanInputsController.createDefaultMouseInput(value, 'mouse0'); - await this._modifyProfileSettings([{ - action: 'splice', - path: 'scanning.inputs', - start: inputs.length, - deleteCount: 0, - items: [input] - }]); - } else { - // Modify existing - await this._modifyProfileSettings([{ - action: 'set', - path: `scanning.inputs[${index}].include`, - value - }]); - } - } - - async _modifyProfileSettings(targets) { - await this._settingsController.modifyProfileSettings(targets); - this._settingsController.trigger('scanInputsChanged', {source: this}); - } - - _getIndexOfMainScanInput(inputs) { - for (let i = 0, ii = inputs.length; i < ii; ++i) { - const {include, exclude, types: {mouse}} = inputs[i]; - if (!mouse) { continue; } - const includeValues = this._splitValue(include); - const excludeValues = this._splitValue(exclude); - if ( - ( - includeValues.length === 0 || - (includeValues.length === 1 && !this._isMouseInput(includeValues[0])) - ) && - excludeValues.length === 1 && - excludeValues[0] === 'mouse0' - ) { - return i; - } - } - return -1; - } - - _getIndexOfMiddleMouseButtonScanInput(inputs) { - for (let i = 0, ii = inputs.length; i < ii; ++i) { - const {include, exclude, types: {mouse}} = inputs[i]; - if (!mouse) { continue; } - const includeValues = this._splitValue(include); - const excludeValues = this._splitValue(exclude); - if ( - (includeValues.length === 1 && includeValues[0] === 'mouse2') && - excludeValues.length === 0 - ) { - return i; - } - } - return -1; - } - - _isMouseInput(input) { - return /^mouse\d+$/.test(input); - } - - _setHasMainScanInput(hasMainScanInput) { - if (this._mainScanModifierKeyInputHasOther !== hasMainScanInput) { return; } - this._mainScanModifierKeyInputHasOther = !hasMainScanInput; - this._populateSelect(this._mainScanModifierKeyInput, this._mainScanModifierKeyInputHasOther); - } -}
\ No newline at end of file diff --git a/ext/bg/js/settings/settings-controller.js b/ext/bg/js/settings/settings-controller.js deleted file mode 100644 index 11a9435c..00000000 --- a/ext/bg/js/settings/settings-controller.js +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright (C) 2020-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 - * HtmlTemplateCollection - * OptionsUtil - * PermissionsUtil - * api - */ - -class SettingsController extends EventDispatcher { - constructor(profileIndex=0) { - super(); - this._profileIndex = profileIndex; - this._source = generateId(16); - this._pageExitPreventions = new Set(); - this._pageExitPreventionEventListeners = new EventListenerCollection(); - this._templates = new HtmlTemplateCollection(document); - this._permissionsUtil = new PermissionsUtil(); - } - - get source() { - return this._source; - } - - get profileIndex() { - return this._profileIndex; - } - - set profileIndex(value) { - if (this._profileIndex === value) { return; } - this._setProfileIndex(value); - } - - get permissionsUtil() { - return this._permissionsUtil; - } - - prepare() { - yomichan.on('optionsUpdated', this._onOptionsUpdated.bind(this)); - chrome.permissions.onAdded.addListener(this._onPermissionsChanged.bind(this)); - chrome.permissions.onRemoved.addListener(this._onPermissionsChanged.bind(this)); - } - - async refresh() { - await this._onOptionsUpdatedInternal(); - } - - async getOptions() { - const optionsContext = this.getOptionsContext(); - return await api.optionsGet(optionsContext); - } - - async getOptionsFull() { - return await api.optionsGetFull(); - } - - async setAllSettings(value) { - const profileIndex = value.profileCurrent; - await api.setAllSettings(value, this._source); - this._setProfileIndex(profileIndex); - } - - async getSettings(targets) { - return await this._getSettings(targets, {}); - } - - async getGlobalSettings(targets) { - return await this._getSettings(targets, {scope: 'global'}); - } - - async getProfileSettings(targets) { - return await this._getSettings(targets, {scope: 'profile'}); - } - - async modifySettings(targets) { - return await this._modifySettings(targets, {}); - } - - async modifyGlobalSettings(targets) { - return await this._modifySettings(targets, {scope: 'global'}); - } - - async modifyProfileSettings(targets) { - return await this._modifySettings(targets, {scope: 'profile'}); - } - - async setGlobalSetting(path, value) { - return await this.modifyGlobalSettings([{action: 'set', path, value}]); - } - - async setProfileSetting(path, value) { - return await this.modifyProfileSettings([{action: 'set', path, value}]); - } - - async getDictionaryInfo() { - return await api.getDictionaryInfo(); - } - - getOptionsContext() { - return {index: this._profileIndex}; - } - - preventPageExit() { - const obj = {end: null}; - obj.end = this._endPreventPageExit.bind(this, obj); - if (this._pageExitPreventionEventListeners.size === 0) { - this._pageExitPreventionEventListeners.addEventListener(window, 'beforeunload', this._onBeforeUnload.bind(this), false); - } - this._pageExitPreventions.add(obj); - return obj; - } - - instantiateTemplate(name) { - return this._templates.instantiate(name); - } - - instantiateTemplateFragment(name) { - return this._templates.instantiateFragment(name); - } - - async getDefaultOptions() { - const optionsUtil = new OptionsUtil(); - await optionsUtil.prepare(); - const optionsFull = optionsUtil.getDefault(); - return optionsFull; - } - - // Private - - _setProfileIndex(value) { - this._profileIndex = value; - this.trigger('optionsContextChanged'); - this._onOptionsUpdatedInternal(); - } - - _onOptionsUpdated({source}) { - if (source === this._source) { return; } - this._onOptionsUpdatedInternal(); - } - - async _onOptionsUpdatedInternal() { - const optionsContext = this.getOptionsContext(); - const options = await this.getOptions(); - this.trigger('optionsChanged', {options, optionsContext}); - } - - _setupTargets(targets, extraFields) { - return targets.map((target) => { - target = Object.assign({}, extraFields, target); - if (target.scope === 'profile') { - target.optionsContext = this.getOptionsContext(); - } - return target; - }); - } - - async _getSettings(targets, extraFields) { - targets = this._setupTargets(targets, extraFields); - return await api.getSettings(targets); - } - - async _modifySettings(targets, extraFields) { - targets = this._setupTargets(targets, extraFields); - return await api.modifySettings(targets, this._source); - } - - _onBeforeUnload(e) { - if (this._pageExitPreventions.size === 0) { - return; - } - - e.preventDefault(); - e.returnValue = ''; - return ''; - } - - _endPreventPageExit(obj) { - this._pageExitPreventions.delete(obj); - if (this._pageExitPreventions.size === 0) { - this._pageExitPreventionEventListeners.removeAllEventListeners(); - } - } - - _onPermissionsChanged() { - this._triggerPermissionsChanged(); - } - - async _triggerPermissionsChanged() { - const event = 'permissionsChanged'; - if (!this.hasListeners(event)) { return; } - - const permissions = await this._permissionsUtil.getAllPermissions(); - this.trigger(event, {permissions}); - } -} diff --git a/ext/bg/js/settings/status-footer.js b/ext/bg/js/settings/status-footer.js deleted file mode 100644 index c03e6775..00000000 --- a/ext/bg/js/settings/status-footer.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2020-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 - * PanelElement - */ - -class StatusFooter extends PanelElement { - constructor(node) { - super({ - node, - closingAnimationDuration: 375 // Milliseconds; includes buffer - }); - this._body = node.querySelector('.status-footer'); - } - - prepare() { - this.on('closeCompleted', this._onCloseCompleted.bind(this), false); - this._body.querySelector('.status-footer-header-close').addEventListener('click', this._onCloseClick.bind(this), false); - } - - getTaskContainer(selector) { - return this._body.querySelector(selector); - } - - isTaskActive(selector) { - const target = this.getTaskContainer(selector); - return (target !== null && target.dataset.active); - } - - setTaskActive(selector, active) { - const target = this.getTaskContainer(selector); - if (target === null) { return; } - - const activeElements = new Set(); - for (const element of this._body.querySelectorAll('.status-footer-item')) { - if (element.dataset.active) { - activeElements.add(element); - } - } - - if (active) { - target.dataset.active = 'true'; - if (!this.isVisible()) { - this.setVisible(true); - } - target.hidden = false; - } else { - delete target.dataset.active; - if (activeElements.size <= 1) { - this.setVisible(false); - } - } - } - - // Private - - _onCloseClick(e) { - e.preventDefault(); - this.setVisible(false); - } - - _onCloseCompleted() { - for (const element of this._body.querySelectorAll('.status-footer-item')) { - if (!element.dataset.active) { - element.hidden = true; - } - } - } -} diff --git a/ext/bg/js/settings/storage-controller.js b/ext/bg/js/settings/storage-controller.js deleted file mode 100644 index c27c8690..00000000 --- a/ext/bg/js/settings/storage-controller.js +++ /dev/null @@ -1,183 +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/>. - */ - -class StorageController { - constructor() { - this._mostRecentStorageEstimate = null; - this._storageEstimateFailed = false; - this._isUpdating = false; - this._persistentStorageCheckbox = false; - this._storageUsageNode = null; - this._storageQuotaNode = null; - this._storageUseFiniteNodes = null; - this._storageUseInfiniteNodes = null; - this._storageUseValidNodes = null; - this._storageUseInvalidNodes = null; - } - - prepare() { - this._persistentStorageCheckbox = document.querySelector('#storage-persistent-checkbox'); - this._storageUsageNodes = document.querySelectorAll('.storage-usage'); - this._storageQuotaNodes = document.querySelectorAll('.storage-quota'); - this._storageUseFiniteNodes = document.querySelectorAll('.storage-use-finite'); - this._storageUseInfiniteNodes = document.querySelectorAll('.storage-use-infinite'); - this._storageUseValidNodes = document.querySelectorAll('.storage-use-valid'); - this._storageUseInvalidNodes = document.querySelectorAll('.storage-use-invalid'); - - this._preparePersistentStorage(); - this.updateStats(); - this._persistentStorageCheckbox.addEventListener('change', this._onPersistentStorageCheckboxChange.bind(this), false); - document.querySelector('#storage-refresh').addEventListener('click', this.updateStats.bind(this), false); - } - - async updateStats() { - if (this._isUpdating) { return; } - - try { - this._isUpdating = true; - - const estimate = await this._storageEstimate(); - const valid = (estimate !== null); - - // Firefox reports usage as 0 when persistent storage is enabled. - const finite = valid && (estimate.usage > 0 || !(await this._isStoragePeristent())); - if (finite) { - for (const node of this._storageUsageNodes) { - node.textContent = this._bytesToLabeledString(estimate.usage); - } - for (const node of this._storageQuotaNodes) { - node.textContent = this._bytesToLabeledString(estimate.quota); - } - } - - this._setElementsVisible(this._storageUseFiniteNodes, valid && finite); - this._setElementsVisible(this._storageUseInfiniteNodes, valid && !finite); - this._setElementsVisible(this._storageUseValidNodes, valid); - this._setElementsVisible(this._storageUseInvalidNodes, !valid); - - return valid; - } finally { - this._isUpdating = false; - } - } - - // Private - - async _preparePersistentStorage() { - if (!(navigator.storage && navigator.storage.persist)) { - // Not supported - return; - } - - const info = document.querySelector('#storage-persistent-info'); - if (info !== null) { info.hidden = false; } - - const isStoragePeristent = await this._isStoragePeristent(); - this._updateCheckbox(isStoragePeristent); - - const button = document.querySelector('#storage-persistent-button'); - if (button !== null) { - button.hidden = false; - button.addEventListener('click', this._onPersistStorageButtonClick.bind(this), false); - } - } - - _onPersistentStorageCheckboxChange(e) { - const node = e.currentTarget; - if (!node.checked) { - node.checked = true; - return; - } - this._attemptPersistStorage(); - } - - _onPersistStorageButtonClick() { - const {checked} = this._persistentStorageCheckbox; - if (checked) { return; } - this._persistentStorageCheckbox.checked = !checked; - this._persistentStorageCheckbox.dispatchEvent(new Event('change')); - } - - async _attemptPersistStorage() { - if (await this._isStoragePeristent()) { return; } - - let isStoragePeristent = false; - try { - isStoragePeristent = await navigator.storage.persist(); - } catch (e) { - // NOP - } - - this._updateCheckbox(isStoragePeristent); - - if (isStoragePeristent) { - this.updateStats(); - } else { - const node = document.querySelector('#storage-persistent-fail-warning'); - if (node !== null) { node.hidden = false; } - } - } - - async _storageEstimate() { - if (this._storageEstimateFailed && this._mostRecentStorageEstimate === null) { - return null; - } - try { - const value = await navigator.storage.estimate(); - this._mostRecentStorageEstimate = value; - return value; - } catch (e) { - this._storageEstimateFailed = true; - } - return null; - } - - _bytesToLabeledString(size) { - const base = 1000; - const labels = [' bytes', 'KB', 'MB', 'GB', 'TB']; - const maxLabelIndex = labels.length - 1; - let labelIndex = 0; - while (size >= base && labelIndex < maxLabelIndex) { - size /= base; - ++labelIndex; - } - const label = labelIndex === 0 ? `${size}` : size.toFixed(1); - return `${label}${labels[labelIndex]}`; - } - - async _isStoragePeristent() { - try { - return await navigator.storage.persisted(); - } catch (e) { - // NOP - } - return false; - } - - _updateCheckbox(isStoragePeristent) { - const checkbox = this._persistentStorageCheckbox; - checkbox.checked = isStoragePeristent; - checkbox.readOnly = isStoragePeristent; - } - - _setElementsVisible(elements, visible) { - visible = !visible; - for (const element of elements) { - element.hidden = visible; - } - } -} |