aboutsummaryrefslogtreecommitdiff
path: root/ext/bg/js/settings
diff options
context:
space:
mode:
Diffstat (limited to 'ext/bg/js/settings')
-rw-r--r--ext/bg/js/settings/anki-templates.js226
-rw-r--r--ext/bg/js/settings/anki.js454
-rw-r--r--ext/bg/js/settings/audio-ui.js139
-rw-r--r--ext/bg/js/settings/audio.js273
-rw-r--r--ext/bg/js/settings/backup.js571
-rw-r--r--ext/bg/js/settings/clipboard-popups-controller.js51
-rw-r--r--ext/bg/js/settings/dictionaries.js560
-rw-r--r--ext/bg/js/settings/generic-setting-controller.js132
-rw-r--r--ext/bg/js/settings/main.js331
-rw-r--r--ext/bg/js/settings/popup-preview-frame-main.js22
-rw-r--r--ext/bg/js/settings/popup-preview-frame.js161
-rw-r--r--ext/bg/js/settings/popup-preview.js141
-rw-r--r--ext/bg/js/settings/profiles.js419
-rw-r--r--ext/bg/js/settings/settings-controller.js150
-rw-r--r--ext/bg/js/settings/storage.js199
15 files changed, 1964 insertions, 1865 deletions
diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js
index d5b6e677..88d4fe04 100644
--- a/ext/bg/js/settings/anki-templates.js
+++ b/ext/bg/js/settings/anki-templates.js
@@ -17,144 +17,146 @@
/* global
* AnkiNoteBuilder
- * ankiGetFieldMarkers
- * ankiGetFieldMarkersHtml
- * apiGetDefaultAnkiFieldTemplates
- * apiOptionsGet
- * apiTemplateRender
- * apiTermsFind
- * getOptionsContext
- * getOptionsMutable
- * settingsSaveOptions
+ * api
*/
-function onAnkiFieldTemplatesReset(e) {
- e.preventDefault();
- $('#field-template-reset-modal').modal('show');
-}
+class AnkiTemplatesController {
+ constructor(settingsController, ankiController) {
+ this._settingsController = settingsController;
+ this._ankiController = ankiController;
+ this._cachedDefinitionValue = null;
+ this._cachedDefinitionText = null;
+ this._defaultFieldTemplates = null;
+ }
-async function onAnkiFieldTemplatesResetConfirm(e) {
- e.preventDefault();
+ async prepare() {
+ this._defaultFieldTemplates = await api.getDefaultAnkiFieldTemplates();
- $('#field-template-reset-modal').modal('hide');
+ const markers = new Set([
+ ...this._ankiController.getFieldMarkers('terms'),
+ ...this._ankiController.getFieldMarkers('kanji')
+ ]);
+ const fragment = this._ankiController.getFieldMarkersHtml(markers);
- const value = await apiGetDefaultAnkiFieldTemplates();
+ const list = document.querySelector('#field-templates-list');
+ list.appendChild(fragment);
+ for (const node of list.querySelectorAll('.marker-link')) {
+ node.addEventListener('click', this._onMarkerClicked.bind(this), false);
+ }
- const element = document.querySelector('#field-templates');
- element.value = value;
- element.dispatchEvent(new Event('change'));
-}
+ document.querySelector('#field-templates').addEventListener('change', this._onChanged.bind(this), false);
+ document.querySelector('#field-template-render').addEventListener('click', this._onRender.bind(this), false);
+ document.querySelector('#field-templates-reset').addEventListener('click', this._onReset.bind(this), false);
+ document.querySelector('#field-templates-reset-confirm').addEventListener('click', this._onResetConfirm.bind(this), false);
-function ankiTemplatesInitialize() {
- const markers = new Set(ankiGetFieldMarkers('terms').concat(ankiGetFieldMarkers('kanji')));
- const fragment = ankiGetFieldMarkersHtml(markers);
+ this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
- const list = document.querySelector('#field-templates-list');
- list.appendChild(fragment);
- for (const node of list.querySelectorAll('.marker-link')) {
- node.addEventListener('click', onAnkiTemplateMarkerClicked, false);
+ const options = await this._settingsController.getOptions();
+ this._onOptionsChanged({options});
}
- $('#field-templates').on('change', onAnkiFieldTemplatesChanged);
- $('#field-template-render').on('click', onAnkiTemplateRender);
- $('#field-templates-reset').on('click', onAnkiFieldTemplatesReset);
- $('#field-templates-reset-confirm').on('click', onAnkiFieldTemplatesResetConfirm);
+ // Private
- ankiTemplatesUpdateValue();
-}
+ _onOptionsChanged({options}) {
+ let templates = options.anki.fieldTemplates;
+ if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; }
+ document.querySelector('#field-templates').value = templates;
-async function ankiTemplatesUpdateValue() {
- const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
- let templates = options.anki.fieldTemplates;
- if (typeof templates !== 'string') { templates = await apiGetDefaultAnkiFieldTemplates(); }
- $('#field-templates').val(templates);
+ this._onValidateCompile();
+ }
- onAnkiTemplatesValidateCompile();
-}
+ _onReset(e) {
+ e.preventDefault();
+ $('#field-template-reset-modal').modal('show');
+ }
-const ankiTemplatesValidateGetDefinition = (() => {
- let cachedValue = null;
- let cachedText = null;
+ _onResetConfirm(e) {
+ e.preventDefault();
- return async (text, optionsContext) => {
- if (cachedText !== text) {
- const {definitions} = await apiTermsFind(text, {}, optionsContext);
- if (definitions.length === 0) { return null; }
+ $('#field-template-reset-modal').modal('hide');
- cachedValue = definitions[0];
- cachedText = text;
- }
- return cachedValue;
- };
-})();
-
-async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, invalidateInput) {
- const text = document.querySelector('#field-templates-preview-text').value || '';
- const exceptions = [];
- let result = `No definition found for ${text}`;
- try {
- const optionsContext = getOptionsContext();
- const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext);
- if (definition !== null) {
- const options = await apiOptionsGet(optionsContext);
- const context = {
- document: {
- title: document.title
- }
- };
- let templates = options.anki.fieldTemplates;
- if (typeof templates !== 'string') { templates = await apiGetDefaultAnkiFieldTemplates(); }
- const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: apiTemplateRender});
- result = await ankiNoteBuilder.formatField(field, definition, mode, context, options, templates, exceptions);
+ const value = this._defaultFieldTemplates;
+
+ const element = document.querySelector('#field-templates');
+ element.value = value;
+ element.dispatchEvent(new Event('change'));
+ }
+
+ async _onChanged(e) {
+ // Get value
+ let templates = e.currentTarget.value;
+ if (templates === this._defaultFieldTemplates) {
+ // Default
+ templates = null;
}
- } catch (e) {
- exceptions.push(e);
+
+ // Overwrite
+ await this._settingsController.setProfileSetting('anki.fieldTemplates', templates);
+
+ // Compile
+ this._onValidateCompile();
}
- const hasException = exceptions.length > 0;
- infoNode.hidden = !(showSuccessResult || hasException);
- infoNode.textContent = hasException ? exceptions.map((e) => `${e}`).join('\n') : (showSuccessResult ? result : '');
- infoNode.classList.toggle('text-danger', hasException);
- if (invalidateInput) {
- const input = document.querySelector('#field-templates');
- input.classList.toggle('is-invalid', hasException);
+ _onValidateCompile() {
+ const infoNode = document.querySelector('#field-template-compile-result');
+ this._validate(infoNode, '{expression}', 'term-kanji', false, true);
}
-}
-async function onAnkiFieldTemplatesChanged(e) {
- // Get value
- let templates = e.currentTarget.value;
- if (templates === await apiGetDefaultAnkiFieldTemplates()) {
- // Default
- templates = null;
+ _onMarkerClicked(e) {
+ e.preventDefault();
+ document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`;
}
- // Overwrite
- const optionsContext = getOptionsContext();
- const options = await getOptionsMutable(optionsContext);
- options.anki.fieldTemplates = templates;
- await settingsSaveOptions();
+ _onRender(e) {
+ e.preventDefault();
- // Compile
- onAnkiTemplatesValidateCompile();
-}
+ const field = document.querySelector('#field-template-render-text').value;
+ const infoNode = document.querySelector('#field-template-render-result');
+ infoNode.hidden = true;
+ this._validate(infoNode, field, 'term-kanji', true, false);
+ }
-function onAnkiTemplatesValidateCompile() {
- const infoNode = document.querySelector('#field-template-compile-result');
- ankiTemplatesValidate(infoNode, '{expression}', 'term-kanji', false, true);
-}
+ async _getDefinition(text, optionsContext) {
+ if (this._cachedDefinitionText !== text) {
+ const {definitions} = await api.termsFind(text, {}, optionsContext);
+ if (definitions.length === 0) { return null; }
-function onAnkiTemplateMarkerClicked(e) {
- e.preventDefault();
- document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`;
-}
+ this._cachedDefinitionValue = definitions[0];
+ this._cachedDefinitionText = text;
+ }
+ return this._cachedDefinitionValue;
+ }
-function onAnkiTemplateRender(e) {
- e.preventDefault();
+ async _validate(infoNode, field, mode, showSuccessResult, invalidateInput) {
+ const text = document.querySelector('#field-templates-preview-text').value || '';
+ const exceptions = [];
+ 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 = {
+ document: {
+ title: document.title
+ }
+ };
+ let templates = options.anki.fieldTemplates;
+ if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; }
+ const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: api.templateRender.bind(api)});
+ result = await ankiNoteBuilder.formatField(field, definition, mode, context, options, templates, exceptions);
+ }
+ } catch (e) {
+ exceptions.push(e);
+ }
- const field = document.querySelector('#field-template-render-text').value;
- const infoNode = document.querySelector('#field-template-render-result');
- infoNode.hidden = true;
- ankiTemplatesValidate(infoNode, field, 'term-kanji', true, false);
+ const hasException = exceptions.length > 0;
+ infoNode.hidden = !(showSuccessResult || hasException);
+ infoNode.textContent = hasException ? exceptions.map((e) => `${e}`).join('\n') : (showSuccessResult ? result : '');
+ infoNode.classList.toggle('text-danger', hasException);
+ if (invalidateInput) {
+ const input = document.querySelector('#field-templates');
+ input.classList.toggle('is-invalid', hasException);
+ }
+ }
}
diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js
index ff1277ed..51dabba4 100644
--- a/ext/bg/js/settings/anki.js
+++ b/ext/bg/js/settings/anki.js
@@ -16,278 +16,282 @@
*/
/* global
- * apiGetAnkiDeckNames
- * apiGetAnkiModelFieldNames
- * apiGetAnkiModelNames
- * getOptionsContext
- * getOptionsMutable
- * onFormOptionsChanged
- * settingsSaveOptions
- * utilBackgroundIsolate
+ * api
*/
-// Private
-
-let _ankiDataPopulated = false;
-
-
-function _ankiSpinnerShow(show) {
- const spinner = $('#anki-spinner');
- if (show) {
- spinner.show();
- } else {
- spinner.hide();
+class AnkiController {
+ constructor(settingsController) {
+ this._settingsController = settingsController;
}
-}
-function _ankiSetError(error) {
- const node = document.querySelector('#anki-error');
- const node2 = document.querySelector('#anki-invalid-response-error');
- if (error) {
- const errorString = `${error}`;
- if (node !== null) {
- node.hidden = false;
- node.textContent = errorString;
- _ankiSetErrorData(node, error);
+ async prepare() {
+ for (const element of document.querySelectorAll('#anki-fields-container input,#anki-fields-container select')) {
+ element.addEventListener('change', this._onFieldsChanged.bind(this), false);
}
- if (node2 !== null) {
- node2.hidden = (errorString.indexOf('Invalid response') < 0);
- }
- } else {
- if (node !== null) {
- node.hidden = true;
- node.textContent = '';
+ for (const element of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) {
+ element.addEventListener('change', this._onModelChanged.bind(this), false);
}
- if (node2 !== null) {
- node2.hidden = true;
- }
- }
-}
+ this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
-function _ankiSetErrorData(node, error) {
- const data = error.data;
- let message = '';
- if (typeof data !== 'undefined') {
- message += `${JSON.stringify(data, null, 4)}\n\n`;
+ const options = await this._settingsController.getOptions();
+ this._onOptionsChanged({options});
}
- message += `${error.stack}`.trimRight();
- const button = document.createElement('a');
- button.className = 'error-data-show-button';
+ getFieldMarkers(type) {
+ switch (type) {
+ case 'terms':
+ return [
+ 'audio',
+ 'cloze-body',
+ 'cloze-prefix',
+ 'cloze-suffix',
+ 'dictionary',
+ 'document-title',
+ 'expression',
+ 'furigana',
+ 'furigana-plain',
+ 'glossary',
+ 'glossary-brief',
+ 'reading',
+ 'screenshot',
+ 'sentence',
+ 'tags',
+ 'url'
+ ];
+ case 'kanji':
+ return [
+ 'character',
+ 'dictionary',
+ 'document-title',
+ 'glossary',
+ 'kunyomi',
+ 'onyomi',
+ 'screenshot',
+ 'sentence',
+ 'tags',
+ 'url'
+ ];
+ default:
+ return [];
+ }
+ }
- const content = document.createElement('div');
- content.className = 'error-data-container';
- content.textContent = message;
- content.hidden = true;
+ getFieldMarkersHtml(markers) {
+ const template = document.querySelector('#anki-field-marker-template').content;
+ const fragment = document.createDocumentFragment();
+ for (const marker of markers) {
+ const markerNode = document.importNode(template, true).firstChild;
+ markerNode.querySelector('.marker-link').textContent = marker;
+ fragment.appendChild(markerNode);
+ }
+ return fragment;
+ }
- button.addEventListener('click', () => content.hidden = !content.hidden, false);
+ // Private
- node.appendChild(button);
- node.appendChild(content);
-}
+ async _onOptionsChanged({options}) {
+ if (!options.anki.enable) {
+ return;
+ }
-function _ankiSetDropdownOptions(dropdown, optionValues) {
- const fragment = document.createDocumentFragment();
- for (const optionValue of optionValues) {
- const option = document.createElement('option');
- option.value = optionValue;
- option.textContent = optionValue;
- fragment.appendChild(option);
+ await this._deckAndModelPopulate(options);
+ await Promise.all([
+ this._populateFields('terms', options.anki.terms.fields),
+ this._populateFields('kanji', options.anki.kanji.fields)
+ ]);
}
- dropdown.textContent = '';
- dropdown.appendChild(fragment);
-}
-async function _ankiDeckAndModelPopulate(options) {
- const termsDeck = {value: options.anki.terms.deck, selector: '#anki-terms-deck'};
- const kanjiDeck = {value: options.anki.kanji.deck, selector: '#anki-kanji-deck'};
- const termsModel = {value: options.anki.terms.model, selector: '#anki-terms-model'};
- const kanjiModel = {value: options.anki.kanji.model, selector: '#anki-kanji-model'};
- try {
- _ankiSpinnerShow(true);
- const [deckNames, modelNames] = await Promise.all([apiGetAnkiDeckNames(), apiGetAnkiModelNames()]);
- deckNames.sort();
- modelNames.sort();
- termsDeck.values = deckNames;
- kanjiDeck.values = deckNames;
- termsModel.values = modelNames;
- kanjiModel.values = modelNames;
- _ankiSetError(null);
- } catch (error) {
- _ankiSetError(error);
- } finally {
- _ankiSpinnerShow(false);
+ _fieldsToDict(elements) {
+ const result = {};
+ for (const element of elements) {
+ result[element.dataset.field] = element.value;
+ }
+ return result;
}
- for (const {value, values, selector} of [termsDeck, kanjiDeck, termsModel, kanjiModel]) {
- const node = document.querySelector(selector);
- _ankiSetDropdownOptions(node, Array.isArray(values) ? values : [value]);
- node.value = value;
+ _spinnerShow(show) {
+ const spinner = document.querySelector('#anki-spinner');
+ spinner.hidden = !show;
}
-}
-function _ankiCreateFieldTemplate(name, value, markers) {
- const template = document.querySelector('#anki-field-template').content;
- const content = document.importNode(template, true).firstChild;
+ _setError(error) {
+ const node = document.querySelector('#anki-error');
+ const node2 = document.querySelector('#anki-invalid-response-error');
+ if (error) {
+ const errorString = `${error}`;
+ if (node !== null) {
+ node.hidden = false;
+ node.textContent = errorString;
+ this._setErrorData(node, error);
+ }
+
+ if (node2 !== null) {
+ node2.hidden = (errorString.indexOf('Invalid response') < 0);
+ }
+ } else {
+ if (node !== null) {
+ node.hidden = true;
+ node.textContent = '';
+ }
+
+ if (node2 !== null) {
+ node2.hidden = true;
+ }
+ }
+ }
- content.querySelector('.anki-field-name').textContent = name;
+ _setErrorData(node, error) {
+ const data = error.data;
+ let message = '';
+ if (typeof data !== 'undefined') {
+ message += `${JSON.stringify(data, null, 4)}\n\n`;
+ }
+ message += `${error.stack}`.trimRight();
- const field = content.querySelector('.anki-field-value');
- field.dataset.field = name;
- field.value = value;
+ const button = document.createElement('a');
+ button.className = 'error-data-show-button';
- content.querySelector('.anki-field-marker-list').appendChild(ankiGetFieldMarkersHtml(markers));
+ const content = document.createElement('div');
+ content.className = 'error-data-container';
+ content.textContent = message;
+ content.hidden = true;
- return content;
-}
+ button.addEventListener('click', () => content.hidden = !content.hidden, false);
-async function _ankiFieldsPopulate(tabId, options) {
- const tab = document.querySelector(`.tab-pane[data-anki-card-type=${tabId}]`);
- const container = tab.querySelector('tbody');
- const markers = ankiGetFieldMarkers(tabId);
-
- const fragment = document.createDocumentFragment();
- const fields = options.anki[tabId].fields;
- for (const name of Object.keys(fields)) {
- const value = fields[name];
- const html = _ankiCreateFieldTemplate(name, value, markers);
- fragment.appendChild(html);
+ node.appendChild(button);
+ node.appendChild(content);
}
- container.textContent = '';
- container.appendChild(fragment);
-
- for (const node of container.querySelectorAll('.anki-field-value')) {
- node.addEventListener('change', onFormOptionsChanged, false);
- }
- for (const node of container.querySelectorAll('.marker-link')) {
- node.addEventListener('click', _onAnkiMarkerClicked, false);
+ _setDropdownOptions(dropdown, optionValues) {
+ const fragment = document.createDocumentFragment();
+ for (const optionValue of optionValues) {
+ const option = document.createElement('option');
+ option.value = optionValue;
+ option.textContent = optionValue;
+ fragment.appendChild(option);
+ }
+ dropdown.textContent = '';
+ dropdown.appendChild(fragment);
}
-}
-function _onAnkiMarkerClicked(e) {
- e.preventDefault();
- const link = e.currentTarget;
- const input = $(link).closest('.input-group').find('.anki-field-value')[0];
- input.value = `{${link.textContent}}`;
- input.dispatchEvent(new Event('change'));
-}
+ async _deckAndModelPopulate(options) {
+ const termsDeck = {value: options.anki.terms.deck, selector: '#anki-terms-deck'};
+ const kanjiDeck = {value: options.anki.kanji.deck, selector: '#anki-kanji-deck'};
+ const termsModel = {value: options.anki.terms.model, selector: '#anki-terms-model'};
+ const kanjiModel = {value: options.anki.kanji.model, selector: '#anki-kanji-model'};
+ try {
+ this._spinnerShow(true);
+ const [deckNames, modelNames] = await Promise.all([api.getAnkiDeckNames(), api.getAnkiModelNames()]);
+ deckNames.sort();
+ modelNames.sort();
+ termsDeck.values = deckNames;
+ kanjiDeck.values = deckNames;
+ termsModel.values = modelNames;
+ kanjiModel.values = modelNames;
+ this._setError(null);
+ } catch (error) {
+ this._setError(error);
+ } finally {
+ this._spinnerShow(false);
+ }
-async function _onAnkiModelChanged(e) {
- const node = e.currentTarget;
- let fieldNames;
- try {
- const modelName = node.value;
- fieldNames = await apiGetAnkiModelFieldNames(modelName);
- _ankiSetError(null);
- } catch (error) {
- _ankiSetError(error);
- return;
- } finally {
- _ankiSpinnerShow(false);
+ for (const {value, values, selector} of [termsDeck, kanjiDeck, termsModel, kanjiModel]) {
+ const node = document.querySelector(selector);
+ this._setDropdownOptions(node, Array.isArray(values) ? values : [value]);
+ node.value = value;
+ }
}
- const tabId = node.dataset.ankiCardType;
- if (tabId !== 'terms' && tabId !== 'kanji') { return; }
-
- const fields = {};
- for (const name of fieldNames) {
- fields[name] = '';
- }
+ _createFieldTemplate(name, value, markers) {
+ const template = document.querySelector('#anki-field-template').content;
+ const content = document.importNode(template, true).firstChild;
- const optionsContext = getOptionsContext();
- const options = await getOptionsMutable(optionsContext);
- options.anki[tabId].fields = utilBackgroundIsolate(fields);
- await settingsSaveOptions();
+ content.querySelector('.anki-field-name').textContent = name;
- await _ankiFieldsPopulate(tabId, options);
-}
+ const field = content.querySelector('.anki-field-value');
+ field.dataset.field = name;
+ field.value = value;
+ content.querySelector('.anki-field-marker-list').appendChild(this.getFieldMarkersHtml(markers));
-// Public
+ return content;
+ }
-function ankiErrorShown() {
- const node = document.querySelector('#anki-error');
- return node && !node.hidden;
-}
+ async _populateFields(tabId, fields) {
+ const tab = document.querySelector(`.tab-pane[data-anki-card-type=${tabId}]`);
+ const container = tab.querySelector('tbody');
+ const markers = this.getFieldMarkers(tabId);
-function ankiFieldsToDict(elements) {
- const result = {};
- for (const element of elements) {
- result[element.dataset.field] = element.value;
- }
- return result;
-}
+ const fragment = document.createDocumentFragment();
+ for (const [name, value] of Object.entries(fields)) {
+ const html = this._createFieldTemplate(name, value, markers);
+ fragment.appendChild(html);
+ }
+ container.textContent = '';
+ container.appendChild(fragment);
-function ankiGetFieldMarkersHtml(markers) {
- const template = document.querySelector('#anki-field-marker-template').content;
- const fragment = document.createDocumentFragment();
- for (const marker of markers) {
- const markerNode = document.importNode(template, true).firstChild;
- markerNode.querySelector('.marker-link').textContent = marker;
- fragment.appendChild(markerNode);
+ for (const node of container.querySelectorAll('.anki-field-value')) {
+ node.addEventListener('change', this._onFieldsChanged.bind(this), false);
+ }
+ for (const node of container.querySelectorAll('.marker-link')) {
+ node.addEventListener('click', this._onMarkerClicked.bind(this), false);
+ }
}
- return fragment;
-}
-function ankiGetFieldMarkers(type) {
- switch (type) {
- case 'terms':
- return [
- 'audio',
- 'cloze-body',
- 'cloze-prefix',
- 'cloze-suffix',
- 'dictionary',
- 'document-title',
- 'expression',
- 'furigana',
- 'furigana-plain',
- 'glossary',
- 'glossary-brief',
- 'reading',
- 'screenshot',
- 'sentence',
- 'tags',
- 'url'
- ];
- case 'kanji':
- return [
- 'character',
- 'dictionary',
- 'document-title',
- 'glossary',
- 'kunyomi',
- 'onyomi',
- 'screenshot',
- 'sentence',
- 'tags',
- 'url'
- ];
- default:
- return [];
+ _onMarkerClicked(e) {
+ e.preventDefault();
+ const link = e.currentTarget;
+ const input = link.closest('.input-group').querySelector('.anki-field-value');
+ input.value = `{${link.textContent}}`;
+ input.dispatchEvent(new Event('change'));
}
-}
+ async _onModelChanged(e) {
+ const node = e.currentTarget;
+ let fieldNames;
+ try {
+ const modelName = node.value;
+ fieldNames = await api.getAnkiModelFieldNames(modelName);
+ this._setError(null);
+ } catch (error) {
+ this._setError(error);
+ return;
+ } finally {
+ this._spinnerShow(false);
+ }
-function ankiInitialize() {
- for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) {
- node.addEventListener('change', _onAnkiModelChanged, false);
- }
-}
+ const tabId = node.dataset.ankiCardType;
+ if (tabId !== 'terms' && tabId !== 'kanji') { return; }
-async function onAnkiOptionsChanged(options) {
- if (!options.anki.enable) {
- _ankiDataPopulated = false;
- return;
- }
+ const fields = {};
+ for (const name of fieldNames) {
+ fields[name] = '';
+ }
- if (_ankiDataPopulated) { return; }
+ await this._settingsController.setProfileSetting(`anki["${tabId}"].fields`, fields);
+ await this._populateFields(tabId, fields);
+ }
- await _ankiDeckAndModelPopulate(options);
- _ankiDataPopulated = true;
- await Promise.all([_ankiFieldsPopulate('terms', options), _ankiFieldsPopulate('kanji', options)]);
+ async _onFieldsChanged() {
+ const termsDeck = document.querySelector('#anki-terms-deck').value;
+ const termsModel = document.querySelector('#anki-terms-model').value;
+ const termsFields = this._fieldsToDict(document.querySelectorAll('#terms .anki-field-value'));
+ const kanjiDeck = document.querySelector('#anki-kanji-deck').value;
+ const kanjiModel = document.querySelector('#anki-kanji-model').value;
+ const kanjiFields = this._fieldsToDict(document.querySelectorAll('#kanji .anki-field-value'));
+
+ const targets = [
+ {action: 'set', path: 'anki.terms.deck', value: termsDeck},
+ {action: 'set', path: 'anki.terms.model', value: termsModel},
+ {action: 'set', path: 'anki.terms.fields', value: termsFields},
+ {action: 'set', path: 'anki.kanji.deck', value: kanjiDeck},
+ {action: 'set', path: 'anki.kanji.model', value: kanjiModel},
+ {action: 'set', path: 'anki.kanji.fields', value: kanjiFields}
+ ];
+
+ await this._settingsController.modifyProfileSettings(targets);
+ }
}
diff --git a/ext/bg/js/settings/audio-ui.js b/ext/bg/js/settings/audio-ui.js
deleted file mode 100644
index 73c64227..00000000
--- a/ext/bg/js/settings/audio-ui.js
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * 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/>.
- */
-
-class AudioSourceUI {
- static instantiateTemplate(templateSelector) {
- const template = document.querySelector(templateSelector);
- const content = document.importNode(template.content, true);
- return content.firstChild;
- }
-}
-
-AudioSourceUI.Container = class Container {
- constructor(audioSources, container, addButton) {
- this.audioSources = audioSources;
- this.container = container;
- this.addButton = addButton;
- this.children = [];
-
- this.container.textContent = '';
-
- for (const audioSource of toIterable(audioSources)) {
- this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length));
- }
-
- this._clickListener = this.onAddAudioSource.bind(this);
- this.addButton.addEventListener('click', this._clickListener, false);
- }
-
- cleanup() {
- for (const child of this.children) {
- child.cleanup();
- }
-
- this.addButton.removeEventListener('click', this._clickListener, false);
- this.container.textContent = '';
- this._clickListener = null;
- }
-
- save() {
- // Override
- }
-
- remove(child) {
- const index = this.children.indexOf(child);
- if (index < 0) {
- return;
- }
-
- child.cleanup();
- this.children.splice(index, 1);
- this.audioSources.splice(index, 1);
-
- for (let i = index; i < this.children.length; ++i) {
- this.children[i].index = i;
- }
- }
-
- onAddAudioSource() {
- const audioSource = this.getUnusedAudioSource();
- this.audioSources.push(audioSource);
- this.save();
- this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length));
- }
-
- getUnusedAudioSource() {
- const audioSourcesAvailable = [
- 'jpod101',
- 'jpod101-alternate',
- 'jisho',
- 'custom'
- ];
- for (const source of audioSourcesAvailable) {
- if (this.audioSources.indexOf(source) < 0) {
- return source;
- }
- }
- return audioSourcesAvailable[0];
- }
-};
-
-AudioSourceUI.AudioSource = class AudioSource {
- constructor(parent, audioSource, index) {
- this.parent = parent;
- this.audioSource = audioSource;
- this.index = index;
-
- this.container = AudioSourceUI.instantiateTemplate('#audio-source-template');
- this.select = this.container.querySelector('.audio-source-select');
- this.removeButton = this.container.querySelector('.audio-source-remove');
-
- this.select.value = audioSource;
-
- this._selectChangeListener = this.onSelectChanged.bind(this);
- this._removeClickListener = this.onRemoveClicked.bind(this);
-
- this.select.addEventListener('change', this._selectChangeListener, false);
- this.removeButton.addEventListener('click', this._removeClickListener, false);
-
- parent.container.appendChild(this.container);
- }
-
- cleanup() {
- this.select.removeEventListener('change', this._selectChangeListener, false);
- this.removeButton.removeEventListener('click', this._removeClickListener, false);
-
- if (this.container.parentNode !== null) {
- this.container.parentNode.removeChild(this.container);
- }
- }
-
- save() {
- this.parent.save();
- }
-
- onSelectChanged() {
- this.audioSource = this.select.value;
- this.parent.audioSources[this.index] = this.audioSource;
- this.save();
- }
-
- onRemoveClicked() {
- this.parent.remove(this);
- this.save();
- }
-};
diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js
index ac2d82f3..d389acb5 100644
--- a/ext/bg/js/settings/audio.js
+++ b/ext/bg/js/settings/audio.js
@@ -16,110 +16,219 @@
*/
/* global
- * AudioSourceUI
* AudioSystem
- * getOptionsContext
- * getOptionsMutable
- * settingsSaveOptions
*/
-let audioSourceUI = null;
-let audioSystem = null;
-
-async function audioSettingsInitialize() {
- audioSystem = new AudioSystem({
- audioUriBuilder: null,
- useCache: true
- });
-
- const optionsContext = getOptionsContext();
- const options = await getOptionsMutable(optionsContext);
- audioSourceUI = new AudioSourceUI.Container(
- options.audio.sources,
- document.querySelector('.audio-source-list'),
- document.querySelector('.audio-source-add')
- );
- audioSourceUI.save = settingsSaveOptions;
-
- textToSpeechInitialize();
-}
+class AudioController {
+ constructor(settingsController) {
+ this._settingsController = settingsController;
+ this._audioSystem = null;
+ this._audioSourceContainer = null;
+ this._audioSourceAddButton = null;
+ this._audioSourceEntries = [];
+ }
-function textToSpeechInitialize() {
- if (typeof speechSynthesis === 'undefined') { return; }
+ async prepare() {
+ this._audioSystem = new AudioSystem({
+ audioUriBuilder: null,
+ useCache: true
+ });
- speechSynthesis.addEventListener('voiceschanged', updateTextToSpeechVoices, false);
- updateTextToSpeechVoices();
+ this._audioSourceContainer = document.querySelector('.audio-source-list');
+ this._audioSourceAddButton = document.querySelector('.audio-source-add');
+ this._audioSourceContainer.textContent = '';
- document.querySelector('#text-to-speech-voice').addEventListener('change', onTextToSpeechVoiceChange, false);
- document.querySelector('#text-to-speech-voice-test').addEventListener('click', textToSpeechTest, false);
-}
+ this._audioSourceAddButton.addEventListener('click', this._onAddAudioSource.bind(this), false);
+
+ this._prepareTextToSpeech();
+
+ this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
-function updateTextToSpeechVoices() {
- const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index}));
- voices.sort(textToSpeechVoiceCompare);
+ const options = await this._settingsController.getOptions();
+ this._onOptionsChanged({options});
+ }
+
+ // Private
- document.querySelector('#text-to-speech-voice-container').hidden = (voices.length === 0);
+ _onOptionsChanged({options}) {
+ for (let i = this._audioSourceEntries.length - 1; i >= 0; --i) {
+ this._cleanupAudioSourceEntry(i);
+ }
- const fragment = document.createDocumentFragment();
+ for (const audioSource of options.audio.sources) {
+ this._createAudioSourceEntry(audioSource);
+ }
+ }
- let option = document.createElement('option');
- option.value = '';
- option.textContent = 'None';
- fragment.appendChild(option);
+ _prepareTextToSpeech() {
+ if (typeof speechSynthesis === 'undefined') { return; }
- for (const {voice} of voices) {
- option = document.createElement('option');
- option.value = voice.voiceURI;
- option.textContent = `${voice.name} (${voice.lang})`;
+ speechSynthesis.addEventListener('voiceschanged', this._updateTextToSpeechVoices.bind(this), false);
+ this._updateTextToSpeechVoices();
+
+ document.querySelector('#text-to-speech-voice').addEventListener('change', this._onTextToSpeechVoiceChange.bind(this), false);
+ document.querySelector('#text-to-speech-voice-test').addEventListener('click', this._testTextToSpeech.bind(this), false);
+ }
+
+ _updateTextToSpeechVoices() {
+ const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index}));
+ voices.sort(this._textToSpeechVoiceCompare.bind(this));
+
+ document.querySelector('#text-to-speech-voice-container').hidden = (voices.length === 0);
+
+ 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);
+ }
+
+ const select = document.querySelector('#text-to-speech-voice');
+ select.textContent = '';
+ select.appendChild(fragment);
+ select.value = select.dataset.value;
}
- const select = document.querySelector('#text-to-speech-voice');
- select.textContent = '';
- select.appendChild(fragment);
- select.value = select.dataset.value;
-}
+ _textToSpeechVoiceCompare(a, b) {
+ const aIsJapanese = this._languageTagIsJapanese(a.voice.lang);
+ const bIsJapanese = this._languageTagIsJapanese(b.voice.lang);
+ if (aIsJapanese) {
+ if (!bIsJapanese) { return -1; }
+ } else {
+ if (bIsJapanese) { return 1; }
+ }
+
+ const aIsDefault = a.voice.default;
+ const bIsDefault = b.voice.default;
+ if (aIsDefault) {
+ if (!bIsDefault) { return -1; }
+ } else {
+ if (bIsDefault) { return 1; }
+ }
+
+ return a.index - b.index;
+ }
-function languageTagIsJapanese(languageTag) {
- return (
- languageTag.startsWith('ja-') ||
- languageTag.startsWith('jpn-')
- );
-}
+ _languageTagIsJapanese(languageTag) {
+ return (
+ languageTag.startsWith('ja_') ||
+ languageTag.startsWith('ja-') ||
+ languageTag.startsWith('jpn-')
+ );
+ }
-function textToSpeechVoiceCompare(a, b) {
- const aIsJapanese = languageTagIsJapanese(a.voice.lang);
- const bIsJapanese = languageTagIsJapanese(b.voice.lang);
- if (aIsJapanese) {
- if (!bIsJapanese) { return -1; }
- } else {
- if (bIsJapanese) { return 1; }
+ _testTextToSpeech() {
+ try {
+ const text = document.querySelector('#text-to-speech-voice-test').dataset.speechText || '';
+ const voiceUri = document.querySelector('#text-to-speech-voice').value;
+
+ const audio = this._audioSystem.createTextToSpeechAudio(text, voiceUri);
+ audio.volume = 1.0;
+ audio.play();
+ } catch (e) {
+ // NOP
+ }
}
- const aIsDefault = a.voice.default;
- const bIsDefault = b.voice.default;
- if (aIsDefault) {
- if (!bIsDefault) { return -1; }
- } else {
- if (bIsDefault) { return 1; }
+ _instantiateTemplate(templateSelector) {
+ const template = document.querySelector(templateSelector);
+ const content = document.importNode(template.content, true);
+ return content.firstChild;
}
- return a.index - b.index;
-}
+ _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._instantiateTemplate('#audio-source-template');
+ const select = container.querySelector('.audio-source-select');
+ const removeButton = container.querySelector('.audio-source-remove');
+
+ select.value = value;
+
+ const entry = {
+ container,
+ eventListeners,
+ value
+ };
-function textToSpeechTest() {
- try {
- const text = document.querySelector('#text-to-speech-voice-test').dataset.speechText || '';
- const voiceUri = document.querySelector('#text-to-speech-voice').value;
+ eventListeners.addEventListener(select, 'change', this._onAudioSourceSelectChange.bind(this, entry), false);
+ eventListeners.addEventListener(removeButton, 'click', this._onAudioSourceRemoveClicked.bind(this, entry), false);
- const audio = audioSystem.createTextToSpeechAudio(text, voiceUri);
- audio.volume = 1.0;
- audio.play();
- } catch (e) {
- // NOP
+ 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);
+ }
+
+ _onTextToSpeechVoiceChange(e) {
+ e.currentTarget.dataset.value = e.currentTarget.value;
+ }
+
+ 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]
+ }]);
}
-}
-function onTextToSpeechVoiceChange(e) {
- e.currentTarget.dataset.value = e.currentTarget.value;
+ 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);
+ }
+
+ async _onAudioSourceRemoveClicked(entry) {
+ await this._removeAudioSourceEntry(entry);
+ }
}
diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js
index faf4e592..13f90886 100644
--- a/ext/bg/js/settings/backup.js
+++ b/ext/bg/js/settings/backup.js
@@ -16,363 +16,362 @@
*/
/* global
- * apiGetDefaultAnkiFieldTemplates
- * apiGetEnvironmentInfo
- * apiOptionsGetFull
+ * api
* optionsGetDefault
* optionsUpdateVersion
- * utilBackend
- * utilBackgroundIsolate
- * utilIsolate
- * utilReadFileArrayBuffer
*/
-// Exporting
-
-let _settingsExportToken = null;
-let _settingsExportRevoke = null;
-const SETTINGS_EXPORT_CURRENT_VERSION = 0;
-
-function _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('');
-}
+class SettingsBackup {
+ constructor(settingsController) {
+ this._settingsController = settingsController;
+ this._settingsExportToken = null;
+ this._settingsExportRevoke = null;
+ this._currentVersion = 0;
+ }
-async function _getSettingsExportData(date) {
- const optionsFull = await apiOptionsGetFull();
- const environment = await apiGetEnvironmentInfo();
- const fieldTemplatesDefault = await apiGetDefaultAnkiFieldTemplates();
+ prepare() {
+ document.querySelector('#settings-export').addEventListener('click', this._onSettingsExportClick.bind(this), false);
+ document.querySelector('#settings-import').addEventListener('click', this._onSettingsImportClick.bind(this), false);
+ document.querySelector('#settings-import-file').addEventListener('change', this._onSettingsImportFileChange.bind(this), false);
+ document.querySelector('#settings-reset').addEventListener('click', this._onSettingsResetClick.bind(this), false);
+ document.querySelector('#settings-reset-modal-confirm').addEventListener('click', this._onSettingsResetConfirmClick.bind(this), false);
+ }
- // Format options
- for (const {options} of optionsFull.profiles) {
- if (options.anki.fieldTemplates === fieldTemplatesDefault || !options.anki.fieldTemplates) {
- delete options.anki.fieldTemplates; // Default
- }
+ // Private
+
+ _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('');
}
- const data = {
- version: SETTINGS_EXPORT_CURRENT_VERSION,
- date: _getSettingsExportDateString(date, '-', ' ', ':', 6),
- url: chrome.runtime.getURL('/'),
- manifest: chrome.runtime.getManifest(),
- environment,
- userAgent: navigator.userAgent,
- options: optionsFull
- };
-
- return data;
-}
+ async _getSettingsExportData(date) {
+ const optionsFull = await this._settingsController.getOptionsFull();
+ const environment = await api.getEnvironmentInfo();
+ const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates();
-function _saveBlob(blob, fileName) {
- if (typeof navigator === 'object' && typeof navigator.msSaveBlob === 'function') {
- if (navigator.msSaveBlob(blob)) {
- return;
+ // Format options
+ for (const {options} of optionsFull.profiles) {
+ if (options.anki.fieldTemplates === fieldTemplatesDefault || !options.anki.fieldTemplates) {
+ delete options.anki.fieldTemplates; // Default
+ }
}
- }
- const blobUrl = URL.createObjectURL(blob);
+ const data = {
+ version: this._currentVersion,
+ date: this._getSettingsExportDateString(date, '-', ' ', ':', 6),
+ url: chrome.runtime.getURL('/'),
+ manifest: chrome.runtime.getManifest(),
+ environment,
+ userAgent: navigator.userAgent,
+ options: optionsFull
+ };
- const a = document.createElement('a');
- a.href = blobUrl;
- a.download = fileName;
- a.rel = 'noopener';
- a.target = '_blank';
+ return data;
+ }
- const revoke = () => {
- URL.revokeObjectURL(blobUrl);
- a.href = '';
- _settingsExportRevoke = null;
- };
- _settingsExportRevoke = revoke;
+ _saveBlob(blob, fileName) {
+ if (typeof navigator === 'object' && typeof navigator.msSaveBlob === 'function') {
+ if (navigator.msSaveBlob(blob)) {
+ return;
+ }
+ }
- a.dispatchEvent(new MouseEvent('click'));
- setTimeout(revoke, 60000);
-}
+ const blobUrl = URL.createObjectURL(blob);
-async function _onSettingsExportClick() {
- if (_settingsExportRevoke !== null) {
- _settingsExportRevoke();
- _settingsExportRevoke = null;
- }
+ const a = document.createElement('a');
+ a.href = blobUrl;
+ a.download = fileName;
+ a.rel = 'noopener';
+ a.target = '_blank';
- const date = new Date(Date.now());
+ const revoke = () => {
+ URL.revokeObjectURL(blobUrl);
+ a.href = '';
+ this._settingsExportRevoke = null;
+ };
+ this._settingsExportRevoke = revoke;
- const token = {};
- _settingsExportToken = token;
- const data = await _getSettingsExportData(date);
- if (_settingsExportToken !== token) {
- // A new export has been started
- return;
+ a.dispatchEvent(new MouseEvent('click'));
+ setTimeout(revoke, 60000);
}
- _settingsExportToken = null;
- const fileName = `yomichan-settings-${_getSettingsExportDateString(date, '-', '-', '-', 6)}.json`;
- const blob = new Blob([JSON.stringify(data, null, 4)], {type: 'application/json'});
- _saveBlob(blob, fileName);
-}
-
-
-// Importing
+ async _onSettingsExportClick() {
+ if (this._settingsExportRevoke !== null) {
+ this._settingsExportRevoke();
+ this._settingsExportRevoke = null;
+ }
-async function _settingsImportSetOptionsFull(optionsFull) {
- return utilIsolate(utilBackend().setFullOptions(
- utilBackgroundIsolate(optionsFull)
- ));
-}
+ const date = new Date(Date.now());
-function _showSettingsImportError(error) {
- yomichan.logError(error);
- document.querySelector('#settings-import-error-modal-message').textContent = `${error}`;
- $('#settings-import-error-modal').modal('show');
-}
+ 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;
-async function _showSettingsImportWarnings(warnings) {
- const modalNode = $('#settings-import-warning-modal');
- const buttons = document.querySelectorAll('.settings-import-warning-modal-import-button');
- const messageContainer = document.querySelector('#settings-import-warning-modal-message');
- if (modalNode.length === 0 || buttons.length === 0 || messageContainer === null) {
- return {result: false};
+ 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);
}
- // Set message
- const fragment = document.createDocumentFragment();
- for (const warning of warnings) {
- const node = document.createElement('li');
- node.textContent = `${warning}`;
- fragment.appendChild(node);
+ _readFileArrayBuffer(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = () => reject(reader.error);
+ reader.readAsArrayBuffer(file);
+ });
}
- messageContainer.textContent = '';
- messageContainer.appendChild(fragment);
-
- // Show modal
- modalNode.modal('show');
-
- // Wait for modal to close
- return new Promise((resolve) => {
- const onButtonClick = (e) => {
- e.preventDefault();
- complete({
- result: true,
- sanitize: e.currentTarget.dataset.importSanitize === 'true'
- });
- modalNode.modal('hide');
- };
- const onModalHide = () => {
- complete({result: false});
- };
- let completed = false;
- const complete = (result) => {
- if (completed) { return; }
- completed = true;
+ // Importing
- modalNode.off('hide.bs.modal', onModalHide);
- for (const button of buttons) {
- button.removeEventListener('click', onButtonClick, false);
- }
+ async _settingsImportSetOptionsFull(optionsFull) {
+ await this._settingsController.setAllSettings(optionsFull);
+ }
- resolve(result);
- };
+ _showSettingsImportError(error) {
+ yomichan.logError(error);
+ document.querySelector('#settings-import-error-modal-message').textContent = `${error}`;
+ $('#settings-import-error-modal').modal('show');
+ }
- // Hook events
- modalNode.on('hide.bs.modal', onModalHide);
- for (const button of buttons) {
- button.addEventListener('click', onButtonClick, false);
+ async _showSettingsImportWarnings(warnings) {
+ const modalNode = $('#settings-import-warning-modal');
+ const buttons = document.querySelectorAll('.settings-import-warning-modal-import-button');
+ const messageContainer = document.querySelector('#settings-import-warning-modal-message');
+ if (modalNode.length === 0 || buttons.length === 0 || messageContainer === null) {
+ return {result: false};
}
- });
-}
-function _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;
+ // Set message
+ const fragment = document.createDocumentFragment();
+ for (const warning of warnings) {
+ const node = document.createElement('li');
+ node.textContent = `${warning}`;
+ fragment.appendChild(node);
}
- } catch (e) {
- // NOP
- }
- return false;
-}
+ messageContainer.textContent = '';
+ messageContainer.appendChild(fragment);
+
+ // Show modal
+ modalNode.modal('show');
+
+ // Wait for modal to close
+ return new Promise((resolve) => {
+ const onButtonClick = (e) => {
+ e.preventDefault();
+ complete({
+ result: true,
+ sanitize: e.currentTarget.dataset.importSanitize === 'true'
+ });
+ modalNode.modal('hide');
+ };
+ const onModalHide = () => {
+ complete({result: false});
+ };
+
+ let completed = false;
+ const complete = (result) => {
+ if (completed) { return; }
+ completed = true;
+
+ modalNode.off('hide.bs.modal', onModalHide);
+ for (const button of buttons) {
+ button.removeEventListener('click', onButtonClick, false);
+ }
-function _settingsImportSanitizeProfileOptions(options, dryRun) {
- const warnings = [];
+ resolve(result);
+ };
- 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) {
- delete anki.fieldTemplates;
- }
- }
- const server = anki.server;
- if (typeof server === 'string' && server.length > 0 && !_isLocalhostUrl(server)) {
- warnings.push('anki.server uses a non-localhost URL');
- if (!dryRun) {
- delete anki.server;
+ // Hook events
+ modalNode.on('hide.bs.modal', onModalHide);
+ for (const button of buttons) {
+ button.addEventListener('click', onButtonClick, false);
}
- }
+ });
}
- const audio = options.audio;
- if (isObject(audio)) {
- const customSourceUrl = audio.customSourceUrl;
- if (typeof customSourceUrl === 'string' && customSourceUrl.length > 0 && !_isLocalhostUrl(customSourceUrl)) {
- warnings.push('audio.customSourceUrl uses a non-localhost URL');
- if (!dryRun) {
- delete audio.customSourceUrl;
+ _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;
}
- return warnings;
-}
-
-function _settingsImportSanitizeOptions(optionsFull, dryRun) {
- const warnings = new Set();
+ _settingsImportSanitizeProfileOptions(options, dryRun) {
+ const warnings = [];
- 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 = _settingsImportSanitizeProfileOptions(options, dryRun);
- for (const warning of warnings2) {
- warnings.add(warning);
+ 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) {
+ delete anki.fieldTemplates;
+ }
+ }
+ 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) {
+ delete anki.server;
+ }
}
}
- }
- return warnings;
-}
+ 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) {
+ delete audio.customSourceUrl;
+ }
+ }
+ }
-function _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));
+ return warnings;
}
-}
-async function _importSettingsFile(file) {
- const dataString = _utf8Decode(await utilReadFileArrayBuffer(file));
- const data = JSON.parse(dataString);
+ _settingsImportSanitizeOptions(optionsFull, dryRun) {
+ const warnings = new Set();
- // Type check
- if (!isObject(data)) {
- throw new Error(`Invalid data type: ${typeof data}`);
- }
+ 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; }
- // Version check
- const version = data.version;
- if (!(
- typeof version === 'number' &&
- Number.isFinite(version) &&
- version === Math.floor(version)
- )) {
- throw new Error(`Invalid version: ${version}`);
- }
+ const warnings2 = this._settingsImportSanitizeProfileOptions(options, dryRun);
+ for (const warning of warnings2) {
+ warnings.add(warning);
+ }
+ }
+ }
- if (!(
- version >= 0 &&
- version <= SETTINGS_EXPORT_CURRENT_VERSION
- )) {
- throw new Error(`Unsupported version: ${version}`);
+ return warnings;
}
- // Verify options exists
- let optionsFull = data.options;
- if (!isObject(optionsFull)) {
- throw new Error(`Invalid options type: ${typeof optionsFull}`);
+ _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));
+ }
}
- // Upgrade options
- optionsFull = optionsUpdateVersion(optionsFull, {});
+ async _importSettingsFile(file) {
+ const dataString = this._utf8Decode(await this._readFileArrayBuffer(file));
+ const data = JSON.parse(dataString);
- // Check for warnings
- const sanitizationWarnings = _settingsImportSanitizeOptions(optionsFull, true);
-
- // Show sanitization warnings
- if (sanitizationWarnings.size > 0) {
- const {result, sanitize} = await _showSettingsImportWarnings(sanitizationWarnings);
- if (!result) { return; }
+ // Type check
+ if (!isObject(data)) {
+ throw new Error(`Invalid data type: ${typeof data}`);
+ }
- if (sanitize !== false) {
- _settingsImportSanitizeOptions(optionsFull, false);
+ // Version check
+ const version = data.version;
+ if (!(
+ typeof version === 'number' &&
+ Number.isFinite(version) &&
+ version === Math.floor(version)
+ )) {
+ throw new Error(`Invalid version: ${version}`);
}
- }
- // Assign options
- await _settingsImportSetOptionsFull(optionsFull);
+ if (!(
+ version >= 0 &&
+ version <= this._currentVersion
+ )) {
+ throw new Error(`Unsupported version: ${version}`);
+ }
- // Reload settings page
- window.location.reload();
-}
+ // Verify options exists
+ let optionsFull = data.options;
+ if (!isObject(optionsFull)) {
+ throw new Error(`Invalid options type: ${typeof optionsFull}`);
+ }
-function _onSettingsImportClick() {
- document.querySelector('#settings-import-file').click();
-}
+ // Upgrade options
+ optionsFull = optionsUpdateVersion(optionsFull, {});
-function _onSettingsImportFileChange(e) {
- const files = e.target.files;
- if (files.length === 0) { return; }
+ // Check for warnings
+ const sanitizationWarnings = this._settingsImportSanitizeOptions(optionsFull, true);
- const file = files[0];
- e.target.value = null;
- _importSettingsFile(file).catch(_showSettingsImportError);
-}
+ // Show sanitization warnings
+ if (sanitizationWarnings.size > 0) {
+ const {result, sanitize} = await this._showSettingsImportWarnings(sanitizationWarnings);
+ if (!result) { return; }
+ if (sanitize !== false) {
+ this._settingsImportSanitizeOptions(optionsFull, false);
+ }
+ }
-// Resetting
+ // Assign options
+ await this._settingsImportSetOptionsFull(optionsFull);
+ }
-function _onSettingsResetClick() {
- $('#settings-reset-modal').modal('show');
-}
+ _onSettingsImportClick() {
+ document.querySelector('#settings-import-file').click();
+ }
-async function _onSettingsResetConfirmClick() {
- $('#settings-reset-modal').modal('hide');
+ async _onSettingsImportFileChange(e) {
+ const files = e.target.files;
+ if (files.length === 0) { return; }
- // Get default options
- const optionsFull = optionsGetDefault();
+ const file = files[0];
+ e.target.value = null;
+ try {
+ await this._importSettingsFile(file);
+ } catch (error) {
+ this._showSettingsImportError(error);
+ }
+ }
- // Assign options
- await _settingsImportSetOptionsFull(optionsFull);
+ // Resetting
- // Reload settings page
- window.location.reload();
-}
+ _onSettingsResetClick() {
+ $('#settings-reset-modal').modal('show');
+ }
+ async _onSettingsResetConfirmClick() {
+ $('#settings-reset-modal').modal('hide');
-// Setup
+ // Get default options
+ const optionsFull = optionsGetDefault();
-function backupInitialize() {
- document.querySelector('#settings-export').addEventListener('click', _onSettingsExportClick, false);
- document.querySelector('#settings-import').addEventListener('click', _onSettingsImportClick, false);
- document.querySelector('#settings-import-file').addEventListener('change', _onSettingsImportFileChange, false);
- document.querySelector('#settings-reset').addEventListener('click', _onSettingsResetClick, false);
- document.querySelector('#settings-reset-modal-confirm').addEventListener('click', _onSettingsResetConfirmClick, false);
+ // Assign options
+ await this._settingsImportSetOptionsFull(optionsFull);
+ }
}
diff --git a/ext/bg/js/settings/clipboard-popups-controller.js b/ext/bg/js/settings/clipboard-popups-controller.js
new file mode 100644
index 00000000..294663f9
--- /dev/null
+++ b/ext/bg/js/settings/clipboard-popups-controller.js
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 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/>.
+ */
+
+class ClipboardPopupsController {
+ constructor(settingsController) {
+ this._settingsController = settingsController;
+ this._checkbox = document.querySelector('#enable-clipboard-popups');
+ }
+
+ async prepare() {
+ this._checkbox.addEventListener('change', this._onEnableClipboardPopupsChanged.bind(this), false);
+ this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
+
+ const options = await this._settingsController.getOptions();
+ this._onOptionsChanged({options});
+ }
+
+ // Private
+
+ _onOptionsChanged({options}) {
+ this._checkbox.checked = options.general.enableClipboardPopups;
+ }
+
+ async _onEnableClipboardPopupsChanged(e) {
+ const checkbox = e.currentTarget;
+ let value = checkbox.checked;
+
+ if (value) {
+ value = await new Promise((resolve) => {
+ chrome.permissions.request({permissions: ['clipboardRead']}, resolve);
+ });
+ checkbox.checked = value;
+ }
+
+ await this._settingsController.setProfileSetting('general.enableClipboardPopups', value);
+ }
+}
diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js
index 632c01ea..94a71233 100644
--- a/ext/bg/js/settings/dictionaries.js
+++ b/ext/bg/js/settings/dictionaries.js
@@ -17,27 +17,13 @@
/* global
* PageExitPrevention
- * apiDeleteDictionary
- * apiGetDictionaryCounts
- * apiGetDictionaryInfo
- * apiImportDictionaryArchive
- * apiOptionsGet
- * apiOptionsGetFull
- * apiPurgeDatabase
- * getOptionsContext
- * getOptionsFullMutable
- * getOptionsMutable
- * settingsSaveOptions
- * storageEstimate
- * storageUpdateStats
+ * api
* utilBackgroundIsolate
*/
-let dictionaryUI = null;
-
-
-class SettingsDictionaryListUI {
+class SettingsDictionaryListUI extends EventDispatcher {
constructor(container, template, extraContainer, extraTemplate) {
+ super();
this.container = container;
this.template = template;
this.extraContainer = extraContainer;
@@ -312,15 +298,15 @@ class SettingsDictionaryEntryUI {
progressBar.style.width = `${percent}%`;
};
- await apiDeleteDictionary(this.dictionaryInfo.title, onProgress);
+ 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.parent.trigger('databaseUpdated');
}
}
@@ -394,340 +380,342 @@ class SettingsDictionaryExtraUI {
}
}
+class DictionaryController {
+ constructor(settingsController, storageController) {
+ this._settingsController = settingsController;
+ 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(
- document.querySelector('#dict-groups'),
- document.querySelector('#dict-template'),
- document.querySelector('#dict-groups-extra'),
- document.querySelector('#dict-extra-template')
- );
- 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);
-
- await onDictionaryOptionsChanged();
- await onDatabaseUpdated();
-}
-
-async function onDictionaryOptionsChanged() {
- if (dictionaryUI === null) { return; }
+ 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.on('databaseUpdated', this._onDatabaseUpdated.bind(this));
- const optionsContext = getOptionsContext();
- const options = await getOptionsMutable(optionsContext);
+ 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);
- dictionaryUI.setOptionsDictionaries(options.dictionaries);
+ this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
- const optionsFull = await apiOptionsGetFull();
- document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported;
+ await this._onOptionsChanged();
+ await this._onDatabaseUpdated();
+ }
- await updateMainDictionarySelectValue();
-}
+ // Private
-async function onDatabaseUpdated() {
- try {
- const dictionaries = await apiGetDictionaryInfo();
- dictionaryUI.setDictionaries(dictionaries);
+ async _onOptionsChanged() {
+ const options = await this._settingsController.getOptionsMutable();
- document.querySelector('#dict-warning').hidden = (dictionaries.length > 0);
+ this._dictionaryUI.setOptionsDictionaries(options.dictionaries);
- updateMainDictionarySelectOptions(dictionaries);
- await updateMainDictionarySelectValue();
+ const optionsFull = await this._settingsController.getOptionsFull();
+ document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported;
- const {counts, total} = await apiGetDictionaryCounts(dictionaries.map((v) => v.title), true);
- dictionaryUI.setCounts(counts, total);
- } catch (e) {
- dictionaryErrorsShow([e]);
+ await this._updateMainDictionarySelectValue();
}
-}
-function updateMainDictionarySelectOptions(dictionaries) {
- const select = document.querySelector('#dict-main');
- select.textContent = ''; // Empty
+ _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);
+ 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; }
+ for (const {title, sequenced} of toIterable(dictionaries)) {
+ if (!sequenced) { continue; }
- option = document.createElement('option');
- option.value = title;
- option.textContent = title;
- select.appendChild(option);
+ option = document.createElement('option');
+ option.value = title;
+ option.textContent = title;
+ select.appendChild(option);
+ }
}
-}
-async function updateMainDictionarySelectValue() {
- const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ async _updateMainDictionarySelectValue() {
+ const options = await this._settingsController.getOptions();
- const value = options.general.mainDictionary;
+ 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;
+ 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);
+ 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;
}
- select.value = value;
-}
+ _dictionaryErrorToString(error) {
+ if (error.toString) {
+ error = error.toString();
+ } else {
+ error = `${error}`;
+ }
-async function onDictionaryMainChanged(e) {
- const select = e.target;
- const value = select.value;
+ for (const [match, subst] of this._dictionaryErrorToStringOverrides) {
+ if (error.includes(match)) {
+ error = subst;
+ break;
+ }
+ }
- const missingNodeOption = select.querySelector('option[data-not-installed=true]');
- if (missingNodeOption !== null && missingNodeOption.value !== value) {
- missingNodeOption.parentNode.removeChild(missingNodeOption);
+ return error;
}
- const optionsContext = getOptionsContext();
- const options = await getOptionsMutable(optionsContext);
- options.general.mainDictionary = value;
- await settingsSaveOptions();
-}
+ _dictionaryErrorsShow(errors) {
+ const dialog = document.querySelector('#dict-error');
+ dialog.textContent = '';
+ if (errors !== null && errors.length > 0) {
+ const uniqueErrors = new Map();
+ for (let e of errors) {
+ yomichan.logError(e);
+ e = this._dictionaryErrorToString(e);
+ let count = uniqueErrors.get(e);
+ if (typeof count === 'undefined') {
+ count = 0;
+ }
+ uniqueErrors.set(e, count + 1);
+ }
-function dictionaryErrorToString(error) {
- if (error.toString) {
- error = error.toString();
- } else {
- error = `${error}`;
- }
+ 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}`;
+ }
+ dialog.appendChild(div);
+ }
- for (const [match, subst] of dictionaryErrorToString.overrides) {
- if (error.includes(match)) {
- error = subst;
- break;
+ dialog.hidden = false;
+ } else {
+ dialog.hidden = true;
}
}
- 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) {
- const dialog = document.querySelector('#dict-error');
- dialog.textContent = '';
-
- if (errors !== null && errors.length > 0) {
- const uniqueErrors = new Map();
- for (let e of errors) {
- yomichan.logError(e);
- e = dictionaryErrorToString(e);
- let count = uniqueErrors.get(e);
- if (typeof count === 'undefined') {
- count = 0;
- }
- uniqueErrors.set(e, count + 1);
- }
-
- 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}`;
- }
- dialog.appendChild(div);
+ _dictionarySpinnerShow(show) {
+ const spinner = $('#dict-spinner');
+ if (show) {
+ spinner.show();
+ } else {
+ spinner.hide();
}
+ }
- dialog.hidden = false;
- } else {
- dialog.hidden = true;
+ _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);
-function dictionarySpinnerShow(show) {
- const spinner = $('#dict-spinner');
- if (show) {
- spinner.show();
- } else {
- spinner.hide();
+ 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]);
+ }
}
-}
-function onDictionaryImportButtonClick() {
- const dictFile = document.querySelector('#dict-file');
- dictFile.click();
-}
+ async _onDictionaryMainChanged(e) {
+ const select = e.target;
+ const value = select.value;
-function onDictionaryPurgeButtonClick(e) {
- e.preventDefault();
- $('#dict-purge-modal').modal('show');
-}
+ const missingNodeOption = select.querySelector('option[data-not-installed=true]');
+ if (missingNodeOption !== null && missingNodeOption.value !== value) {
+ missingNodeOption.parentNode.removeChild(missingNodeOption);
+ }
-async function onDictionaryPurge(e) {
- e.preventDefault();
+ const options = await this._settingsController.getOptionsMutable();
+ options.general.mainDictionary = value;
+ await this._settingsController.save();
+ }
- $('#dict-purge-modal').modal('hide');
+ _onImportButtonClick() {
+ const dictFile = document.querySelector('#dict-file');
+ dictFile.click();
+ }
- const dictControls = $('#dict-importer, #dict-groups, #dict-groups-extra, #dict-main-group').hide();
- const dictProgress = document.querySelector('#dict-purge');
- dictProgress.hidden = false;
+ _onPurgeButtonClick(e) {
+ e.preventDefault();
+ $('#dict-purge-modal').modal('show');
+ }
- const prevention = new PageExitPrevention();
+ async _onPurgeConfirmButtonClick(e) {
+ e.preventDefault();
- try {
- prevention.start();
- dictionaryErrorsShow(null);
- dictionarySpinnerShow(true);
+ $('#dict-purge-modal').modal('hide');
- await apiPurgeDatabase();
- for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
- options.dictionaries = utilBackgroundIsolate({});
- options.general.mainDictionary = '';
- }
- await settingsSaveOptions();
+ const dictControls = $('#dict-importer, #dict-groups, #dict-groups-extra, #dict-main-group').hide();
+ const dictProgress = document.querySelector('#dict-purge');
+ dictProgress.hidden = false;
+
+ const prevention = new PageExitPrevention();
- onDatabaseUpdated();
- } catch (err) {
- dictionaryErrorsShow([err]);
- } finally {
- prevention.end();
+ try {
+ prevention.start();
+ this._dictionaryErrorsShow(null);
+ this._dictionarySpinnerShow(true);
+
+ await api.purgeDatabase();
+ const optionsFull = await this._settingsController.getOptionsFullMutable();
+ for (const {options} of toIterable(optionsFull.profiles)) {
+ options.dictionaries = utilBackgroundIsolate({});
+ options.general.mainDictionary = '';
+ }
+ await this._settingsController.save();
+
+ this._onDatabaseUpdated();
+ } catch (err) {
+ this._dictionaryErrorsShow([err]);
+ } finally {
+ prevention.end();
- dictionarySpinnerShow(false);
+ this._dictionarySpinnerShow(false);
- dictControls.show();
- dictProgress.hidden = true;
+ dictControls.show();
+ dictProgress.hidden = true;
- if (storageEstimate.mostRecent !== null) {
- storageUpdateStats();
+ this._storageController.updateStats();
}
}
-}
-async function onDictionaryImport(e) {
- const files = [...e.target.files];
- e.target.value = null;
+ async _onImportFileChange(e) {
+ const files = [...e.target.files];
+ e.target.value = null;
- const dictFile = $('#dict-file');
- const dictControls = $('#dict-importer').hide();
- const dictProgress = $('#dict-import-progress').show();
- const dictImportInfo = document.querySelector('#dict-import-info');
+ const dictFile = $('#dict-file');
+ const dictControls = $('#dict-importer').hide();
+ const dictProgress = $('#dict-import-progress').show();
+ const dictImportInfo = document.querySelector('#dict-import-info');
- const prevention = new PageExitPrevention();
+ const prevention = new PageExitPrevention();
- try {
- prevention.start();
- dictionaryErrorsShow(null);
- dictionarySpinnerShow(true);
+ try {
+ prevention.start();
+ 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();
- }
- };
+ const setProgress = (percent) => dictProgress.find('.progress-bar').css('width', `${percent}%`);
+ const updateProgress = (total, current) => {
+ setProgress(current / total * 100.0);
+ this._storageController.updateStats();
+ };
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await this._settingsController.getOptionsFull();
- const importDetails = {
- prefixWildcardsSupported: optionsFull.global.database.prefixWildcardsSupported
- };
+ const importDetails = {
+ prefixWildcardsSupported: optionsFull.global.database.prefixWildcardsSupported
+ };
- for (let i = 0, ii = files.length; i < ii; ++i) {
- setProgress(0.0);
- if (ii > 1) {
- dictImportInfo.hidden = false;
- dictImportInfo.textContent = `(${i + 1} of ${ii})`;
- }
+ for (let i = 0, ii = files.length; i < ii; ++i) {
+ setProgress(0.0);
+ if (ii > 1) {
+ dictImportInfo.hidden = false;
+ dictImportInfo.textContent = `(${i + 1} of ${ii})`;
+ }
- const archiveContent = await dictReadFile(files[i]);
- const {result, errors} = await apiImportDictionaryArchive(archiveContent, importDetails, updateProgress);
- for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
- const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions();
- dictionaryOptions.enabled = true;
- options.dictionaries[result.title] = dictionaryOptions;
- if (result.sequenced && options.general.mainDictionary === '') {
- options.general.mainDictionary = result.title;
+ const archiveContent = await this._dictReadFile(files[i]);
+ const {result, errors} = await api.importDictionaryArchive(archiveContent, importDetails, updateProgress);
+ const optionsFull2 = await this._settingsController.getOptionsFullMutable();
+ for (const {options} of toIterable(optionsFull2.profiles)) {
+ const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions();
+ dictionaryOptions.enabled = true;
+ options.dictionaries[result.title] = dictionaryOptions;
+ if (result.sequenced && options.general.mainDictionary === '') {
+ options.general.mainDictionary = result.title;
+ }
}
- }
- await settingsSaveOptions();
+ await this._settingsController.save();
- 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);
+ 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.`);
+ this._dictionaryErrorsShow(errors2);
+ }
+
+ this._onDatabaseUpdated();
}
+ } catch (err) {
+ this._dictionaryErrorsShow([err]);
+ } finally {
+ prevention.end();
+ this._dictionarySpinnerShow(false);
- onDatabaseUpdated();
+ dictImportInfo.hidden = false;
+ dictImportInfo.textContent = '';
+ dictFile.val('');
+ dictControls.show();
+ dictProgress.hide();
}
- } catch (err) {
- dictionaryErrorsShow([err]);
- } finally {
- prevention.end();
- dictionarySpinnerShow(false);
-
- dictImportInfo.hidden = false;
- dictImportInfo.textContent = '';
- dictFile.val('');
- dictControls.show();
- dictProgress.hide();
}
-}
-
-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) {
- 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();
+ 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();
+ }
}
diff --git a/ext/bg/js/settings/generic-setting-controller.js b/ext/bg/js/settings/generic-setting-controller.js
new file mode 100644
index 00000000..bdea8e3d
--- /dev/null
+++ b/ext/bg/js/settings/generic-setting-controller.js
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 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/>.
+ */
+
+/* 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([
+ ['setDocumentAttribute', this._setDocumentAttribute.bind(this)],
+ ['splitTags', this._splitTags.bind(this)],
+ ['joinTags', this._joinTags.bind(this)]
+ ]);
+ }
+
+ async prepare() {
+ this._dataBinder.observe(document.body);
+ this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
+ }
+
+ // Private
+
+ _onOptionsChanged() {
+ this._dataBinder.refresh();
+ }
+
+ _createElementMetadata(element) {
+ return {
+ path: element.dataset.setting,
+ scope: element.dataset.scope,
+ transformPre: element.dataset.transformPre,
+ transformPost: element.dataset.transformPost
+ };
+ }
+
+ _compareElementMetadata(metadata1, metadata2) {
+ return (
+ metadata1.path === metadata2.path &&
+ metadata1.scope === metadata2.scope &&
+ metadata1.transformPre === metadata2.transformPre &&
+ metadata1.transformPost === metadata2.transformPost
+ );
+ }
+
+ 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, value, element} of targets) {
+ const {path, scope, transformPre} = metadata;
+ const target = {
+ path,
+ scope: scope || defaultScope,
+ action: 'set',
+ value: this._transform(value, transformPre, metadata, element)
+ };
+ 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 jsonToError(error); }
+ const {metadata, element} = targets[i];
+ const result = this._transform(value.result, metadata.transformPost, metadata, element);
+ return {result};
+ });
+ }
+
+ _transform(value, transform, metadata, element) {
+ if (typeof transform === 'string') {
+ const transformFunction = this._transforms.get(transform);
+ if (typeof transformFunction !== 'undefined') {
+ value = transformFunction(value, metadata, element);
+ }
+ }
+ return value;
+ }
+
+ // Transforms
+
+ _setDocumentAttribute(value, metadata, element) {
+ document.documentElement.setAttribute(element.dataset.documentAttribute, `${value}`);
+ return value;
+ }
+
+ _splitTags(value) {
+ return `${value}`.split(/[,; ]+/).filter((v) => !!v);
+ }
+
+ _joinTags(value) {
+ return value.join(' ');
+ }
+}
diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
index 61395b1c..e22c5e53 100644
--- a/ext/bg/js/settings/main.js
+++ b/ext/bg/js/settings/main.js
@@ -16,268 +16,20 @@
*/
/* global
- * ankiErrorShown
- * ankiFieldsToDict
- * ankiInitialize
- * ankiTemplatesInitialize
- * ankiTemplatesUpdateValue
- * apiForwardLogsToBackend
- * apiGetEnvironmentInfo
- * apiOptionsSave
- * appearanceInitialize
- * audioSettingsInitialize
- * backupInitialize
- * dictSettingsInitialize
- * getOptionsContext
- * onAnkiOptionsChanged
- * onDictionaryOptionsChanged
- * profileOptionsSetup
- * storageInfoInitialize
- * utilBackend
- * utilBackgroundIsolate
- * utilIsolate
+ * AnkiController
+ * AnkiTemplatesController
+ * AudioController
+ * ClipboardPopupsController
+ * DictionaryController
+ * GenericSettingController
+ * PopupPreviewController
+ * ProfileController
+ * SettingsBackup
+ * SettingsController
+ * StorageController
+ * api
*/
-function getOptionsMutable(optionsContext) {
- return utilBackend().getOptions(
- utilBackgroundIsolate(optionsContext)
- );
-}
-
-function getOptionsFullMutable() {
- return utilBackend().getFullOptions();
-}
-
-async function formRead(options) {
- options.general.enable = $('#enable').prop('checked');
- const enableClipboardPopups = $('#enable-clipboard-popups').prop('checked');
- if (enableClipboardPopups) {
- options.general.enableClipboardPopups = await new Promise((resolve, _reject) => {
- chrome.permissions.request(
- {permissions: ['clipboardRead']},
- (granted) => {
- if (!granted) {
- $('#enable-clipboard-popups').prop('checked', false);
- }
- resolve(granted);
- }
- );
- });
- } else {
- options.general.enableClipboardPopups = false;
- }
- options.general.showGuide = $('#show-usage-guide').prop('checked');
- options.general.compactTags = $('#compact-tags').prop('checked');
- options.general.compactGlossaries = $('#compact-glossaries').prop('checked');
- options.general.resultOutputMode = $('#result-output-mode').val();
- options.general.debugInfo = $('#show-debug-info').prop('checked');
- options.general.showAdvanced = $('#show-advanced-options').prop('checked');
- options.general.maxResults = parseInt($('#max-displayed-results').val(), 10);
- options.general.popupDisplayMode = $('#popup-display-mode').val();
- options.general.popupHorizontalTextPosition = $('#popup-horizontal-text-position').val();
- options.general.popupVerticalTextPosition = $('#popup-vertical-text-position').val();
- options.general.popupWidth = parseInt($('#popup-width').val(), 10);
- options.general.popupHeight = parseInt($('#popup-height').val(), 10);
- options.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0);
- options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10);
- options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0);
- options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10);
- options.general.popupScalingFactor = parseFloat($('#popup-scaling-factor').val());
- options.general.popupScaleRelativeToPageZoom = $('#popup-scale-relative-to-page-zoom').prop('checked');
- options.general.popupScaleRelativeToVisualViewport = $('#popup-scale-relative-to-visual-viewport').prop('checked');
- options.general.showPitchAccentDownstepNotation = $('#show-pitch-accent-downstep-notation').prop('checked');
- options.general.showPitchAccentPositionNotation = $('#show-pitch-accent-position-notation').prop('checked');
- options.general.showPitchAccentGraph = $('#show-pitch-accent-graph').prop('checked');
- options.general.showIframePopupsInRootFrame = $('#show-iframe-popups-in-root-frame').prop('checked');
- options.general.popupTheme = $('#popup-theme').val();
- options.general.popupOuterTheme = $('#popup-outer-theme').val();
- options.general.customPopupCss = $('#custom-popup-css').val();
- options.general.customPopupOuterCss = $('#custom-popup-outer-css').val();
-
- options.audio.enabled = $('#audio-playback-enabled').prop('checked');
- options.audio.autoPlay = $('#auto-play-audio').prop('checked');
- options.audio.volume = parseFloat($('#audio-playback-volume').val());
- options.audio.customSourceUrl = $('#audio-custom-source').val();
- options.audio.textToSpeechVoice = $('#text-to-speech-voice').val();
-
- options.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked');
- options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked');
- options.scanning.selectText = $('#select-matched-text').prop('checked');
- options.scanning.alphanumeric = $('#search-alphanumeric').prop('checked');
- options.scanning.autoHideResults = $('#auto-hide-results').prop('checked');
- options.scanning.deepDomScan = $('#deep-dom-scan').prop('checked');
- options.scanning.enablePopupSearch = $('#enable-search-within-first-popup').prop('checked');
- options.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked');
- options.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked');
- options.scanning.enableSearchTags = $('#enable-search-tags').prop('checked');
- options.scanning.delay = parseInt($('#scan-delay').val(), 10);
- options.scanning.length = parseInt($('#scan-length').val(), 10);
- options.scanning.modifier = $('#scan-modifier-key').val();
- options.scanning.popupNestingMaxDepth = parseInt($('#popup-nesting-max-depth').val(), 10);
-
- options.translation.convertHalfWidthCharacters = $('#translation-convert-half-width-characters').val();
- options.translation.convertNumericCharacters = $('#translation-convert-numeric-characters').val();
- options.translation.convertAlphabeticCharacters = $('#translation-convert-alphabetic-characters').val();
- options.translation.convertHiraganaToKatakana = $('#translation-convert-hiragana-to-katakana').val();
- options.translation.convertKatakanaToHiragana = $('#translation-convert-katakana-to-hiragana').val();
- options.translation.collapseEmphaticSequences = $('#translation-collapse-emphatic-sequences').val();
-
- options.parsing.enableScanningParser = $('#parsing-scan-enable').prop('checked');
- options.parsing.enableMecabParser = $('#parsing-mecab-enable').prop('checked');
- options.parsing.termSpacing = $('#parsing-term-spacing').prop('checked');
- options.parsing.readingMode = $('#parsing-reading-mode').val();
-
- const optionsAnkiEnableOld = options.anki.enable;
- options.anki.enable = $('#anki-enable').prop('checked');
- options.anki.tags = utilBackgroundIsolate($('#card-tags').val().split(/[,; ]+/));
- options.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10);
- options.anki.server = $('#interface-server').val();
- options.anki.duplicateScope = $('#duplicate-scope').val();
- options.anki.screenshot.format = $('#screenshot-format').val();
- options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10);
-
- if (optionsAnkiEnableOld && !ankiErrorShown()) {
- options.anki.terms.deck = $('#anki-terms-deck').val();
- options.anki.terms.model = $('#anki-terms-model').val();
- options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#terms .anki-field-value')));
- options.anki.kanji.deck = $('#anki-kanji-deck').val();
- options.anki.kanji.model = $('#anki-kanji-model').val();
- options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#kanji .anki-field-value')));
- }
-}
-
-async function formWrite(options) {
- $('#enable').prop('checked', options.general.enable);
- $('#enable-clipboard-popups').prop('checked', options.general.enableClipboardPopups);
- $('#show-usage-guide').prop('checked', options.general.showGuide);
- $('#compact-tags').prop('checked', options.general.compactTags);
- $('#compact-glossaries').prop('checked', options.general.compactGlossaries);
- $('#result-output-mode').val(options.general.resultOutputMode);
- $('#show-debug-info').prop('checked', options.general.debugInfo);
- $('#show-advanced-options').prop('checked', options.general.showAdvanced);
- $('#max-displayed-results').val(options.general.maxResults);
- $('#popup-display-mode').val(options.general.popupDisplayMode);
- $('#popup-horizontal-text-position').val(options.general.popupHorizontalTextPosition);
- $('#popup-vertical-text-position').val(options.general.popupVerticalTextPosition);
- $('#popup-width').val(options.general.popupWidth);
- $('#popup-height').val(options.general.popupHeight);
- $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset);
- $('#popup-vertical-offset').val(options.general.popupVerticalOffset);
- $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2);
- $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2);
- $('#popup-scaling-factor').val(options.general.popupScalingFactor);
- $('#popup-scale-relative-to-page-zoom').prop('checked', options.general.popupScaleRelativeToPageZoom);
- $('#popup-scale-relative-to-visual-viewport').prop('checked', options.general.popupScaleRelativeToVisualViewport);
- $('#show-pitch-accent-downstep-notation').prop('checked', options.general.showPitchAccentDownstepNotation);
- $('#show-pitch-accent-position-notation').prop('checked', options.general.showPitchAccentPositionNotation);
- $('#show-pitch-accent-graph').prop('checked', options.general.showPitchAccentGraph);
- $('#show-iframe-popups-in-root-frame').prop('checked', options.general.showIframePopupsInRootFrame);
- $('#popup-theme').val(options.general.popupTheme);
- $('#popup-outer-theme').val(options.general.popupOuterTheme);
- $('#custom-popup-css').val(options.general.customPopupCss);
- $('#custom-popup-outer-css').val(options.general.customPopupOuterCss);
-
- $('#audio-playback-enabled').prop('checked', options.audio.enabled);
- $('#auto-play-audio').prop('checked', options.audio.autoPlay);
- $('#audio-playback-volume').val(options.audio.volume);
- $('#audio-custom-source').val(options.audio.customSourceUrl);
- $('#text-to-speech-voice').val(options.audio.textToSpeechVoice).attr('data-value', options.audio.textToSpeechVoice);
-
- $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse);
- $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled);
- $('#select-matched-text').prop('checked', options.scanning.selectText);
- $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric);
- $('#auto-hide-results').prop('checked', options.scanning.autoHideResults);
- $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan);
- $('#enable-search-within-first-popup').prop('checked', options.scanning.enablePopupSearch);
- $('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions);
- $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage);
- $('#enable-search-tags').prop('checked', options.scanning.enableSearchTags);
- $('#scan-delay').val(options.scanning.delay);
- $('#scan-length').val(options.scanning.length);
- $('#scan-modifier-key').val(options.scanning.modifier);
- $('#popup-nesting-max-depth').val(options.scanning.popupNestingMaxDepth);
-
- $('#translation-convert-half-width-characters').val(options.translation.convertHalfWidthCharacters);
- $('#translation-convert-numeric-characters').val(options.translation.convertNumericCharacters);
- $('#translation-convert-alphabetic-characters').val(options.translation.convertAlphabeticCharacters);
- $('#translation-convert-hiragana-to-katakana').val(options.translation.convertHiraganaToKatakana);
- $('#translation-convert-katakana-to-hiragana').val(options.translation.convertKatakanaToHiragana);
- $('#translation-collapse-emphatic-sequences').val(options.translation.collapseEmphaticSequences);
-
- $('#parsing-scan-enable').prop('checked', options.parsing.enableScanningParser);
- $('#parsing-mecab-enable').prop('checked', options.parsing.enableMecabParser);
- $('#parsing-term-spacing').prop('checked', options.parsing.termSpacing);
- $('#parsing-reading-mode').val(options.parsing.readingMode);
-
- $('#anki-enable').prop('checked', options.anki.enable);
- $('#card-tags').val(options.anki.tags.join(' '));
- $('#sentence-detection-extent').val(options.anki.sentenceExt);
- $('#interface-server').val(options.anki.server);
- $('#duplicate-scope').val(options.anki.duplicateScope);
- $('#screenshot-format').val(options.anki.screenshot.format);
- $('#screenshot-quality').val(options.anki.screenshot.quality);
-
- await ankiTemplatesUpdateValue();
- await onAnkiOptionsChanged(options);
- await onDictionaryOptionsChanged();
-
- formUpdateVisibility(options);
-}
-
-function formSetupEventListeners() {
- $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(onFormOptionsChanged);
-}
-
-function formUpdateVisibility(options) {
- document.documentElement.dataset.optionsAnkiEnable = `${!!options.anki.enable}`;
- document.documentElement.dataset.optionsGeneralDebugInfo = `${!!options.general.debugInfo}`;
- document.documentElement.dataset.optionsGeneralShowAdvanced = `${!!options.general.showAdvanced}`;
- document.documentElement.dataset.optionsGeneralResultOutputMode = `${options.general.resultOutputMode}`;
-
- if (options.general.debugInfo) {
- const temp = utilIsolate(options);
- if (typeof temp.anki.fieldTemplates === 'string') {
- temp.anki.fieldTemplates = '...';
- }
- const text = JSON.stringify(temp, null, 4);
- $('#debug').text(text);
- }
-}
-
-async function onFormOptionsChanged() {
- const optionsContext = getOptionsContext();
- const options = await getOptionsMutable(optionsContext);
-
- await formRead(options);
- await settingsSaveOptions();
- formUpdateVisibility(options);
-
- await onAnkiOptionsChanged(options);
-}
-
-
-function settingsGetSource() {
- return new Promise((resolve) => {
- chrome.tabs.getCurrent((tab) => resolve(`settings${tab ? tab.id : ''}`));
- });
-}
-
-async function settingsSaveOptions() {
- const source = await settingsGetSource();
- await apiOptionsSave(source);
-}
-
-async function onOptionsUpdated({source}) {
- const thisSource = await settingsGetSource();
- if (source === thisSource) { return; }
-
- const optionsContext = getOptionsContext();
- const options = await getOptionsMutable(optionsContext);
- await formWrite(options);
-}
-
-
function showExtensionInformation() {
const node = document.getElementById('extension-info');
if (node === null) { return; }
@@ -290,7 +42,7 @@ async function settingsPopulateModifierKeys() {
const scanModifierKeySelect = document.querySelector('#scan-modifier-key');
scanModifierKeySelect.textContent = '';
- const environment = await apiGetEnvironmentInfo();
+ const environment = await api.getEnvironmentInfo();
const modifierKeys = [
{value: 'none', name: 'None'},
...environment.modifiers.keys
@@ -303,26 +55,53 @@ async function settingsPopulateModifierKeys() {
}
}
+async function setupEnvironmentInfo() {
+ const {browser, platform} = await api.getEnvironmentInfo();
+ document.documentElement.dataset.browser = browser;
+ document.documentElement.dataset.operatingSystem = platform.os;
+}
+
-async function onReady() {
- apiForwardLogsToBackend();
+(async () => {
+ api.forwardLogsToBackend();
await yomichan.prepare();
+ setupEnvironmentInfo();
showExtensionInformation();
+ settingsPopulateModifierKeys();
- await settingsPopulateModifierKeys();
- formSetupEventListeners();
- appearanceInitialize();
- await audioSettingsInitialize();
- await profileOptionsSetup();
- await dictSettingsInitialize();
- ankiInitialize();
- ankiTemplatesInitialize();
- backupInitialize();
+ const optionsFull = await api.optionsGetFull();
- storageInfoInitialize();
+ const settingsController = new SettingsController(optionsFull.profileCurrent);
+ settingsController.prepare();
- yomichan.on('optionsUpdated', onOptionsUpdated);
-}
+ const storageController = new StorageController();
+ storageController.prepare();
+
+ const genericSettingController = new GenericSettingController(settingsController);
+ genericSettingController.prepare();
+
+ const clipboardPopupsController = new ClipboardPopupsController(settingsController);
+ clipboardPopupsController.prepare();
+
+ const popupPreviewController = new PopupPreviewController(settingsController);
+ popupPreviewController.prepare();
+
+ const audioController = new AudioController(settingsController);
+ audioController.prepare();
+
+ const profileController = new ProfileController(settingsController);
+ profileController.prepare();
+
+ const dictionaryController = new DictionaryController(settingsController, storageController);
+ dictionaryController.prepare();
+
+ const ankiController = new AnkiController(settingsController);
+ ankiController.prepare();
+
+ const ankiTemplatesController = new AnkiTemplatesController(settingsController, ankiController);
+ ankiTemplatesController.prepare();
-$(document).ready(() => onReady());
+ const settingsBackup = new SettingsBackup(settingsController);
+ settingsBackup.prepare();
+})();
diff --git a/ext/bg/js/settings/popup-preview-frame-main.js b/ext/bg/js/settings/popup-preview-frame-main.js
index 8228125f..866b9f57 100644
--- a/ext/bg/js/settings/popup-preview-frame-main.js
+++ b/ext/bg/js/settings/popup-preview-frame-main.js
@@ -16,11 +16,23 @@
*/
/* global
- * SettingsPopupPreview
- * apiForwardLogsToBackend
+ * PopupFactory
+ * PopupPreviewFrame
+ * api
*/
-(() => {
- apiForwardLogsToBackend();
- new SettingsPopupPreview();
+(async () => {
+ try {
+ api.forwardLogsToBackend();
+
+ const {frameId} = await api.frameInformationGet();
+
+ const popupFactory = new PopupFactory(frameId);
+ popupFactory.prepare();
+
+ const preview = new PopupPreviewFrame(frameId, popupFactory);
+ await preview.prepare();
+ } catch (e) {
+ yomichan.logError(e);
+ }
})();
diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js
index 8901a0c4..98630503 100644
--- a/ext/bg/js/settings/popup-preview-frame.js
+++ b/ext/bg/js/settings/popup-preview-frame.js
@@ -18,68 +18,78 @@
/* global
* Frontend
* Popup
- * PopupFactory
* TextSourceRange
- * apiFrameInformationGet
- * apiOptionsGet
+ * api
*/
-class SettingsPopupPreview {
- constructor() {
- this.frontend = null;
- this.apiOptionsGetOld = apiOptionsGet;
- this.popup = null;
- this.popupSetCustomOuterCssOld = null;
- this.popupShown = false;
- this.themeChangeTimeout = null;
- this.textSource = null;
- this.optionsContext = null;
+class PopupPreviewFrame {
+ constructor(frameId, popupFactory) {
+ this._frameId = frameId;
+ this._popupFactory = popupFactory;
+ this._frontend = null;
+ this._frontendGetOptionsContextOld = null;
+ this._apiOptionsGetOld = null;
+ this._popupSetCustomOuterCssOld = null;
+ this._popupShown = false;
+ this._themeChangeTimeout = null;
+ this._textSource = null;
+ this._optionsContext = null;
this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
this._windowMessageHandlers = new Map([
- ['prepare', ({optionsContext}) => this.prepare(optionsContext)],
- ['setText', ({text}) => this.setText(text)],
- ['setCustomCss', ({css}) => this.setCustomCss(css)],
- ['setCustomOuterCss', ({css}) => this.setCustomOuterCss(css)],
- ['updateOptionsContext', ({optionsContext}) => this.updateOptionsContext(optionsContext)]
+ ['setText', this._setText.bind(this)],
+ ['setCustomCss', this._setCustomCss.bind(this)],
+ ['setCustomOuterCss', this._setCustomOuterCss.bind(this)],
+ ['updateOptionsContext', this._updateOptionsContext.bind(this)]
]);
-
- window.addEventListener('message', this.onMessage.bind(this), false);
}
- async prepare(optionsContext) {
- this.optionsContext = optionsContext;
+ async prepare() {
+ window.addEventListener('message', this._onMessage.bind(this), false);
// Setup events
- document.querySelector('#theme-dark-checkbox').addEventListener('change', this.onThemeDarkCheckboxChanged.bind(this), false);
+ document.querySelector('#theme-dark-checkbox').addEventListener('change', this._onThemeDarkCheckboxChanged.bind(this), false);
// Overwrite API functions
- window.apiOptionsGet = this.apiOptionsGet.bind(this);
+ this._apiOptionsGetOld = api.optionsGet.bind(api);
+ api.optionsGet = this._apiOptionsGet.bind(this);
// Overwrite frontend
- const {frameId} = await apiFrameInformationGet();
-
- const popupFactory = new PopupFactory(frameId);
- await popupFactory.prepare();
+ this._frontend = new Frontend(
+ this._frameId,
+ this._popupFactory,
+ {
+ allowRootFramePopupProxy: false
+ }
+ );
+ this._frontendGetOptionsContextOld = this._frontend.getOptionsContext.bind(this._frontend);
+ this._frontend.getOptionsContext = this._getOptionsContext.bind(this);
+ await this._frontend.prepare();
+ this._frontend.setDisabledOverride(true);
+ this._frontend.canClearSelection = false;
+
+ const popup = this._frontend.popup;
+ popup.setChildrenSupported(false);
+
+ this._popupSetCustomOuterCssOld = popup.setCustomOuterCss.bind(popup);
+ popup.setCustomOuterCss = this._popupSetCustomOuterCss.bind(this);
- this.popup = popupFactory.getOrCreatePopup();
- this.popup.setChildrenSupported(false);
-
- this.popupSetCustomOuterCssOld = this.popup.setCustomOuterCss;
- this.popup.setCustomOuterCss = this.popupSetCustomOuterCss.bind(this);
+ // Update search
+ this._updateSearch();
+ }
- this.frontend = new Frontend(this.popup);
- this.frontend.getOptionsContext = async () => this.optionsContext;
- await this.frontend.prepare();
- this.frontend.setDisabledOverride(true);
- this.frontend.canClearSelection = false;
+ // Private
- // Update search
- this.updateSearch();
+ async _getOptionsContext() {
+ let optionsContext = this._optionsContext;
+ if (optionsContext === null) {
+ optionsContext = this._frontendGetOptionsContextOld();
+ }
+ return optionsContext;
}
- async apiOptionsGet(...args) {
- const options = await this.apiOptionsGetOld(...args);
+ async _apiOptionsGet(...args) {
+ const options = await this._apiOptionsGetOld(...args);
options.general.enable = true;
options.general.debugInfo = false;
options.general.popupWidth = 400;
@@ -94,9 +104,9 @@ class SettingsPopupPreview {
return options;
}
- async popupSetCustomOuterCss(...args) {
+ async _popupSetCustomOuterCss(...args) {
// This simulates the stylesheet priorities when injecting using the web extension API.
- const result = await this.popupSetCustomOuterCssOld.call(this.popup, ...args);
+ const result = await this._popupSetCustomOuterCssOld(...args);
const node = document.querySelector('#client-css');
if (node !== null && result !== null) {
@@ -106,7 +116,7 @@ class SettingsPopupPreview {
return result;
}
- onMessage(e) {
+ _onMessage(e) {
if (e.origin !== this._targetOrigin) { return; }
const {action, params} = e.data;
@@ -116,49 +126,57 @@ class SettingsPopupPreview {
handler(params);
}
- onThemeDarkCheckboxChanged(e) {
+ _onThemeDarkCheckboxChanged(e) {
document.documentElement.classList.toggle('dark', e.target.checked);
- if (this.themeChangeTimeout !== null) {
- clearTimeout(this.themeChangeTimeout);
+ if (this._themeChangeTimeout !== null) {
+ clearTimeout(this._themeChangeTimeout);
}
- this.themeChangeTimeout = setTimeout(() => {
- this.themeChangeTimeout = null;
- this.popup.updateTheme();
+ this._themeChangeTimeout = setTimeout(() => {
+ this._themeChangeTimeout = null;
+ const popup = this._frontend.popup;
+ if (popup === null) { return; }
+ popup.updateTheme();
}, 300);
}
- setText(text) {
+ _setText({text}) {
const exampleText = document.querySelector('#example-text');
if (exampleText === null) { return; }
exampleText.textContent = text;
- this.updateSearch();
+ if (this._frontend === null) { return; }
+ this._updateSearch();
}
- setInfoVisible(visible) {
+ _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; }
- this.popup.setCustomCss(css);
+ _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; }
- this.popup.setCustomOuterCss(css, false);
+ _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;
- await this.frontend.updateOptions();
- await this.updateSearch();
+ async _updateOptionsContext({optionsContext}) {
+ this._optionsContext = optionsContext;
+ if (this._frontend === null) { return; }
+ await this._frontend.updateOptions();
+ await this._updateSearch();
}
- async updateSearch() {
+ async _updateSearch() {
const exampleText = document.querySelector('#example-text');
if (exampleText === null) { return; }
@@ -170,17 +188,18 @@ class SettingsPopupPreview {
const source = new TextSourceRange(range, range.toString(), null, null);
try {
- await this.frontend.setTextSource(source);
+ await this._frontend.setTextSource(source);
} finally {
source.cleanup();
}
- this.textSource = source;
- await this.frontend.showContentCompleted();
+ this._textSource = source;
+ await this._frontend.showContentCompleted();
- if (this.popup.isVisibleSync()) {
- this.popupShown = true;
+ const popup = this._frontend.popup;
+ if (popup !== null && popup.isVisibleSync()) {
+ this._popupShown = true;
}
- this.setInfoVisible(!this.popupShown);
+ this._setInfoVisible(!this._popupShown);
}
}
diff --git a/ext/bg/js/settings/popup-preview.js b/ext/bg/js/settings/popup-preview.js
index fdc3dd94..d4145b76 100644
--- a/ext/bg/js/settings/popup-preview.js
+++ b/ext/bg/js/settings/popup-preview.js
@@ -16,69 +16,88 @@
*/
/* global
- * getOptionsContext
* wanakana
*/
-function appearanceInitialize() {
- let previewVisible = false;
- $('#settings-popup-preview-button').on('click', () => {
- if (previewVisible) { return; }
- showAppearancePreview();
- previewVisible = true;
- });
-}
+class PopupPreviewController {
+ constructor(settingsController) {
+ this._settingsController = settingsController;
+ this._previewVisible = false;
+ this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
+ this._frame = null;
+ this._previewTextInput = null;
+ }
+
+ prepare() {
+ document.querySelector('#settings-popup-preview-button').addEventListener('click', this._onShowPopupPreviewButtonClick.bind(this), false);
+ }
+
+ // 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;
+
+ 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 = '/bg/settings-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);
+ }
+
+ _onTextChange(e) {
+ this._setText(e.currentTarget.value);
+ }
+
+ _onCustomCssChange(e) {
+ this._invoke('setCustomCss', {css: e.currentTarget.value});
+ }
+
+ _onCustomOuterCssChange(e) {
+ this._invoke('setCustomOuterCss', {css: e.currentTarget.value});
+ }
+
+ _onOptionsContextChange() {
+ const optionsContext = this._settingsController.getOptionsContext();
+ this._invoke('updateOptionsContext', {optionsContext});
+ }
+
+ _setText(text) {
+ this._invoke('setText', {text});
+ }
-function showAppearancePreview() {
- const container = $('#settings-popup-preview-container');
- const buttonContainer = $('#settings-popup-preview-button-container');
- const settings = $('#settings-popup-preview-settings');
- const text = $('#settings-popup-preview-text');
- const customCss = $('#custom-popup-css');
- const customOuterCss = $('#custom-popup-outer-css');
-
- const frame = document.createElement('iframe');
- frame.src = '/bg/settings-popup-preview.html';
- frame.id = 'settings-popup-preview-frame';
-
- wanakana.bind(text[0]);
-
- const targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
-
- text.on('input', () => {
- const action = 'setText';
- const params = {text: text.val()};
- frame.contentWindow.postMessage({action, params}, targetOrigin);
- });
- customCss.on('input', () => {
- const action = 'setCustomCss';
- const params = {css: customCss.val()};
- frame.contentWindow.postMessage({action, params}, targetOrigin);
- });
- customOuterCss.on('input', () => {
- const action = 'setCustomOuterCss';
- const params = {css: customOuterCss.val()};
- frame.contentWindow.postMessage({action, params}, targetOrigin);
- });
-
- const updateOptionsContext = () => {
- const action = 'updateOptionsContext';
- const params = {
- optionsContext: getOptionsContext()
- };
- frame.contentWindow.postMessage({action, params}, targetOrigin);
- };
- yomichan.on('modifyingProfileChange', updateOptionsContext);
-
- frame.addEventListener('load', () => {
- const action = 'prepare';
- const params = {
- optionsContext: getOptionsContext()
- };
- frame.contentWindow.postMessage({action, params}, targetOrigin);
- });
-
- container.append(frame);
- buttonContainer.remove();
- settings.css('display', '');
+ _invoke(action, params) {
+ if (this._frame === null || this._frame.contentWindow === null) { return; }
+ this._frame.contentWindow.postMessage({action, params}, this._targetOrigin);
+ }
}
diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js
index bdf5a13d..2449ab44 100644
--- a/ext/bg/js/settings/profiles.js
+++ b/ext/bg/js/settings/profiles.js
@@ -17,288 +17,271 @@
/* global
* ConditionsUI
- * apiOptionsGetFull
* conditionsClearCaches
- * formWrite
- * getOptionsFullMutable
- * getOptionsMutable
* profileConditionsDescriptor
* profileConditionsDescriptorPromise
- * settingsSaveOptions
* utilBackgroundIsolate
*/
-let currentProfileIndex = 0;
-let profileConditionsContainer = null;
-
-function getOptionsContext() {
- return {
- index: currentProfileIndex
- };
-}
-
-
-async function profileOptionsSetup() {
- const optionsFull = await getOptionsFullMutable();
- currentProfileIndex = optionsFull.profileCurrent;
-
- profileOptionsSetupEventListeners();
- await profileOptionsUpdateTarget(optionsFull);
-}
-
-function profileOptionsSetupEventListeners() {
- $('#profile-target').change(onTargetProfileChanged);
- $('#profile-name').change(onProfileNameChanged);
- $('#profile-add').click(onProfileAdd);
- $('#profile-remove').click(onProfileRemove);
- $('#profile-remove-confirm').click(onProfileRemoveConfirm);
- $('#profile-copy').click(onProfileCopy);
- $('#profile-copy-confirm').click(onProfileCopyConfirm);
- $('#profile-move-up').click(() => onProfileMove(-1));
- $('#profile-move-down').click(() => onProfileMove(1));
- $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change(onProfileOptionsChanged);
-}
+class ProfileController {
+ constructor(settingsController) {
+ this._settingsController = settingsController;
+ this._conditionsContainer = null;
+ }
-function tryGetIntegerValue(selector, min, max) {
- const value = parseInt($(selector).val(), 10);
- return (
- typeof value === 'number' &&
- Number.isFinite(value) &&
- Math.floor(value) === value &&
- value >= min &&
- value < max
- ) ? value : null;
-}
+ async prepare() {
+ $('#profile-target').change(this._onTargetProfileChanged.bind(this));
+ $('#profile-name').change(this._onNameChanged.bind(this));
+ $('#profile-add').click(this._onAdd.bind(this));
+ $('#profile-remove').click(this._onRemove.bind(this));
+ $('#profile-remove-confirm').click(this._onRemoveConfirm.bind(this));
+ $('#profile-copy').click(this._onCopy.bind(this));
+ $('#profile-copy-confirm').click(this._onCopyConfirm.bind(this));
+ $('#profile-move-up').click(() => this._onMove(-1));
+ $('#profile-move-down').click(() => this._onMove(1));
+ $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change(this._onInputChanged.bind(this));
+
+ this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
+
+ this._onOptionsChanged();
+ }
-async function profileFormRead(optionsFull) {
- const profile = optionsFull.profiles[currentProfileIndex];
+ // Private
- // Current profile
- const index = tryGetIntegerValue('#profile-active', 0, optionsFull.profiles.length);
- if (index !== null) {
- optionsFull.profileCurrent = index;
+ async _onOptionsChanged() {
+ const optionsFull = await this._settingsController.getOptionsFullMutable();
+ await this._formWrite(optionsFull);
}
- // Profile name
- profile.name = $('#profile-name').val();
-}
-
-async function profileFormWrite(optionsFull) {
- const profile = optionsFull.profiles[currentProfileIndex];
+ _tryGetIntegerValue(selector, min, max) {
+ const value = parseInt($(selector).val(), 10);
+ return (
+ typeof value === 'number' &&
+ Number.isFinite(value) &&
+ Math.floor(value) === value &&
+ value >= min &&
+ value < max
+ ) ? value : null;
+ }
- profileOptionsPopulateSelect($('#profile-active'), optionsFull.profiles, optionsFull.profileCurrent, null);
- profileOptionsPopulateSelect($('#profile-target'), optionsFull.profiles, currentProfileIndex, null);
- $('#profile-remove').prop('disabled', optionsFull.profiles.length <= 1);
- $('#profile-copy').prop('disabled', optionsFull.profiles.length <= 1);
- $('#profile-move-up').prop('disabled', currentProfileIndex <= 0);
- $('#profile-move-down').prop('disabled', currentProfileIndex >= optionsFull.profiles.length - 1);
+ async _formRead(optionsFull) {
+ const currentProfileIndex = this._settingsController.profileIndex;
+ const profile = optionsFull.profiles[currentProfileIndex];
- $('#profile-name').val(profile.name);
+ // Current profile
+ const index = this._tryGetIntegerValue('#profile-active', 0, optionsFull.profiles.length);
+ if (index !== null) {
+ optionsFull.profileCurrent = index;
+ }
- if (profileConditionsContainer !== null) {
- profileConditionsContainer.cleanup();
+ // Profile name
+ profile.name = $('#profile-name').val();
}
- await profileConditionsDescriptorPromise;
- profileConditionsContainer = new ConditionsUI.Container(
- profileConditionsDescriptor,
- 'popupLevel',
- profile.conditionGroups,
- $('#profile-condition-groups'),
- $('#profile-add-condition-group')
- );
- profileConditionsContainer.save = () => {
- settingsSaveOptions();
- conditionsClearCaches(profileConditionsDescriptor);
- };
- profileConditionsContainer.isolate = utilBackgroundIsolate;
-}
+ async _formWrite(optionsFull) {
+ const currentProfileIndex = this._settingsController.profileIndex;
+ const profile = optionsFull.profiles[currentProfileIndex];
-function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndices) {
- select.empty();
+ this._populateSelect($('#profile-active'), optionsFull.profiles, optionsFull.profileCurrent, null);
+ this._populateSelect($('#profile-target'), optionsFull.profiles, currentProfileIndex, null);
+ $('#profile-remove').prop('disabled', optionsFull.profiles.length <= 1);
+ $('#profile-copy').prop('disabled', optionsFull.profiles.length <= 1);
+ $('#profile-move-up').prop('disabled', currentProfileIndex <= 0);
+ $('#profile-move-down').prop('disabled', currentProfileIndex >= optionsFull.profiles.length - 1);
+ $('#profile-name').val(profile.name);
- for (let i = 0; i < profiles.length; ++i) {
- if (ignoreIndices !== null && ignoreIndices.indexOf(i) >= 0) {
- continue;
+ if (this._conditionsContainer !== null) {
+ this._conditionsContainer.cleanup();
}
- const profile = profiles[i];
- select.append($(`<option value="${i}">${profile.name}</option>`));
- }
- select.val(`${currentValue}`);
-}
+ await profileConditionsDescriptorPromise;
+ this._conditionsContainer = new ConditionsUI.Container(
+ profileConditionsDescriptor,
+ 'popupLevel',
+ profile.conditionGroups,
+ $('#profile-condition-groups'),
+ $('#profile-add-condition-group')
+ );
+ this._conditionsContainer.save = () => {
+ this._settingsController.save();
+ conditionsClearCaches(profileConditionsDescriptor);
+ };
+ this._conditionsContainer.isolate = utilBackgroundIsolate;
+ }
-async function profileOptionsUpdateTarget(optionsFull) {
- await profileFormWrite(optionsFull);
+ _populateSelect(select, profiles, currentValue, ignoreIndices) {
+ select.empty();
- const optionsContext = getOptionsContext();
- const options = await getOptionsMutable(optionsContext);
- await formWrite(options);
-}
-function profileOptionsCreateCopyName(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;
+ for (let i = 0; i < profiles.length; ++i) {
+ if (ignoreIndices !== null && ignoreIndices.indexOf(i) >= 0) {
+ continue;
+ }
+ const profile = profiles[i];
+ select.append($(`<option value="${i}">${profile.name}</option>`));
}
+
+ select.val(`${currentValue}`);
}
- 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 = ' ';
+ _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 {
- ++index;
+ prefix = match[1];
+ suffix = match[5];
+ if (typeof match[2] === 'string') {
+ space = match[3];
+ index = parseInt(match[4], 10) + 1;
+ } else {
+ space = ' ';
+ index = 2;
+ }
}
- }
-}
-async function onProfileOptionsChanged(e) {
- if (!e.originalEvent && !e.isTrigger) {
- return;
+ 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;
+ }
+ }
}
- const optionsFull = await getOptionsFullMutable();
- await profileFormRead(optionsFull);
- await settingsSaveOptions();
-}
+ async _onInputChanged(e) {
+ if (!e.originalEvent && !e.isTrigger) {
+ return;
+ }
-async function onTargetProfileChanged() {
- const optionsFull = await getOptionsFullMutable();
- const index = tryGetIntegerValue('#profile-target', 0, optionsFull.profiles.length);
- if (index === null || currentProfileIndex === index) {
- return;
+ const optionsFull = await this._settingsController.getOptionsFullMutable();
+ await this._formRead(optionsFull);
+ await this._settingsController.save();
}
- currentProfileIndex = index;
+ async _onTargetProfileChanged() {
+ const optionsFull = await this._settingsController.getOptionsFullMutable();
+ const currentProfileIndex = this._settingsController.profileIndex;
+ const index = this._tryGetIntegerValue('#profile-target', 0, optionsFull.profiles.length);
+ if (index === null || currentProfileIndex === index) {
+ return;
+ }
- await profileOptionsUpdateTarget(optionsFull);
+ this._settingsController.profileIndex = index;
+ }
- yomichan.trigger('modifyingProfileChange');
-}
+ async _onAdd() {
+ const optionsFull = await this._settingsController.getOptionsFullMutable();
+ const currentProfileIndex = this._settingsController.profileIndex;
+ const profile = utilBackgroundIsolate(optionsFull.profiles[currentProfileIndex]);
+ profile.name = this._createCopyName(profile.name, optionsFull.profiles, 100);
+ optionsFull.profiles.push(profile);
-async function onProfileAdd() {
- const optionsFull = await getOptionsFullMutable();
- const profile = utilBackgroundIsolate(optionsFull.profiles[currentProfileIndex]);
- profile.name = profileOptionsCreateCopyName(profile.name, optionsFull.profiles, 100);
- optionsFull.profiles.push(profile);
+ this._settingsController.profileIndex = optionsFull.profiles.length - 1;
- currentProfileIndex = optionsFull.profiles.length - 1;
+ await this._settingsController.save();
+ }
- await profileOptionsUpdateTarget(optionsFull);
- await settingsSaveOptions();
+ async _onRemove(e) {
+ if (e.shiftKey) {
+ return await this._onRemoveConfirm();
+ }
- yomichan.trigger('modifyingProfileChange');
-}
+ const optionsFull = await this._settingsController.getOptionsFull();
+ if (optionsFull.profiles.length <= 1) {
+ return;
+ }
-async function onProfileRemove(e) {
- if (e.shiftKey) {
- return await onProfileRemoveConfirm();
- }
+ const currentProfileIndex = this._settingsController.profileIndex;
+ const profile = optionsFull.profiles[currentProfileIndex];
- const optionsFull = await apiOptionsGetFull();
- if (optionsFull.profiles.length <= 1) {
- return;
+ $('#profile-remove-modal-profile-name').text(profile.name);
+ $('#profile-remove-modal').modal('show');
}
- const profile = optionsFull.profiles[currentProfileIndex];
+ async _onRemoveConfirm() {
+ $('#profile-remove-modal').modal('hide');
- $('#profile-remove-modal-profile-name').text(profile.name);
- $('#profile-remove-modal').modal('show');
-}
+ const optionsFull = await this._settingsController.getOptionsFullMutable();
+ if (optionsFull.profiles.length <= 1) {
+ return;
+ }
-async function onProfileRemoveConfirm() {
- $('#profile-remove-modal').modal('hide');
+ const currentProfileIndex = this._settingsController.profileIndex;
+ optionsFull.profiles.splice(currentProfileIndex, 1);
- const optionsFull = await getOptionsFullMutable();
- if (optionsFull.profiles.length <= 1) {
- return;
- }
+ if (currentProfileIndex >= optionsFull.profiles.length) {
+ this._settingsController.profileIndex = optionsFull.profiles.length - 1;
+ }
- optionsFull.profiles.splice(currentProfileIndex, 1);
+ if (optionsFull.profileCurrent >= optionsFull.profiles.length) {
+ optionsFull.profileCurrent = optionsFull.profiles.length - 1;
+ }
- if (currentProfileIndex >= optionsFull.profiles.length) {
- --currentProfileIndex;
+ await this._settingsController.save();
}
- if (optionsFull.profileCurrent >= optionsFull.profiles.length) {
- optionsFull.profileCurrent = optionsFull.profiles.length - 1;
+ _onNameChanged() {
+ const currentProfileIndex = this._settingsController.profileIndex;
+ $('#profile-active, #profile-target').find(`[value="${currentProfileIndex}"]`).text(this.value);
}
- await profileOptionsUpdateTarget(optionsFull);
- await settingsSaveOptions();
-
- yomichan.trigger('modifyingProfileChange');
-}
+ async _onMove(offset) {
+ const optionsFull = await this._settingsController.getOptionsFullMutable();
+ const currentProfileIndex = this._settingsController.profileIndex;
+ const index = currentProfileIndex + offset;
+ if (index < 0 || index >= optionsFull.profiles.length) {
+ return;
+ }
-function onProfileNameChanged() {
- $('#profile-active, #profile-target').find(`[value="${currentProfileIndex}"]`).text(this.value);
-}
+ const profile = optionsFull.profiles[currentProfileIndex];
+ optionsFull.profiles.splice(currentProfileIndex, 1);
+ optionsFull.profiles.splice(index, 0, profile);
-async function onProfileMove(offset) {
- const optionsFull = await getOptionsFullMutable();
- const index = currentProfileIndex + offset;
- if (index < 0 || index >= optionsFull.profiles.length) {
- return;
- }
+ if (optionsFull.profileCurrent === currentProfileIndex) {
+ optionsFull.profileCurrent = index;
+ }
- const profile = optionsFull.profiles[currentProfileIndex];
- optionsFull.profiles.splice(currentProfileIndex, 1);
- optionsFull.profiles.splice(index, 0, profile);
+ this._settingsController.profileIndex = index;
- if (optionsFull.profileCurrent === currentProfileIndex) {
- optionsFull.profileCurrent = index;
+ await this._settingsController.save();
}
- currentProfileIndex = index;
-
- await profileOptionsUpdateTarget(optionsFull);
- await settingsSaveOptions();
-
- yomichan.trigger('modifyingProfileChange');
-}
+ async _onCopy() {
+ const optionsFull = await this._settingsController.getOptionsFullMutable();
+ if (optionsFull.profiles.length <= 1) {
+ return;
+ }
-async function onProfileCopy() {
- const optionsFull = await apiOptionsGetFull();
- if (optionsFull.profiles.length <= 1) {
- return;
+ const currentProfileIndex = this._settingsController.profileIndex;
+ this._populateSelect($('#profile-copy-source'), optionsFull.profiles, currentProfileIndex === 0 ? 1 : 0, [currentProfileIndex]);
+ $('#profile-copy-modal').modal('show');
}
- profileOptionsPopulateSelect($('#profile-copy-source'), optionsFull.profiles, currentProfileIndex === 0 ? 1 : 0, [currentProfileIndex]);
- $('#profile-copy-modal').modal('show');
-}
-
-async function onProfileCopyConfirm() {
- $('#profile-copy-modal').modal('hide');
+ async _onCopyConfirm() {
+ $('#profile-copy-modal').modal('hide');
- const optionsFull = await getOptionsFullMutable();
- const index = tryGetIntegerValue('#profile-copy-source', 0, optionsFull.profiles.length);
- if (index === null || index === currentProfileIndex) {
- return;
- }
+ const optionsFull = await this._settingsController.getOptionsFullMutable();
+ const index = this._tryGetIntegerValue('#profile-copy-source', 0, optionsFull.profiles.length);
+ const currentProfileIndex = this._settingsController.profileIndex;
+ if (index === null || index === currentProfileIndex) {
+ return;
+ }
- const profileOptions = utilBackgroundIsolate(optionsFull.profiles[index].options);
- optionsFull.profiles[currentProfileIndex].options = profileOptions;
+ const profileOptions = utilBackgroundIsolate(optionsFull.profiles[index].options);
+ optionsFull.profiles[currentProfileIndex].options = profileOptions;
- await profileOptionsUpdateTarget(optionsFull);
- await settingsSaveOptions();
+ await this._settingsController.save();
+ }
}
diff --git a/ext/bg/js/settings/settings-controller.js b/ext/bg/js/settings/settings-controller.js
new file mode 100644
index 00000000..87dea408
--- /dev/null
+++ b/ext/bg/js/settings/settings-controller.js
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 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
+ * utilBackend
+ * utilBackgroundIsolate
+ */
+
+class SettingsController extends EventDispatcher {
+ constructor(profileIndex=0) {
+ super();
+ this._profileIndex = profileIndex;
+ this._source = yomichan.generateId(16);
+ }
+
+ get source() {
+ return this._source;
+ }
+
+ get profileIndex() {
+ return this._profileIndex;
+ }
+
+ set profileIndex(value) {
+ if (this._profileIndex === value) { return; }
+ this._setProfileIndex(value);
+ }
+
+ prepare() {
+ yomichan.on('optionsUpdated', this._onOptionsUpdated.bind(this));
+ }
+
+ async save() {
+ await api.optionsSave(this._source);
+ }
+
+ async getOptions() {
+ const optionsContext = this.getOptionsContext();
+ return await api.optionsGet(optionsContext);
+ }
+
+ async getOptionsFull() {
+ return await api.optionsGetFull();
+ }
+
+ async getOptionsMutable() {
+ const optionsContext = this.getOptionsContext();
+ return utilBackend().getOptions(utilBackgroundIsolate(optionsContext));
+ }
+
+ async getOptionsFullMutable() {
+ return utilBackend().getFullOptions();
+ }
+
+ async setAllSettings(value) {
+ const profileIndex = value.profileCurrent;
+ await 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}]);
+ }
+
+ getOptionsContext() {
+ return {index: this._profileIndex};
+ }
+
+ // 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 api.getSettings(targets);
+ }
+
+ async _modifySettings(targets, extraFields) {
+ targets = this._setupTargets(targets, extraFields);
+ return await api.modifySettings(targets, this._source);
+ }
+}
diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js
index d754a109..24c6d7ef 100644
--- a/ext/bg/js/settings/storage.js
+++ b/ext/bg/js/settings/storage.js
@@ -15,126 +15,117 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/* global
- * apiGetEnvironmentInfo
- */
-
-function storageBytesToLabeledString(size) {
- const base = 1000;
- const labels = [' bytes', 'KB', 'MB', 'GB'];
- let labelIndex = 0;
- while (size >= base) {
- size /= base;
- ++labelIndex;
+class StorageController {
+ constructor() {
+ this._mostRecentStorageEstimate = null;
+ this._storageEstimateFailed = false;
+ this._isUpdating = false;
}
- const label = labelIndex === 0 ? `${size}` : size.toFixed(1);
- return `${label}${labels[labelIndex]}`;
-}
-async function storageEstimate() {
- try {
- return (storageEstimate.mostRecent = await navigator.storage.estimate());
- } catch (e) {
- // NOP
+ prepare() {
+ this._preparePersistentStorage();
+ this.updateStats();
+ document.querySelector('#storage-refresh').addEventListener('click', this.updateStats.bind(this), false);
}
- return null;
-}
-storageEstimate.mostRecent = null;
-
-async function isStoragePeristent() {
- try {
- return await navigator.storage.persisted();
- } catch (e) {
- // NOP
- }
- return false;
-}
-
-async function storageInfoInitialize() {
- storagePersistInitialize();
- const {browser, platform} = await apiGetEnvironmentInfo();
- 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 valid = (estimate !== null);
-
- if (valid) {
- // Firefox reports usage as 0 when persistent storage is enabled.
- const finite = (estimate.usage > 0 || !(await isStoragePeristent()));
- if (finite) {
- document.querySelector('#storage-usage').textContent = storageBytesToLabeledString(estimate.usage);
- document.querySelector('#storage-quota').textContent = storageBytesToLabeledString(estimate.quota);
+ async updateStats() {
+ try {
+ this._isUpdating = true;
+
+ 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 this._isStoragePeristent()));
+ if (finite) {
+ 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);
+ }
+
+ document.querySelector('#storage-use').classList.toggle('storage-hidden', !valid);
+ document.querySelector('#storage-error').classList.toggle('storage-hidden', valid);
+
+ return valid;
+ } finally {
+ this._isUpdating = false;
}
- 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);
+ // Private
- storageSpinnerShow(false);
-}
+ async _preparePersistentStorage() {
+ if (!(navigator.storage && navigator.storage.persist)) {
+ // Not supported
+ return;
+ }
-function storageSpinnerShow(show) {
- const spinner = $('#storage-spinner');
- if (show) {
- spinner.show();
- } else {
- spinner.hide();
+ const info = document.querySelector('#storage-persist-info');
+ const button = document.querySelector('#storage-persist-button');
+ const checkbox = document.querySelector('#storage-persist-button-checkbox');
+
+ info.classList.remove('storage-hidden');
+ button.classList.remove('storage-hidden');
+
+ let persisted = await this._isStoragePeristent();
+ checkbox.checked = persisted;
+
+ button.addEventListener('click', async () => {
+ if (persisted) {
+ return;
+ }
+ let result = false;
+ try {
+ result = await navigator.storage.persist();
+ } catch (e) {
+ // NOP
+ }
+
+ if (result) {
+ persisted = true;
+ checkbox.checked = true;
+ this.updateStats();
+ } else {
+ document.querySelector('.storage-persist-fail-warning').classList.remove('storage-hidden');
+ }
+ }, false);
}
-}
-async function storagePersistInitialize() {
- if (!(navigator.storage && navigator.storage.persist)) {
- // Not supported
- return;
+ 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;
}
- const info = document.querySelector('#storage-persist-info');
- const button = document.querySelector('#storage-persist-button');
- const checkbox = document.querySelector('#storage-persist-button-checkbox');
-
- info.classList.remove('storage-hidden');
- button.classList.remove('storage-hidden');
-
- let persisted = await isStoragePeristent();
- checkbox.checked = persisted;
-
- button.addEventListener('click', async () => {
- if (persisted) {
- return;
+ _bytesToLabeledString(size) {
+ const base = 1000;
+ const labels = [' bytes', 'KB', 'MB', 'GB'];
+ let labelIndex = 0;
+ while (size >= base) {
+ size /= base;
+ ++labelIndex;
}
- let result = false;
+ const label = labelIndex === 0 ? `${size}` : size.toFixed(1);
+ return `${label}${labels[labelIndex]}`;
+ }
+
+ async _isStoragePeristent() {
try {
- result = await navigator.storage.persist();
+ return await navigator.storage.persisted();
} catch (e) {
// NOP
}
-
- if (result) {
- persisted = true;
- checkbox.checked = true;
- storageShowInfo();
- } else {
- $('.storage-persist-fail-warning').removeClass('storage-hidden');
- }
- }, false);
+ return false;
+ }
}