aboutsummaryrefslogtreecommitdiff
path: root/ext/js/settings
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/settings')
-rw-r--r--ext/js/settings/anki-controller.js729
-rw-r--r--ext/js/settings/anki-templates-controller.js227
-rw-r--r--ext/js/settings/audio-controller.js234
-rw-r--r--ext/js/settings/backup-controller.js418
-rw-r--r--ext/js/settings/dictionary-controller.js557
-rw-r--r--ext/js/settings/dictionary-import-controller.js345
-rw-r--r--ext/js/settings/extension-keyboard-shortcuts-controller.js291
-rw-r--r--ext/js/settings/generic-setting-controller.js232
-rw-r--r--ext/js/settings/keyboard-mouse-input-field.js243
-rw-r--r--ext/js/settings/keyboard-shortcuts-controller.js367
-rw-r--r--ext/js/settings/main.js109
-rw-r--r--ext/js/settings/mecab-controller.js63
-rw-r--r--ext/js/settings/modal-controller.js60
-rw-r--r--ext/js/settings/modal-jquery.js73
-rw-r--r--ext/js/settings/modal.js80
-rw-r--r--ext/js/settings/nested-popups-controller.js71
-rw-r--r--ext/js/settings/permissions-toggle-controller.js131
-rw-r--r--ext/js/settings/pitch-accents-preview-main.js35
-rw-r--r--ext/js/settings/popup-preview-controller.js129
-rw-r--r--ext/js/settings/popup-preview-frame-main.js43
-rw-r--r--ext/js/settings/popup-preview-frame.js232
-rw-r--r--ext/js/settings/popup-window-controller.js34
-rw-r--r--ext/js/settings/profile-conditions-ui.js712
-rw-r--r--ext/js/settings/profile-controller.js697
-rw-r--r--ext/js/settings/scan-inputs-controller.js309
-rw-r--r--ext/js/settings/scan-inputs-simple-controller.js246
-rw-r--r--ext/js/settings/secondary-search-dictionary-controller.js73
-rw-r--r--ext/js/settings/sentence-termination-characters-controller.js243
-rw-r--r--ext/js/settings/settings-controller.js209
-rw-r--r--ext/js/settings/settings-display-controller.js400
-rw-r--r--ext/js/settings/settings-main.js154
-rw-r--r--ext/js/settings/status-footer.js84
-rw-r--r--ext/js/settings/storage-controller.js183
-rw-r--r--ext/js/settings/translation-text-replacements-controller.js242
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;
- }
-}