yomichan/ext/bg/js/settings/dictionary-controller.js
toasted-nutbread 5ec5d0c91c
Database change event (#826)
* Add api.triggerDatabaseUpdated and yomichan.on('databaseUpdated')

* Update databaseUpdated event usage
2020-09-13 18:43:44 -04:00

519 lines
18 KiB
JavaScript

/*
* Copyright (C) 2019-2020 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
* api
* utilBackgroundIsolate
*/
class SettingsDictionaryListUI extends EventDispatcher {
constructor(container, template, extraContainer, extraTemplate) {
super();
this.container = container;
this.template = template;
this.extraContainer = extraContainer;
this.extraTemplate = extraTemplate;
this.optionsDictionaries = null;
this.dictionaries = null;
this.dictionaryEntries = [];
this.extra = null;
document.querySelector('#dict-delete-confirm').addEventListener('click', this.onDictionaryConfirmDelete.bind(this), false);
}
setOptionsDictionaries(optionsDictionaries) {
this.optionsDictionaries = optionsDictionaries;
if (this.dictionaries !== null) {
this.setDictionaries(this.dictionaries);
}
}
setDictionaries(dictionaries) {
for (const dictionaryEntry of this.dictionaryEntries) {
dictionaryEntry.cleanup();
}
this.dictionaryEntries = [];
this.dictionaries = toIterable(dictionaries);
if (this.optionsDictionaries === null) {
return;
}
let changed = false;
for (const dictionaryInfo of this.dictionaries) {
if (this.createEntry(dictionaryInfo)) {
changed = true;
}
}
this.updateDictionaryOrder();
const titles = this.dictionaryEntries.map((e) => e.dictionaryInfo.title);
const removeKeys = Object.keys(this.optionsDictionaries).filter((key) => titles.indexOf(key) < 0);
if (removeKeys.length > 0) {
for (const key of toIterable(removeKeys)) {
delete this.optionsDictionaries[key];
}
changed = true;
}
if (changed) {
this.save();
}
}
createEntry(dictionaryInfo) {
const title = dictionaryInfo.title;
let changed = false;
let optionsDictionary;
const optionsDictionaries = this.optionsDictionaries;
if (hasOwn(optionsDictionaries, title)) {
optionsDictionary = optionsDictionaries[title];
} else {
optionsDictionary = SettingsDictionaryListUI.createDictionaryOptions();
optionsDictionaries[title] = optionsDictionary;
changed = true;
}
const content = document.importNode(this.template.content, true).firstChild;
this.dictionaryEntries.push(new SettingsDictionaryEntryUI(this, dictionaryInfo, content, optionsDictionary));
return changed;
}
static createDictionaryOptions() {
return utilBackgroundIsolate({
priority: 0,
enabled: false,
allowSecondarySearches: false
});
}
createExtra(totalCounts, remainders, totalRemainder) {
const content = document.importNode(this.extraTemplate.content, true).firstChild;
this.extraContainer.appendChild(content);
return new SettingsDictionaryExtraUI(this, totalCounts, remainders, totalRemainder, content);
}
setCounts(dictionaryCounts, totalCounts) {
const remainders = Object.assign({}, totalCounts);
const keys = Object.keys(remainders);
for (let i = 0, ii = Math.min(this.dictionaryEntries.length, dictionaryCounts.length); i < ii; ++i) {
const counts = dictionaryCounts[i];
this.dictionaryEntries[i].setCounts(counts);
for (const key of keys) {
remainders[key] -= counts[key];
}
}
let totalRemainder = 0;
for (const key of keys) {
totalRemainder += remainders[key];
}
if (this.extra !== null) {
this.extra.cleanup();
this.extra = null;
}
if (totalRemainder > 0) {
this.extra = this.createExtra(totalCounts, remainders, totalRemainder);
}
}
updateDictionaryOrder() {
const sortInfo = this.dictionaryEntries.map((e, i) => [e, i]);
sortInfo.sort((a, b) => {
const i = b[0].optionsDictionary.priority - a[0].optionsDictionary.priority;
return (i !== 0 ? i : a[1] - b[1]);
});
for (const [e] of sortInfo) {
this.container.appendChild(e.content);
}
}
save() {
// Overwrite
}
preventPageExit() {
// Overwrite
return {end: () => {}};
}
onDictionaryConfirmDelete(e) {
e.preventDefault();
const n = document.querySelector('#dict-delete-modal');
const title = n.dataset.dict;
delete n.dataset.dict;
$(n).modal('hide');
const index = this.dictionaryEntries.findIndex((entry) => entry.dictionaryInfo.title === title);
if (index >= 0) {
this.dictionaryEntries[index].deleteDictionary();
}
}
}
class SettingsDictionaryEntryUI {
constructor(parent, dictionaryInfo, content, optionsDictionary) {
this.parent = parent;
this.dictionaryInfo = dictionaryInfo;
this.optionsDictionary = optionsDictionary;
this.counts = null;
this.eventListeners = new EventListenerCollection();
this.isDeleting = false;
this.content = content;
this.enabledCheckbox = this.content.querySelector('.dict-enabled');
this.allowSecondarySearchesCheckbox = this.content.querySelector('.dict-allow-secondary-searches');
this.priorityInput = this.content.querySelector('.dict-priority');
this.deleteButton = this.content.querySelector('.dict-delete-button');
this.detailsToggleLink = this.content.querySelector('.dict-details-toggle-link');
this.detailsContainer = this.content.querySelector('.dict-details');
this.detailsTable = this.content.querySelector('.dict-details-table');
if (this.dictionaryInfo.version < 3) {
this.content.querySelector('.dict-outdated').hidden = false;
}
this.setupDetails(dictionaryInfo);
this.content.querySelector('.dict-title').textContent = this.dictionaryInfo.title;
this.content.querySelector('.dict-revision').textContent = `rev.${this.dictionaryInfo.revision}`;
this.content.querySelector('.dict-prefix-wildcard-searches-supported').checked = !!this.dictionaryInfo.prefixWildcardsSupported;
this.applyValues();
this.eventListeners.addEventListener(this.enabledCheckbox, 'change', this.onEnabledChanged.bind(this), false);
this.eventListeners.addEventListener(this.allowSecondarySearchesCheckbox, 'change', this.onAllowSecondarySearchesChanged.bind(this), false);
this.eventListeners.addEventListener(this.priorityInput, 'change', this.onPriorityChanged.bind(this), false);
this.eventListeners.addEventListener(this.deleteButton, 'click', this.onDeleteButtonClicked.bind(this), false);
this.eventListeners.addEventListener(this.detailsToggleLink, 'click', this.onDetailsToggleLinkClicked.bind(this), false);
}
setupDetails(dictionaryInfo) {
const targets = [
['Author', 'author'],
['URL', 'url'],
['Description', 'description'],
['Attribution', 'attribution']
];
let count = 0;
for (const [label, key] of targets) {
const info = dictionaryInfo[key];
if (typeof info !== 'string') { continue; }
const n1 = document.createElement('div');
n1.className = 'dict-details-entry';
n1.dataset.type = key;
const n2 = document.createElement('span');
n2.className = 'dict-details-entry-label';
n2.textContent = `${label}:`;
n1.appendChild(n2);
const n3 = document.createElement('span');
n3.className = 'dict-details-entry-info';
n3.textContent = info;
n1.appendChild(n3);
this.detailsTable.appendChild(n1);
++count;
}
if (count === 0) {
this.detailsContainer.hidden = true;
this.detailsToggleLink.hidden = true;
}
}
cleanup() {
if (this.content !== null) {
if (this.content.parentNode !== null) {
this.content.parentNode.removeChild(this.content);
}
this.content = null;
}
this.dictionaryInfo = null;
this.eventListeners.removeAllEventListeners();
}
setCounts(counts) {
this.counts = counts;
const node = this.content.querySelector('.dict-counts');
node.textContent = JSON.stringify({
info: this.dictionaryInfo,
counts
}, null, 4);
node.removeAttribute('hidden');
}
save() {
this.parent.save();
}
applyValues() {
this.enabledCheckbox.checked = this.optionsDictionary.enabled;
this.allowSecondarySearchesCheckbox.checked = this.optionsDictionary.allowSecondarySearches;
this.priorityInput.value = `${this.optionsDictionary.priority}`;
}
async deleteDictionary() {
if (this.isDeleting) {
return;
}
const progress = this.content.querySelector('.progress');
progress.hidden = false;
const progressBar = this.content.querySelector('.progress-bar');
this.isDeleting = true;
const prevention = this.parent.preventPageExit();
try {
const onProgress = ({processed, count, storeCount, storesProcesed}) => {
let percent = 0.0;
if (count > 0 && storesProcesed > 0) {
percent = (processed / count) * (storesProcesed / storeCount) * 100.0;
}
progressBar.style.width = `${percent}%`;
};
await api.deleteDictionary(this.dictionaryInfo.title, onProgress);
} catch (e) {
this.dictionaryErrorsShow([e]);
} finally {
prevention.end();
this.isDeleting = false;
progress.hidden = true;
}
}
onEnabledChanged(e) {
this.optionsDictionary.enabled = !!e.target.checked;
this.save();
}
onAllowSecondarySearchesChanged(e) {
this.optionsDictionary.allowSecondarySearches = !!e.target.checked;
this.save();
}
onPriorityChanged(e) {
let value = Number.parseFloat(e.target.value);
if (Number.isNaN(value)) {
value = this.optionsDictionary.priority;
} else {
this.optionsDictionary.priority = value;
this.save();
}
e.target.value = `${value}`;
this.parent.updateDictionaryOrder();
}
onDeleteButtonClicked(e) {
e.preventDefault();
if (this.isDeleting) {
return;
}
const title = this.dictionaryInfo.title;
const n = document.querySelector('#dict-delete-modal');
n.dataset.dict = title;
document.querySelector('#dict-remove-modal-dict-name').textContent = title;
$(n).modal('show');
}
onDetailsToggleLinkClicked(e) {
e.preventDefault();
this.detailsContainer.hidden = !this.detailsContainer.hidden;
}
}
class SettingsDictionaryExtraUI {
constructor(parent, totalCounts, remainders, totalRemainder, content) {
this.parent = parent;
this.content = content;
this.content.querySelector('.dict-total-count').textContent = `${totalRemainder} item${totalRemainder !== 1 ? 's' : ''}`;
const node = this.content.querySelector('.dict-counts');
node.textContent = JSON.stringify({
counts: totalCounts,
remainders: remainders
}, null, 4);
node.removeAttribute('hidden');
}
cleanup() {
if (this.content !== null) {
if (this.content.parentNode !== null) {
this.content.parentNode.removeChild(this.content);
}
this.content = null;
}
}
}
class DictionaryController {
constructor(settingsController) {
this._settingsController = settingsController;
this._dictionaryUI = null;
}
async prepare() {
this._dictionaryUI = new SettingsDictionaryListUI(
document.querySelector('#dict-groups'),
document.querySelector('#dict-template'),
document.querySelector('#dict-groups-extra'),
document.querySelector('#dict-extra-template')
);
this._dictionaryUI.save = () => this._settingsController.save();
this._dictionaryUI.preventPageExit = this._preventPageExit.bind(this);
document.querySelector('#dict-main').addEventListener('change', this._onDictionaryMainChanged.bind(this), false);
document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', this._onDatabaseEnablePrefixWildcardSearchesChanged.bind(this), false);
this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
yomichan.on('databaseUpdated', this._onDatabaseUpdated.bind(this));
await this._onOptionsChanged();
await this._onDatabaseUpdated();
}
// Private
async _onOptionsChanged() {
const options = await this._settingsController.getOptionsMutable();
this._dictionaryUI.setOptionsDictionaries(options.dictionaries);
const optionsFull = await this._settingsController.getOptionsFull();
document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported;
await this._updateMainDictionarySelectValue();
}
_updateMainDictionarySelectOptions(dictionaries) {
const select = document.querySelector('#dict-main');
select.textContent = ''; // Empty
let option = document.createElement('option');
option.className = 'text-muted';
option.value = '';
option.textContent = 'Not selected';
select.appendChild(option);
for (const {title, sequenced} of toIterable(dictionaries)) {
if (!sequenced) { continue; }
option = document.createElement('option');
option.value = title;
option.textContent = title;
select.appendChild(option);
}
}
async _updateMainDictionarySelectValue() {
const options = await this._settingsController.getOptions();
const value = options.general.mainDictionary;
const select = document.querySelector('#dict-main');
let selectValue = null;
for (const child of select.children) {
if (child.nodeName.toUpperCase() === 'OPTION' && child.value === value) {
selectValue = value;
break;
}
}
let missingNodeOption = select.querySelector('option[data-not-installed=true]');
if (selectValue === null) {
if (missingNodeOption === null) {
missingNodeOption = document.createElement('option');
missingNodeOption.className = 'text-muted';
missingNodeOption.value = value;
missingNodeOption.textContent = `${value} (Not installed)`;
missingNodeOption.dataset.notInstalled = 'true';
select.appendChild(missingNodeOption);
}
} else {
if (missingNodeOption !== null) {
missingNodeOption.parentNode.removeChild(missingNodeOption);
}
}
select.value = value;
}
async _onDatabaseUpdated() {
try {
const dictionaries = await api.getDictionaryInfo();
this._dictionaryUI.setDictionaries(dictionaries);
document.querySelector('#dict-warning').hidden = (dictionaries.length > 0);
this._updateMainDictionarySelectOptions(dictionaries);
await this._updateMainDictionarySelectValue();
const {counts, total} = await api.getDictionaryCounts(dictionaries.map((v) => v.title), true);
this._dictionaryUI.setCounts(counts, total);
} catch (e) {
yomichan.logError(e);
}
}
async _onDictionaryMainChanged(e) {
const select = e.target;
const value = select.value;
const missingNodeOption = select.querySelector('option[data-not-installed=true]');
if (missingNodeOption !== null && missingNodeOption.value !== value) {
missingNodeOption.parentNode.removeChild(missingNodeOption);
}
const options = await this._settingsController.getOptionsMutable();
options.general.mainDictionary = value;
await this._settingsController.save();
}
async _onDatabaseEnablePrefixWildcardSearchesChanged(e) {
const optionsFull = await this._settingsController.getOptionsFullMutable();
const v = !!e.target.checked;
if (optionsFull.global.database.prefixWildcardsSupported === v) { return; }
optionsFull.global.database.prefixWildcardsSupported = !!e.target.checked;
await this._settingsController.save();
}
_preventPageExit() {
return this._settingsController.preventPageExit();
}
}