aboutsummaryrefslogtreecommitdiff
path: root/ext/js/pages/settings/dictionary-controller.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/pages/settings/dictionary-controller.js')
-rw-r--r--ext/js/pages/settings/dictionary-controller.js557
1 files changed, 557 insertions, 0 deletions
diff --git a/ext/js/pages/settings/dictionary-controller.js b/ext/js/pages/settings/dictionary-controller.js
new file mode 100644
index 00000000..e12017f2
--- /dev/null
+++ b/ext/js/pages/settings/dictionary-controller.js
@@ -0,0 +1,557 @@
+/*
+ * 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
+ };
+ }
+}