diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2021-02-14 23:10:01 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-14 23:10:01 -0500 |
commit | 450912c1098b0ec4c0ec29b7aec8b47143cfd6fc (patch) | |
tree | 6aae6ab69e037915faf7681991952113ad6d6094 /ext/js/settings | |
parent | d6332d2bc723f9aa60f2c886564bef49a6b91b84 (diff) |
Move js/settings (#1397)
* Move js/settings to js/pages/settings
* Fix script ordering
Diffstat (limited to 'ext/js/settings')
34 files changed, 0 insertions, 8255 deletions
diff --git a/ext/js/settings/anki-controller.js b/ext/js/settings/anki-controller.js deleted file mode 100644 index 26cab68f..00000000 --- a/ext/js/settings/anki-controller.js +++ /dev/null @@ -1,729 +0,0 @@ -/* - * Copyright (C) 2019-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * AnkiConnect - * AnkiNoteBuilder - * ObjectPropertyAccessor - * SelectorObserver - */ - -class AnkiController { - constructor(settingsController) { - this._settingsController = settingsController; - this._ankiConnect = new AnkiConnect(); - this._ankiNoteBuilder = new AnkiNoteBuilder(false); - this._selectorObserver = new SelectorObserver({ - selector: '.anki-card', - ignoreSelector: null, - onAdded: this._createCardController.bind(this), - onRemoved: this._removeCardController.bind(this), - isStale: this._isCardControllerStale.bind(this) - }); - this._stringComparer = new Intl.Collator(); // Locale does not matter - this._getAnkiDataPromise = null; - this._ankiErrorContainer = null; - this._ankiErrorMessageNode = null; - this._ankiErrorMessageNodeDefaultContent = ''; - this._ankiErrorMessageDetailsNode = null; - this._ankiErrorMessageDetailsContainer = null; - this._ankiErrorMessageDetailsToggle = null; - this._ankiErrorInvalidResponseInfo = null; - this._ankiCardPrimary = null; - this._ankiCardPrimaryType = null; - this._validateFieldsToken = null; - } - - get settingsController() { - return this._settingsController; - } - - async prepare() { - this._ankiErrorContainer = document.querySelector('#anki-error'); - this._ankiErrorMessageNode = document.querySelector('#anki-error-message'); - this._ankiErrorMessageNodeDefaultContent = this._ankiErrorMessageNode.textContent; - this._ankiErrorMessageDetailsNode = document.querySelector('#anki-error-message-details'); - this._ankiErrorMessageDetailsContainer = document.querySelector('#anki-error-message-details-container'); - this._ankiErrorMessageDetailsToggle = document.querySelector('#anki-error-message-details-toggle'); - this._ankiErrorInvalidResponseInfo = document.querySelector('#anki-error-invalid-response-info'); - this._ankiEnableCheckbox = document.querySelector('[data-setting="anki.enable"]'); - this._ankiCardPrimary = document.querySelector('#anki-card-primary'); - this._ankiCardPrimaryType = document.querySelector('#anki-card-primary-type'); - - this._setupFieldMenus(); - - this._ankiErrorMessageDetailsToggle.addEventListener('click', this._onAnkiErrorMessageDetailsToggleClick.bind(this), false); - if (this._ankiEnableCheckbox !== null) { this._ankiEnableCheckbox.addEventListener('settingChanged', this._onAnkiEnableChanged.bind(this), false); } - if (this._ankiCardPrimaryType !== null) { this._ankiCardPrimaryType.addEventListener('change', this._onAnkiCardPrimaryTypeChange.bind(this), false); } - - const options = await this._settingsController.getOptions(); - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - this._onOptionsChanged({options}); - } - - getFieldMarkers(type) { - switch (type) { - case 'terms': - return [ - 'audio', - 'clipboard-image', - 'clipboard-text', - 'cloze-body', - 'cloze-prefix', - 'cloze-suffix', - 'conjugation', - 'dictionary', - 'document-title', - 'expression', - 'frequencies', - 'furigana', - 'furigana-plain', - 'glossary', - 'glossary-brief', - 'glossary-no-dictionary', - 'pitch-accents', - 'pitch-accent-graphs', - 'pitch-accent-positions', - 'reading', - 'screenshot', - 'sentence', - 'tags', - 'url' - ]; - case 'kanji': - return [ - 'character', - 'clipboard-image', - 'clipboard-text', - 'cloze-body', - 'cloze-prefix', - 'cloze-suffix', - 'dictionary', - 'document-title', - 'glossary', - 'kunyomi', - 'onyomi', - 'screenshot', - 'sentence', - 'stroke-count', - 'tags', - 'url' - ]; - default: - return []; - } - } - - getFieldMarkersHtml(markers) { - const fragment = document.createDocumentFragment(); - for (const marker of markers) { - const markerNode = this._settingsController.instantiateTemplate('anki-card-field-marker'); - markerNode.querySelector('.marker-link').textContent = marker; - fragment.appendChild(markerNode); - } - return fragment; - } - - async getAnkiData() { - let promise = this._getAnkiDataPromise; - if (promise === null) { - promise = this._getAnkiData(); - this._getAnkiDataPromise = promise; - promise.finally(() => { this._getAnkiDataPromise = null; }); - } - return promise; - } - - async getModelFieldNames(model) { - return await this._ankiConnect.getModelFieldNames(model); - } - - getRequiredPermissions(fieldValue) { - return this._settingsController.permissionsUtil.getRequiredPermissionsForAnkiFieldValue(fieldValue); - } - - containsAnyMarker(field) { - return this._ankiNoteBuilder.containsAnyMarker(field); - } - - // Private - - async _onOptionsChanged({options: {anki}}) { - this._ankiConnect.server = anki.server; - this._ankiConnect.enabled = anki.enable; - - this._selectorObserver.disconnect(); - this._selectorObserver.observe(document.documentElement, true); - } - - _onAnkiErrorMessageDetailsToggleClick() { - const node = this._ankiErrorMessageDetailsContainer; - node.hidden = !node.hidden; - } - - _onAnkiEnableChanged({detail: {value}}) { - if (this._ankiConnect.server === null) { return; } - this._ankiConnect.enabled = value; - - for (const cardController of this._selectorObserver.datas()) { - cardController.updateAnkiState(); - } - } - - _onAnkiCardPrimaryTypeChange(e) { - if (this._ankiCardPrimary === null) { return; } - const node = e.currentTarget; - let ankiCardMenu; - if (node.selectedIndex >= 0) { - const option = node.options[node.selectedIndex]; - ankiCardMenu = option.dataset.ankiCardMenu; - } - - this._ankiCardPrimary.dataset.ankiCardType = node.value; - if (typeof ankiCardMenu !== 'undefined') { - this._ankiCardPrimary.dataset.ankiCardMenu = ankiCardMenu; - } else { - delete this._ankiCardPrimary.dataset.ankiCardMenu; - } - } - - _createCardController(node) { - const cardController = new AnkiCardController(this._settingsController, this, node); - cardController.prepare(); - return cardController; - } - - _removeCardController(node, cardController) { - cardController.cleanup(); - } - - _isCardControllerStale(node, cardController) { - return cardController.isStale(); - } - - _setupFieldMenus() { - const fieldMenuTargets = [ - [['terms'], '#anki-card-terms-field-menu-template'], - [['kanji'], '#anki-card-kanji-field-menu-template'], - [['terms', 'kanji'], '#anki-card-all-field-menu-template'] - ]; - for (const [types, selector] of fieldMenuTargets) { - const element = document.querySelector(selector); - if (element === null) { continue; } - - let markers = []; - for (const type of types) { - markers.push(...this.getFieldMarkers(type)); - } - markers = [...new Set(markers)]; - - const container = element.content.querySelector('.popup-menu-body'); - if (container === null) { return; } - - const fragment = document.createDocumentFragment(); - for (const marker of markers) { - const option = document.createElement('button'); - option.textContent = marker; - option.className = 'popup-menu-item'; - option.dataset.menuAction = 'setFieldMarker'; - option.dataset.marker = marker; - fragment.appendChild(option); - } - container.appendChild(fragment); - } - } - - async _getAnkiData() { - this._setAnkiStatusChanging(); - const [ - [deckNames, error1], - [modelNames, error2] - ] = await Promise.all([ - this._getDeckNames(), - this._getModelNames() - ]); - - if (error1 !== null) { - this._showAnkiError(error1); - } else if (error2 !== null) { - this._showAnkiError(error2); - } else { - this._hideAnkiError(); - } - - return {deckNames, modelNames}; - } - - async _getDeckNames() { - try { - const result = await this._ankiConnect.getDeckNames(); - this._sortStringArray(result); - return [result, null]; - } catch (e) { - return [[], e]; - } - } - - async _getModelNames() { - try { - const result = await this._ankiConnect.getModelNames(); - this._sortStringArray(result); - return [result, null]; - } catch (e) { - return [[], e]; - } - } - - _setAnkiStatusChanging() { - this._ankiErrorMessageNode.textContent = this._ankiErrorMessageNodeDefaultContent; - this._ankiErrorMessageNode.classList.remove('danger-text'); - } - - _hideAnkiError() { - if (this._ankiErrorContainer !== null) { - this._ankiErrorContainer.hidden = true; - } - this._ankiErrorMessageDetailsContainer.hidden = true; - this._ankiErrorMessageDetailsToggle.hidden = true; - this._ankiErrorInvalidResponseInfo.hidden = true; - this._ankiErrorMessageNode.textContent = (this._ankiConnect.enabled ? 'Connected' : 'Not enabled'); - this._ankiErrorMessageNode.classList.remove('danger-text'); - this._ankiErrorMessageDetailsNode.textContent = ''; - } - - _showAnkiError(error) { - let errorString = typeof error === 'object' && error !== null ? error.message : null; - if (!errorString) { errorString = `${error}`; } - if (!/[.!?]$/.test(errorString)) { errorString += '.'; } - this._ankiErrorMessageNode.textContent = errorString; - this._ankiErrorMessageNode.classList.add('danger-text'); - - const data = error.data; - let details = ''; - if (typeof data !== 'undefined') { - details += `${JSON.stringify(data, null, 4)}\n\n`; - } - details += `${error.stack}`.trimRight(); - this._ankiErrorMessageDetailsNode.textContent = details; - - if (this._ankiErrorContainer !== null) { - this._ankiErrorContainer.hidden = false; - } - this._ankiErrorMessageDetailsContainer.hidden = true; - this._ankiErrorInvalidResponseInfo.hidden = (errorString.indexOf('Invalid response') < 0); - this._ankiErrorMessageDetailsToggle.hidden = false; - } - - _sortStringArray(array) { - const stringComparer = this._stringComparer; - array.sort((a, b) => stringComparer.compare(a, b)); - } -} - -class AnkiCardController { - constructor(settingsController, ankiController, node) { - this._settingsController = settingsController; - this._ankiController = ankiController; - this._node = node; - this._cardType = node.dataset.ankiCardType; - this._cardMenu = node.dataset.ankiCardMenu; - this._eventListeners = new EventListenerCollection(); - this._fieldEventListeners = new EventListenerCollection(); - this._deck = null; - this._model = null; - this._fields = null; - this._modelChangingTo = null; - this._ankiCardDeckSelect = null; - this._ankiCardModelSelect = null; - this._ankiCardFieldsContainer = null; - this._cleaned = false; - this._fieldEntries = []; - } - - async prepare() { - const options = await this._settingsController.getOptions(); - const ankiOptions = options.anki; - if (this._cleaned) { return; } - - const cardOptions = this._getCardOptions(ankiOptions, this._cardType); - if (cardOptions === null) { return; } - const {deck, model, fields} = cardOptions; - this._deck = deck; - this._model = model; - this._fields = fields; - - this._ankiCardDeckSelect = this._node.querySelector('.anki-card-deck'); - this._ankiCardModelSelect = this._node.querySelector('.anki-card-model'); - this._ankiCardFieldsContainer = this._node.querySelector('.anki-card-fields'); - - this._setupSelects([], []); - this._setupFields(); - - this._eventListeners.addEventListener(this._ankiCardDeckSelect, 'change', this._onCardDeckChange.bind(this), false); - this._eventListeners.addEventListener(this._ankiCardModelSelect, 'change', this._onCardModelChange.bind(this), false); - this._eventListeners.on(this._settingsController, 'permissionsChanged', this._onPermissionsChanged.bind(this)); - - await this.updateAnkiState(); - } - - cleanup() { - this._cleaned = true; - this._fieldEntries = []; - this._eventListeners.removeAllEventListeners(); - } - - async updateAnkiState() { - if (this._fields === null) { return; } - const {deckNames, modelNames} = await this._ankiController.getAnkiData(); - if (this._cleaned) { return; } - this._setupSelects(deckNames, modelNames); - } - - isStale() { - return (this._cardType !== this._node.dataset.ankiCardType); - } - - // Private - - _onCardDeckChange(e) { - this._setDeck(e.currentTarget.value); - } - - _onCardModelChange(e) { - this._setModel(e.currentTarget.value); - } - - _onFieldChange(index, e) { - const node = e.currentTarget; - this._validateFieldPermissions(node, index, true); - this._validateField(node, index); - } - - _onFieldInput(index, e) { - const node = e.currentTarget; - this._validateField(node, index); - } - - _onFieldSettingChanged(index, e) { - const node = e.currentTarget; - this._validateFieldPermissions(node, index, false); - } - - _onFieldMenuClose({currentTarget: button, detail: {action, item}}) { - switch (action) { - case 'setFieldMarker': - this._setFieldMarker(button, item.dataset.marker); - break; - } - } - - _onFieldMarkerLinkClick(e) { - e.preventDefault(); - const link = e.currentTarget; - this._setFieldMarker(link, link.textContent); - } - - _validateField(node, index) { - let valid = (node.dataset.hasPermissions !== 'false'); - if (valid && index === 0 && !this._ankiController.containsAnyMarker(node.value)) { - valid = false; - } - node.dataset.invalid = `${!valid}`; - } - - _setFieldMarker(element, marker) { - const input = element.closest('.anki-card-field-value-container').querySelector('.anki-card-field-value'); - input.value = `{${marker}}`; - input.dispatchEvent(new Event('change')); - } - - _getCardOptions(ankiOptions, cardType) { - switch (cardType) { - case 'terms': return ankiOptions.terms; - case 'kanji': return ankiOptions.kanji; - default: return null; - } - } - - _setupSelects(deckNames, modelNames) { - const deck = this._deck; - const model = this._model; - if (!deckNames.includes(deck)) { deckNames = [...deckNames, deck]; } - if (!modelNames.includes(model)) { modelNames = [...modelNames, model]; } - - this._setSelectOptions(this._ankiCardDeckSelect, deckNames); - this._ankiCardDeckSelect.value = deck; - - this._setSelectOptions(this._ankiCardModelSelect, modelNames); - this._ankiCardModelSelect.value = model; - } - - _setSelectOptions(select, optionValues) { - const fragment = document.createDocumentFragment(); - for (const optionValue of optionValues) { - const option = document.createElement('option'); - option.value = optionValue; - option.textContent = optionValue; - fragment.appendChild(option); - } - select.textContent = ''; - select.appendChild(fragment); - } - - _setupFields() { - this._fieldEventListeners.removeAllEventListeners(); - - const markers = this._ankiController.getFieldMarkers(this._cardType); - const totalFragment = document.createDocumentFragment(); - this._fieldEntries = []; - let index = 0; - for (const [fieldName, fieldValue] of Object.entries(this._fields)) { - const content = this._settingsController.instantiateTemplateFragment('anki-card-field'); - - const fieldNameContainerNode = content.querySelector('.anki-card-field-name-container'); - fieldNameContainerNode.dataset.index = `${index}`; - const fieldNameNode = content.querySelector('.anki-card-field-name'); - fieldNameNode.textContent = fieldName; - - const valueContainer = content.querySelector('.anki-card-field-value-container'); - valueContainer.dataset.index = `${index}`; - - const inputField = content.querySelector('.anki-card-field-value'); - inputField.value = fieldValue; - inputField.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields', fieldName]); - this._validateFieldPermissions(inputField, index, false); - - this._fieldEventListeners.addEventListener(inputField, 'change', this._onFieldChange.bind(this, index), false); - this._fieldEventListeners.addEventListener(inputField, 'input', this._onFieldInput.bind(this, index), false); - this._fieldEventListeners.addEventListener(inputField, 'settingChanged', this._onFieldSettingChanged.bind(this, index), false); - this._validateField(inputField, index); - - const markerList = content.querySelector('.anki-card-field-marker-list'); - if (markerList !== null) { - const markersFragment = this._ankiController.getFieldMarkersHtml(markers); - for (const element of markersFragment.querySelectorAll('.marker-link')) { - this._fieldEventListeners.addEventListener(element, 'click', this._onFieldMarkerLinkClick.bind(this), false); - } - markerList.appendChild(markersFragment); - } - - const menuButton = content.querySelector('.anki-card-field-value-menu-button'); - if (menuButton !== null) { - if (typeof this._cardMenu !== 'undefined') { - menuButton.dataset.menu = this._cardMenu; - } else { - delete menuButton.dataset.menu; - } - this._fieldEventListeners.addEventListener(menuButton, 'menuClose', this._onFieldMenuClose.bind(this), false); - } - - totalFragment.appendChild(content); - this._fieldEntries.push({fieldName, inputField, fieldNameContainerNode}); - - ++index; - } - - const ELEMENT_NODE = Node.ELEMENT_NODE; - const container = this._ankiCardFieldsContainer; - for (const node of [...container.childNodes]) { - if (node.nodeType === ELEMENT_NODE && node.dataset.persistent === 'true') { continue; } - container.removeChild(node); - } - container.appendChild(totalFragment); - - this._validateFields(); - } - - async _validateFields() { - const token = {}; - this._validateFieldsToken = token; - - let fieldNames; - try { - fieldNames = await this._ankiController.getModelFieldNames(this._model); - } catch (e) { - return; - } - - if (token !== this._validateFieldsToken) { return; } - - const fieldNamesSet = new Set(fieldNames); - let index = 0; - for (const {fieldName, fieldNameContainerNode} of this._fieldEntries) { - fieldNameContainerNode.dataset.invalid = `${!fieldNamesSet.has(fieldName)}`; - fieldNameContainerNode.dataset.orderMatches = `${index < fieldNames.length && fieldName === fieldNames[index]}`; - ++index; - } - } - - async _setDeck(value) { - if (this._deck === value) { return; } - this._deck = value; - - await this._settingsController.modifyProfileSettings([{ - action: 'set', - path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'deck']), - value - }]); - } - - async _setModel(value) { - if (this._modelChangingTo !== null) { - // Revert - this._ankiCardModelSelect.value = this._modelChangingTo; - return; - } - if (this._model === value) { return; } - - let fieldNames; - let options; - try { - this._modelChangingTo = value; - fieldNames = await this._ankiController.getModelFieldNames(value); - options = await this._ankiController.settingsController.getOptions(); - } catch (e) { - // Revert - this._ankiCardModelSelect.value = this._model; - return; - } finally { - this._modelChangingTo = null; - } - - const cardType = this._cardType; - const cardOptions = this._getCardOptions(options.anki, cardType); - const oldFields = cardOptions !== null ? cardOptions.fields : null; - - const fields = {}; - for (let i = 0, ii = fieldNames.length; i < ii; ++i) { - const fieldName = fieldNames[i]; - fields[fieldName] = this._getDefaultFieldValue(fieldName, i, cardType, oldFields); - } - - const targets = [ - { - action: 'set', - path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'model']), - value - }, - { - action: 'set', - path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields']), - value: fields - } - ]; - - this._model = value; - this._fields = fields; - - await this._settingsController.modifyProfileSettings(targets); - - this._setupFields(); - } - - async _requestPermissions(permissions) { - try { - await this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true); - } catch (e) { - log.error(e); - } - } - - async _validateFieldPermissions(node, index, request) { - const fieldValue = node.value; - const permissions = this._ankiController.getRequiredPermissions(fieldValue); - if (permissions.length > 0) { - node.dataset.requiredPermission = permissions.join(' '); - const hasPermissions = await ( - request ? - this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true) : - this._settingsController.permissionsUtil.hasPermissions({permissions}) - ); - node.dataset.hasPermissions = `${hasPermissions}`; - } else { - delete node.dataset.requiredPermission; - delete node.dataset.hasPermissions; - } - - this._validateField(node, index); - } - - _onPermissionsChanged({permissions: {permissions}}) { - const permissionsSet = new Set(permissions); - for (let i = 0, ii = this._fieldEntries.length; i < ii; ++i) { - const {inputField} = this._fieldEntries[i]; - let {requiredPermission} = inputField.dataset; - if (typeof requiredPermission !== 'string') { continue; } - requiredPermission = (requiredPermission.length === 0 ? [] : requiredPermission.split(' ')); - - let hasPermissions = true; - for (const permission of requiredPermission) { - if (!permissionsSet.has(permission)) { - hasPermissions = false; - break; - } - } - - inputField.dataset.hasPermissions = `${hasPermissions}`; - this._validateField(inputField, i); - } - } - - _getDefaultFieldValue(fieldName, index, cardType, oldFields) { - if ( - typeof oldFields === 'object' && - oldFields !== null && - Object.prototype.hasOwnProperty.call(oldFields, fieldName) - ) { - return oldFields[fieldName]; - } - - if (index === 0) { - return (cardType === 'kanji' ? '{character}' : '{expression}'); - } - - const markers = this._ankiController.getFieldMarkers(cardType); - const markerAliases = new Map([ - ['glossary', ['definition', 'meaning']], - ['audio', ['sound']], - ['dictionary', ['dict']] - ]); - - const hyphenPattern = /-/g; - for (const marker of markers) { - const names = [marker]; - const aliases = markerAliases.get(marker); - if (typeof aliases !== 'undefined') { - names.push(...aliases); - } - - let pattern = '^(?:'; - for (let i = 0, ii = names.length; i < ii; ++i) { - const name = names[i]; - if (i > 0) { pattern += '|'; } - pattern += name.replace(hyphenPattern, '[-_ ]*'); - } - pattern += ')$'; - pattern = new RegExp(pattern, 'i'); - - if (pattern.test(fieldName)) { - return `{${marker}}`; - } - } - - return ''; - } -} diff --git a/ext/js/settings/anki-templates-controller.js b/ext/js/settings/anki-templates-controller.js deleted file mode 100644 index 8e3a1a70..00000000 --- a/ext/js/settings/anki-templates-controller.js +++ /dev/null @@ -1,227 +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 - */ - -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 yomichan.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 yomichan.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/js/settings/audio-controller.js b/ext/js/settings/audio-controller.js deleted file mode 100644 index e62383a8..00000000 --- a/ext/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/js/settings/backup-controller.js b/ext/js/settings/backup-controller.js deleted file mode 100644 index 649645d4..00000000 --- a/ext/js/settings/backup-controller.js +++ /dev/null @@ -1,418 +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 - */ - -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 yomichan.api.getEnvironmentInfo(); - const fieldTemplatesDefault = await yomichan.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) { - log.error(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) { - log.error(e); - } - } -} diff --git a/ext/js/settings/dictionary-controller.js b/ext/js/settings/dictionary-controller.js deleted file mode 100644 index e12017f2..00000000 --- a/ext/js/settings/dictionary-controller.js +++ /dev/null @@ -1,557 +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 - */ - -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 yomichan.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) { - log.error(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); - yomichan.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/js/settings/dictionary-import-controller.js b/ext/js/settings/dictionary-import-controller.js deleted file mode 100644 index 1389b7f0..00000000 --- a/ext/js/settings/dictionary-import-controller.js +++ /dev/null @@ -1,345 +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 - */ - -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 yomichan.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); - yomichan.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) { - log.error(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/js/settings/extension-keyboard-shortcuts-controller.js b/ext/js/settings/extension-keyboard-shortcuts-controller.js deleted file mode 100644 index 032f9dcc..00000000 --- a/ext/js/settings/extension-keyboard-shortcuts-controller.js +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright (C) 2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * HotkeyUtil - * KeyboardMouseInputField - */ - -class ExtensionKeyboardShortcutController { - constructor(settingsController) { - this._settingsController = settingsController; - this._resetButton = null; - this._clearButton = null; - this._listContainer = null; - this._hotkeyUtil = new HotkeyUtil(); - this._os = null; - this._entries = []; - } - - get hotkeyUtil() { - return this._hotkeyUtil; - } - - async prepare() { - this._resetButton = document.querySelector('#extension-hotkey-list-reset-all'); - this._clearButton = document.querySelector('#extension-hotkey-list-clear-all'); - this._listContainer = document.querySelector('#extension-hotkey-list'); - - const canResetCommands = this.canResetCommands(); - const canModifyCommands = this.canModifyCommands(); - this._resetButton.hidden = !canResetCommands; - this._clearButton.hidden = !canModifyCommands; - - if (canResetCommands) { - this._resetButton.addEventListener('click', this._onResetClick.bind(this)); - } - if (canModifyCommands) { - this._clearButton.addEventListener('click', this._onClearClick.bind(this)); - } - - const {platform: {os}} = await yomichan.api.getEnvironmentInfo(); - this._os = os; - this._hotkeyUtil.os = os; - - const commands = await this._getCommands(); - this._setupCommands(commands); - } - - async resetCommand(name) { - await this._resetCommand(name); - - let key = null; - let modifiers = []; - - const commands = await this._getCommands(); - for (const {name: name2, shortcut} of commands) { - if (name === name2) { - ({key, modifiers} = this._hotkeyUtil.convertCommandToInput(shortcut)); - break; - } - } - - return {key, modifiers}; - } - - async updateCommand(name, key, modifiers) { - // Firefox-only; uses Promise API - const shortcut = this._hotkeyUtil.convertInputToCommand(key, modifiers); - return await chrome.commands.update({name, shortcut}); - } - - canResetCommands() { - return isObject(chrome.commands) && typeof chrome.commands.reset === 'function'; - } - - canModifyCommands() { - return isObject(chrome.commands) && typeof chrome.commands.update === 'function'; - } - - // Add - - _onResetClick(e) { - e.preventDefault(); - this._resetAllCommands(); - } - - _onClearClick(e) { - e.preventDefault(); - this._clearAllCommands(); - } - - _getCommands() { - return new Promise((resolve, reject) => { - if (!(isObject(chrome.commands) && typeof chrome.commands.getAll === 'function')) { - resolve([]); - return; - } - - chrome.commands.getAll((result) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(result); - } - }); - }); - } - - _setupCommands(commands) { - for (const entry of this._entries) { - entry.cleanup(); - } - this._entries = []; - - const fragment = document.createDocumentFragment(); - - for (const {name, description, shortcut} of commands) { - if (name.startsWith('_')) { continue; } - - const {key, modifiers} = this._hotkeyUtil.convertCommandToInput(shortcut); - - const node = this._settingsController.instantiateTemplate('extension-hotkey-list-item'); - fragment.appendChild(node); - - const entry = new ExtensionKeyboardShortcutHotkeyEntry(this, node, name, description, key, modifiers, this._os); - entry.prepare(); - this._entries.push(entry); - } - - this._listContainer.textContent = ''; - this._listContainer.appendChild(fragment); - } - - async _resetAllCommands() { - if (!this.canModifyCommands()) { return; } - - let commands = await this._getCommands(); - const promises = []; - - for (const {name} of commands) { - if (name.startsWith('_')) { continue; } - promises.push(this._resetCommand(name)); - } - - await Promise.all(promises); - - commands = await this._getCommands(); - this._setupCommands(commands); - } - - async _clearAllCommands() { - if (!this.canModifyCommands()) { return; } - - let commands = await this._getCommands(); - const promises = []; - - for (const {name} of commands) { - if (name.startsWith('_')) { continue; } - promises.push(this.updateCommand(name, null, [])); - } - - await Promise.all(promises); - - commands = await this._getCommands(); - this._setupCommands(commands); - } - - async _resetCommand(name) { - // Firefox-only; uses Promise API - return await chrome.commands.reset(name); - } -} - -class ExtensionKeyboardShortcutHotkeyEntry { - constructor(parent, node, name, description, key, modifiers, os) { - this._parent = parent; - this._node = node; - this._name = name; - this._description = description; - this._key = key; - this._modifiers = modifiers; - this._os = os; - this._input = null; - this._inputField = null; - this._eventListeners = new EventListenerCollection(); - } - - prepare() { - this._node.querySelector('.settings-item-label').textContent = this._description || this._name; - - const button = this._node.querySelector('.extension-hotkey-list-item-button'); - const input = this._node.querySelector('input'); - - this._input = input; - - if (this._parent.canModifyCommands()) { - this._inputField = new KeyboardMouseInputField(input, null, this._os); - this._inputField.prepare(this._key, this._modifiers, false, true); - this._eventListeners.on(this._inputField, 'change', this._onInputFieldChange.bind(this)); - this._eventListeners.addEventListener(button, 'menuClose', this._onMenuClose.bind(this)); - this._eventListeners.addEventListener(input, 'blur', this._onInputFieldBlur.bind(this)); - } else { - input.readOnly = true; - input.value = this._parent.hotkeyUtil.getInputDisplayValue(this._key, this._modifiers); - button.hidden = true; - } - } - - cleanup() { - this._eventListeners.removeAllEventListeners(); - if (this._node.parentNode !== null) { - this._node.parentNode.removeChild(this._node); - } - if (this._inputField !== null) { - this._inputField.cleanup(); - this._inputField = null; - } - } - - // Private - - _onInputFieldChange(e) { - const {key, modifiers} = e; - this._tryUpdateInput(key, modifiers, false); - } - - _onInputFieldBlur() { - this._updateInput(); - } - - _onMenuClose(e) { - switch (e.detail.action) { - case 'clearInput': - this._tryUpdateInput(null, [], true); - break; - case 'resetInput': - this._resetInput(); - break; - } - } - - _updateInput() { - this._inputField.setInput(this._key, this._modifiers); - delete this._input.dataset.invalid; - } - - async _tryUpdateInput(key, modifiers, updateInput) { - let okay = (key === null ? (modifiers.length === 0) : (modifiers.length !== 0)); - if (okay) { - try { - await this._parent.updateCommand(this._name, key, modifiers); - } catch (e) { - okay = false; - } - } - - if (okay) { - this._key = key; - this._modifiers = modifiers; - delete this._input.dataset.invalid; - } else { - this._input.dataset.invalid = 'true'; - } - - if (updateInput) { - this._updateInput(); - } - } - - async _resetInput() { - const {key, modifiers} = await this._parent.resetCommand(this._name); - this._key = key; - this._modifiers = modifiers; - this._updateInput(); - } -} diff --git a/ext/js/settings/generic-setting-controller.js b/ext/js/settings/generic-setting-controller.js deleted file mode 100644 index 7d6fc2e6..00000000 --- a/ext/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/js/settings/keyboard-mouse-input-field.js b/ext/js/settings/keyboard-mouse-input-field.js deleted file mode 100644 index 09477519..00000000 --- a/ext/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/js/settings/keyboard-shortcuts-controller.js b/ext/js/settings/keyboard-shortcuts-controller.js deleted file mode 100644 index 99b16f06..00000000 --- a/ext/js/settings/keyboard-shortcuts-controller.js +++ /dev/null @@ -1,367 +0,0 @@ -/* - * Copyright (C) 2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * KeyboardMouseInputField - */ - -class KeyboardShortcutController { - constructor(settingsController) { - this._settingsController = settingsController; - this._entries = []; - this._os = null; - this._addButton = null; - this._resetButton = null; - this._listContainer = null; - this._emptyIndicator = null; - this._stringComparer = new Intl.Collator('en-US'); // Invariant locale - this._scrollContainer = null; - } - - get settingsController() { - return this._settingsController; - } - - async prepare() { - const {platform: {os}} = await yomichan.api.getEnvironmentInfo(); - this._os = os; - - this._addButton = document.querySelector('#hotkey-list-add'); - this._resetButton = document.querySelector('#hotkey-list-reset'); - this._listContainer = document.querySelector('#hotkey-list'); - this._emptyIndicator = document.querySelector('#hotkey-list-empty'); - this._scrollContainer = document.querySelector('#keyboard-shortcuts-modal .modal-body'); - - this._addButton.addEventListener('click', this._onAddClick.bind(this)); - this._resetButton.addEventListener('click', this._onResetClick.bind(this)); - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - - await this._updateOptions(); - } - - async addEntry(terminationCharacterEntry) { - const options = await this._settingsController.getOptions(); - const {inputs: {hotkeys}} = options; - - await this._settingsController.modifyProfileSettings([{ - action: 'splice', - path: 'inputs.hotkeys', - start: hotkeys.length, - deleteCount: 0, - items: [terminationCharacterEntry] - }]); - - await this._updateOptions(); - this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight; - } - - async deleteEntry(index) { - const options = await this._settingsController.getOptions(); - const {inputs: {hotkeys}} = options; - - if (index < 0 || index >= hotkeys.length) { return false; } - - await this._settingsController.modifyProfileSettings([{ - action: 'splice', - path: 'inputs.hotkeys', - start: index, - deleteCount: 1, - items: [] - }]); - - await this._updateOptions(); - return true; - } - - async modifyProfileSettings(targets) { - return await this._settingsController.modifyProfileSettings(targets); - } - - async getDefaultHotkeys() { - const defaultOptions = await this._settingsController.getDefaultOptions(); - return defaultOptions.profiles[0].options.inputs.hotkeys; - } - - // Private - - _onOptionsChanged({options}) { - for (const entry of this._entries) { - entry.cleanup(); - } - - this._entries = []; - const {inputs: {hotkeys}} = options; - const fragment = document.createDocumentFragment(); - - for (let i = 0, ii = hotkeys.length; i < ii; ++i) { - const hotkeyEntry = hotkeys[i]; - const node = this._settingsController.instantiateTemplate('hotkey-list-item'); - fragment.appendChild(node); - const entry = new KeyboardShortcutHotkeyEntry(this, hotkeyEntry, i, node, this._os, this._stringComparer); - this._entries.push(entry); - entry.prepare(); - } - - this._listContainer.appendChild(fragment); - this._listContainer.hidden = (hotkeys.length === 0); - this._emptyIndicator.hidden = (hotkeys.length !== 0); - } - - _onAddClick(e) { - e.preventDefault(); - this._addNewEntry(); - } - - _onResetClick(e) { - e.preventDefault(); - this._reset(); - } - - async _addNewEntry() { - const newEntry = { - action: '', - key: null, - modifiers: [], - scopes: ['popup', 'search'], - enabled: true - }; - return await this.addEntry(newEntry); - } - - async _updateOptions() { - const options = await this._settingsController.getOptions(); - this._onOptionsChanged({options}); - } - - async _reset() { - const value = await this.getDefaultHotkeys(); - await this._settingsController.setProfileSetting('inputs.hotkeys', value); - await this._updateOptions(); - } -} - -class KeyboardShortcutHotkeyEntry { - constructor(parent, data, index, node, os, stringComparer) { - this._parent = parent; - this._data = data; - this._index = index; - this._node = node; - this._os = os; - this._eventListeners = new EventListenerCollection(); - this._inputField = null; - this._actionSelect = null; - this._scopeCheckboxes = null; - this._scopeCheckboxContainers = null; - this._basePath = `inputs.hotkeys[${this._index}]`; - this._stringComparer = stringComparer; - } - - prepare() { - const node = this._node; - - const menuButton = node.querySelector('.hotkey-list-item-button'); - const input = node.querySelector('.hotkey-list-item-input'); - const action = node.querySelector('.hotkey-list-item-action'); - const scopeCheckboxes = node.querySelectorAll('.hotkey-scope-checkbox'); - const scopeCheckboxContainers = node.querySelectorAll('.hotkey-scope-checkbox-container'); - const enabledToggle = node.querySelector('.hotkey-list-item-enabled'); - - this._actionSelect = action; - this._scopeCheckboxes = scopeCheckboxes; - this._scopeCheckboxContainers = scopeCheckboxContainers; - - this._inputField = new KeyboardMouseInputField(input, null, this._os); - this._inputField.prepare(this._data.key, this._data.modifiers, false, true); - - action.value = this._data.action; - - enabledToggle.checked = this._data.enabled; - enabledToggle.dataset.setting = `${this._basePath}.enabled`; - - this._updateCheckboxVisibility(); - this._updateCheckboxStates(); - - for (const scopeCheckbox of scopeCheckboxes) { - this._eventListeners.addEventListener(scopeCheckbox, 'change', this._onScopeCheckboxChange.bind(this), false); - } - this._eventListeners.addEventListener(menuButton, 'menuClose', this._onMenuClose.bind(this), false); - this._eventListeners.addEventListener(this._actionSelect, 'change', this._onActionSelectChange.bind(this), false); - this._eventListeners.on(this._inputField, 'change', this._onInputFieldChange.bind(this)); - } - - cleanup() { - this._eventListeners.removeAllEventListeners(); - this._inputField.cleanup(); - if (this._node.parentNode !== null) { - this._node.parentNode.removeChild(this._node); - } - } - - // Private - - _onMenuClose(e) { - switch (e.detail.action) { - case 'delete': - this._delete(); - break; - case 'clearInputs': - this._inputField.clearInputs(); - break; - case 'resetInput': - this._resetInput(); - break; - } - } - - _onInputFieldChange({key, modifiers}) { - this._setKeyAndModifiers(key, modifiers); - } - - _onScopeCheckboxChange(e) { - const node = e.currentTarget; - const {scope} = node.dataset; - if (typeof scope !== 'string') { return; } - this._setScopeEnabled(scope, node.checked); - } - - _onActionSelectChange(e) { - const value = e.currentTarget.value; - this._setAction(value); - } - - async _delete() { - this._parent.deleteEntry(this._index); - } - - async _setKeyAndModifiers(key, modifiers) { - this._data.key = key; - this._data.modifiers = modifiers; - await this._modifyProfileSettings([ - { - action: 'set', - path: `${this._basePath}.key`, - value: key - }, - { - action: 'set', - path: `${this._basePath}.modifiers`, - value: modifiers - } - ]); - } - - async _setScopeEnabled(scope, enabled) { - const scopes = this._data.scopes; - const index = scopes.indexOf(scope); - if ((index >= 0) === enabled) { return; } - - if (enabled) { - scopes.push(scope); - const stringComparer = this._stringComparer; - scopes.sort((scope1, scope2) => stringComparer.compare(scope1, scope2)); - } else { - scopes.splice(index, 1); - } - - await this._modifyProfileSettings([{ - action: 'set', - path: `${this._basePath}.scopes`, - value: scopes - }]); - } - - async _modifyProfileSettings(targets) { - return await this._parent.settingsController.modifyProfileSettings(targets); - } - - async _resetInput() { - const defaultHotkeys = await this._parent.getDefaultHotkeys(); - const defaultValue = this._getDefaultKeyAndModifiers(defaultHotkeys, this._data.action); - if (defaultValue === null) { return; } - - const {key, modifiers} = defaultValue; - await this._setKeyAndModifiers(key, modifiers); - this._inputField.setInput(key, modifiers); - } - - _getDefaultKeyAndModifiers(defaultHotkeys, action) { - for (const {action: action2, key, modifiers} of defaultHotkeys) { - if (action2 !== action) { continue; } - return {modifiers, key}; - } - return null; - } - - async _setAction(value) { - const targets = [{ - action: 'set', - path: `${this._basePath}.action`, - value - }]; - - this._data.action = value; - - const scopes = this._data.scopes; - const validScopes = this._getValidScopesForAction(value); - if (validScopes !== null) { - let changed = false; - for (let i = 0, ii = scopes.length; i < ii; ++i) { - if (!validScopes.has(scopes[i])) { - scopes.splice(i, 1); - --i; - --ii; - changed = true; - } - } - if (changed) { - if (scopes.length === 0) { - scopes.push(...validScopes); - } - targets.push({ - action: 'set', - path: `${this._basePath}.scopes`, - value: scopes - }); - this._updateCheckboxStates(); - } - } - - await this._modifyProfileSettings(targets); - - this._updateCheckboxVisibility(); - } - - _updateCheckboxStates() { - const scopes = this._data.scopes; - for (const scopeCheckbox of this._scopeCheckboxes) { - scopeCheckbox.checked = scopes.includes(scopeCheckbox.dataset.scope); - } - } - - _updateCheckboxVisibility() { - const validScopes = this._getValidScopesForAction(this._data.action); - for (const node of this._scopeCheckboxContainers) { - node.hidden = !(validScopes === null || validScopes.has(node.dataset.scope)); - } - } - - _getValidScopesForAction(action) { - const optionNode = this._actionSelect.querySelector(`option[value="${action}"]`); - const scopesString = (optionNode !== null ? optionNode.dataset.scopes : void 0); - return (typeof scopesString === 'string' ? new Set(scopesString.split(' ')) : null); - } -} diff --git a/ext/js/settings/main.js b/ext/js/settings/main.js deleted file mode 100644 index 9785ee0e..00000000 --- a/ext/js/settings/main.js +++ /dev/null @@ -1,109 +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 - */ - -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 yomichan.api.getEnvironmentInfo(); - document.documentElement.dataset.browser = browser; - document.documentElement.dataset.operatingSystem = platform.os; -} - - -(async () => { - try { - await yomichan.prepare(); - - setupEnvironmentInfo(); - showExtensionInformation(); - - const optionsFull = await yomichan.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) { - log.error(e); - } -})(); diff --git a/ext/js/settings/mecab-controller.js b/ext/js/settings/mecab-controller.js deleted file mode 100644 index 122f82f9..00000000 --- a/ext/js/settings/mecab-controller.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class MecabController { - constructor(settingsController) { - this._settingsController = settingsController; - this._testButton = null; - this._resultsContainer = null; - this._testActive = false; - } - - prepare() { - this._testButton = document.querySelector('#test-mecab-button'); - this._resultsContainer = document.querySelector('#test-mecab-results'); - - this._testButton.addEventListener('click', this._onTestButtonClick.bind(this), false); - } - - // Private - - _onTestButtonClick(e) { - e.preventDefault(); - this._testMecab(); - } - - async _testMecab() { - if (this._testActive) { return; } - - try { - this._testActive = true; - this._testButton.disabled = true; - this._resultsContainer.textContent = ''; - this._resultsContainer.hidden = true; - await yomichan.api.testMecab(); - this._setStatus('Connection was successful', false); - } catch (e) { - this._setStatus(e.message, true); - } finally { - this._testActive = false; - this._testButton.disabled = false; - } - } - - _setStatus(message, isError) { - this._resultsContainer.textContent = message; - this._resultsContainer.hidden = false; - this._resultsContainer.classList.toggle('danger-text', isError); - } -} diff --git a/ext/js/settings/modal-controller.js b/ext/js/settings/modal-controller.js deleted file mode 100644 index fe4f911b..00000000 --- a/ext/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/js/settings/modal-jquery.js b/ext/js/settings/modal-jquery.js deleted file mode 100644 index 8c69ae6d..00000000 --- a/ext/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/js/settings/modal.js b/ext/js/settings/modal.js deleted file mode 100644 index 2ef49540..00000000 --- a/ext/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/js/settings/nested-popups-controller.js b/ext/js/settings/nested-popups-controller.js deleted file mode 100644 index 1ebc7389..00000000 --- a/ext/js/settings/nested-popups-controller.js +++ /dev/null @@ -1,71 +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 - * DOMDataBinder - */ - -class NestedPopupsController { - constructor(settingsController) { - this._settingsController = settingsController; - this._popupNestingMaxDepth = 0; - } - - async prepare() { - this._nestedPopupsEnabled = document.querySelector('#nested-popups-enabled'); - this._nestedPopupsCount = document.querySelector('#nested-popups-count'); - this._nestedPopupsEnabledMoreOptions = document.querySelector('#nested-popups-enabled-more-options'); - - const options = await this._settingsController.getOptions(); - - this._nestedPopupsEnabled.addEventListener('change', this._onNestedPopupsEnabledChange.bind(this), false); - this._nestedPopupsCount.addEventListener('change', this._onNestedPopupsCountChange.bind(this), false); - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - this._onOptionsChanged({options}); - } - - // Private - - _onOptionsChanged({options}) { - this._updatePopupNestingMaxDepth(options.scanning.popupNestingMaxDepth); - } - - _onNestedPopupsEnabledChange(e) { - const value = e.currentTarget.checked; - if (value && this._popupNestingMaxDepth > 0) { return; } - this._setPopupNestingMaxDepth(value ? 1 : 0); - } - - _onNestedPopupsCountChange(e) { - const node = e.currentTarget; - const value = Math.max(1, DOMDataBinder.convertToNumber(node.value, node)); - this._setPopupNestingMaxDepth(value); - } - - _updatePopupNestingMaxDepth(value) { - const enabled = (value > 0); - this._popupNestingMaxDepth = value; - this._nestedPopupsEnabled.checked = enabled; - this._nestedPopupsCount.value = `${value}`; - this._nestedPopupsEnabledMoreOptions.hidden = !enabled; - } - - async _setPopupNestingMaxDepth(value) { - this._updatePopupNestingMaxDepth(value); - await this._settingsController.setProfileSetting('scanning.popupNestingMaxDepth', value); - } -} diff --git a/ext/js/settings/permissions-toggle-controller.js b/ext/js/settings/permissions-toggle-controller.js deleted file mode 100644 index f80e7585..00000000 --- a/ext/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/js/settings/pitch-accents-preview-main.js b/ext/js/settings/pitch-accents-preview-main.js deleted file mode 100644 index d9d56727..00000000 --- a/ext/js/settings/pitch-accents-preview-main.js +++ /dev/null @@ -1,35 +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 { - await yomichan.prepare(); - - const displayGenerator = new DisplayGenerator({ - japaneseUtil: null, - mediaLoader: null - }); - await displayGenerator.prepare(); - displayGenerator.preparePitchAccents(); - } catch (e) { - log.error(e); - } -})(); diff --git a/ext/js/settings/popup-preview-controller.js b/ext/js/settings/popup-preview-controller.js deleted file mode 100644 index f98b0679..00000000 --- a/ext/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/js/settings/popup-preview-frame-main.js b/ext/js/settings/popup-preview-frame-main.js deleted file mode 100644 index 80e248be..00000000 --- a/ext/js/settings/popup-preview-frame-main.js +++ /dev/null @@ -1,43 +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 - */ - -(async () => { - try { - await yomichan.prepare(); - - const {tabId, frameId} = await yomichan.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) { - log.error(e); - } -})(); diff --git a/ext/js/settings/popup-preview-frame.js b/ext/js/settings/popup-preview-frame.js deleted file mode 100644 index 638dd414..00000000 --- a/ext/js/settings/popup-preview-frame.js +++ /dev/null @@ -1,232 +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 - * 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 = yomichan.api.optionsGet.bind(yomichan.api); - yomichan.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/js/settings/popup-window-controller.js b/ext/js/settings/popup-window-controller.js deleted file mode 100644 index 403c060c..00000000 --- a/ext/js/settings/popup-window-controller.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class PopupWindowController { - prepare() { - const testLink = document.querySelector('#test-window-open-link'); - testLink.addEventListener('click', this._onTestWindowOpenLinkClick.bind(this), false); - } - - // Private - - _onTestWindowOpenLinkClick(e) { - e.preventDefault(); - this._testWindowOpen(); - } - - async _testWindowOpen() { - await yomichan.api.getOrCreateSearchPopup({focus: true}); - } -} diff --git a/ext/js/settings/profile-conditions-ui.js b/ext/js/settings/profile-conditions-ui.js deleted file mode 100644 index 5fda1dc0..00000000 --- a/ext/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/js/settings/profile-controller.js b/ext/js/settings/profile-controller.js deleted file mode 100644 index 3883e80a..00000000 --- a/ext/js/settings/profile-controller.js +++ /dev/null @@ -1,697 +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 - */ - -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 yomichan.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/js/settings/scan-inputs-controller.js b/ext/js/settings/scan-inputs-controller.js deleted file mode 100644 index 79b2bdf4..00000000 --- a/ext/js/settings/scan-inputs-controller.js +++ /dev/null @@ -1,309 +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 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 yomichan.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/js/settings/scan-inputs-simple-controller.js b/ext/js/settings/scan-inputs-simple-controller.js deleted file mode 100644 index b011af5d..00000000 --- a/ext/js/settings/scan-inputs-simple-controller.js +++ /dev/null @@ -1,246 +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 - */ - -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 yomichan.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/js/settings/secondary-search-dictionary-controller.js b/ext/js/settings/secondary-search-dictionary-controller.js deleted file mode 100644 index 2fb3de67..00000000 --- a/ext/js/settings/secondary-search-dictionary-controller.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/>. - */ - -/* global - * ObjectPropertyAccessor - */ - -class SecondarySearchDictionaryController { - constructor(settingsController) { - this._settingsController = settingsController; - this._getDictionaryInfoToken = null; - this._container = null; - this._eventListeners = new EventListenerCollection(); - } - - async prepare() { - this._container = document.querySelector('#secondary-search-dictionary-list'); - - yomichan.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); - - await this._onDatabaseUpdated(); - } - - // Private - - async _onDatabaseUpdated() { - this._eventListeners.removeAllEventListeners(); - - const token = {}; - this._getDictionaryInfoToken = token; - const dictionaries = await this._settingsController.getDictionaryInfo(); - if (this._getDictionaryInfoToken !== token) { return; } - this._getDictionaryInfoToken = null; - - const fragment = document.createDocumentFragment(); - for (const {title, revision} of dictionaries) { - const node = this._settingsController.instantiateTemplate('secondary-search-dictionary'); - fragment.appendChild(node); - - const nameNode = node.querySelector('.dictionary-title'); - nameNode.textContent = title; - - const versionNode = node.querySelector('.dictionary-version'); - versionNode.textContent = `rev.${revision}`; - - const toggle = node.querySelector('.dictionary-allow-secondary-searches'); - toggle.dataset.setting = ObjectPropertyAccessor.getPathString(['dictionaries', title, 'allowSecondarySearches']); - this._eventListeners.addEventListener(toggle, 'settingChanged', this._onEnabledChanged.bind(this, node), false); - } - - this._container.textContent = ''; - this._container.appendChild(fragment); - } - - _onEnabledChanged(node, e) { - const {detail: {value}} = e; - node.dataset.enabled = `${value}`; - } -}
\ No newline at end of file diff --git a/ext/js/settings/sentence-termination-characters-controller.js b/ext/js/settings/sentence-termination-characters-controller.js deleted file mode 100644 index d62771ec..00000000 --- a/ext/js/settings/sentence-termination-characters-controller.js +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright (C) 2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class SentenceTerminationCharactersController { - constructor(settingsController) { - this._settingsController = settingsController; - this._entries = []; - this._addButton = null; - this._resetButton = null; - this._listTable = null; - this._listContainer = null; - this._emptyIndicator = null; - } - - get settingsController() { - return this._settingsController; - } - - async prepare() { - this._addButton = document.querySelector('#sentence-termination-character-list-add'); - this._resetButton = document.querySelector('#sentence-termination-character-list-reset'); - this._listTable = document.querySelector('#sentence-termination-character-list-table'); - this._listContainer = document.querySelector('#sentence-termination-character-list'); - this._emptyIndicator = document.querySelector('#sentence-termination-character-list-empty'); - - this._addButton.addEventListener('click', this._onAddClick.bind(this)); - this._resetButton.addEventListener('click', this._onResetClick.bind(this)); - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - - await this._updateOptions(); - } - - async addEntry(terminationCharacterEntry) { - const options = await this._settingsController.getOptions(); - const {sentenceParsing: {terminationCharacters}} = options; - - await this._settingsController.modifyProfileSettings([{ - action: 'splice', - path: 'sentenceParsing.terminationCharacters', - start: terminationCharacters.length, - deleteCount: 0, - items: [terminationCharacterEntry] - }]); - - await this._updateOptions(); - } - - async deleteEntry(index) { - const options = await this._settingsController.getOptions(); - const {sentenceParsing: {terminationCharacters}} = options; - - if (index < 0 || index >= terminationCharacters.length) { return false; } - - await this._settingsController.modifyProfileSettings([{ - action: 'splice', - path: 'sentenceParsing.terminationCharacters', - start: index, - deleteCount: 1, - items: [] - }]); - - await this._updateOptions(); - return true; - } - - async modifyProfileSettings(targets) { - return await this._settingsController.modifyProfileSettings(targets); - } - - // Private - - _onOptionsChanged({options}) { - for (const entry of this._entries) { - entry.cleanup(); - } - - this._entries = []; - const {sentenceParsing: {terminationCharacters}} = options; - - for (let i = 0, ii = terminationCharacters.length; i < ii; ++i) { - const terminationCharacterEntry = terminationCharacters[i]; - const node = this._settingsController.instantiateTemplate('sentence-termination-character-entry'); - this._listContainer.appendChild(node); - const entry = new SentenceTerminationCharacterEntry(this, terminationCharacterEntry, i, node); - this._entries.push(entry); - entry.prepare(); - } - - this._listTable.hidden = (terminationCharacters.length === 0); - this._emptyIndicator.hidden = (terminationCharacters.length !== 0); - } - - _onAddClick(e) { - e.preventDefault(); - this._addNewEntry(); - } - - _onResetClick(e) { - e.preventDefault(); - this._reset(); - } - - async _addNewEntry() { - const newEntry = { - enabled: true, - character1: '"', - character2: '"', - includeCharacterAtStart: false, - includeCharacterAtEnd: false - }; - return await this.addEntry(newEntry); - } - - async _updateOptions() { - const options = await this._settingsController.getOptions(); - this._onOptionsChanged({options}); - } - - async _reset() { - const defaultOptions = await this._settingsController.getDefaultOptions(); - const value = defaultOptions.profiles[0].options.sentenceParsing.terminationCharacters; - await this._settingsController.setProfileSetting('sentenceParsing.terminationCharacters', value); - await this._updateOptions(); - } -} - -class SentenceTerminationCharacterEntry { - constructor(parent, data, index, node) { - this._parent = parent; - this._data = data; - this._index = index; - this._node = node; - this._eventListeners = new EventListenerCollection(); - this._character1Input = null; - this._character2Input = null; - this._basePath = `sentenceParsing.terminationCharacters[${this._index}]`; - } - - prepare() { - const {enabled, character1, character2, includeCharacterAtStart, includeCharacterAtEnd} = this._data; - const node = this._node; - - const enabledToggle = node.querySelector('.sentence-termination-character-enabled'); - const typeSelect = node.querySelector('.sentence-termination-character-type'); - const character1Input = node.querySelector('.sentence-termination-character-input1'); - const character2Input = node.querySelector('.sentence-termination-character-input2'); - const includeAtStartCheckbox = node.querySelector('.sentence-termination-character-include-at-start'); - const includeAtEndheckbox = node.querySelector('.sentence-termination-character-include-at-end'); - const menuButton = node.querySelector('.sentence-termination-character-entry-button'); - - this._character1Input = character1Input; - this._character2Input = character2Input; - - const type = (character2 === null ? 'terminator' : 'quote'); - node.dataset.type = type; - - enabledToggle.checked = enabled; - typeSelect.value = type; - character1Input.value = character1; - character2Input.value = (character2 !== null ? character2 : ''); - includeAtStartCheckbox.checked = includeCharacterAtStart; - includeAtEndheckbox.checked = includeCharacterAtEnd; - - enabledToggle.dataset.setting = `${this._basePath}.enabled`; - includeAtStartCheckbox.dataset.setting = `${this._basePath}.includeCharacterAtStart`; - includeAtEndheckbox.dataset.setting = `${this._basePath}.includeCharacterAtEnd`; - - this._eventListeners.addEventListener(typeSelect, 'change', this._onTypeSelectChange.bind(this), false); - this._eventListeners.addEventListener(character1Input, 'change', this._onCharacterChange.bind(this, 1), false); - this._eventListeners.addEventListener(character2Input, 'change', this._onCharacterChange.bind(this, 2), false); - this._eventListeners.addEventListener(menuButton, 'menuClose', this._onMenuClose.bind(this), false); - } - - cleanup() { - this._eventListeners.removeAllEventListeners(); - if (this._node.parentNode !== null) { - this._node.parentNode.removeChild(this._node); - } - } - - // Private - - _onTypeSelectChange(e) { - this._setHasCharacter2(e.currentTarget.value === 'quote'); - } - - _onCharacterChange(characterNumber, e) { - const node = e.currentTarget; - if (characterNumber === 2 && this._data.character2 === null) { - node.value = ''; - } - - const value = node.value.substring(0, 1); - this._setCharacterValue(node, characterNumber, value); - } - - _onMenuClose(e) { - switch (e.detail.action) { - case 'delete': - this._delete(); - break; - } - } - - async _delete() { - this._parent.deleteEntry(this._index); - } - - async _setHasCharacter2(has) { - const okay = await this._setCharacterValue(this._character2Input, 2, has ? this._data.character1 : null); - if (okay) { - const type = (!has ? 'terminator' : 'quote'); - this._node.dataset.type = type; - } - } - - async _setCharacterValue(inputNode, characterNumber, value) { - const pathEnd = `character${characterNumber}`; - const r = await this._parent.settingsController.setProfileSetting(`${this._basePath}.${pathEnd}`, value); - const okay = !r[0].error; - if (okay) { - this._data[pathEnd] = value; - } else { - value = this._data[pathEnd]; - } - inputNode.value = (value !== null ? value : ''); - return okay; - } -} diff --git a/ext/js/settings/settings-controller.js b/ext/js/settings/settings-controller.js deleted file mode 100644 index 4a86470d..00000000 --- a/ext/js/settings/settings-controller.js +++ /dev/null @@ -1,209 +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 - */ - -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 yomichan.api.optionsGet(optionsContext); - } - - async getOptionsFull() { - return await yomichan.api.optionsGetFull(); - } - - async setAllSettings(value) { - const profileIndex = value.profileCurrent; - await yomichan.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 yomichan.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 yomichan.api.getSettings(targets); - } - - async _modifySettings(targets, extraFields) { - targets = this._setupTargets(targets, extraFields); - return await yomichan.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/js/settings/settings-display-controller.js b/ext/js/settings/settings-display-controller.js deleted file mode 100644 index 9d3e5459..00000000 --- a/ext/js/settings/settings-display-controller.js +++ /dev/null @@ -1,400 +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 - * PopupMenu - * SelectorObserver - */ - -class SettingsDisplayController { - constructor(settingsController, modalController) { - this._settingsController = settingsController; - this._modalController = modalController; - this._contentNode = null; - this._menuContainer = null; - this._onMoreToggleClickBind = null; - this._onMenuButtonClickBind = null; - } - - prepare() { - this._contentNode = document.querySelector('.content'); - this._menuContainer = document.querySelector('#popup-menus'); - - const onFabButtonClick = this._onFabButtonClick.bind(this); - for (const fabButton of document.querySelectorAll('.fab-button')) { - fabButton.addEventListener('click', onFabButtonClick, false); - } - - const onModalAction = this._onModalAction.bind(this); - for (const node of document.querySelectorAll('[data-modal-action]')) { - node.addEventListener('click', onModalAction, false); - } - - const onSelectOnClickElementClick = this._onSelectOnClickElementClick.bind(this); - for (const node of document.querySelectorAll('[data-select-on-click]')) { - node.addEventListener('click', onSelectOnClickElementClick, false); - } - - const onInputTabActionKeyDown = this._onInputTabActionKeyDown.bind(this); - for (const node of document.querySelectorAll('[data-tab-action]')) { - node.addEventListener('keydown', onInputTabActionKeyDown, false); - } - - const onSpecialUrlLinkClick = this._onSpecialUrlLinkClick.bind(this); - const onSpecialUrlLinkMouseDown = this._onSpecialUrlLinkMouseDown.bind(this); - for (const node of document.querySelectorAll('[data-special-url]')) { - node.addEventListener('click', onSpecialUrlLinkClick, false); - node.addEventListener('auxclick', onSpecialUrlLinkClick, false); - node.addEventListener('mousedown', onSpecialUrlLinkMouseDown, false); - } - - for (const node of document.querySelectorAll('.defer-load-iframe')) { - this._setupDeferLoadIframe(node); - } - - this._onMoreToggleClickBind = this._onMoreToggleClick.bind(this); - const moreSelectorObserver = new SelectorObserver({ - selector: '.more-toggle', - onAdded: this._onMoreSetup.bind(this), - onRemoved: this._onMoreCleanup.bind(this) - }); - moreSelectorObserver.observe(document.documentElement, false); - - this._onMenuButtonClickBind = this._onMenuButtonClick.bind(this); - const menuSelectorObserver = new SelectorObserver({ - selector: '[data-menu]', - onAdded: this._onMenuSetup.bind(this), - onRemoved: this._onMenuCleanup.bind(this) - }); - menuSelectorObserver.observe(document.documentElement, false); - - window.addEventListener('keydown', this._onKeyDown.bind(this), false); - window.addEventListener('popstate', this._onPopState.bind(this), false); - this._updateScrollTarget(); - } - - // Private - - _onMoreSetup(element) { - element.addEventListener('click', this._onMoreToggleClickBind, false); - return null; - } - - _onMoreCleanup(element) { - element.removeEventListener('click', this._onMoreToggleClickBind, false); - } - - _onMenuSetup(element) { - element.addEventListener('click', this._onMenuButtonClickBind, false); - return null; - } - - _onMenuCleanup(element) { - element.removeEventListener('click', this._onMenuButtonClickBind, false); - } - - _onMenuButtonClick(e) { - const element = e.currentTarget; - const {menu} = element.dataset; - this._showMenu(element, menu); - } - - _onFabButtonClick(e) { - const action = e.currentTarget.dataset.action; - switch (action) { - case 'toggle-sidebar': - document.body.classList.toggle('sidebar-visible'); - break; - case 'toggle-preview-sidebar': - document.body.classList.toggle('preview-sidebar-visible'); - break; - } - } - - _onMoreToggleClick(e) { - const container = this._getMoreContainer(e.currentTarget); - if (container === null) { return; } - - const more = container.querySelector('.more'); - if (more === null) { return; } - - const moreVisible = more.hidden; - more.hidden = !moreVisible; - for (const moreToggle of container.querySelectorAll('.more-toggle')) { - const container2 = this._getMoreContainer(moreToggle); - if (container2 === null) { continue; } - - const more2 = container2.querySelector('.more'); - if (more2 === null || more2 !== more) { continue; } - - moreToggle.dataset.expanded = `${moreVisible}`; - } - - e.preventDefault(); - return false; - } - - _onPopState() { - this._updateScrollTarget(); - } - - _onKeyDown(e) { - switch (e.code) { - case 'Escape': - if (!this._isElementAnInput(document.activeElement)) { - this._closeTopMenuOrModal(); - e.preventDefault(); - } - break; - } - } - - _onModalAction(e) { - const node = e.currentTarget; - const {modalAction} = node.dataset; - if (typeof modalAction !== 'string') { return; } - - let [action, target] = modalAction.split(','); - if (typeof target === 'undefined') { - const currentModal = node.closest('.modal'); - if (currentModal === null) { return; } - target = currentModal; - } - - const modal = this._modalController.getModal(target); - if (modal === null) { return; } - - switch (action) { - case 'show': - modal.setVisible(true); - break; - case 'hide': - modal.setVisible(false); - break; - case 'toggle': - modal.setVisible(!modal.isVisible()); - break; - } - - e.preventDefault(); - } - - _onSelectOnClickElementClick(e) { - if (e.button !== 0) { return; } - - const node = e.currentTarget; - const range = document.createRange(); - range.selectNode(node); - - const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - - e.preventDefault(); - e.stopPropagation(); - return false; - } - - _onInputTabActionKeyDown(e) { - if (e.key !== 'Tab' || e.ctrlKey) { return; } - - const node = e.currentTarget; - const {tabAction} = node.dataset; - if (typeof tabAction !== 'string') { return; } - - const args = tabAction.split(','); - switch (args[0]) { - case 'ignore': - e.preventDefault(); - break; - case 'indent': - e.preventDefault(); - this._indentInput(e, node, args); - break; - } - } - - _onSpecialUrlLinkClick(e) { - switch (e.button) { - case 0: - case 1: - e.preventDefault(); - this._createTab(e.currentTarget.dataset.specialUrl, true); - break; - } - } - - _onSpecialUrlLinkMouseDown(e) { - switch (e.button) { - case 0: - case 1: - e.preventDefault(); - break; - } - } - - async _createTab(url, useOpener) { - let openerTabId; - if (useOpener) { - try { - const tab = await new Promise((resolve, reject) => { - chrome.tabs.getCurrent((result) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(result); - } - }); - }); - openerTabId = tab.id; - } catch (e) { - // NOP - } - } - - return await new Promise((resolve, reject) => { - chrome.tabs.create({url, openerTabId}, (tab2) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(tab2); - } - }); - }); - } - - _updateScrollTarget() { - const hash = window.location.hash; - if (!hash.startsWith('#!')) { return; } - - const content = this._contentNode; - const target = document.getElementById(hash.substring(2)); - if (content === null || target === null) { return; } - - const rect1 = content.getBoundingClientRect(); - const rect2 = target.getBoundingClientRect(); - content.scrollTop += rect2.top - rect1.top; - } - - _getMoreContainer(link) { - const v = link.dataset.parentDistance; - const distance = v ? parseInt(v, 10) : 1; - if (Number.isNaN(distance)) { return null; } - - for (let i = 0; i < distance; ++i) { - link = link.parentNode; - if (link === null) { break; } - } - return link; - } - - _closeTopMenuOrModal() { - for (const popupMenu of PopupMenu.openMenus) { - popupMenu.close(); - return; - } - - const modal = this._modalController.getTopVisibleModal(); - if (modal !== null) { - modal.setVisible(false); - } - } - - _showMenu(element, menuName) { - const menu = this._settingsController.instantiateTemplate(menuName); - if (menu === null) { return; } - - this._menuContainer.appendChild(menu); - - const popupMenu = new PopupMenu(element, menu); - popupMenu.prepare(); - } - - _indentInput(e, node, args) { - let indent = '\t'; - if (args.length > 1) { - const count = parseInt(args[1], 10); - indent = (Number.isFinite(count) && count >= 0 ? ' '.repeat(count) : args[1]); - } - - const {selectionStart: start, selectionEnd: end, value} = node; - const lineStart = value.substring(0, start).lastIndexOf('\n') + 1; - const lineWhitespace = /^[ \t]*/.exec(value.substring(lineStart))[0]; - - if (e.shiftKey) { - const whitespaceLength = Math.max(0, Math.floor((lineWhitespace.length - 1) / 4) * 4); - const selectionStartNew = lineStart + whitespaceLength; - const selectionEndNew = lineStart + lineWhitespace.length; - const removeCount = selectionEndNew - selectionStartNew; - if (removeCount > 0) { - node.selectionStart = selectionStartNew; - node.selectionEnd = selectionEndNew; - document.execCommand('delete', false); - node.selectionStart = Math.max(lineStart, start - removeCount); - node.selectionEnd = Math.max(lineStart, end - removeCount); - } - } else { - if (indent.length > 0) { - const indentLength = (Math.ceil((start - lineStart + 1) / indent.length) * indent.length - (start - lineStart)); - document.execCommand('insertText', false, indent.substring(0, indentLength)); - } - } - } - - _isElementAnInput(element) { - const type = element !== null ? element.nodeName.toUpperCase() : null; - switch (type) { - case 'INPUT': - case 'TEXTAREA': - case 'SELECT': - return true; - default: - return false; - } - } - - _setupDeferLoadIframe(element) { - const parent = this._getMoreContainer(element); - if (parent === null) { return; } - - let mutationObserver = null; - const callback = () => { - if (!this._isElementVisible(element)) { return false; } - - const src = element.dataset.src; - delete element.dataset.src; - element.src = src; - - if (mutationObserver === null) { return true; } - - mutationObserver.disconnect(); - mutationObserver = null; - return true; - }; - - if (callback()) { return; } - - mutationObserver = new MutationObserver(callback); - mutationObserver.observe(parent, {attributes: true}); - } - - _isElementVisible(element) { - return (element.offsetParent !== null); - } -} diff --git a/ext/js/settings/settings-main.js b/ext/js/settings/settings-main.js deleted file mode 100644 index 273142cd..00000000 --- a/ext/js/settings/settings-main.js +++ /dev/null @@ -1,154 +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 - * AnkiController - * AnkiTemplatesController - * AudioController - * BackupController - * DictionaryController - * DictionaryImportController - * DocumentFocusController - * ExtensionKeyboardShortcutController - * GenericSettingController - * KeyboardShortcutController - * MecabController - * ModalController - * NestedPopupsController - * PermissionsToggleController - * PopupPreviewController - * PopupWindowController - * ProfileController - * ScanInputsController - * ScanInputsSimpleController - * SecondarySearchDictionaryController - * SentenceTerminationCharactersController - * SettingsController - * SettingsDisplayController - * StatusFooter - * StorageController - * TranslationTextReplacementsController - */ - -async function setupEnvironmentInfo() { - const {manifest_version: manifestVersion} = chrome.runtime.getManifest(); - const {browser, platform} = await yomichan.api.getEnvironmentInfo(); - document.documentElement.dataset.browser = browser; - document.documentElement.dataset.os = platform.os; - document.documentElement.dataset.manifestVersion = `${manifestVersion}`; -} - -async function setupGenericSettingsController(genericSettingController) { - await genericSettingController.prepare(); - await genericSettingController.refresh(); -} - -(async () => { - try { - const documentFocusController = new DocumentFocusController(); - documentFocusController.prepare(); - - const statusFooter = new StatusFooter(document.querySelector('.status-footer-container')); - statusFooter.prepare(); - - await yomichan.prepare(); - - setupEnvironmentInfo(); - - const optionsFull = await yomichan.api.optionsGetFull(); - - const preparePromises = []; - - const modalController = new ModalController(); - modalController.prepare(); - - const settingsController = new SettingsController(optionsFull.profileCurrent); - settingsController.prepare(); - - const storageController = new StorageController(); - storageController.prepare(); - - const dictionaryController = new DictionaryController(settingsController, modalController, storageController, statusFooter); - dictionaryController.prepare(); - - const dictionaryImportController = new DictionaryImportController(settingsController, modalController, storageController, statusFooter); - dictionaryImportController.prepare(); - - const genericSettingController = new GenericSettingController(settingsController); - preparePromises.push(setupGenericSettingsController(genericSettingController)); - - const audioController = new AudioController(settingsController); - audioController.prepare(); - - const profileController = new ProfileController(settingsController, modalController); - profileController.prepare(); - - const settingsBackup = new BackupController(settingsController, modalController); - settingsBackup.prepare(); - - const ankiController = new AnkiController(settingsController); - ankiController.prepare(); - - const ankiTemplatesController = new AnkiTemplatesController(settingsController, modalController, ankiController); - ankiTemplatesController.prepare(); - - const popupPreviewController = new PopupPreviewController(settingsController); - popupPreviewController.prepare(); - - const scanInputsController = new ScanInputsController(settingsController); - scanInputsController.prepare(); - - const simpleScanningInputController = new ScanInputsSimpleController(settingsController); - simpleScanningInputController.prepare(); - - const nestedPopupsController = new NestedPopupsController(settingsController); - nestedPopupsController.prepare(); - - const permissionsToggleController = new PermissionsToggleController(settingsController); - permissionsToggleController.prepare(); - - const secondarySearchDictionaryController = new SecondarySearchDictionaryController(settingsController); - secondarySearchDictionaryController.prepare(); - - const translationTextReplacementsController = new TranslationTextReplacementsController(settingsController); - translationTextReplacementsController.prepare(); - - const sentenceTerminationCharactersController = new SentenceTerminationCharactersController(settingsController); - sentenceTerminationCharactersController.prepare(); - - const keyboardShortcutController = new KeyboardShortcutController(settingsController); - keyboardShortcutController.prepare(); - - const extensionKeyboardShortcutController = new ExtensionKeyboardShortcutController(settingsController); - extensionKeyboardShortcutController.prepare(); - - const popupWindowController = new PopupWindowController(); - popupWindowController.prepare(); - - const mecabController = new MecabController(); - mecabController.prepare(); - - await Promise.all(preparePromises); - - document.documentElement.dataset.loaded = 'true'; - - const settingsDisplayController = new SettingsDisplayController(settingsController, modalController); - settingsDisplayController.prepare(); - } catch (e) { - log.error(e); - } -})(); diff --git a/ext/js/settings/status-footer.js b/ext/js/settings/status-footer.js deleted file mode 100644 index c03e6775..00000000 --- a/ext/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/js/settings/storage-controller.js b/ext/js/settings/storage-controller.js deleted file mode 100644 index c27c8690..00000000 --- a/ext/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; - } - } -} diff --git a/ext/js/settings/translation-text-replacements-controller.js b/ext/js/settings/translation-text-replacements-controller.js deleted file mode 100644 index 8d13f7e9..00000000 --- a/ext/js/settings/translation-text-replacements-controller.js +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright (C) 2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class TranslationTextReplacementsController { - constructor(settingsController) { - this._settingsController = settingsController; - this._entryContainer = null; - this._entries = []; - } - - async prepare() { - this._entryContainer = document.querySelector('#translation-text-replacement-list'); - const addButton = document.querySelector('#translation-text-replacement-add'); - - addButton.addEventListener('click', this._onAdd.bind(this), false); - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - - await this._updateOptions(); - } - - - async addGroup() { - const options = await this._settingsController.getOptions(); - const {groups} = options.translation.textReplacements; - const newEntry = this._createNewEntry(); - const target = ( - (groups.length === 0) ? - { - action: 'splice', - path: 'translation.textReplacements.groups', - start: 0, - deleteCount: 0, - items: [[newEntry]] - } : - { - action: 'splice', - path: 'translation.textReplacements.groups[0]', - start: groups[0].length, - deleteCount: 0, - items: [newEntry] - } - ); - - await this._settingsController.modifyProfileSettings([target]); - await this._updateOptions(); - } - - async deleteGroup(index) { - const options = await this._settingsController.getOptions(); - const {groups} = options.translation.textReplacements; - if (groups.length === 0) { return false; } - - const group0 = groups[0]; - if (index < 0 || index >= group0.length) { return false; } - - const target = ( - (group0.length > 1) ? - { - action: 'splice', - path: 'translation.textReplacements.groups[0]', - start: index, - deleteCount: 1, - items: [] - } : - { - action: 'splice', - path: 'translation.textReplacements.groups', - start: 0, - deleteCount: group0.length, - items: [] - } - ); - - await this._settingsController.modifyProfileSettings([target]); - await this._updateOptions(); - return true; - } - - // Private - - _onOptionsChanged({options}) { - for (const entry of this._entries) { - entry.cleanup(); - } - this._entries = []; - - const {groups} = options.translation.textReplacements; - if (groups.length > 0) { - const group0 = groups[0]; - for (let i = 0, ii = group0.length; i < ii; ++i) { - const data = group0[i]; - const node = this._settingsController.instantiateTemplate('translation-text-replacement-entry'); - this._entryContainer.appendChild(node); - const entry = new TranslationTextReplacementsEntry(this, node, i, data); - this._entries.push(entry); - entry.prepare(); - } - } - } - - _onAdd() { - this.addGroup(); - } - - async _updateOptions() { - const options = await this._settingsController.getOptions(); - this._onOptionsChanged({options}); - } - - _createNewEntry() { - return {pattern: '', ignoreCase: false, replacement: ''}; - } -} - -class TranslationTextReplacementsEntry { - constructor(parent, node, index) { - this._parent = parent; - this._node = node; - this._index = index; - this._eventListeners = new EventListenerCollection(); - this._patternInput = null; - this._replacementInput = null; - this._ignoreCaseToggle = null; - this._testInput = null; - this._testOutput = null; - } - - prepare() { - const patternInput = this._node.querySelector('.translation-text-replacement-pattern'); - const replacementInput = this._node.querySelector('.translation-text-replacement-replacement'); - const ignoreCaseToggle = this._node.querySelector('.translation-text-replacement-pattern-ignore-case'); - const menuButton = this._node.querySelector('.translation-text-replacement-button'); - const testInput = this._node.querySelector('.translation-text-replacement-test-input'); - const testOutput = this._node.querySelector('.translation-text-replacement-test-output'); - - this._patternInput = patternInput; - this._replacementInput = replacementInput; - this._ignoreCaseToggle = ignoreCaseToggle; - this._testInput = testInput; - this._testOutput = testOutput; - - const pathBase = `translation.textReplacements.groups[0][${this._index}]`; - patternInput.dataset.setting = `${pathBase}.pattern`; - replacementInput.dataset.setting = `${pathBase}.replacement`; - ignoreCaseToggle.dataset.setting = `${pathBase}.ignoreCase`; - - this._eventListeners.addEventListener(menuButton, 'menuOpen', this._onMenuOpen.bind(this), false); - this._eventListeners.addEventListener(menuButton, 'menuClose', this._onMenuClose.bind(this), false); - this._eventListeners.addEventListener(patternInput, 'settingChanged', this._onPatternChanged.bind(this), false); - this._eventListeners.addEventListener(ignoreCaseToggle, 'settingChanged', this._updateTestInput.bind(this), false); - this._eventListeners.addEventListener(replacementInput, 'settingChanged', this._updateTestInput.bind(this), false); - this._eventListeners.addEventListener(testInput, 'input', this._updateTestInput.bind(this), false); - } - - cleanup() { - this._eventListeners.removeAllEventListeners(); - if (this._node.parentNode !== null) { - this._node.parentNode.removeChild(this._node); - } - } - - // Private - - _onMenuOpen(e) { - const bodyNode = e.detail.menu.bodyNode; - const testVisible = this._isTestVisible(); - bodyNode.querySelector('[data-menu-action=showTest]').hidden = testVisible; - bodyNode.querySelector('[data-menu-action=hideTest]').hidden = !testVisible; - } - - _onMenuClose(e) { - switch (e.detail.action) { - case 'remove': - this._parent.deleteGroup(this._index); - break; - case 'showTest': - this._setTestVisible(true); - break; - case 'hideTest': - this._setTestVisible(false); - break; - } - } - - _onPatternChanged({detail: {value}}) { - this._validatePattern(value); - this._updateTestInput(); - } - - _validatePattern(value) { - let okay = false; - try { - new RegExp(value, 'g'); - okay = true; - } catch (e) { - // NOP - } - - this._patternInput.dataset.invalid = `${!okay}`; - } - - _isTestVisible() { - return this._node.dataset.testVisible === 'true'; - } - - _setTestVisible(visible) { - this._node.dataset.testVisible = `${visible}`; - this._updateTestInput(); - } - - _updateTestInput() { - if (!this._isTestVisible()) { return; } - - const ignoreCase = this._ignoreCaseToggle.checked; - const pattern = this._patternInput.value; - let regex; - try { - regex = new RegExp(pattern, ignoreCase ? 'gi' : 'g'); - } catch (e) { - return; - } - - const replacement = this._replacementInput.value; - const input = this._testInput.value; - const output = input.replace(regex, replacement); - this._testOutput.value = output; - } -} |