From 8408bee90a0a78a77e7c5834e633a0387f9f434c Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 9 Sep 2020 17:37:58 -0400 Subject: Settings controller file renaming (#794) * Rename SettingsBackup to BackupController * Rename files to more closely match classes * Improve organization of script imports --- ext/bg/js/settings/anki-controller.js | 305 ++++++++++++++ ext/bg/js/settings/anki-templates-controller.js | 180 ++++++++ ext/bg/js/settings/anki-templates.js | 180 -------- ext/bg/js/settings/anki.js | 305 -------------- ext/bg/js/settings/audio-controller.js | 234 +++++++++++ ext/bg/js/settings/audio.js | 234 ----------- ext/bg/js/settings/backup-controller.js | 376 +++++++++++++++++ ext/bg/js/settings/backup.js | 376 ----------------- ext/bg/js/settings/dictionaries.js | 520 ------------------------ ext/bg/js/settings/dictionary-controller.js | 520 ++++++++++++++++++++++++ ext/bg/js/settings/main.js | 4 +- ext/bg/js/settings/popup-preview-controller.js | 103 +++++ ext/bg/js/settings/popup-preview.js | 103 ----- ext/bg/js/settings/profile-controller.js | 282 +++++++++++++ ext/bg/js/settings/profiles.js | 282 ------------- ext/bg/js/settings/storage-controller.js | 131 ++++++ ext/bg/js/settings/storage.js | 131 ------ ext/bg/settings.html | 37 +- 18 files changed, 2152 insertions(+), 2151 deletions(-) create mode 100644 ext/bg/js/settings/anki-controller.js create mode 100644 ext/bg/js/settings/anki-templates-controller.js delete mode 100644 ext/bg/js/settings/anki-templates.js delete mode 100644 ext/bg/js/settings/anki.js create mode 100644 ext/bg/js/settings/audio-controller.js delete mode 100644 ext/bg/js/settings/audio.js create mode 100644 ext/bg/js/settings/backup-controller.js delete mode 100644 ext/bg/js/settings/backup.js delete mode 100644 ext/bg/js/settings/dictionaries.js create mode 100644 ext/bg/js/settings/dictionary-controller.js create mode 100644 ext/bg/js/settings/popup-preview-controller.js delete mode 100644 ext/bg/js/settings/popup-preview.js create mode 100644 ext/bg/js/settings/profile-controller.js delete mode 100644 ext/bg/js/settings/profiles.js create mode 100644 ext/bg/js/settings/storage-controller.js delete mode 100644 ext/bg/js/settings/storage.js diff --git a/ext/bg/js/settings/anki-controller.js b/ext/bg/js/settings/anki-controller.js new file mode 100644 index 00000000..0965e633 --- /dev/null +++ b/ext/bg/js/settings/anki-controller.js @@ -0,0 +1,305 @@ +/* + * 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 . + */ + +/* global + * api + */ + +class AnkiController { + constructor(settingsController) { + this._settingsController = settingsController; + } + + async prepare() { + for (const element of document.querySelectorAll('#anki-fields-container input,#anki-fields-container select')) { + element.addEventListener('change', this._onFieldsChanged.bind(this), false); + } + + for (const element of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) { + element.addEventListener('change', this._onModelChanged.bind(this), false); + } + + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); + } + + getFieldMarkers(type) { + switch (type) { + case 'terms': + return [ + 'audio', + 'clipboard-image', + 'cloze-body', + 'cloze-prefix', + 'cloze-suffix', + 'dictionary', + 'document-title', + 'expression', + 'furigana', + 'furigana-plain', + 'glossary', + 'glossary-brief', + 'pitch-accents', + 'pitch-accent-graphs', + 'pitch-accent-positions', + 'reading', + 'screenshot', + 'sentence', + 'tags', + 'url' + ]; + case 'kanji': + return [ + 'character', + 'clipboard-image', + 'cloze-body', + 'cloze-prefix', + 'cloze-suffix', + 'dictionary', + 'document-title', + 'glossary', + 'kunyomi', + 'onyomi', + 'screenshot', + 'sentence', + 'tags', + 'url' + ]; + default: + return []; + } + } + + 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; + } + + // Private + + async _onOptionsChanged({options}) { + if (!options.anki.enable) { + return; + } + + await this._deckAndModelPopulate(options); + await Promise.all([ + this._populateFields('terms', options.anki.terms.fields), + this._populateFields('kanji', options.anki.kanji.fields) + ]); + } + + _fieldsToDict(elements) { + const result = {}; + for (const element of elements) { + result[element.dataset.field] = element.value; + } + return result; + } + + _spinnerShow(show) { + const spinner = document.querySelector('#anki-spinner'); + spinner.hidden = !show; + } + + _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; + } + } + } + + _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 button = document.createElement('a'); + button.className = 'error-data-show-button'; + + const content = document.createElement('div'); + content.className = 'error-data-container'; + content.textContent = message; + content.hidden = true; + + button.addEventListener('click', () => content.hidden = !content.hidden, false); + + node.appendChild(button); + node.appendChild(content); + } + + _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); + } + + 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); + } + + 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; + } + } + + _createFieldTemplate(name, value, markers) { + const template = document.querySelector('#anki-field-template').content; + const content = document.importNode(template, true).firstChild; + + content.querySelector('.anki-field-name').textContent = name; + + const field = content.querySelector('.anki-field-value'); + field.dataset.field = name; + field.value = value; + + content.querySelector('.anki-field-marker-list').appendChild(this.getFieldMarkersHtml(markers)); + + return content; + } + + 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); + + 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); + + 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); + } + } + + _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); + } + + const tabId = node.dataset.ankiCardType; + if (tabId !== 'terms' && tabId !== 'kanji') { return; } + + const fields = {}; + for (const name of fieldNames) { + fields[name] = ''; + } + + await this._settingsController.setProfileSetting(`anki["${tabId}"].fields`, fields); + await this._populateFields(tabId, fields); + } + + 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/anki-templates-controller.js b/ext/bg/js/settings/anki-templates-controller.js new file mode 100644 index 00000000..87f13100 --- /dev/null +++ b/ext/bg/js/settings/anki-templates-controller.js @@ -0,0 +1,180 @@ +/* + * 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 . + */ + +/* global + * AnkiNoteBuilder + * api + */ + +class AnkiTemplatesController { + constructor(settingsController, ankiController) { + this._settingsController = settingsController; + this._ankiController = ankiController; + this._cachedDefinitionValue = null; + this._cachedDefinitionText = null; + this._defaultFieldTemplates = null; + } + + async prepare() { + this._defaultFieldTemplates = await api.getDefaultAnkiFieldTemplates(); + + const markers = new Set([ + ...this._ankiController.getFieldMarkers('terms'), + ...this._ankiController.getFieldMarkers('kanji') + ]); + const fragment = this._ankiController.getFieldMarkersHtml(markers); + + 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); + } + + 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); + + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); + } + + // Private + + _onOptionsChanged({options}) { + let templates = options.anki.fieldTemplates; + if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; } + document.querySelector('#field-templates').value = templates; + + this._onValidateCompile(); + } + + _onReset(e) { + e.preventDefault(); + $('#field-template-reset-modal').modal('show'); + } + + _onResetConfirm(e) { + e.preventDefault(); + + $('#field-template-reset-modal').modal('hide'); + + 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; + } + + // Overwrite + await this._settingsController.setProfileSetting('anki.fieldTemplates', templates); + + // Compile + this._onValidateCompile(); + } + + _onValidateCompile() { + const infoNode = document.querySelector('#field-template-compile-result'); + this._validate(infoNode, '{expression}', 'term-kanji', false, true); + } + + _onMarkerClicked(e) { + e.preventDefault(); + document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`; + } + + _onRender(e) { + e.preventDefault(); + + 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); + } + + async _getDefinition(text, optionsContext) { + if (this._cachedDefinitionText !== text) { + const {definitions} = await api.termsFind(text, {}, optionsContext); + if (definitions.length === 0) { return null; } + + this._cachedDefinitionValue = definitions[0]; + this._cachedDefinitionText = text; + } + return this._cachedDefinitionValue; + } + + 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), + getClipboardImage: api.clipboardGetImage.bind(api) + }); + const {general: {resultOutputMode, compactGlossaries}} = options; + const note = await ankiNoteBuilder.createNote({ + definition, + mode, + context, + templates, + resultOutputMode, + compactGlossaries, + modeOptions: { + fields: {field}, + deck: '', + model: '' + }, + errors: exceptions + }); + result = note.fields.field; + } + } catch (e) { + exceptions.push(e); + } + + 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-templates.js b/ext/bg/js/settings/anki-templates.js deleted file mode 100644 index 87f13100..00000000 --- a/ext/bg/js/settings/anki-templates.js +++ /dev/null @@ -1,180 +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 . - */ - -/* global - * AnkiNoteBuilder - * api - */ - -class AnkiTemplatesController { - constructor(settingsController, ankiController) { - this._settingsController = settingsController; - this._ankiController = ankiController; - this._cachedDefinitionValue = null; - this._cachedDefinitionText = null; - this._defaultFieldTemplates = null; - } - - async prepare() { - this._defaultFieldTemplates = await api.getDefaultAnkiFieldTemplates(); - - const markers = new Set([ - ...this._ankiController.getFieldMarkers('terms'), - ...this._ankiController.getFieldMarkers('kanji') - ]); - const fragment = this._ankiController.getFieldMarkersHtml(markers); - - 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); - } - - 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); - - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - - const options = await this._settingsController.getOptions(); - this._onOptionsChanged({options}); - } - - // Private - - _onOptionsChanged({options}) { - let templates = options.anki.fieldTemplates; - if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; } - document.querySelector('#field-templates').value = templates; - - this._onValidateCompile(); - } - - _onReset(e) { - e.preventDefault(); - $('#field-template-reset-modal').modal('show'); - } - - _onResetConfirm(e) { - e.preventDefault(); - - $('#field-template-reset-modal').modal('hide'); - - 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; - } - - // Overwrite - await this._settingsController.setProfileSetting('anki.fieldTemplates', templates); - - // Compile - this._onValidateCompile(); - } - - _onValidateCompile() { - const infoNode = document.querySelector('#field-template-compile-result'); - this._validate(infoNode, '{expression}', 'term-kanji', false, true); - } - - _onMarkerClicked(e) { - e.preventDefault(); - document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`; - } - - _onRender(e) { - e.preventDefault(); - - 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); - } - - async _getDefinition(text, optionsContext) { - if (this._cachedDefinitionText !== text) { - const {definitions} = await api.termsFind(text, {}, optionsContext); - if (definitions.length === 0) { return null; } - - this._cachedDefinitionValue = definitions[0]; - this._cachedDefinitionText = text; - } - return this._cachedDefinitionValue; - } - - 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), - getClipboardImage: api.clipboardGetImage.bind(api) - }); - const {general: {resultOutputMode, compactGlossaries}} = options; - const note = await ankiNoteBuilder.createNote({ - definition, - mode, - context, - templates, - resultOutputMode, - compactGlossaries, - modeOptions: { - fields: {field}, - deck: '', - model: '' - }, - errors: exceptions - }); - result = note.fields.field; - } - } catch (e) { - exceptions.push(e); - } - - 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 deleted file mode 100644 index 0965e633..00000000 --- a/ext/bg/js/settings/anki.js +++ /dev/null @@ -1,305 +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 . - */ - -/* global - * api - */ - -class AnkiController { - constructor(settingsController) { - this._settingsController = settingsController; - } - - async prepare() { - for (const element of document.querySelectorAll('#anki-fields-container input,#anki-fields-container select')) { - element.addEventListener('change', this._onFieldsChanged.bind(this), false); - } - - for (const element of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) { - element.addEventListener('change', this._onModelChanged.bind(this), false); - } - - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - - const options = await this._settingsController.getOptions(); - this._onOptionsChanged({options}); - } - - getFieldMarkers(type) { - switch (type) { - case 'terms': - return [ - 'audio', - 'clipboard-image', - 'cloze-body', - 'cloze-prefix', - 'cloze-suffix', - 'dictionary', - 'document-title', - 'expression', - 'furigana', - 'furigana-plain', - 'glossary', - 'glossary-brief', - 'pitch-accents', - 'pitch-accent-graphs', - 'pitch-accent-positions', - 'reading', - 'screenshot', - 'sentence', - 'tags', - 'url' - ]; - case 'kanji': - return [ - 'character', - 'clipboard-image', - 'cloze-body', - 'cloze-prefix', - 'cloze-suffix', - 'dictionary', - 'document-title', - 'glossary', - 'kunyomi', - 'onyomi', - 'screenshot', - 'sentence', - 'tags', - 'url' - ]; - default: - return []; - } - } - - 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; - } - - // Private - - async _onOptionsChanged({options}) { - if (!options.anki.enable) { - return; - } - - await this._deckAndModelPopulate(options); - await Promise.all([ - this._populateFields('terms', options.anki.terms.fields), - this._populateFields('kanji', options.anki.kanji.fields) - ]); - } - - _fieldsToDict(elements) { - const result = {}; - for (const element of elements) { - result[element.dataset.field] = element.value; - } - return result; - } - - _spinnerShow(show) { - const spinner = document.querySelector('#anki-spinner'); - spinner.hidden = !show; - } - - _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; - } - } - } - - _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 button = document.createElement('a'); - button.className = 'error-data-show-button'; - - const content = document.createElement('div'); - content.className = 'error-data-container'; - content.textContent = message; - content.hidden = true; - - button.addEventListener('click', () => content.hidden = !content.hidden, false); - - node.appendChild(button); - node.appendChild(content); - } - - _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); - } - - 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); - } - - 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; - } - } - - _createFieldTemplate(name, value, markers) { - const template = document.querySelector('#anki-field-template').content; - const content = document.importNode(template, true).firstChild; - - content.querySelector('.anki-field-name').textContent = name; - - const field = content.querySelector('.anki-field-value'); - field.dataset.field = name; - field.value = value; - - content.querySelector('.anki-field-marker-list').appendChild(this.getFieldMarkersHtml(markers)); - - return content; - } - - 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); - - 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); - - 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); - } - } - - _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); - } - - const tabId = node.dataset.ankiCardType; - if (tabId !== 'terms' && tabId !== 'kanji') { return; } - - const fields = {}; - for (const name of fieldNames) { - fields[name] = ''; - } - - await this._settingsController.setProfileSetting(`anki["${tabId}"].fields`, fields); - await this._populateFields(tabId, fields); - } - - 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-controller.js b/ext/bg/js/settings/audio-controller.js new file mode 100644 index 00000000..d389acb5 --- /dev/null +++ b/ext/bg/js/settings/audio-controller.js @@ -0,0 +1,234 @@ +/* + * 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 . + */ + +/* global + * AudioSystem + */ + +class AudioController { + constructor(settingsController) { + this._settingsController = settingsController; + this._audioSystem = null; + this._audioSourceContainer = null; + this._audioSourceAddButton = null; + this._audioSourceEntries = []; + } + + async prepare() { + this._audioSystem = new AudioSystem({ + audioUriBuilder: null, + useCache: true + }); + + this._audioSourceContainer = document.querySelector('.audio-source-list'); + this._audioSourceAddButton = document.querySelector('.audio-source-add'); + this._audioSourceContainer.textContent = ''; + + this._audioSourceAddButton.addEventListener('click', this._onAddAudioSource.bind(this), false); + + this._prepareTextToSpeech(); + + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); + } + + // Private + + _onOptionsChanged({options}) { + for (let i = this._audioSourceEntries.length - 1; i >= 0; --i) { + this._cleanupAudioSourceEntry(i); + } + + for (const audioSource of options.audio.sources) { + this._createAudioSourceEntry(audioSource); + } + } + + _prepareTextToSpeech() { + if (typeof speechSynthesis === 'undefined') { return; } + + 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; + } + + _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; + } + + _languageTagIsJapanese(languageTag) { + return ( + languageTag.startsWith('ja_') || + languageTag.startsWith('ja-') || + languageTag.startsWith('jpn-') + ); + } + + _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 + } + } + + _instantiateTemplate(templateSelector) { + const template = document.querySelector(templateSelector); + const content = document.importNode(template.content, true); + return content.firstChild; + } + + _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 + }; + + eventListeners.addEventListener(select, 'change', this._onAudioSourceSelectChange.bind(this, entry), false); + eventListeners.addEventListener(removeButton, 'click', this._onAudioSourceRemoveClicked.bind(this, entry), false); + + 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] + }]); + } + + 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/audio.js b/ext/bg/js/settings/audio.js deleted file mode 100644 index d389acb5..00000000 --- a/ext/bg/js/settings/audio.js +++ /dev/null @@ -1,234 +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 . - */ - -/* global - * AudioSystem - */ - -class AudioController { - constructor(settingsController) { - this._settingsController = settingsController; - this._audioSystem = null; - this._audioSourceContainer = null; - this._audioSourceAddButton = null; - this._audioSourceEntries = []; - } - - async prepare() { - this._audioSystem = new AudioSystem({ - audioUriBuilder: null, - useCache: true - }); - - this._audioSourceContainer = document.querySelector('.audio-source-list'); - this._audioSourceAddButton = document.querySelector('.audio-source-add'); - this._audioSourceContainer.textContent = ''; - - this._audioSourceAddButton.addEventListener('click', this._onAddAudioSource.bind(this), false); - - this._prepareTextToSpeech(); - - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - - const options = await this._settingsController.getOptions(); - this._onOptionsChanged({options}); - } - - // Private - - _onOptionsChanged({options}) { - for (let i = this._audioSourceEntries.length - 1; i >= 0; --i) { - this._cleanupAudioSourceEntry(i); - } - - for (const audioSource of options.audio.sources) { - this._createAudioSourceEntry(audioSource); - } - } - - _prepareTextToSpeech() { - if (typeof speechSynthesis === 'undefined') { return; } - - 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; - } - - _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; - } - - _languageTagIsJapanese(languageTag) { - return ( - languageTag.startsWith('ja_') || - languageTag.startsWith('ja-') || - languageTag.startsWith('jpn-') - ); - } - - _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 - } - } - - _instantiateTemplate(templateSelector) { - const template = document.querySelector(templateSelector); - const content = document.importNode(template.content, true); - return content.firstChild; - } - - _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 - }; - - eventListeners.addEventListener(select, 'change', this._onAudioSourceSelectChange.bind(this, entry), false); - eventListeners.addEventListener(removeButton, 'click', this._onAudioSourceRemoveClicked.bind(this, entry), false); - - 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] - }]); - } - - 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-controller.js b/ext/bg/js/settings/backup-controller.js new file mode 100644 index 00000000..ac1294e7 --- /dev/null +++ b/ext/bg/js/settings/backup-controller.js @@ -0,0 +1,376 @@ +/* + * 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. 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 . + */ + +/* global + * OptionsUtil + * api + */ + +class BackupController { + constructor(settingsController) { + this._settingsController = settingsController; + this._settingsExportToken = null; + this._settingsExportRevoke = null; + this._currentVersion = 0; + } + + 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); + } + + // 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(''); + } + + async _getSettingsExportData(date) { + const optionsFull = await this._settingsController.getOptionsFull(); + const environment = await api.getEnvironmentInfo(); + const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates(); + + // Format options + for (const {options} of optionsFull.profiles) { + if (options.anki.fieldTemplates === fieldTemplatesDefault || !options.anki.fieldTemplates) { + delete options.anki.fieldTemplates; // Default + } + } + + const data = { + version: this._currentVersion, + date: this._getSettingsExportDateString(date, '-', ' ', ':', 6), + url: chrome.runtime.getURL('/'), + manifest: chrome.runtime.getManifest(), + environment, + userAgent: navigator.userAgent, + options: optionsFull + }; + + return data; + } + + _saveBlob(blob, fileName) { + if (typeof navigator === 'object' && typeof navigator.msSaveBlob === 'function') { + if (navigator.msSaveBlob(blob)) { + return; + } + } + + const blobUrl = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = blobUrl; + a.download = fileName; + a.rel = 'noopener'; + a.target = '_blank'; + + const revoke = () => { + URL.revokeObjectURL(blobUrl); + a.href = ''; + this._settingsExportRevoke = null; + }; + this._settingsExportRevoke = revoke; + + a.dispatchEvent(new MouseEvent('click')); + setTimeout(revoke, 60000); + } + + async _onSettingsExportClick() { + if (this._settingsExportRevoke !== null) { + this._settingsExportRevoke(); + this._settingsExportRevoke = null; + } + + const date = new Date(Date.now()); + + 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; + + 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); + } + + _readFileArrayBuffer(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(reader.error); + reader.readAsArrayBuffer(file); + }); + } + + // Importing + + async _settingsImportSetOptionsFull(optionsFull) { + await this._settingsController.setAllSettings(optionsFull); + } + + _showSettingsImportError(error) { + yomichan.logError(error); + document.querySelector('#settings-import-error-modal-message').textContent = `${error}`; + $('#settings-import-error-modal').modal('show'); + } + + 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}; + } + + // Set message + const fragment = document.createDocumentFragment(); + for (const warning of warnings) { + const node = document.createElement('li'); + node.textContent = `${warning}`; + fragment.appendChild(node); + } + 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); + } + + resolve(result); + }; + + // Hook events + modalNode.on('hide.bs.modal', onModalHide); + for (const button of buttons) { + button.addEventListener('click', onButtonClick, false); + } + }); + } + + _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; + } + + _settingsImportSanitizeProfileOptions(options, dryRun) { + const warnings = []; + + 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; + } + } + } + + 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; + } + } + } + + return warnings; + } + + _settingsImportSanitizeOptions(optionsFull, dryRun) { + const warnings = new Set(); + + 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 = this._settingsImportSanitizeProfileOptions(options, dryRun); + for (const warning of warnings2) { + warnings.add(warning); + } + } + } + + return warnings; + } + + _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)); + } + } + + async _importSettingsFile(file) { + const dataString = this._utf8Decode(await this._readFileArrayBuffer(file)); + const data = JSON.parse(dataString); + + // Type check + if (!isObject(data)) { + throw new Error(`Invalid data type: ${typeof data}`); + } + + // Version check + const version = data.version; + if (!( + typeof version === 'number' && + Number.isFinite(version) && + version === Math.floor(version) + )) { + throw new Error(`Invalid version: ${version}`); + } + + if (!( + version >= 0 && + version <= this._currentVersion + )) { + throw new Error(`Unsupported version: ${version}`); + } + + // Verify options exists + let optionsFull = data.options; + if (!isObject(optionsFull)) { + throw new Error(`Invalid options type: ${typeof optionsFull}`); + } + + // Upgrade options + optionsFull = await OptionsUtil.update(optionsFull); + + // Check for warnings + const sanitizationWarnings = this._settingsImportSanitizeOptions(optionsFull, true); + + // Show sanitization warnings + if (sanitizationWarnings.size > 0) { + const {result, sanitize} = await this._showSettingsImportWarnings(sanitizationWarnings); + if (!result) { return; } + + if (sanitize !== false) { + this._settingsImportSanitizeOptions(optionsFull, false); + } + } + + // Assign options + await this._settingsImportSetOptionsFull(optionsFull); + } + + _onSettingsImportClick() { + document.querySelector('#settings-import-file').click(); + } + + async _onSettingsImportFileChange(e) { + const files = e.target.files; + if (files.length === 0) { return; } + + const file = files[0]; + e.target.value = null; + try { + await this._importSettingsFile(file); + } catch (error) { + this._showSettingsImportError(error); + } + } + + // Resetting + + _onSettingsResetClick() { + $('#settings-reset-modal').modal('show'); + } + + async _onSettingsResetConfirmClick() { + $('#settings-reset-modal').modal('hide'); + + // Get default options + const optionsFull = await OptionsUtil.getDefault(); + + // Assign options + await this._settingsImportSetOptionsFull(optionsFull); + } +} diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js deleted file mode 100644 index 57963cec..00000000 --- a/ext/bg/js/settings/backup.js +++ /dev/null @@ -1,376 +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. 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 . - */ - -/* global - * OptionsUtil - * api - */ - -class SettingsBackup { - constructor(settingsController) { - this._settingsController = settingsController; - this._settingsExportToken = null; - this._settingsExportRevoke = null; - this._currentVersion = 0; - } - - 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); - } - - // 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(''); - } - - async _getSettingsExportData(date) { - const optionsFull = await this._settingsController.getOptionsFull(); - const environment = await api.getEnvironmentInfo(); - const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates(); - - // Format options - for (const {options} of optionsFull.profiles) { - if (options.anki.fieldTemplates === fieldTemplatesDefault || !options.anki.fieldTemplates) { - delete options.anki.fieldTemplates; // Default - } - } - - const data = { - version: this._currentVersion, - date: this._getSettingsExportDateString(date, '-', ' ', ':', 6), - url: chrome.runtime.getURL('/'), - manifest: chrome.runtime.getManifest(), - environment, - userAgent: navigator.userAgent, - options: optionsFull - }; - - return data; - } - - _saveBlob(blob, fileName) { - if (typeof navigator === 'object' && typeof navigator.msSaveBlob === 'function') { - if (navigator.msSaveBlob(blob)) { - return; - } - } - - const blobUrl = URL.createObjectURL(blob); - - const a = document.createElement('a'); - a.href = blobUrl; - a.download = fileName; - a.rel = 'noopener'; - a.target = '_blank'; - - const revoke = () => { - URL.revokeObjectURL(blobUrl); - a.href = ''; - this._settingsExportRevoke = null; - }; - this._settingsExportRevoke = revoke; - - a.dispatchEvent(new MouseEvent('click')); - setTimeout(revoke, 60000); - } - - async _onSettingsExportClick() { - if (this._settingsExportRevoke !== null) { - this._settingsExportRevoke(); - this._settingsExportRevoke = null; - } - - const date = new Date(Date.now()); - - 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; - - 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); - } - - _readFileArrayBuffer(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result); - reader.onerror = () => reject(reader.error); - reader.readAsArrayBuffer(file); - }); - } - - // Importing - - async _settingsImportSetOptionsFull(optionsFull) { - await this._settingsController.setAllSettings(optionsFull); - } - - _showSettingsImportError(error) { - yomichan.logError(error); - document.querySelector('#settings-import-error-modal-message').textContent = `${error}`; - $('#settings-import-error-modal').modal('show'); - } - - 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}; - } - - // Set message - const fragment = document.createDocumentFragment(); - for (const warning of warnings) { - const node = document.createElement('li'); - node.textContent = `${warning}`; - fragment.appendChild(node); - } - 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); - } - - resolve(result); - }; - - // Hook events - modalNode.on('hide.bs.modal', onModalHide); - for (const button of buttons) { - button.addEventListener('click', onButtonClick, false); - } - }); - } - - _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; - } - - _settingsImportSanitizeProfileOptions(options, dryRun) { - const warnings = []; - - 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; - } - } - } - - 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; - } - } - } - - return warnings; - } - - _settingsImportSanitizeOptions(optionsFull, dryRun) { - const warnings = new Set(); - - 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 = this._settingsImportSanitizeProfileOptions(options, dryRun); - for (const warning of warnings2) { - warnings.add(warning); - } - } - } - - return warnings; - } - - _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)); - } - } - - async _importSettingsFile(file) { - const dataString = this._utf8Decode(await this._readFileArrayBuffer(file)); - const data = JSON.parse(dataString); - - // Type check - if (!isObject(data)) { - throw new Error(`Invalid data type: ${typeof data}`); - } - - // Version check - const version = data.version; - if (!( - typeof version === 'number' && - Number.isFinite(version) && - version === Math.floor(version) - )) { - throw new Error(`Invalid version: ${version}`); - } - - if (!( - version >= 0 && - version <= this._currentVersion - )) { - throw new Error(`Unsupported version: ${version}`); - } - - // Verify options exists - let optionsFull = data.options; - if (!isObject(optionsFull)) { - throw new Error(`Invalid options type: ${typeof optionsFull}`); - } - - // Upgrade options - optionsFull = await OptionsUtil.update(optionsFull); - - // Check for warnings - const sanitizationWarnings = this._settingsImportSanitizeOptions(optionsFull, true); - - // Show sanitization warnings - if (sanitizationWarnings.size > 0) { - const {result, sanitize} = await this._showSettingsImportWarnings(sanitizationWarnings); - if (!result) { return; } - - if (sanitize !== false) { - this._settingsImportSanitizeOptions(optionsFull, false); - } - } - - // Assign options - await this._settingsImportSetOptionsFull(optionsFull); - } - - _onSettingsImportClick() { - document.querySelector('#settings-import-file').click(); - } - - async _onSettingsImportFileChange(e) { - const files = e.target.files; - if (files.length === 0) { return; } - - const file = files[0]; - e.target.value = null; - try { - await this._importSettingsFile(file); - } catch (error) { - this._showSettingsImportError(error); - } - } - - // Resetting - - _onSettingsResetClick() { - $('#settings-reset-modal').modal('show'); - } - - async _onSettingsResetConfirmClick() { - $('#settings-reset-modal').modal('hide'); - - // Get default options - const optionsFull = await OptionsUtil.getDefault(); - - // Assign options - await this._settingsImportSetOptionsFull(optionsFull); - } -} diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js deleted file mode 100644 index 9292d2c4..00000000 --- a/ext/bg/js/settings/dictionaries.js +++ /dev/null @@ -1,520 +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 . - */ - -/* global - * api - * utilBackgroundIsolate - */ - -class SettingsDictionaryListUI extends EventDispatcher { - constructor(container, template, extraContainer, extraTemplate) { - super(); - this.container = container; - this.template = template; - this.extraContainer = extraContainer; - this.extraTemplate = extraTemplate; - this.optionsDictionaries = null; - this.dictionaries = null; - this.dictionaryEntries = []; - this.extra = null; - - document.querySelector('#dict-delete-confirm').addEventListener('click', this.onDictionaryConfirmDelete.bind(this), false); - } - - setOptionsDictionaries(optionsDictionaries) { - this.optionsDictionaries = optionsDictionaries; - if (this.dictionaries !== null) { - this.setDictionaries(this.dictionaries); - } - } - - setDictionaries(dictionaries) { - for (const dictionaryEntry of this.dictionaryEntries) { - dictionaryEntry.cleanup(); - } - - this.dictionaryEntries = []; - this.dictionaries = toIterable(dictionaries); - - if (this.optionsDictionaries === null) { - return; - } - - let changed = false; - for (const dictionaryInfo of this.dictionaries) { - if (this.createEntry(dictionaryInfo)) { - changed = true; - } - } - - this.updateDictionaryOrder(); - - const titles = this.dictionaryEntries.map((e) => e.dictionaryInfo.title); - const removeKeys = Object.keys(this.optionsDictionaries).filter((key) => titles.indexOf(key) < 0); - if (removeKeys.length > 0) { - for (const key of toIterable(removeKeys)) { - delete this.optionsDictionaries[key]; - } - changed = true; - } - - if (changed) { - this.save(); - } - } - - createEntry(dictionaryInfo) { - const title = dictionaryInfo.title; - let changed = false; - let optionsDictionary; - const optionsDictionaries = this.optionsDictionaries; - if (hasOwn(optionsDictionaries, title)) { - optionsDictionary = optionsDictionaries[title]; - } else { - optionsDictionary = SettingsDictionaryListUI.createDictionaryOptions(); - optionsDictionaries[title] = optionsDictionary; - changed = true; - } - - const content = document.importNode(this.template.content, true).firstChild; - - this.dictionaryEntries.push(new SettingsDictionaryEntryUI(this, dictionaryInfo, content, optionsDictionary)); - - return changed; - } - - static createDictionaryOptions() { - return utilBackgroundIsolate({ - priority: 0, - enabled: false, - allowSecondarySearches: false - }); - } - - createExtra(totalCounts, remainders, totalRemainder) { - const content = document.importNode(this.extraTemplate.content, true).firstChild; - this.extraContainer.appendChild(content); - return new SettingsDictionaryExtraUI(this, totalCounts, remainders, totalRemainder, content); - } - - setCounts(dictionaryCounts, totalCounts) { - const remainders = Object.assign({}, totalCounts); - const keys = Object.keys(remainders); - - for (let i = 0, ii = Math.min(this.dictionaryEntries.length, dictionaryCounts.length); i < ii; ++i) { - const counts = dictionaryCounts[i]; - this.dictionaryEntries[i].setCounts(counts); - - for (const key of keys) { - remainders[key] -= counts[key]; - } - } - - let totalRemainder = 0; - for (const key of keys) { - totalRemainder += remainders[key]; - } - - if (this.extra !== null) { - this.extra.cleanup(); - this.extra = null; - } - - if (totalRemainder > 0) { - this.extra = this.createExtra(totalCounts, remainders, totalRemainder); - } - } - - updateDictionaryOrder() { - const sortInfo = this.dictionaryEntries.map((e, i) => [e, i]); - sortInfo.sort((a, b) => { - const i = b[0].optionsDictionary.priority - a[0].optionsDictionary.priority; - return (i !== 0 ? i : a[1] - b[1]); - }); - - for (const [e] of sortInfo) { - this.container.appendChild(e.content); - } - } - - save() { - // Overwrite - } - - preventPageExit() { - // Overwrite - return {end: () => {}}; - } - - onDictionaryConfirmDelete(e) { - e.preventDefault(); - const n = document.querySelector('#dict-delete-modal'); - const title = n.dataset.dict; - delete n.dataset.dict; - $(n).modal('hide'); - - const index = this.dictionaryEntries.findIndex((entry) => entry.dictionaryInfo.title === title); - if (index >= 0) { - this.dictionaryEntries[index].deleteDictionary(); - } - } -} - -class SettingsDictionaryEntryUI { - constructor(parent, dictionaryInfo, content, optionsDictionary) { - this.parent = parent; - this.dictionaryInfo = dictionaryInfo; - this.optionsDictionary = optionsDictionary; - this.counts = null; - this.eventListeners = new EventListenerCollection(); - this.isDeleting = false; - - this.content = content; - this.enabledCheckbox = this.content.querySelector('.dict-enabled'); - this.allowSecondarySearchesCheckbox = this.content.querySelector('.dict-allow-secondary-searches'); - this.priorityInput = this.content.querySelector('.dict-priority'); - this.deleteButton = this.content.querySelector('.dict-delete-button'); - this.detailsToggleLink = this.content.querySelector('.dict-details-toggle-link'); - this.detailsContainer = this.content.querySelector('.dict-details'); - this.detailsTable = this.content.querySelector('.dict-details-table'); - - if (this.dictionaryInfo.version < 3) { - this.content.querySelector('.dict-outdated').hidden = false; - } - - this.setupDetails(dictionaryInfo); - - this.content.querySelector('.dict-title').textContent = this.dictionaryInfo.title; - this.content.querySelector('.dict-revision').textContent = `rev.${this.dictionaryInfo.revision}`; - this.content.querySelector('.dict-prefix-wildcard-searches-supported').checked = !!this.dictionaryInfo.prefixWildcardsSupported; - - this.applyValues(); - - this.eventListeners.addEventListener(this.enabledCheckbox, 'change', this.onEnabledChanged.bind(this), false); - this.eventListeners.addEventListener(this.allowSecondarySearchesCheckbox, 'change', this.onAllowSecondarySearchesChanged.bind(this), false); - this.eventListeners.addEventListener(this.priorityInput, 'change', this.onPriorityChanged.bind(this), false); - this.eventListeners.addEventListener(this.deleteButton, 'click', this.onDeleteButtonClicked.bind(this), false); - this.eventListeners.addEventListener(this.detailsToggleLink, 'click', this.onDetailsToggleLinkClicked.bind(this), false); - } - - setupDetails(dictionaryInfo) { - const targets = [ - ['Author', 'author'], - ['URL', 'url'], - ['Description', 'description'], - ['Attribution', 'attribution'] - ]; - - let count = 0; - for (const [label, key] of targets) { - const info = dictionaryInfo[key]; - if (typeof info !== 'string') { continue; } - - const n1 = document.createElement('div'); - n1.className = 'dict-details-entry'; - n1.dataset.type = key; - - const n2 = document.createElement('span'); - n2.className = 'dict-details-entry-label'; - n2.textContent = `${label}:`; - n1.appendChild(n2); - - const n3 = document.createElement('span'); - n3.className = 'dict-details-entry-info'; - n3.textContent = info; - n1.appendChild(n3); - - this.detailsTable.appendChild(n1); - - ++count; - } - - if (count === 0) { - this.detailsContainer.hidden = true; - this.detailsToggleLink.hidden = true; - } - } - - cleanup() { - if (this.content !== null) { - if (this.content.parentNode !== null) { - this.content.parentNode.removeChild(this.content); - } - this.content = null; - } - this.dictionaryInfo = null; - this.eventListeners.removeAllEventListeners(); - } - - setCounts(counts) { - this.counts = counts; - const node = this.content.querySelector('.dict-counts'); - node.textContent = JSON.stringify({ - info: this.dictionaryInfo, - counts - }, null, 4); - node.removeAttribute('hidden'); - } - - save() { - this.parent.save(); - } - - applyValues() { - this.enabledCheckbox.checked = this.optionsDictionary.enabled; - this.allowSecondarySearchesCheckbox.checked = this.optionsDictionary.allowSecondarySearches; - this.priorityInput.value = `${this.optionsDictionary.priority}`; - } - - async deleteDictionary() { - if (this.isDeleting) { - return; - } - - const progress = this.content.querySelector('.progress'); - progress.hidden = false; - const progressBar = this.content.querySelector('.progress-bar'); - this.isDeleting = true; - - const prevention = this.parent.preventPageExit(); - try { - const onProgress = ({processed, count, storeCount, storesProcesed}) => { - let percent = 0.0; - if (count > 0 && storesProcesed > 0) { - percent = (processed / count) * (storesProcesed / storeCount) * 100.0; - } - progressBar.style.width = `${percent}%`; - }; - - await api.deleteDictionary(this.dictionaryInfo.title, onProgress); - } catch (e) { - this.dictionaryErrorsShow([e]); - } finally { - prevention.end(); - this.isDeleting = false; - progress.hidden = true; - - this.parent.trigger('databaseUpdated'); - } - } - - onEnabledChanged(e) { - this.optionsDictionary.enabled = !!e.target.checked; - this.save(); - } - - onAllowSecondarySearchesChanged(e) { - this.optionsDictionary.allowSecondarySearches = !!e.target.checked; - this.save(); - } - - onPriorityChanged(e) { - let value = Number.parseFloat(e.target.value); - if (Number.isNaN(value)) { - value = this.optionsDictionary.priority; - } else { - this.optionsDictionary.priority = value; - this.save(); - } - - e.target.value = `${value}`; - - this.parent.updateDictionaryOrder(); - } - - onDeleteButtonClicked(e) { - e.preventDefault(); - - if (this.isDeleting) { - return; - } - - const title = this.dictionaryInfo.title; - const n = document.querySelector('#dict-delete-modal'); - n.dataset.dict = title; - document.querySelector('#dict-remove-modal-dict-name').textContent = title; - $(n).modal('show'); - } - - onDetailsToggleLinkClicked(e) { - e.preventDefault(); - - this.detailsContainer.hidden = !this.detailsContainer.hidden; - } -} - -class SettingsDictionaryExtraUI { - constructor(parent, totalCounts, remainders, totalRemainder, content) { - this.parent = parent; - this.content = content; - - this.content.querySelector('.dict-total-count').textContent = `${totalRemainder} item${totalRemainder !== 1 ? 's' : ''}`; - - const node = this.content.querySelector('.dict-counts'); - node.textContent = JSON.stringify({ - counts: totalCounts, - remainders: remainders - }, null, 4); - node.removeAttribute('hidden'); - } - - cleanup() { - if (this.content !== null) { - if (this.content.parentNode !== null) { - this.content.parentNode.removeChild(this.content); - } - this.content = null; - } - } -} - -class DictionaryController { - constructor(settingsController) { - this._settingsController = settingsController; - this._dictionaryUI = null; - } - - async prepare() { - this._dictionaryUI = new SettingsDictionaryListUI( - document.querySelector('#dict-groups'), - document.querySelector('#dict-template'), - document.querySelector('#dict-groups-extra'), - document.querySelector('#dict-extra-template') - ); - this._dictionaryUI.save = () => this._settingsController.save(); - this._dictionaryUI.preventPageExit = this._preventPageExit.bind(this); - this._dictionaryUI.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); - - document.querySelector('#dict-main').addEventListener('change', this._onDictionaryMainChanged.bind(this), false); - document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', this._onDatabaseEnablePrefixWildcardSearchesChanged.bind(this), false); - - this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - this._settingsController.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); - - await this._onOptionsChanged(); - await this._onDatabaseUpdated(); - } - - // Private - - async _onOptionsChanged() { - const options = await this._settingsController.getOptionsMutable(); - - this._dictionaryUI.setOptionsDictionaries(options.dictionaries); - - const optionsFull = await this._settingsController.getOptionsFull(); - document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported; - - await this._updateMainDictionarySelectValue(); - } - - _updateMainDictionarySelectOptions(dictionaries) { - const select = document.querySelector('#dict-main'); - select.textContent = ''; // Empty - - let option = document.createElement('option'); - option.className = 'text-muted'; - option.value = ''; - option.textContent = 'Not selected'; - select.appendChild(option); - - for (const {title, sequenced} of toIterable(dictionaries)) { - if (!sequenced) { continue; } - - option = document.createElement('option'); - option.value = title; - option.textContent = title; - select.appendChild(option); - } - } - - async _updateMainDictionarySelectValue() { - const options = await this._settingsController.getOptions(); - - const value = options.general.mainDictionary; - - const select = document.querySelector('#dict-main'); - let selectValue = null; - for (const child of select.children) { - if (child.nodeName.toUpperCase() === 'OPTION' && child.value === value) { - selectValue = value; - break; - } - } - - let missingNodeOption = select.querySelector('option[data-not-installed=true]'); - if (selectValue === null) { - if (missingNodeOption === null) { - missingNodeOption = document.createElement('option'); - missingNodeOption.className = 'text-muted'; - missingNodeOption.value = value; - missingNodeOption.textContent = `${value} (Not installed)`; - missingNodeOption.dataset.notInstalled = 'true'; - select.appendChild(missingNodeOption); - } - } else { - if (missingNodeOption !== null) { - missingNodeOption.parentNode.removeChild(missingNodeOption); - } - } - - select.value = value; - } - - async _onDatabaseUpdated() { - try { - const dictionaries = await api.getDictionaryInfo(); - this._dictionaryUI.setDictionaries(dictionaries); - - document.querySelector('#dict-warning').hidden = (dictionaries.length > 0); - - this._updateMainDictionarySelectOptions(dictionaries); - await this._updateMainDictionarySelectValue(); - - const {counts, total} = await api.getDictionaryCounts(dictionaries.map((v) => v.title), true); - this._dictionaryUI.setCounts(counts, total); - } catch (e) { - yomichan.logError(e); - } - } - - async _onDictionaryMainChanged(e) { - const select = e.target; - const value = select.value; - - const missingNodeOption = select.querySelector('option[data-not-installed=true]'); - if (missingNodeOption !== null && missingNodeOption.value !== value) { - missingNodeOption.parentNode.removeChild(missingNodeOption); - } - - const options = await this._settingsController.getOptionsMutable(); - options.general.mainDictionary = value; - await this._settingsController.save(); - } - - async _onDatabaseEnablePrefixWildcardSearchesChanged(e) { - const optionsFull = await this._settingsController.getOptionsFullMutable(); - const v = !!e.target.checked; - if (optionsFull.global.database.prefixWildcardsSupported === v) { return; } - optionsFull.global.database.prefixWildcardsSupported = !!e.target.checked; - await this._settingsController.save(); - } - - _preventPageExit() { - return this._settingsController.preventPageExit(); - } -} diff --git a/ext/bg/js/settings/dictionary-controller.js b/ext/bg/js/settings/dictionary-controller.js new file mode 100644 index 00000000..9292d2c4 --- /dev/null +++ b/ext/bg/js/settings/dictionary-controller.js @@ -0,0 +1,520 @@ +/* + * 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 . + */ + +/* global + * api + * utilBackgroundIsolate + */ + +class SettingsDictionaryListUI extends EventDispatcher { + constructor(container, template, extraContainer, extraTemplate) { + super(); + this.container = container; + this.template = template; + this.extraContainer = extraContainer; + this.extraTemplate = extraTemplate; + this.optionsDictionaries = null; + this.dictionaries = null; + this.dictionaryEntries = []; + this.extra = null; + + document.querySelector('#dict-delete-confirm').addEventListener('click', this.onDictionaryConfirmDelete.bind(this), false); + } + + setOptionsDictionaries(optionsDictionaries) { + this.optionsDictionaries = optionsDictionaries; + if (this.dictionaries !== null) { + this.setDictionaries(this.dictionaries); + } + } + + setDictionaries(dictionaries) { + for (const dictionaryEntry of this.dictionaryEntries) { + dictionaryEntry.cleanup(); + } + + this.dictionaryEntries = []; + this.dictionaries = toIterable(dictionaries); + + if (this.optionsDictionaries === null) { + return; + } + + let changed = false; + for (const dictionaryInfo of this.dictionaries) { + if (this.createEntry(dictionaryInfo)) { + changed = true; + } + } + + this.updateDictionaryOrder(); + + const titles = this.dictionaryEntries.map((e) => e.dictionaryInfo.title); + const removeKeys = Object.keys(this.optionsDictionaries).filter((key) => titles.indexOf(key) < 0); + if (removeKeys.length > 0) { + for (const key of toIterable(removeKeys)) { + delete this.optionsDictionaries[key]; + } + changed = true; + } + + if (changed) { + this.save(); + } + } + + createEntry(dictionaryInfo) { + const title = dictionaryInfo.title; + let changed = false; + let optionsDictionary; + const optionsDictionaries = this.optionsDictionaries; + if (hasOwn(optionsDictionaries, title)) { + optionsDictionary = optionsDictionaries[title]; + } else { + optionsDictionary = SettingsDictionaryListUI.createDictionaryOptions(); + optionsDictionaries[title] = optionsDictionary; + changed = true; + } + + const content = document.importNode(this.template.content, true).firstChild; + + this.dictionaryEntries.push(new SettingsDictionaryEntryUI(this, dictionaryInfo, content, optionsDictionary)); + + return changed; + } + + static createDictionaryOptions() { + return utilBackgroundIsolate({ + priority: 0, + enabled: false, + allowSecondarySearches: false + }); + } + + createExtra(totalCounts, remainders, totalRemainder) { + const content = document.importNode(this.extraTemplate.content, true).firstChild; + this.extraContainer.appendChild(content); + return new SettingsDictionaryExtraUI(this, totalCounts, remainders, totalRemainder, content); + } + + setCounts(dictionaryCounts, totalCounts) { + const remainders = Object.assign({}, totalCounts); + const keys = Object.keys(remainders); + + for (let i = 0, ii = Math.min(this.dictionaryEntries.length, dictionaryCounts.length); i < ii; ++i) { + const counts = dictionaryCounts[i]; + this.dictionaryEntries[i].setCounts(counts); + + for (const key of keys) { + remainders[key] -= counts[key]; + } + } + + let totalRemainder = 0; + for (const key of keys) { + totalRemainder += remainders[key]; + } + + if (this.extra !== null) { + this.extra.cleanup(); + this.extra = null; + } + + if (totalRemainder > 0) { + this.extra = this.createExtra(totalCounts, remainders, totalRemainder); + } + } + + updateDictionaryOrder() { + const sortInfo = this.dictionaryEntries.map((e, i) => [e, i]); + sortInfo.sort((a, b) => { + const i = b[0].optionsDictionary.priority - a[0].optionsDictionary.priority; + return (i !== 0 ? i : a[1] - b[1]); + }); + + for (const [e] of sortInfo) { + this.container.appendChild(e.content); + } + } + + save() { + // Overwrite + } + + preventPageExit() { + // Overwrite + return {end: () => {}}; + } + + onDictionaryConfirmDelete(e) { + e.preventDefault(); + const n = document.querySelector('#dict-delete-modal'); + const title = n.dataset.dict; + delete n.dataset.dict; + $(n).modal('hide'); + + const index = this.dictionaryEntries.findIndex((entry) => entry.dictionaryInfo.title === title); + if (index >= 0) { + this.dictionaryEntries[index].deleteDictionary(); + } + } +} + +class SettingsDictionaryEntryUI { + constructor(parent, dictionaryInfo, content, optionsDictionary) { + this.parent = parent; + this.dictionaryInfo = dictionaryInfo; + this.optionsDictionary = optionsDictionary; + this.counts = null; + this.eventListeners = new EventListenerCollection(); + this.isDeleting = false; + + this.content = content; + this.enabledCheckbox = this.content.querySelector('.dict-enabled'); + this.allowSecondarySearchesCheckbox = this.content.querySelector('.dict-allow-secondary-searches'); + this.priorityInput = this.content.querySelector('.dict-priority'); + this.deleteButton = this.content.querySelector('.dict-delete-button'); + this.detailsToggleLink = this.content.querySelector('.dict-details-toggle-link'); + this.detailsContainer = this.content.querySelector('.dict-details'); + this.detailsTable = this.content.querySelector('.dict-details-table'); + + if (this.dictionaryInfo.version < 3) { + this.content.querySelector('.dict-outdated').hidden = false; + } + + this.setupDetails(dictionaryInfo); + + this.content.querySelector('.dict-title').textContent = this.dictionaryInfo.title; + this.content.querySelector('.dict-revision').textContent = `rev.${this.dictionaryInfo.revision}`; + this.content.querySelector('.dict-prefix-wildcard-searches-supported').checked = !!this.dictionaryInfo.prefixWildcardsSupported; + + this.applyValues(); + + this.eventListeners.addEventListener(this.enabledCheckbox, 'change', this.onEnabledChanged.bind(this), false); + this.eventListeners.addEventListener(this.allowSecondarySearchesCheckbox, 'change', this.onAllowSecondarySearchesChanged.bind(this), false); + this.eventListeners.addEventListener(this.priorityInput, 'change', this.onPriorityChanged.bind(this), false); + this.eventListeners.addEventListener(this.deleteButton, 'click', this.onDeleteButtonClicked.bind(this), false); + this.eventListeners.addEventListener(this.detailsToggleLink, 'click', this.onDetailsToggleLinkClicked.bind(this), false); + } + + setupDetails(dictionaryInfo) { + const targets = [ + ['Author', 'author'], + ['URL', 'url'], + ['Description', 'description'], + ['Attribution', 'attribution'] + ]; + + let count = 0; + for (const [label, key] of targets) { + const info = dictionaryInfo[key]; + if (typeof info !== 'string') { continue; } + + const n1 = document.createElement('div'); + n1.className = 'dict-details-entry'; + n1.dataset.type = key; + + const n2 = document.createElement('span'); + n2.className = 'dict-details-entry-label'; + n2.textContent = `${label}:`; + n1.appendChild(n2); + + const n3 = document.createElement('span'); + n3.className = 'dict-details-entry-info'; + n3.textContent = info; + n1.appendChild(n3); + + this.detailsTable.appendChild(n1); + + ++count; + } + + if (count === 0) { + this.detailsContainer.hidden = true; + this.detailsToggleLink.hidden = true; + } + } + + cleanup() { + if (this.content !== null) { + if (this.content.parentNode !== null) { + this.content.parentNode.removeChild(this.content); + } + this.content = null; + } + this.dictionaryInfo = null; + this.eventListeners.removeAllEventListeners(); + } + + setCounts(counts) { + this.counts = counts; + const node = this.content.querySelector('.dict-counts'); + node.textContent = JSON.stringify({ + info: this.dictionaryInfo, + counts + }, null, 4); + node.removeAttribute('hidden'); + } + + save() { + this.parent.save(); + } + + applyValues() { + this.enabledCheckbox.checked = this.optionsDictionary.enabled; + this.allowSecondarySearchesCheckbox.checked = this.optionsDictionary.allowSecondarySearches; + this.priorityInput.value = `${this.optionsDictionary.priority}`; + } + + async deleteDictionary() { + if (this.isDeleting) { + return; + } + + const progress = this.content.querySelector('.progress'); + progress.hidden = false; + const progressBar = this.content.querySelector('.progress-bar'); + this.isDeleting = true; + + const prevention = this.parent.preventPageExit(); + try { + const onProgress = ({processed, count, storeCount, storesProcesed}) => { + let percent = 0.0; + if (count > 0 && storesProcesed > 0) { + percent = (processed / count) * (storesProcesed / storeCount) * 100.0; + } + progressBar.style.width = `${percent}%`; + }; + + await api.deleteDictionary(this.dictionaryInfo.title, onProgress); + } catch (e) { + this.dictionaryErrorsShow([e]); + } finally { + prevention.end(); + this.isDeleting = false; + progress.hidden = true; + + this.parent.trigger('databaseUpdated'); + } + } + + onEnabledChanged(e) { + this.optionsDictionary.enabled = !!e.target.checked; + this.save(); + } + + onAllowSecondarySearchesChanged(e) { + this.optionsDictionary.allowSecondarySearches = !!e.target.checked; + this.save(); + } + + onPriorityChanged(e) { + let value = Number.parseFloat(e.target.value); + if (Number.isNaN(value)) { + value = this.optionsDictionary.priority; + } else { + this.optionsDictionary.priority = value; + this.save(); + } + + e.target.value = `${value}`; + + this.parent.updateDictionaryOrder(); + } + + onDeleteButtonClicked(e) { + e.preventDefault(); + + if (this.isDeleting) { + return; + } + + const title = this.dictionaryInfo.title; + const n = document.querySelector('#dict-delete-modal'); + n.dataset.dict = title; + document.querySelector('#dict-remove-modal-dict-name').textContent = title; + $(n).modal('show'); + } + + onDetailsToggleLinkClicked(e) { + e.preventDefault(); + + this.detailsContainer.hidden = !this.detailsContainer.hidden; + } +} + +class SettingsDictionaryExtraUI { + constructor(parent, totalCounts, remainders, totalRemainder, content) { + this.parent = parent; + this.content = content; + + this.content.querySelector('.dict-total-count').textContent = `${totalRemainder} item${totalRemainder !== 1 ? 's' : ''}`; + + const node = this.content.querySelector('.dict-counts'); + node.textContent = JSON.stringify({ + counts: totalCounts, + remainders: remainders + }, null, 4); + node.removeAttribute('hidden'); + } + + cleanup() { + if (this.content !== null) { + if (this.content.parentNode !== null) { + this.content.parentNode.removeChild(this.content); + } + this.content = null; + } + } +} + +class DictionaryController { + constructor(settingsController) { + this._settingsController = settingsController; + this._dictionaryUI = null; + } + + async prepare() { + this._dictionaryUI = new SettingsDictionaryListUI( + document.querySelector('#dict-groups'), + document.querySelector('#dict-template'), + document.querySelector('#dict-groups-extra'), + document.querySelector('#dict-extra-template') + ); + this._dictionaryUI.save = () => this._settingsController.save(); + this._dictionaryUI.preventPageExit = this._preventPageExit.bind(this); + this._dictionaryUI.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); + + document.querySelector('#dict-main').addEventListener('change', this._onDictionaryMainChanged.bind(this), false); + document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', this._onDatabaseEnablePrefixWildcardSearchesChanged.bind(this), false); + + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + this._settingsController.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); + + await this._onOptionsChanged(); + await this._onDatabaseUpdated(); + } + + // Private + + async _onOptionsChanged() { + const options = await this._settingsController.getOptionsMutable(); + + this._dictionaryUI.setOptionsDictionaries(options.dictionaries); + + const optionsFull = await this._settingsController.getOptionsFull(); + document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported; + + await this._updateMainDictionarySelectValue(); + } + + _updateMainDictionarySelectOptions(dictionaries) { + const select = document.querySelector('#dict-main'); + select.textContent = ''; // Empty + + let option = document.createElement('option'); + option.className = 'text-muted'; + option.value = ''; + option.textContent = 'Not selected'; + select.appendChild(option); + + for (const {title, sequenced} of toIterable(dictionaries)) { + if (!sequenced) { continue; } + + option = document.createElement('option'); + option.value = title; + option.textContent = title; + select.appendChild(option); + } + } + + async _updateMainDictionarySelectValue() { + const options = await this._settingsController.getOptions(); + + const value = options.general.mainDictionary; + + const select = document.querySelector('#dict-main'); + let selectValue = null; + for (const child of select.children) { + if (child.nodeName.toUpperCase() === 'OPTION' && child.value === value) { + selectValue = value; + break; + } + } + + let missingNodeOption = select.querySelector('option[data-not-installed=true]'); + if (selectValue === null) { + if (missingNodeOption === null) { + missingNodeOption = document.createElement('option'); + missingNodeOption.className = 'text-muted'; + missingNodeOption.value = value; + missingNodeOption.textContent = `${value} (Not installed)`; + missingNodeOption.dataset.notInstalled = 'true'; + select.appendChild(missingNodeOption); + } + } else { + if (missingNodeOption !== null) { + missingNodeOption.parentNode.removeChild(missingNodeOption); + } + } + + select.value = value; + } + + async _onDatabaseUpdated() { + try { + const dictionaries = await api.getDictionaryInfo(); + this._dictionaryUI.setDictionaries(dictionaries); + + document.querySelector('#dict-warning').hidden = (dictionaries.length > 0); + + this._updateMainDictionarySelectOptions(dictionaries); + await this._updateMainDictionarySelectValue(); + + const {counts, total} = await api.getDictionaryCounts(dictionaries.map((v) => v.title), true); + this._dictionaryUI.setCounts(counts, total); + } catch (e) { + yomichan.logError(e); + } + } + + async _onDictionaryMainChanged(e) { + const select = e.target; + const value = select.value; + + const missingNodeOption = select.querySelector('option[data-not-installed=true]'); + if (missingNodeOption !== null && missingNodeOption.value !== value) { + missingNodeOption.parentNode.removeChild(missingNodeOption); + } + + const options = await this._settingsController.getOptionsMutable(); + options.general.mainDictionary = value; + await this._settingsController.save(); + } + + async _onDatabaseEnablePrefixWildcardSearchesChanged(e) { + const optionsFull = await this._settingsController.getOptionsFullMutable(); + const v = !!e.target.checked; + if (optionsFull.global.database.prefixWildcardsSupported === v) { return; } + optionsFull.global.database.prefixWildcardsSupported = !!e.target.checked; + await this._settingsController.save(); + } + + _preventPageExit() { + return this._settingsController.preventPageExit(); + } +} diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index 4932586b..37c6375d 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -19,6 +19,7 @@ * AnkiController * AnkiTemplatesController * AudioController + * BackupController * ClipboardPopupsController * DictionaryController * DictionaryImportController @@ -26,7 +27,6 @@ * PopupPreviewController * ProfileController * ScanInputsController - * SettingsBackup * SettingsController * StorageController * api @@ -90,7 +90,7 @@ async function setupEnvironmentInfo() { const ankiTemplatesController = new AnkiTemplatesController(settingsController, ankiController); ankiTemplatesController.prepare(); - const settingsBackup = new SettingsBackup(settingsController); + const settingsBackup = new BackupController(settingsController); settingsBackup.prepare(); const scanInputsController = new ScanInputsController(settingsController); diff --git a/ext/bg/js/settings/popup-preview-controller.js b/ext/bg/js/settings/popup-preview-controller.js new file mode 100644 index 00000000..d4145b76 --- /dev/null +++ b/ext/bg/js/settings/popup-preview-controller.js @@ -0,0 +1,103 @@ +/* + * 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 . + */ + +/* global + * wanakana + */ + +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}); + } + + _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/popup-preview.js b/ext/bg/js/settings/popup-preview.js deleted file mode 100644 index d4145b76..00000000 --- a/ext/bg/js/settings/popup-preview.js +++ /dev/null @@ -1,103 +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 . - */ - -/* global - * wanakana - */ - -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}); - } - - _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/profile-controller.js b/ext/bg/js/settings/profile-controller.js new file mode 100644 index 00000000..a32c03d1 --- /dev/null +++ b/ext/bg/js/settings/profile-controller.js @@ -0,0 +1,282 @@ +/* + * 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 . + */ + +/* global + * ProfileConditionsUI + * api + * utilBackgroundIsolate + */ + +class ProfileController { + constructor(settingsController) { + this._settingsController = settingsController; + this._profileConditionsUI = new ProfileConditionsUI(settingsController); + } + + async prepare() { + const {platform: {os}} = await api.getEnvironmentInfo(); + this._profileConditionsUI.os = os; + + $('#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(); + } + + // Private + + async _onOptionsChanged() { + const optionsFull = await this._settingsController.getOptionsFullMutable(); + this._formWrite(optionsFull); + } + + _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 _formRead(optionsFull) { + const currentProfileIndex = this._settingsController.profileIndex; + const profile = optionsFull.profiles[currentProfileIndex]; + + // Current profile + const index = this._tryGetIntegerValue('#profile-active', 0, optionsFull.profiles.length); + if (index !== null) { + optionsFull.profileCurrent = index; + } + + // Profile name + profile.name = $('#profile-name').val(); + } + + _formWrite(optionsFull) { + const currentProfileIndex = this._settingsController.profileIndex; + const profile = optionsFull.profiles[currentProfileIndex]; + + 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); + + this._refreshProfileConditions(optionsFull); + } + + _refreshProfileConditions(optionsFull) { + this._profileConditionsUI.cleanup(); + + const profileIndex = this._settingsController.profileIndex; + if (profileIndex < 0 || profileIndex >= optionsFull.profiles.length) { return; } + + const {conditionGroups} = optionsFull.profiles[profileIndex]; + this._profileConditionsUI.prepare(conditionGroups); + } + + _populateSelect(select, profiles, currentValue, ignoreIndices) { + select.empty(); + + + for (let i = 0; i < profiles.length; ++i) { + if (ignoreIndices !== null && ignoreIndices.indexOf(i) >= 0) { + continue; + } + const profile = profiles[i]; + select.append($(``)); + } + + select.val(`${currentValue}`); + } + + _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 { + prefix = match[1]; + suffix = match[5]; + if (typeof match[2] === 'string') { + space = match[3]; + index = parseInt(match[4], 10) + 1; + } else { + space = ' '; + index = 2; + } + } + + 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; + } + } + } + + async _onInputChanged(e) { + if (!e.originalEvent && !e.isTrigger) { + return; + } + + const optionsFull = await this._settingsController.getOptionsFullMutable(); + await this._formRead(optionsFull); + await this._settingsController.save(); + } + + 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; + } + + this._settingsController.profileIndex = index; + } + + 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); + + this._settingsController.profileIndex = optionsFull.profiles.length - 1; + + await this._settingsController.save(); + } + + async _onRemove(e) { + if (e.shiftKey) { + return await this._onRemoveConfirm(); + } + + const optionsFull = await this._settingsController.getOptionsFull(); + if (optionsFull.profiles.length <= 1) { + return; + } + + const currentProfileIndex = this._settingsController.profileIndex; + const profile = optionsFull.profiles[currentProfileIndex]; + + $('#profile-remove-modal-profile-name').text(profile.name); + $('#profile-remove-modal').modal('show'); + } + + async _onRemoveConfirm() { + $('#profile-remove-modal').modal('hide'); + + const optionsFull = await this._settingsController.getOptionsFullMutable(); + if (optionsFull.profiles.length <= 1) { + return; + } + + const currentProfileIndex = this._settingsController.profileIndex; + optionsFull.profiles.splice(currentProfileIndex, 1); + + if (currentProfileIndex >= optionsFull.profiles.length) { + this._settingsController.profileIndex = optionsFull.profiles.length - 1; + } + + if (optionsFull.profileCurrent >= optionsFull.profiles.length) { + optionsFull.profileCurrent = optionsFull.profiles.length - 1; + } + + await this._settingsController.save(); + } + + _onNameChanged() { + const currentProfileIndex = this._settingsController.profileIndex; + $('#profile-active, #profile-target').find(`[value="${currentProfileIndex}"]`).text(this.value); + } + + 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; + } + + const profile = optionsFull.profiles[currentProfileIndex]; + optionsFull.profiles.splice(currentProfileIndex, 1); + optionsFull.profiles.splice(index, 0, profile); + + if (optionsFull.profileCurrent === currentProfileIndex) { + optionsFull.profileCurrent = index; + } + + this._settingsController.profileIndex = index; + + await this._settingsController.save(); + } + + async _onCopy() { + const optionsFull = await this._settingsController.getOptionsFullMutable(); + 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'); + } + + async _onCopyConfirm() { + $('#profile-copy-modal').modal('hide'); + + 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; + + await this._settingsController.save(); + } +} diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js deleted file mode 100644 index a32c03d1..00000000 --- a/ext/bg/js/settings/profiles.js +++ /dev/null @@ -1,282 +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 . - */ - -/* global - * ProfileConditionsUI - * api - * utilBackgroundIsolate - */ - -class ProfileController { - constructor(settingsController) { - this._settingsController = settingsController; - this._profileConditionsUI = new ProfileConditionsUI(settingsController); - } - - async prepare() { - const {platform: {os}} = await api.getEnvironmentInfo(); - this._profileConditionsUI.os = os; - - $('#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(); - } - - // Private - - async _onOptionsChanged() { - const optionsFull = await this._settingsController.getOptionsFullMutable(); - this._formWrite(optionsFull); - } - - _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 _formRead(optionsFull) { - const currentProfileIndex = this._settingsController.profileIndex; - const profile = optionsFull.profiles[currentProfileIndex]; - - // Current profile - const index = this._tryGetIntegerValue('#profile-active', 0, optionsFull.profiles.length); - if (index !== null) { - optionsFull.profileCurrent = index; - } - - // Profile name - profile.name = $('#profile-name').val(); - } - - _formWrite(optionsFull) { - const currentProfileIndex = this._settingsController.profileIndex; - const profile = optionsFull.profiles[currentProfileIndex]; - - 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); - - this._refreshProfileConditions(optionsFull); - } - - _refreshProfileConditions(optionsFull) { - this._profileConditionsUI.cleanup(); - - const profileIndex = this._settingsController.profileIndex; - if (profileIndex < 0 || profileIndex >= optionsFull.profiles.length) { return; } - - const {conditionGroups} = optionsFull.profiles[profileIndex]; - this._profileConditionsUI.prepare(conditionGroups); - } - - _populateSelect(select, profiles, currentValue, ignoreIndices) { - select.empty(); - - - for (let i = 0; i < profiles.length; ++i) { - if (ignoreIndices !== null && ignoreIndices.indexOf(i) >= 0) { - continue; - } - const profile = profiles[i]; - select.append($(``)); - } - - select.val(`${currentValue}`); - } - - _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 { - prefix = match[1]; - suffix = match[5]; - if (typeof match[2] === 'string') { - space = match[3]; - index = parseInt(match[4], 10) + 1; - } else { - space = ' '; - index = 2; - } - } - - 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; - } - } - } - - async _onInputChanged(e) { - if (!e.originalEvent && !e.isTrigger) { - return; - } - - const optionsFull = await this._settingsController.getOptionsFullMutable(); - await this._formRead(optionsFull); - await this._settingsController.save(); - } - - 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; - } - - this._settingsController.profileIndex = index; - } - - 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); - - this._settingsController.profileIndex = optionsFull.profiles.length - 1; - - await this._settingsController.save(); - } - - async _onRemove(e) { - if (e.shiftKey) { - return await this._onRemoveConfirm(); - } - - const optionsFull = await this._settingsController.getOptionsFull(); - if (optionsFull.profiles.length <= 1) { - return; - } - - const currentProfileIndex = this._settingsController.profileIndex; - const profile = optionsFull.profiles[currentProfileIndex]; - - $('#profile-remove-modal-profile-name').text(profile.name); - $('#profile-remove-modal').modal('show'); - } - - async _onRemoveConfirm() { - $('#profile-remove-modal').modal('hide'); - - const optionsFull = await this._settingsController.getOptionsFullMutable(); - if (optionsFull.profiles.length <= 1) { - return; - } - - const currentProfileIndex = this._settingsController.profileIndex; - optionsFull.profiles.splice(currentProfileIndex, 1); - - if (currentProfileIndex >= optionsFull.profiles.length) { - this._settingsController.profileIndex = optionsFull.profiles.length - 1; - } - - if (optionsFull.profileCurrent >= optionsFull.profiles.length) { - optionsFull.profileCurrent = optionsFull.profiles.length - 1; - } - - await this._settingsController.save(); - } - - _onNameChanged() { - const currentProfileIndex = this._settingsController.profileIndex; - $('#profile-active, #profile-target').find(`[value="${currentProfileIndex}"]`).text(this.value); - } - - 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; - } - - const profile = optionsFull.profiles[currentProfileIndex]; - optionsFull.profiles.splice(currentProfileIndex, 1); - optionsFull.profiles.splice(index, 0, profile); - - if (optionsFull.profileCurrent === currentProfileIndex) { - optionsFull.profileCurrent = index; - } - - this._settingsController.profileIndex = index; - - await this._settingsController.save(); - } - - async _onCopy() { - const optionsFull = await this._settingsController.getOptionsFullMutable(); - 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'); - } - - async _onCopyConfirm() { - $('#profile-copy-modal').modal('hide'); - - 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; - - await this._settingsController.save(); - } -} diff --git a/ext/bg/js/settings/storage-controller.js b/ext/bg/js/settings/storage-controller.js new file mode 100644 index 00000000..24c6d7ef --- /dev/null +++ b/ext/bg/js/settings/storage-controller.js @@ -0,0 +1,131 @@ +/* + * 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 . + */ + +class StorageController { + constructor() { + this._mostRecentStorageEstimate = null; + this._storageEstimateFailed = false; + this._isUpdating = false; + } + + prepare() { + this._preparePersistentStorage(); + this.updateStats(); + document.querySelector('#storage-refresh').addEventListener('click', this.updateStats.bind(this), false); + } + + 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; + } + } + + // Private + + async _preparePersistentStorage() { + if (!(navigator.storage && navigator.storage.persist)) { + // Not supported + return; + } + + 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 _storageEstimate() { + if (this._storageEstimateFailed && this._mostRecentStorageEstimate === null) { + return null; + } + try { + const value = await navigator.storage.estimate(); + this._mostRecentStorageEstimate = value; + return value; + } catch (e) { + this._storageEstimateFailed = true; + } + return null; + } + + _bytesToLabeledString(size) { + const base = 1000; + const labels = [' bytes', 'KB', 'MB', 'GB']; + let labelIndex = 0; + while (size >= base) { + size /= base; + ++labelIndex; + } + const label = labelIndex === 0 ? `${size}` : size.toFixed(1); + return `${label}${labels[labelIndex]}`; + } + + async _isStoragePeristent() { + try { + return await navigator.storage.persisted(); + } catch (e) { + // NOP + } + return false; + } +} diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js deleted file mode 100644 index 24c6d7ef..00000000 --- a/ext/bg/js/settings/storage.js +++ /dev/null @@ -1,131 +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 . - */ - -class StorageController { - constructor() { - this._mostRecentStorageEstimate = null; - this._storageEstimateFailed = false; - this._isUpdating = false; - } - - prepare() { - this._preparePersistentStorage(); - this.updateStats(); - document.querySelector('#storage-refresh').addEventListener('click', this.updateStats.bind(this), false); - } - - 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; - } - } - - // Private - - async _preparePersistentStorage() { - if (!(navigator.storage && navigator.storage.persist)) { - // Not supported - return; - } - - 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 _storageEstimate() { - if (this._storageEstimateFailed && this._mostRecentStorageEstimate === null) { - return null; - } - try { - const value = await navigator.storage.estimate(); - this._mostRecentStorageEstimate = value; - return value; - } catch (e) { - this._storageEstimateFailed = true; - } - return null; - } - - _bytesToLabeledString(size) { - const base = 1000; - const labels = [' bytes', 'KB', 'MB', 'GB']; - let labelIndex = 0; - while (size >= base) { - size /= base; - ++labelIndex; - } - const label = labelIndex === 0 ? `${size}` : size.toFixed(1); - return `${label}${labels[labelIndex]}`; - } - - async _isStoragePeristent() { - try { - return await navigator.storage.persisted(); - } catch (e) { - // NOP - } - return false; - } -} diff --git a/ext/bg/settings.html b/ext/bg/settings.html index ae89ca1f..b01fbf73 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1165,39 +1165,40 @@ + + + + + + + + - - - - - - - - + + + + + + + - + - - - - + + - - - - - + -- cgit v1.2.3