yomichan/ext/js/pages/settings/dictionary-import-controller.js

352 lines
13 KiB
JavaScript
Raw Normal View History

/*
* 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
* DictionaryController
* DictionaryImporterThreaded
*/
class DictionaryImportController {
constructor(settingsController, modalController, statusFooter) {
this._settingsController = settingsController;
this._modalController = modalController;
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 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);
this._triggerStorageChanged();
}
}
async _importDictionaries(files) {
if (this._modifying) { return; }
const statusFooter = this._statusFooter;
const importInfo = document.querySelector('#dictionary-import-info');
const progressSelector = '.dictionary-import-progress';
const progressContainers = document.querySelectorAll(`#dictionaries-modal ${progressSelector}`);
const progressBars = 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
};
let statusPrefix = '';
let stepIndex = -2;
const onProgress = (data) => {
const {stepIndex: stepIndex2, index, count} = data;
if (stepIndex !== stepIndex2) {
stepIndex = stepIndex2;
const labelText = `${statusPrefix} - Step ${stepIndex2 + 1} of ${data.stepCount}: ${this._getImportLabel(stepIndex2)}...`;
for (const label of infoLabels) { label.textContent = labelText; }
}
const percent = count > 0 ? (index / count * 100.0) : 0.0;
const cssString = `${percent}%`;
const statusString = `${Math.floor(percent).toFixed(0)}%`;
for (const progressBar of progressBars) { progressBar.style.width = cssString; }
for (const label of statusLabels) { label.textContent = statusString; }
switch (stepIndex2) {
case -2: // Initialize
case 5: // Data import
this._triggerStorageChanged();
break;
}
};
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})`;
}
statusPrefix = `Importing dictionary${fileCount > 1 ? ` (${i + 1} of ${fileCount})` : ''}`;
onProgress({
stepIndex: -1,
stepCount: 6,
index: 0,
count: 0
});
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);
this._triggerStorageChanged();
}
}
_getImportLabel(stepIndex) {
switch (stepIndex) {
case -1:
case 0: return 'Loading dictionary';
case 1: return 'Loading schemas';
case 2: return 'Validating data';
case 3: return 'Processing data';
case 4: return 'Post-processing data';
case 5: return 'Importing data';
default: return '';
}
}
async _importDictionary(file, importDetails, onProgress) {
const dictionaryImporter = new DictionaryImporterThreaded(onProgress);
const archiveContent = await this._readFile(file);
const {result, errors} = await dictionaryImporter.importDictionary(archiveContent, importDetails);
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);
}
}
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 = DictionaryController.createDefaultDictionarySettings(title, true);
const path1 = `profiles[${i}].options.dictionaries`;
targets.push({action: 'push', path: path1, items: [value]});
if (sequenced && options.general.mainDictionary === '') {
const path2 = `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 = `profiles[${i}].options.dictionaries`;
targets.push({action: 'set', path: path1, value: []});
const path2 = `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.readAsArrayBuffer(file);
});
}
_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 _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;
}
_triggerStorageChanged() {
yomichan.trigger('storageChanged');
}
}