Convert dictionaries.js and storage.js to classes (#570)

* Convert dictionaries.js to a class

* Remove storage spinner

* Convert storage.js to a class

* Move dataset assignments into main.js
This commit is contained in:
toasted-nutbread 2020-05-29 20:25:22 -04:00 committed by GitHub
parent c62f980f37
commit 418e8a57bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 432 additions and 435 deletions

View File

@ -18,7 +18,7 @@
#anki-spinner,
#dict-spinner, #dict-import-progress,
.storage-hidden, #storage-spinner {
.storage-hidden {
display: none;
}

View File

@ -22,14 +22,9 @@
* getOptionsFullMutable
* getOptionsMutable
* settingsSaveOptions
* storageEstimate
* storageUpdateStats
* utilBackgroundIsolate
*/
let dictionaryUI = null;
class SettingsDictionaryListUI {
constructor(container, template, extraContainer, extraTemplate) {
this.container = container;
@ -308,13 +303,13 @@ class SettingsDictionaryEntryUI {
await api.deleteDictionary(this.dictionaryInfo.title, onProgress);
} catch (e) {
dictionaryErrorsShow([e]);
this.dictionaryErrorsShow([e]);
} finally {
prevention.end();
this.isDeleting = false;
progress.hidden = true;
onDatabaseUpdated();
this.onDatabaseUpdated();
}
}
@ -388,59 +383,63 @@ class SettingsDictionaryExtraUI {
}
}
class DictionaryController {
constructor(storageController) {
this._storageController = storageController;
this._dictionaryUI = null;
this._dictionaryErrorToStringOverrides = [
[
'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.'
],
[
'BulkError',
'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.'
]
];
}
async function dictSettingsInitialize() {
dictionaryUI = new SettingsDictionaryListUI(
async prepare() {
this._dictionaryUI = new SettingsDictionaryListUI(
document.querySelector('#dict-groups'),
document.querySelector('#dict-template'),
document.querySelector('#dict-groups-extra'),
document.querySelector('#dict-extra-template')
);
dictionaryUI.save = settingsSaveOptions;
this._dictionaryUI.save = settingsSaveOptions;
document.querySelector('#dict-purge-button').addEventListener('click', onDictionaryPurgeButtonClick, false);
document.querySelector('#dict-purge-confirm').addEventListener('click', onDictionaryPurge, false);
document.querySelector('#dict-file-button').addEventListener('click', onDictionaryImportButtonClick, false);
document.querySelector('#dict-file').addEventListener('change', onDictionaryImport, false);
document.querySelector('#dict-main').addEventListener('change', onDictionaryMainChanged, false);
document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', onDatabaseEnablePrefixWildcardSearchesChanged, false);
document.querySelector('#dict-purge-button').addEventListener('click', this._onPurgeButtonClick.bind(this), false);
document.querySelector('#dict-purge-confirm').addEventListener('click', this._onPurgeConfirmButtonClick.bind(this), false);
document.querySelector('#dict-file-button').addEventListener('click', this._onImportButtonClick.bind(this), false);
document.querySelector('#dict-file').addEventListener('change', this._onImportFileChange.bind(this), false);
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);
await onDictionaryOptionsChanged();
await onDatabaseUpdated();
await this.optionsChanged();
await this._onDatabaseUpdated();
}
async function onDictionaryOptionsChanged() {
if (dictionaryUI === null) { return; }
async optionsChanged() {
if (this._dictionaryUI === null) { return; }
const optionsContext = getOptionsContext();
const options = await getOptionsMutable(optionsContext);
dictionaryUI.setOptionsDictionaries(options.dictionaries);
this._dictionaryUI.setOptionsDictionaries(options.dictionaries);
const optionsFull = await api.optionsGetFull();
document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported;
await updateMainDictionarySelectValue();
await this._updateMainDictionarySelectValue();
}
async function onDatabaseUpdated() {
try {
const dictionaries = await api.getDictionaryInfo();
dictionaryUI.setDictionaries(dictionaries);
// Private
document.querySelector('#dict-warning').hidden = (dictionaries.length > 0);
updateMainDictionarySelectOptions(dictionaries);
await updateMainDictionarySelectValue();
const {counts, total} = await api.getDictionaryCounts(dictionaries.map((v) => v.title), true);
dictionaryUI.setCounts(counts, total);
} catch (e) {
dictionaryErrorsShow([e]);
}
}
function updateMainDictionarySelectOptions(dictionaries) {
_updateMainDictionarySelectOptions(dictionaries) {
const select = document.querySelector('#dict-main');
select.textContent = ''; // Empty
@ -460,7 +459,7 @@ function updateMainDictionarySelectOptions(dictionaries) {
}
}
async function updateMainDictionarySelectValue() {
async _updateMainDictionarySelectValue() {
const optionsContext = getOptionsContext();
const options = await api.optionsGet(optionsContext);
@ -494,30 +493,14 @@ async function updateMainDictionarySelectValue() {
select.value = value;
}
async function 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 optionsContext = getOptionsContext();
const options = await getOptionsMutable(optionsContext);
options.general.mainDictionary = value;
await settingsSaveOptions();
}
function dictionaryErrorToString(error) {
_dictionaryErrorToString(error) {
if (error.toString) {
error = error.toString();
} else {
error = `${error}`;
}
for (const [match, subst] of dictionaryErrorToString.overrides) {
for (const [match, subst] of this._dictionaryErrorToStringOverrides) {
if (error.includes(match)) {
error = subst;
break;
@ -526,22 +509,8 @@ function dictionaryErrorToString(error) {
return error;
}
dictionaryErrorToString.overrides = [
[
'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.'
],
[
'BulkError',
'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.'
]
];
function dictionaryErrorsShow(errors) {
_dictionaryErrorsShow(errors) {
const dialog = document.querySelector('#dict-error');
dialog.textContent = '';
@ -549,7 +518,7 @@ function dictionaryErrorsShow(errors) {
const uniqueErrors = new Map();
for (let e of errors) {
yomichan.logError(e);
e = dictionaryErrorToString(e);
e = this._dictionaryErrorToString(e);
let count = uniqueErrors.get(e);
if (typeof count === 'undefined') {
count = 0;
@ -576,8 +545,7 @@ function dictionaryErrorsShow(errors) {
}
}
function dictionarySpinnerShow(show) {
_dictionarySpinnerShow(show) {
const spinner = $('#dict-spinner');
if (show) {
spinner.show();
@ -586,17 +554,58 @@ function dictionarySpinnerShow(show) {
}
}
function onDictionaryImportButtonClick() {
_dictReadFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsBinaryString(file);
});
}
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) {
this._dictionaryErrorsShow([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 optionsContext = getOptionsContext();
const options = await getOptionsMutable(optionsContext);
options.general.mainDictionary = value;
await settingsSaveOptions();
}
_onImportButtonClick() {
const dictFile = document.querySelector('#dict-file');
dictFile.click();
}
function onDictionaryPurgeButtonClick(e) {
_onPurgeButtonClick(e) {
e.preventDefault();
$('#dict-purge-modal').modal('show');
}
async function onDictionaryPurge(e) {
async _onPurgeConfirmButtonClick(e) {
e.preventDefault();
$('#dict-purge-modal').modal('hide');
@ -609,8 +618,8 @@ async function onDictionaryPurge(e) {
try {
prevention.start();
dictionaryErrorsShow(null);
dictionarySpinnerShow(true);
this._dictionaryErrorsShow(null);
this._dictionarySpinnerShow(true);
await api.purgeDatabase();
for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
@ -619,24 +628,22 @@ async function onDictionaryPurge(e) {
}
await settingsSaveOptions();
onDatabaseUpdated();
this._onDatabaseUpdated();
} catch (err) {
dictionaryErrorsShow([err]);
this._dictionaryErrorsShow([err]);
} finally {
prevention.end();
dictionarySpinnerShow(false);
this._dictionarySpinnerShow(false);
dictControls.show();
dictProgress.hidden = true;
if (storageEstimate.mostRecent !== null) {
storageUpdateStats();
}
this._storageController.updateStats();
}
}
async function onDictionaryImport(e) {
async _onImportFileChange(e) {
const files = [...e.target.files];
e.target.value = null;
@ -649,15 +656,13 @@ async function onDictionaryImport(e) {
try {
prevention.start();
dictionaryErrorsShow(null);
dictionarySpinnerShow(true);
this._dictionaryErrorsShow(null);
this._dictionarySpinnerShow(true);
const setProgress = (percent) => dictProgress.find('.progress-bar').css('width', `${percent}%`);
const updateProgress = (total, current) => {
setProgress(current / total * 100.0);
if (storageEstimate.mostRecent !== null && !storageUpdateStats.isUpdating) {
storageUpdateStats();
}
this._storageController.updateStats();
};
const optionsFull = await api.optionsGetFull();
@ -673,7 +678,7 @@ async function onDictionaryImport(e) {
dictImportInfo.textContent = `(${i + 1} of ${ii})`;
}
const archiveContent = await dictReadFile(files[i]);
const archiveContent = await this._dictReadFile(files[i]);
const {result, errors} = await api.importDictionaryArchive(archiveContent, importDetails, updateProgress);
for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions();
@ -689,16 +694,16 @@ async function onDictionaryImport(e) {
if (errors.length > 0) {
const errors2 = errors.map((error) => jsonToError(error));
errors2.push(`Dictionary may not have been imported properly: ${errors2.length} error${errors2.length === 1 ? '' : 's'} reported.`);
dictionaryErrorsShow(errors2);
this._dictionaryErrorsShow(errors2);
}
onDatabaseUpdated();
this._onDatabaseUpdated();
}
} catch (err) {
dictionaryErrorsShow([err]);
this._dictionaryErrorsShow([err]);
} finally {
prevention.end();
dictionarySpinnerShow(false);
this._dictionarySpinnerShow(false);
dictImportInfo.hidden = false;
dictImportInfo.textContent = '';
@ -708,20 +713,11 @@ async function onDictionaryImport(e) {
}
}
function dictReadFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsBinaryString(file);
});
}
async function onDatabaseEnablePrefixWildcardSearchesChanged(e) {
async _onDatabaseEnablePrefixWildcardSearchesChanged(e) {
const optionsFull = await getOptionsFullMutable();
const v = !!e.target.checked;
if (optionsFull.global.database.prefixWildcardsSupported === v) { return; }
optionsFull.global.database.prefixWildcardsSupported = !!e.target.checked;
await settingsSaveOptions();
}
}

View File

@ -19,14 +19,13 @@
* AnkiController
* AnkiTemplatesController
* AudioController
* DictionaryController
* ProfileController
* SettingsBackup
* SettingsController
* StorageController
* api
* appearanceInitialize
* dictSettingsInitialize
* onDictionaryOptionsChanged
* storageInfoInitialize
* utilBackend
* utilBackgroundIsolate
*/
@ -270,7 +269,9 @@ async function onOptionsUpdated({source}) {
if (ankiTemplatesController !== null) {
ankiTemplatesController.updateValue();
}
onDictionaryOptionsChanged();
if (dictionaryController !== null) {
dictionaryController.optionsChanged();
}
if (ankiController !== null) {
ankiController.optionsChanged();
}
@ -304,8 +305,15 @@ async function settingsPopulateModifierKeys() {
}
}
async function setupEnvironmentInfo() {
const {browser, platform} = await api.getEnvironmentInfo();
document.documentElement.dataset.browser = browser;
document.documentElement.dataset.operatingSystem = platform.os;
}
let ankiController = null;
let ankiTemplatesController = null;
let dictionaryController = null;
async function onReady() {
api.forwardLogsToBackend();
@ -314,22 +322,25 @@ async function onReady() {
const settingsController = new SettingsController();
settingsController.prepare();
setupEnvironmentInfo();
showExtensionInformation();
const storageController = new StorageController();
storageController.prepare();
await settingsPopulateModifierKeys();
formSetupEventListeners();
appearanceInitialize();
new AudioController().prepare();
await (new ProfileController()).prepare();
await dictSettingsInitialize();
dictionaryController = new DictionaryController(storageController);
dictionaryController.prepare();
ankiController = new AnkiController();
ankiController.prepare();
ankiTemplatesController = new AnkiTemplatesController(ankiController);
ankiTemplatesController.prepare();
new SettingsBackup().prepare();
storageInfoInitialize();
yomichan.on('optionsUpdated', onOptionsUpdated);
}

View File

@ -15,94 +15,49 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* global
* api
*/
function storageBytesToLabeledString(size) {
const base = 1000;
const labels = [' bytes', 'KB', 'MB', 'GB'];
let labelIndex = 0;
while (size >= base) {
size /= base;
++labelIndex;
}
const label = labelIndex === 0 ? `${size}` : size.toFixed(1);
return `${label}${labels[labelIndex]}`;
class StorageController {
constructor() {
this._mostRecentStorageEstimate = null;
this._storageEstimateFailed = false;
this._isUpdating = false;
}
async function storageEstimate() {
prepare() {
this._preparePersistentStorage();
this.updateStats();
document.querySelector('#storage-refresh').addEventListener('click', this.updateStats.bind(this), false);
}
async updateStats() {
try {
return (storageEstimate.mostRecent = await navigator.storage.estimate());
} catch (e) {
// NOP
}
return null;
}
storageEstimate.mostRecent = null;
this._isUpdating = true;
async function isStoragePeristent() {
try {
return await navigator.storage.persisted();
} catch (e) {
// NOP
}
return false;
}
async function storageInfoInitialize() {
storagePersistInitialize();
const {browser, platform} = await api.getEnvironmentInfo();
document.documentElement.dataset.browser = browser;
document.documentElement.dataset.operatingSystem = platform.os;
await storageShowInfo();
document.querySelector('#storage-refresh').addEventListener('click', storageShowInfo, false);
}
async function storageUpdateStats() {
storageUpdateStats.isUpdating = true;
const estimate = await storageEstimate();
const estimate = await this._storageEstimate();
const valid = (estimate !== null);
if (valid) {
// Firefox reports usage as 0 when persistent storage is enabled.
const finite = (estimate.usage > 0 || !(await isStoragePeristent()));
const finite = (estimate.usage > 0 || !(await this._isStoragePeristent()));
if (finite) {
document.querySelector('#storage-usage').textContent = storageBytesToLabeledString(estimate.usage);
document.querySelector('#storage-quota').textContent = storageBytesToLabeledString(estimate.quota);
document.querySelector('#storage-usage').textContent = this._bytesToLabeledString(estimate.usage);
document.querySelector('#storage-quota').textContent = this._bytesToLabeledString(estimate.quota);
}
document.querySelector('#storage-use-finite').classList.toggle('storage-hidden', !finite);
document.querySelector('#storage-use-infinite').classList.toggle('storage-hidden', finite);
}
storageUpdateStats.isUpdating = false;
return valid;
}
storageUpdateStats.isUpdating = false;
async function storageShowInfo() {
storageSpinnerShow(true);
const valid = await storageUpdateStats();
document.querySelector('#storage-use').classList.toggle('storage-hidden', !valid);
document.querySelector('#storage-error').classList.toggle('storage-hidden', valid);
storageSpinnerShow(false);
}
function storageSpinnerShow(show) {
const spinner = $('#storage-spinner');
if (show) {
spinner.show();
} else {
spinner.hide();
return valid;
} finally {
this._isUpdating = false;
}
}
async function storagePersistInitialize() {
// Private
async _preparePersistentStorage() {
if (!(navigator.storage && navigator.storage.persist)) {
// Not supported
return;
@ -115,7 +70,7 @@ async function storagePersistInitialize() {
info.classList.remove('storage-hidden');
button.classList.remove('storage-hidden');
let persisted = await isStoragePeristent();
let persisted = await this._isStoragePeristent();
checkbox.checked = persisted;
button.addEventListener('click', async () => {
@ -132,9 +87,45 @@ async function storagePersistInitialize() {
if (result) {
persisted = true;
checkbox.checked = true;
storageShowInfo();
this.updateStats();
} else {
$('.storage-persist-fail-warning').removeClass('storage-hidden');
document.querySelector('.storage-persist-fail-warning').classList.remove('storage-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'];
let labelIndex = 0;
while (size >= base) {
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;
}
}

View File

@ -711,7 +711,6 @@
<div id="storage-info">
<div>
<img src="/mixed/img/spinner.gif" class="pull-right" id="storage-spinner" />
<h3>Storage</h3>
</div>