From 694120b8a5cce89101871ee58f2b7c410f909920 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 23 May 2020 13:34:55 -0400 Subject: Cross frame communication (#531) * Set up new cross-frame port connector * Create classes for cross-frame API invocation with replies * Remove event listeners on disconnect --- ext/bg/js/backend.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'ext/bg/js') diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 20d31efc..8df4fd9d 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -163,6 +163,7 @@ class Backend { chrome.tabs.onZoomChange.addListener(this._onZoomChange.bind(this)); } chrome.runtime.onMessage.addListener(this.onMessage.bind(this)); + chrome.runtime.onConnect.addListener(this._onConnect.bind(this)); const options = this.getOptions(this.optionsContext); if (options.general.showGuide) { @@ -236,6 +237,45 @@ class Backend { } } + _onConnect(port) { + try { + const match = /^background-cross-frame-communication-port-(\d+)$/.exec(`${port.name}`); + if (match === null) { return; } + + const tabId = (port.sender && port.sender.tab ? port.sender.tab.id : null); + if (typeof tabId !== 'number') { + throw new Error('Port does not have an associated tab ID'); + } + const senderFrameId = port.sender.frameId; + if (typeof tabId !== 'number') { + throw new Error('Port does not have an associated frame ID'); + } + const targetFrameId = parseInt(match[1], 10); + + let forwardPort = chrome.tabs.connect(tabId, {frameId: targetFrameId, name: `cross-frame-communication-port-${senderFrameId}`}); + + const cleanup = () => { + this.checkLastError(chrome.runtime.lastError); + if (forwardPort !== null) { + forwardPort.disconnect(); + forwardPort = null; + } + if (port !== null) { + port.disconnect(); + port = null; + } + }; + + port.onMessage.addListener((message) => { forwardPort.postMessage(message); }); + forwardPort.onMessage.addListener((message) => { port.postMessage(message); }); + port.onDisconnect.addListener(cleanup); + forwardPort.onDisconnect.addListener(cleanup); + } catch (e) { + port.disconnect(); + yomichan.logError(e); + } + } + _onClipboardText({text}) { this._onCommandSearch({mode: 'popup', query: text}); } -- cgit v1.2.3 From c61a87b152b91bdebe01eefdbc3fa00670a3071d Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 24 May 2020 13:30:40 -0400 Subject: API refactor (#532) * Convert api.js into a class instance * Use new api.* functions * Fix missing binds * Group functions with progress callbacks together * Change style * Fix API override not working --- ext/bg/js/context-main.js | 22 +- ext/bg/js/search-main.js | 7 +- ext/bg/js/search-query-parser-generator.js | 4 +- ext/bg/js/search-query-parser.js | 12 +- ext/bg/js/search.js | 14 +- ext/bg/js/settings/anki-templates.js | 21 +- ext/bg/js/settings/anki.js | 8 +- ext/bg/js/settings/backup.js | 10 +- ext/bg/js/settings/dictionaries.js | 24 +- ext/bg/js/settings/main.js | 10 +- ext/bg/js/settings/popup-preview-frame-main.js | 4 +- ext/bg/js/settings/popup-preview-frame.js | 9 +- ext/bg/js/settings/profiles.js | 6 +- ext/bg/js/settings/storage.js | 4 +- ext/fg/js/content-script-main.js | 13 +- ext/fg/js/float-main.js | 7 +- ext/fg/js/float.js | 11 +- ext/fg/js/frame-offset-forwarder.js | 4 +- ext/fg/js/frontend.js | 18 +- ext/fg/js/popup.js | 4 +- ext/mixed/js/api.js | 572 +++++++++++++------------ ext/mixed/js/display-generator.js | 4 +- ext/mixed/js/display.js | 30 +- ext/mixed/js/dynamic-loader.js | 4 +- ext/mixed/js/media-loader.js | 4 +- 25 files changed, 399 insertions(+), 427 deletions(-) (limited to 'ext/bg/js') diff --git a/ext/bg/js/context-main.js b/ext/bg/js/context-main.js index dbba0272..e90e7e2e 100644 --- a/ext/bg/js/context-main.js +++ b/ext/bg/js/context-main.js @@ -16,11 +16,7 @@ */ /* global - * apiCommandExec - * apiForwardLogsToBackend - * apiGetEnvironmentInfo - * apiLogIndicatorClear - * apiOptionsGet + * api */ function showExtensionInfo() { @@ -36,12 +32,12 @@ function setupButtonEvents(selector, command, url) { for (const node of nodes) { node.addEventListener('click', (e) => { if (e.button !== 0) { return; } - apiCommandExec(command, {mode: e.ctrlKey ? 'newTab' : 'existingOrNewTab'}); + api.commandExec(command, {mode: e.ctrlKey ? 'newTab' : 'existingOrNewTab'}); e.preventDefault(); }, false); node.addEventListener('auxclick', (e) => { if (e.button !== 1) { return; } - apiCommandExec(command, {mode: 'newTab'}); + api.commandExec(command, {mode: 'newTab'}); e.preventDefault(); }, false); @@ -54,14 +50,14 @@ function setupButtonEvents(selector, command, url) { } async function mainInner() { - apiForwardLogsToBackend(); + api.forwardLogsToBackend(); await yomichan.prepare(); - await apiLogIndicatorClear(); + await api.logIndicatorClear(); showExtensionInfo(); - apiGetEnvironmentInfo().then(({browser}) => { + api.getEnvironmentInfo().then(({browser}) => { // Firefox mobile opens this page as a full webpage. document.documentElement.dataset.mode = (browser === 'firefox-mobile' ? 'full' : 'mini'); }); @@ -76,14 +72,14 @@ async function mainInner() { depth: 0, url: window.location.href }; - apiOptionsGet(optionsContext).then((options) => { + api.optionsGet(optionsContext).then((options) => { const toggle = document.querySelector('#enable-search'); toggle.checked = options.general.enable; - toggle.addEventListener('change', () => apiCommandExec('toggle'), false); + toggle.addEventListener('change', () => api.commandExec('toggle'), false); const toggle2 = document.querySelector('#enable-search2'); toggle2.checked = options.general.enable; - toggle2.addEventListener('change', () => apiCommandExec('toggle'), false); + toggle2.addEventListener('change', () => api.commandExec('toggle'), false); setTimeout(() => { for (const n of document.querySelectorAll('.toggle-group')) { diff --git a/ext/bg/js/search-main.js b/ext/bg/js/search-main.js index 54fa549d..3e089594 100644 --- a/ext/bg/js/search-main.js +++ b/ext/bg/js/search-main.js @@ -17,8 +17,7 @@ /* global * DisplaySearch - * apiForwardLogsToBackend - * apiOptionsGet + * api * dynamicLoader */ @@ -35,7 +34,7 @@ async function injectSearchFrontend() { } (async () => { - apiForwardLogsToBackend(); + api.forwardLogsToBackend(); await yomichan.prepare(); const displaySearch = new DisplaySearch(); @@ -45,7 +44,7 @@ async function injectSearchFrontend() { const applyOptions = async () => { const optionsContext = {depth: 0, url: window.location.href}; - const options = await apiOptionsGet(optionsContext); + const options = await api.optionsGet(optionsContext); if (!options.scanning.enableOnSearchPage || optionsApplied) { return; } optionsApplied = true; diff --git a/ext/bg/js/search-query-parser-generator.js b/ext/bg/js/search-query-parser-generator.js index 9e7ff8aa..6989e157 100644 --- a/ext/bg/js/search-query-parser-generator.js +++ b/ext/bg/js/search-query-parser-generator.js @@ -17,7 +17,7 @@ /* global * TemplateHandler - * apiGetQueryParserTemplatesHtml + * api */ class QueryParserGenerator { @@ -26,7 +26,7 @@ class QueryParserGenerator { } async prepare() { - const html = await apiGetQueryParserTemplatesHtml(); + const html = await api.getQueryParserTemplatesHtml(); this._templateHandler = new TemplateHandler(html); } diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index e1e37d1c..addfc686 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -18,9 +18,7 @@ /* global * QueryParserGenerator * TextScanner - * apiModifySettings - * apiTermsFind - * apiTextParse + * api * docSentenceExtract */ @@ -59,7 +57,7 @@ class QueryParser { this._setPreview(text); - this._parseResults = await apiTextParse(text, this._getOptionsContext()); + this._parseResults = await api.textParse(text, this._getOptionsContext()); this._refreshSelectedParser(); this._renderParserSelect(); @@ -80,7 +78,7 @@ class QueryParser { const searchText = this._textScanner.getTextSourceContent(textSource, this._options.scanning.length); if (searchText.length === 0) { return null; } - const {definitions, length} = await apiTermsFind(searchText, {}, this._getOptionsContext()); + const {definitions, length} = await api.termsFind(searchText, {}, this._getOptionsContext()); if (definitions.length === 0) { return null; } const sentence = docSentenceExtract(textSource, this._options.anki.sentenceExt); @@ -99,7 +97,7 @@ class QueryParser { _onParserChange(e) { const value = e.target.value; - apiModifySettings([{ + api.modifySettings([{ action: 'set', path: 'parsing.selectedParser', value, @@ -112,7 +110,7 @@ class QueryParser { if (this._parseResults.length > 0) { if (!this._getParseResult()) { const value = this._parseResults[0].id; - apiModifySettings([{ + api.modifySettings([{ action: 'set', path: 'parsing.selectedParser', value, diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index 96e8a70b..75707e6c 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -20,9 +20,7 @@ * DOM * Display * QueryParser - * apiClipboardGet - * apiModifySettings - * apiTermsFind + * api * wanakana */ @@ -52,7 +50,7 @@ class DisplaySearch extends Display { this.introVisible = true; this.introAnimationTimer = null; - this.clipboardMonitor = new ClipboardMonitor({getClipboard: apiClipboardGet}); + this.clipboardMonitor = new ClipboardMonitor({getClipboard: api.clipboardGet.bind(api)}); this._onKeyDownIgnoreKeys = new Map([ ['ANY_MOD', new Set([ @@ -234,7 +232,7 @@ class DisplaySearch extends Display { this.setIntroVisible(!valid, animate); this.updateSearchButton(); if (valid) { - const {definitions} = await apiTermsFind(query, details, this.getOptionsContext()); + const {definitions} = await api.termsFind(query, details, this.getOptionsContext()); this.setContent('terms', {definitions, context: { focus: false, disableHistory: true, @@ -258,7 +256,7 @@ class DisplaySearch extends Display { } else { wanakana.unbind(this.query); } - apiModifySettings([{ + api.modifySettings([{ action: 'set', path: 'general.enableWanakana', value, @@ -274,7 +272,7 @@ class DisplaySearch extends Display { (granted) => { if (granted) { this.clipboardMonitor.start(); - apiModifySettings([{ + api.modifySettings([{ action: 'set', path: 'general.enableClipboardMonitor', value: true, @@ -288,7 +286,7 @@ class DisplaySearch extends Display { ); } else { this.clipboardMonitor.stop(); - apiModifySettings([{ + api.modifySettings([{ action: 'set', path: 'general.enableClipboardMonitor', value: false, diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js index d5b6e677..0dadb433 100644 --- a/ext/bg/js/settings/anki-templates.js +++ b/ext/bg/js/settings/anki-templates.js @@ -19,10 +19,7 @@ * AnkiNoteBuilder * ankiGetFieldMarkers * ankiGetFieldMarkersHtml - * apiGetDefaultAnkiFieldTemplates - * apiOptionsGet - * apiTemplateRender - * apiTermsFind + * api * getOptionsContext * getOptionsMutable * settingsSaveOptions @@ -38,7 +35,7 @@ async function onAnkiFieldTemplatesResetConfirm(e) { $('#field-template-reset-modal').modal('hide'); - const value = await apiGetDefaultAnkiFieldTemplates(); + const value = await api.getDefaultAnkiFieldTemplates(); const element = document.querySelector('#field-templates'); element.value = value; @@ -65,9 +62,9 @@ function ankiTemplatesInitialize() { async function ankiTemplatesUpdateValue() { const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); + const options = await api.optionsGet(optionsContext); let templates = options.anki.fieldTemplates; - if (typeof templates !== 'string') { templates = await apiGetDefaultAnkiFieldTemplates(); } + if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); } $('#field-templates').val(templates); onAnkiTemplatesValidateCompile(); @@ -79,7 +76,7 @@ const ankiTemplatesValidateGetDefinition = (() => { return async (text, optionsContext) => { if (cachedText !== text) { - const {definitions} = await apiTermsFind(text, {}, optionsContext); + const {definitions} = await api.termsFind(text, {}, optionsContext); if (definitions.length === 0) { return null; } cachedValue = definitions[0]; @@ -97,15 +94,15 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i const optionsContext = getOptionsContext(); const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext); if (definition !== null) { - const options = await apiOptionsGet(optionsContext); + const options = await api.optionsGet(optionsContext); const context = { document: { title: document.title } }; let templates = options.anki.fieldTemplates; - if (typeof templates !== 'string') { templates = await apiGetDefaultAnkiFieldTemplates(); } - const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: apiTemplateRender}); + if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); } + const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: api.templateRender.bind(api)}); result = await ankiNoteBuilder.formatField(field, definition, mode, context, options, templates, exceptions); } } catch (e) { @@ -125,7 +122,7 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i async function onAnkiFieldTemplatesChanged(e) { // Get value let templates = e.currentTarget.value; - if (templates === await apiGetDefaultAnkiFieldTemplates()) { + if (templates === await api.getDefaultAnkiFieldTemplates()) { // Default templates = null; } diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js index ff1277ed..ba83f994 100644 --- a/ext/bg/js/settings/anki.js +++ b/ext/bg/js/settings/anki.js @@ -16,9 +16,7 @@ */ /* global - * apiGetAnkiDeckNames - * apiGetAnkiModelFieldNames - * apiGetAnkiModelNames + * api * getOptionsContext * getOptionsMutable * onFormOptionsChanged @@ -107,7 +105,7 @@ async function _ankiDeckAndModelPopulate(options) { const kanjiModel = {value: options.anki.kanji.model, selector: '#anki-kanji-model'}; try { _ankiSpinnerShow(true); - const [deckNames, modelNames] = await Promise.all([apiGetAnkiDeckNames(), apiGetAnkiModelNames()]); + const [deckNames, modelNames] = await Promise.all([api.getAnkiDeckNames(), api.getAnkiModelNames()]); deckNames.sort(); modelNames.sort(); termsDeck.values = deckNames; @@ -180,7 +178,7 @@ async function _onAnkiModelChanged(e) { let fieldNames; try { const modelName = node.value; - fieldNames = await apiGetAnkiModelFieldNames(modelName); + fieldNames = await api.getAnkiModelFieldNames(modelName); _ankiSetError(null); } catch (error) { _ankiSetError(error); diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js index faf4e592..5eb55502 100644 --- a/ext/bg/js/settings/backup.js +++ b/ext/bg/js/settings/backup.js @@ -16,9 +16,7 @@ */ /* global - * apiGetDefaultAnkiFieldTemplates - * apiGetEnvironmentInfo - * apiOptionsGetFull + * api * optionsGetDefault * optionsUpdateVersion * utilBackend @@ -51,9 +49,9 @@ function _getSettingsExportDateString(date, dateSeparator, dateTimeSeparator, ti } async function _getSettingsExportData(date) { - const optionsFull = await apiOptionsGetFull(); - const environment = await apiGetEnvironmentInfo(); - const fieldTemplatesDefault = await apiGetDefaultAnkiFieldTemplates(); + const optionsFull = await api.optionsGetFull(); + const environment = await api.getEnvironmentInfo(); + const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates(); // Format options for (const {options} of optionsFull.profiles) { diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js index 632c01ea..4d307f0f 100644 --- a/ext/bg/js/settings/dictionaries.js +++ b/ext/bg/js/settings/dictionaries.js @@ -17,13 +17,7 @@ /* global * PageExitPrevention - * apiDeleteDictionary - * apiGetDictionaryCounts - * apiGetDictionaryInfo - * apiImportDictionaryArchive - * apiOptionsGet - * apiOptionsGetFull - * apiPurgeDatabase + * api * getOptionsContext * getOptionsFullMutable * getOptionsMutable @@ -312,7 +306,7 @@ class SettingsDictionaryEntryUI { progressBar.style.width = `${percent}%`; }; - await apiDeleteDictionary(this.dictionaryInfo.title, onProgress); + await api.deleteDictionary(this.dictionaryInfo.title, onProgress); } catch (e) { dictionaryErrorsShow([e]); } finally { @@ -423,7 +417,7 @@ async function onDictionaryOptionsChanged() { dictionaryUI.setOptionsDictionaries(options.dictionaries); - const optionsFull = await apiOptionsGetFull(); + const optionsFull = await api.optionsGetFull(); document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported; await updateMainDictionarySelectValue(); @@ -431,7 +425,7 @@ async function onDictionaryOptionsChanged() { async function onDatabaseUpdated() { try { - const dictionaries = await apiGetDictionaryInfo(); + const dictionaries = await api.getDictionaryInfo(); dictionaryUI.setDictionaries(dictionaries); document.querySelector('#dict-warning').hidden = (dictionaries.length > 0); @@ -439,7 +433,7 @@ async function onDatabaseUpdated() { updateMainDictionarySelectOptions(dictionaries); await updateMainDictionarySelectValue(); - const {counts, total} = await apiGetDictionaryCounts(dictionaries.map((v) => v.title), true); + const {counts, total} = await api.getDictionaryCounts(dictionaries.map((v) => v.title), true); dictionaryUI.setCounts(counts, total); } catch (e) { dictionaryErrorsShow([e]); @@ -468,7 +462,7 @@ function updateMainDictionarySelectOptions(dictionaries) { async function updateMainDictionarySelectValue() { const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); + const options = await api.optionsGet(optionsContext); const value = options.general.mainDictionary; @@ -618,7 +612,7 @@ async function onDictionaryPurge(e) { dictionaryErrorsShow(null); dictionarySpinnerShow(true); - await apiPurgeDatabase(); + await api.purgeDatabase(); for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) { options.dictionaries = utilBackgroundIsolate({}); options.general.mainDictionary = ''; @@ -666,7 +660,7 @@ async function onDictionaryImport(e) { } }; - const optionsFull = await apiOptionsGetFull(); + const optionsFull = await api.optionsGetFull(); const importDetails = { prefixWildcardsSupported: optionsFull.global.database.prefixWildcardsSupported @@ -680,7 +674,7 @@ async function onDictionaryImport(e) { } const archiveContent = await dictReadFile(files[i]); - const {result, errors} = await apiImportDictionaryArchive(archiveContent, importDetails, updateProgress); + const {result, errors} = await api.importDictionaryArchive(archiveContent, importDetails, updateProgress); for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) { const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions(); dictionaryOptions.enabled = true; diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index 61395b1c..94f7f8f5 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -21,9 +21,7 @@ * ankiInitialize * ankiTemplatesInitialize * ankiTemplatesUpdateValue - * apiForwardLogsToBackend - * apiGetEnvironmentInfo - * apiOptionsSave + * api * appearanceInitialize * audioSettingsInitialize * backupInitialize @@ -265,7 +263,7 @@ function settingsGetSource() { async function settingsSaveOptions() { const source = await settingsGetSource(); - await apiOptionsSave(source); + await api.optionsSave(source); } async function onOptionsUpdated({source}) { @@ -290,7 +288,7 @@ async function settingsPopulateModifierKeys() { const scanModifierKeySelect = document.querySelector('#scan-modifier-key'); scanModifierKeySelect.textContent = ''; - const environment = await apiGetEnvironmentInfo(); + const environment = await api.getEnvironmentInfo(); const modifierKeys = [ {value: 'none', name: 'None'}, ...environment.modifiers.keys @@ -305,7 +303,7 @@ async function settingsPopulateModifierKeys() { async function onReady() { - apiForwardLogsToBackend(); + api.forwardLogsToBackend(); await yomichan.prepare(); showExtensionInformation(); diff --git a/ext/bg/js/settings/popup-preview-frame-main.js b/ext/bg/js/settings/popup-preview-frame-main.js index 8228125f..a362efa5 100644 --- a/ext/bg/js/settings/popup-preview-frame-main.js +++ b/ext/bg/js/settings/popup-preview-frame-main.js @@ -17,10 +17,10 @@ /* global * SettingsPopupPreview - * apiForwardLogsToBackend + * api */ (() => { - apiForwardLogsToBackend(); + api.forwardLogsToBackend(); new SettingsPopupPreview(); })(); diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js index 8901a0c4..bd9357e9 100644 --- a/ext/bg/js/settings/popup-preview-frame.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -20,14 +20,13 @@ * Popup * PopupFactory * TextSourceRange - * apiFrameInformationGet - * apiOptionsGet + * api */ class SettingsPopupPreview { constructor() { this.frontend = null; - this.apiOptionsGetOld = apiOptionsGet; + this.apiOptionsGetOld = api.optionsGet.bind(api); this.popup = null; this.popupSetCustomOuterCssOld = null; this.popupShown = false; @@ -54,10 +53,10 @@ class SettingsPopupPreview { document.querySelector('#theme-dark-checkbox').addEventListener('change', this.onThemeDarkCheckboxChanged.bind(this), false); // Overwrite API functions - window.apiOptionsGet = this.apiOptionsGet.bind(this); + api.optionsGet = this.apiOptionsGet.bind(this); // Overwrite frontend - const {frameId} = await apiFrameInformationGet(); + const {frameId} = await api.frameInformationGet(); const popupFactory = new PopupFactory(frameId); await popupFactory.prepare(); diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js index bdf5a13d..e32d5525 100644 --- a/ext/bg/js/settings/profiles.js +++ b/ext/bg/js/settings/profiles.js @@ -17,7 +17,7 @@ /* global * ConditionsUI - * apiOptionsGetFull + * api * conditionsClearCaches * formWrite * getOptionsFullMutable @@ -215,7 +215,7 @@ async function onProfileRemove(e) { return await onProfileRemoveConfirm(); } - const optionsFull = await apiOptionsGetFull(); + const optionsFull = await api.optionsGetFull(); if (optionsFull.profiles.length <= 1) { return; } @@ -278,7 +278,7 @@ async function onProfileMove(offset) { } async function onProfileCopy() { - const optionsFull = await apiOptionsGetFull(); + const optionsFull = await api.optionsGetFull(); if (optionsFull.profiles.length <= 1) { return; } diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js index d754a109..73c93fa1 100644 --- a/ext/bg/js/settings/storage.js +++ b/ext/bg/js/settings/storage.js @@ -16,7 +16,7 @@ */ /* global - * apiGetEnvironmentInfo + * api */ function storageBytesToLabeledString(size) { @@ -52,7 +52,7 @@ async function isStoragePeristent() { async function storageInfoInitialize() { storagePersistInitialize(); - const {browser, platform} = await apiGetEnvironmentInfo(); + const {browser, platform} = await api.getEnvironmentInfo(); document.documentElement.dataset.browser = browser; document.documentElement.dataset.operatingSystem = platform.os; diff --git a/ext/fg/js/content-script-main.js b/ext/fg/js/content-script-main.js index 57386b85..b057ae3d 100644 --- a/ext/fg/js/content-script-main.js +++ b/ext/fg/js/content-script-main.js @@ -21,10 +21,7 @@ * Frontend * PopupFactory * PopupProxy - * apiBroadcastTab - * apiForwardLogsToBackend - * apiFrameInformationGet - * apiOptionsGet + * api */ async function createIframePopupProxy(frameOffsetForwarder, setDisabled) { @@ -36,7 +33,7 @@ async function createIframePopupProxy(frameOffsetForwarder, setDisabled) { } } ); - apiBroadcastTab('rootPopupRequestInformationBroadcast'); + api.broadcastTab('rootPopupRequestInformationBroadcast'); const {popupId, frameId: parentFrameId} = await rootPopupInformationPromise; const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder); @@ -48,7 +45,7 @@ async function createIframePopupProxy(frameOffsetForwarder, setDisabled) { } async function getOrCreatePopup(depth) { - const {frameId} = await apiFrameInformationGet(); + const {frameId} = await api.frameInformationGet(); if (typeof frameId !== 'number') { const error = new Error('Failed to get frameId'); yomichan.logError(error); @@ -71,7 +68,7 @@ async function createPopupProxy(depth, id, parentFrameId) { } (async () => { - apiForwardLogsToBackend(); + api.forwardLogsToBackend(); await yomichan.prepare(); const data = window.frontendInitializationData || {}; @@ -112,7 +109,7 @@ async function createPopupProxy(depth, id, parentFrameId) { depth: isSearchPage ? 0 : depth, url: proxy ? await getPopupProxyUrl() : window.location.href }; - const options = await apiOptionsGet(optionsContext); + const options = await api.optionsGet(optionsContext); if (!proxy && frameOffsetForwarder === null) { frameOffsetForwarder = new FrameOffsetForwarder(); diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js index 20771910..249b4dbe 100644 --- a/ext/fg/js/float-main.js +++ b/ext/fg/js/float-main.js @@ -17,8 +17,7 @@ /* global * DisplayFloat - * apiForwardLogsToBackend - * apiOptionsGet + * api * dynamicLoader */ @@ -38,7 +37,7 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) { const applyOptions = async () => { const optionsContext = {depth, url}; - const options = await apiOptionsGet(optionsContext); + const options = await api.optionsGet(optionsContext); const maxPopupDepthExceeded = !(typeof depth === 'number' && depth < options.scanning.popupNestingMaxDepth); if (maxPopupDepthExceeded || optionsApplied) { return; } @@ -55,7 +54,7 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) { } (async () => { - apiForwardLogsToBackend(); + api.forwardLogsToBackend(); const display = new DisplayFloat(); await display.prepare(); })(); diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 845bf7f6..12d27a9f 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -17,8 +17,7 @@ /* global * Display - * apiBroadcastTab - * apiSendMessageToFrame + * api * popupNestedInitialize */ @@ -61,7 +60,7 @@ class DisplayFloat extends Display { yomichan.on('orphaned', this.onOrphaned.bind(this)); window.addEventListener('message', this.onMessage.bind(this), false); - apiBroadcastTab('popupPrepared', {secret: this._secret}); + api.broadcastTab('popupPrepared', {secret: this._secret}); } onError(error) { @@ -153,7 +152,7 @@ class DisplayFloat extends Display { }, 2000 ); - apiBroadcastTab('requestDocumentInformationBroadcast', {uniqueId}); + api.broadcastTab('requestDocumentInformationBroadcast', {uniqueId}); const {title} = await promise; return title; @@ -176,7 +175,7 @@ class DisplayFloat extends Display { const {token, frameId} = params; this._token = token; - apiSendMessageToFrame(frameId, 'popupInitialized', {secret, token}); + api.sendMessageToFrame(frameId, 'popupInitialized', {secret, token}); } async _configure({messageId, frameId, popupId, optionsContext, childrenSupported, scale}) { @@ -192,7 +191,7 @@ class DisplayFloat extends Display { this.setContentScale(scale); - apiSendMessageToFrame(frameId, 'popupConfigured', {messageId}); + api.sendMessageToFrame(frameId, 'popupConfigured', {messageId}); } _isMessageAuthenticated(message) { diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index 9b68d34e..10e3b5be 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -16,7 +16,7 @@ */ /* global - * apiBroadcastTab + * api */ class FrameOffsetForwarder { @@ -161,6 +161,6 @@ class FrameOffsetForwarder { } _forwardFrameOffsetOrigin(offset, uniqueId) { - apiBroadcastTab('frameOffset', {offset, uniqueId}); + api.broadcastTab('frameOffset', {offset, uniqueId}); } } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 575dc413..a263f3e6 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -17,11 +17,7 @@ /* global * TextScanner - * apiBroadcastTab - * apiGetZoom - * apiKanjiFind - * apiOptionsGet - * apiTermsFind + * api * docSentenceExtract */ @@ -69,7 +65,7 @@ class Frontend { async prepare() { try { await this.updateOptions(); - const {zoomFactor} = await apiGetZoom(); + const {zoomFactor} = await api.getZoom(); this._pageZoomFactor = zoomFactor; window.addEventListener('resize', this._onResize.bind(this), false); @@ -120,7 +116,7 @@ class Frontend { async updateOptions() { const optionsContext = await this.getOptionsContext(); - this._options = await apiOptionsGet(optionsContext); + this._options = await api.optionsGet(optionsContext); this._textScanner.setOptions(this._options); this._updateTextScannerEnabled(); @@ -261,7 +257,7 @@ class Frontend { const searchText = this._textScanner.getTextSourceContent(textSource, this._options.scanning.length); if (searchText.length === 0) { return null; } - const {definitions, length} = await apiTermsFind(searchText, {}, optionsContext); + const {definitions, length} = await api.termsFind(searchText, {}, optionsContext); if (definitions.length === 0) { return null; } textSource.setEndOffset(length); @@ -273,7 +269,7 @@ class Frontend { const searchText = this._textScanner.getTextSourceContent(textSource, 1); if (searchText.length === 0) { return null; } - const definitions = await apiKanjiFind(searchText, optionsContext); + const definitions = await api.kanjiFind(searchText, optionsContext); if (definitions.length === 0) { return null; } textSource.setEndOffset(1); @@ -351,12 +347,12 @@ class Frontend { _broadcastRootPopupInformation() { if (!this._popup.isProxy() && this._popup.depth === 0 && this._popup.frameId === 0) { - apiBroadcastTab('rootPopupInformation', {popupId: this._popup.id, frameId: this._popup.frameId}); + api.broadcastTab('rootPopupInformation', {popupId: this._popup.id, frameId: this._popup.frameId}); } } _broadcastDocumentInformation(uniqueId) { - apiBroadcastTab('documentInformationBroadcast', { + api.broadcastTab('documentInformationBroadcast', { uniqueId, frameId: this._popup.frameId, title: document.title diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index b7d4b57e..a8188143 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -17,7 +17,7 @@ /* global * DOM - * apiOptionsGet + * api * dynamicLoader */ @@ -89,7 +89,7 @@ class Popup { this._optionsContext = optionsContext; this._previousOptionsContextSource = source; - this._options = await apiOptionsGet(optionsContext); + this._options = await api.optionsGet(optionsContext); this.updateTheme(); this._invokeApi('setOptionsContext', {optionsContext}); diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index 0bc91759..e09a0db6 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -15,307 +15,321 @@ * along with this program. If not, see . */ +const api = (() => { + class API { + constructor() { + this._forwardLogsToBackendEnabled = false; + } + + forwardLogsToBackend() { + if (this._forwardLogsToBackendEnabled) { return; } + this._forwardLogsToBackendEnabled = true; + + yomichan.on('log', async ({error, level, context}) => { + try { + await this.log(errorToJson(error), level, context); + } catch (e) { + // NOP + } + }); + } + + // Invoke functions + + optionsSchemaGet() { + return this._invoke('optionsSchemaGet'); + } + + optionsGet(optionsContext) { + return this._invoke('optionsGet', {optionsContext}); + } + + optionsGetFull() { + return this._invoke('optionsGetFull'); + } + + optionsSave(source) { + return this._invoke('optionsSave', {source}); + } + + termsFind(text, details, optionsContext) { + return this._invoke('termsFind', {text, details, optionsContext}); + } + + textParse(text, optionsContext) { + return this._invoke('textParse', {text, optionsContext}); + } + + kanjiFind(text, optionsContext) { + return this._invoke('kanjiFind', {text, optionsContext}); + } + + definitionAdd(definition, mode, context, details, optionsContext) { + return this._invoke('definitionAdd', {definition, mode, context, details, optionsContext}); + } + + definitionsAddable(definitions, modes, context, optionsContext) { + return this._invoke('definitionsAddable', {definitions, modes, context, optionsContext}); + } + + noteView(noteId) { + return this._invoke('noteView', {noteId}); + } + + templateRender(template, data) { + return this._invoke('templateRender', {data, template}); + } + + audioGetUri(definition, source, details) { + return this._invoke('audioGetUri', {definition, source, details}); + } + + commandExec(command, params) { + return this._invoke('commandExec', {command, params}); + } + + screenshotGet(options) { + return this._invoke('screenshotGet', {options}); + } + + sendMessageToFrame(frameId, action, params) { + return this._invoke('sendMessageToFrame', {frameId, action, params}); + } + + broadcastTab(action, params) { + return this._invoke('broadcastTab', {action, params}); + } -function apiOptionsSchemaGet() { - return _apiInvoke('optionsSchemaGet'); -} + frameInformationGet() { + return this._invoke('frameInformationGet'); + } -function apiOptionsGet(optionsContext) { - return _apiInvoke('optionsGet', {optionsContext}); -} + injectStylesheet(type, value) { + return this._invoke('injectStylesheet', {type, value}); + } -function apiOptionsGetFull() { - return _apiInvoke('optionsGetFull'); -} + getEnvironmentInfo() { + return this._invoke('getEnvironmentInfo'); + } -function apiOptionsSave(source) { - return _apiInvoke('optionsSave', {source}); -} + clipboardGet() { + return this._invoke('clipboardGet'); + } -function apiTermsFind(text, details, optionsContext) { - return _apiInvoke('termsFind', {text, details, optionsContext}); -} + getDisplayTemplatesHtml() { + return this._invoke('getDisplayTemplatesHtml'); + } -function apiTextParse(text, optionsContext) { - return _apiInvoke('textParse', {text, optionsContext}); -} + getQueryParserTemplatesHtml() { + return this._invoke('getQueryParserTemplatesHtml'); + } -function apiKanjiFind(text, optionsContext) { - return _apiInvoke('kanjiFind', {text, optionsContext}); -} + getZoom() { + return this._invoke('getZoom'); + } -function apiDefinitionAdd(definition, mode, context, details, optionsContext) { - return _apiInvoke('definitionAdd', {definition, mode, context, details, optionsContext}); -} + getDefaultAnkiFieldTemplates() { + return this._invoke('getDefaultAnkiFieldTemplates'); + } -function apiDefinitionsAddable(definitions, modes, context, optionsContext) { - return _apiInvoke('definitionsAddable', {definitions, modes, context, optionsContext}); -} + getAnkiDeckNames() { + return this._invoke('getAnkiDeckNames'); + } -function apiNoteView(noteId) { - return _apiInvoke('noteView', {noteId}); -} + getAnkiModelNames() { + return this._invoke('getAnkiModelNames'); + } -function apiTemplateRender(template, data) { - return _apiInvoke('templateRender', {data, template}); -} + getAnkiModelFieldNames(modelName) { + return this._invoke('getAnkiModelFieldNames', {modelName}); + } -function apiAudioGetUri(definition, source, details) { - return _apiInvoke('audioGetUri', {definition, source, details}); -} + getDictionaryInfo() { + return this._invoke('getDictionaryInfo'); + } -function apiCommandExec(command, params) { - return _apiInvoke('commandExec', {command, params}); -} - -function apiScreenshotGet(options) { - return _apiInvoke('screenshotGet', {options}); -} - -function apiSendMessageToFrame(frameId, action, params) { - return _apiInvoke('sendMessageToFrame', {frameId, action, params}); -} - -function apiBroadcastTab(action, params) { - return _apiInvoke('broadcastTab', {action, params}); -} - -function apiFrameInformationGet() { - return _apiInvoke('frameInformationGet'); -} - -function apiInjectStylesheet(type, value) { - return _apiInvoke('injectStylesheet', {type, value}); -} - -function apiGetEnvironmentInfo() { - return _apiInvoke('getEnvironmentInfo'); -} - -function apiClipboardGet() { - return _apiInvoke('clipboardGet'); -} - -function apiGetDisplayTemplatesHtml() { - return _apiInvoke('getDisplayTemplatesHtml'); -} - -function apiGetQueryParserTemplatesHtml() { - return _apiInvoke('getQueryParserTemplatesHtml'); -} - -function apiGetZoom() { - return _apiInvoke('getZoom'); -} - -function apiGetDefaultAnkiFieldTemplates() { - return _apiInvoke('getDefaultAnkiFieldTemplates'); -} - -function apiGetAnkiDeckNames() { - return _apiInvoke('getAnkiDeckNames'); -} - -function apiGetAnkiModelNames() { - return _apiInvoke('getAnkiModelNames'); -} - -function apiGetAnkiModelFieldNames(modelName) { - return _apiInvoke('getAnkiModelFieldNames', {modelName}); -} - -function apiGetDictionaryInfo() { - return _apiInvoke('getDictionaryInfo'); -} - -function apiGetDictionaryCounts(dictionaryNames, getTotal) { - return _apiInvoke('getDictionaryCounts', {dictionaryNames, getTotal}); -} - -function apiPurgeDatabase() { - return _apiInvoke('purgeDatabase'); -} - -function apiGetMedia(targets) { - return _apiInvoke('getMedia', {targets}); -} - -function apiLog(error, level, context) { - return _apiInvoke('log', {error, level, context}); -} - -function apiLogIndicatorClear() { - return _apiInvoke('logIndicatorClear'); -} - -function apiImportDictionaryArchive(archiveContent, details, onProgress) { - return _apiInvokeWithProgress('importDictionaryArchive', {archiveContent, details}, onProgress); -} - -function apiDeleteDictionary(dictionaryName, onProgress) { - return _apiInvokeWithProgress('deleteDictionary', {dictionaryName}, onProgress); -} - -function apiModifySettings(targets, source) { - return _apiInvoke('modifySettings', {targets, source}); -} - -function _apiCreateActionPort(timeout=5000) { - return new Promise((resolve, reject) => { - let timer = null; - let portNameResolve; - let portNameReject; - const portNamePromise = new Promise((resolve2, reject2) => { - portNameResolve = resolve2; - portNameReject = reject2; - }); - - const onConnect = async (port) => { - try { - const portName = await portNamePromise; - if (port.name !== portName || timer === null) { return; } - } catch (e) { - return; - } - - clearTimeout(timer); - timer = null; - - chrome.runtime.onConnect.removeListener(onConnect); - resolve(port); - }; - - const onError = (e) => { - if (timer !== null) { - clearTimeout(timer); - timer = null; - } - chrome.runtime.onConnect.removeListener(onConnect); - portNameReject(e); - reject(e); - }; - - timer = setTimeout(() => onError(new Error('Timeout')), timeout); - - chrome.runtime.onConnect.addListener(onConnect); - _apiInvoke('createActionPort').then(portNameResolve, onError); - }); -} - -function _apiInvokeWithProgress(action, params, onProgress, timeout=5000) { - return new Promise((resolve, reject) => { - let timer = null; - let port = null; - - if (typeof onProgress !== 'function') { - onProgress = () => {}; - } - - const onMessage = (message) => { - switch (message.type) { - case 'ack': + getDictionaryCounts(dictionaryNames, getTotal) { + return this._invoke('getDictionaryCounts', {dictionaryNames, getTotal}); + } + + purgeDatabase() { + return this._invoke('purgeDatabase'); + } + + getMedia(targets) { + return this._invoke('getMedia', {targets}); + } + + log(error, level, context) { + return this._invoke('log', {error, level, context}); + } + + logIndicatorClear() { + return this._invoke('logIndicatorClear'); + } + + modifySettings(targets, source) { + return this._invoke('modifySettings', {targets, source}); + } + + // Invoke functions with progress + + importDictionaryArchive(archiveContent, details, onProgress) { + return this._invokeWithProgress('importDictionaryArchive', {archiveContent, details}, onProgress); + } + + deleteDictionary(dictionaryName, onProgress) { + return this._invokeWithProgress('deleteDictionary', {dictionaryName}, onProgress); + } + + // Utilities + + _createActionPort(timeout=5000) { + return new Promise((resolve, reject) => { + let timer = null; + let portNameResolve; + let portNameReject; + const portNamePromise = new Promise((resolve2, reject2) => { + portNameResolve = resolve2; + portNameReject = reject2; + }); + + const onConnect = async (port) => { + try { + const portName = await portNamePromise; + if (port.name !== portName || timer === null) { return; } + } catch (e) { + return; + } + + clearTimeout(timer); + timer = null; + + chrome.runtime.onConnect.removeListener(onConnect); + resolve(port); + }; + + const onError = (e) => { if (timer !== null) { clearTimeout(timer); timer = null; } - break; - case 'progress': - try { - onProgress(...message.data); - } catch (e) { - // NOP + chrome.runtime.onConnect.removeListener(onConnect); + portNameReject(e); + reject(e); + }; + + timer = setTimeout(() => onError(new Error('Timeout')), timeout); + + chrome.runtime.onConnect.addListener(onConnect); + this._invoke('createActionPort').then(portNameResolve, onError); + }); + } + + _invokeWithProgress(action, params, onProgress, timeout=5000) { + return new Promise((resolve, reject) => { + let timer = null; + let port = null; + + if (typeof onProgress !== 'function') { + onProgress = () => {}; + } + + const onMessage = (message) => { + switch (message.type) { + case 'ack': + if (timer !== null) { + clearTimeout(timer); + timer = null; + } + break; + case 'progress': + try { + onProgress(...message.data); + } catch (e) { + // NOP + } + break; + case 'complete': + cleanup(); + resolve(message.data); + break; + case 'error': + cleanup(); + reject(jsonToError(message.data)); + break; } - break; - case 'complete': + }; + + const onDisconnect = () => { cleanup(); - resolve(message.data); - break; - case 'error': + reject(new Error('Disconnected')); + }; + + const cleanup = () => { + if (timer !== null) { + clearTimeout(timer); + timer = null; + } + if (port !== null) { + port.onMessage.removeListener(onMessage); + port.onDisconnect.removeListener(onDisconnect); + port.disconnect(); + port = null; + } + onProgress = null; + }; + + timer = setTimeout(() => { cleanup(); - reject(jsonToError(message.data)); - break; - } - }; - - const onDisconnect = () => { - cleanup(); - reject(new Error('Disconnected')); - }; - - const cleanup = () => { - if (timer !== null) { - clearTimeout(timer); - timer = null; - } - if (port !== null) { - port.onMessage.removeListener(onMessage); - port.onDisconnect.removeListener(onDisconnect); - port.disconnect(); - port = null; - } - onProgress = null; - }; - - timer = setTimeout(() => { - cleanup(); - reject(new Error('Timeout')); - }, timeout); - - (async () => { - try { - port = await _apiCreateActionPort(timeout); - port.onMessage.addListener(onMessage); - port.onDisconnect.addListener(onDisconnect); - port.postMessage({action, params}); - } catch (e) { - cleanup(); - reject(e); - } finally { - action = null; - params = null; - } - })(); - }); -} - -function _apiInvoke(action, params={}) { - const data = {action, params}; - return new Promise((resolve, reject) => { - try { - chrome.runtime.sendMessage(data, (response) => { - _apiCheckLastError(chrome.runtime.lastError); - if (response !== null && typeof response === 'object') { - if (typeof response.error !== 'undefined') { - reject(jsonToError(response.error)); - } else { - resolve(response.result); + reject(new Error('Timeout')); + }, timeout); + + (async () => { + try { + port = await this._createActionPort(timeout); + port.onMessage.addListener(onMessage); + port.onDisconnect.addListener(onDisconnect); + port.postMessage({action, params}); + } catch (e) { + cleanup(); + reject(e); + } finally { + action = null; + params = null; } - } else { - const message = response === null ? 'Unexpected null response' : `Unexpected response of type ${typeof response}`; - reject(new Error(`${message} (${JSON.stringify(data)})`)); + })(); + }); + } + + _invoke(action, params={}) { + const data = {action, params}; + return new Promise((resolve, reject) => { + try { + chrome.runtime.sendMessage(data, (response) => { + this._checkLastError(chrome.runtime.lastError); + if (response !== null && typeof response === 'object') { + if (typeof response.error !== 'undefined') { + reject(jsonToError(response.error)); + } else { + resolve(response.result); + } + } else { + const message = response === null ? 'Unexpected null response' : `Unexpected response of type ${typeof response}`; + reject(new Error(`${message} (${JSON.stringify(data)})`)); + } + }); + } catch (e) { + reject(e); + yomichan.triggerOrphaned(e); } }); - } catch (e) { - reject(e); - yomichan.triggerOrphaned(e); - } - }); -} - -function _apiCheckLastError() { - // NOP -} - -let _apiForwardLogsToBackendEnabled = false; -function apiForwardLogsToBackend() { - if (_apiForwardLogsToBackendEnabled) { return; } - _apiForwardLogsToBackendEnabled = true; - - yomichan.on('log', async ({error, level, context}) => { - try { - await apiLog(errorToJson(error), level, context); - } catch (e) { + } + + _checkLastError() { // NOP } - }); -} + } + + return new API(); +})(); diff --git a/ext/mixed/js/display-generator.js b/ext/mixed/js/display-generator.js index a2b2b139..3f3a155e 100644 --- a/ext/mixed/js/display-generator.js +++ b/ext/mixed/js/display-generator.js @@ -17,7 +17,7 @@ /* global * TemplateHandler - * apiGetDisplayTemplatesHtml + * api * jp */ @@ -29,7 +29,7 @@ class DisplayGenerator { } async prepare() { - const html = await apiGetDisplayTemplatesHtml(); + const html = await api.getDisplayTemplatesHtml(); this._templateHandler = new TemplateHandler(html); } diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 2e59b4ff..380134ad 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -22,15 +22,7 @@ * DisplayGenerator * MediaLoader * WindowScroll - * apiAudioGetUri - * apiBroadcastTab - * apiDefinitionAdd - * apiDefinitionsAddable - * apiKanjiFind - * apiNoteView - * apiOptionsGet - * apiScreenshotGet - * apiTermsFind + * api * docRangeFromPoint * docSentenceExtract */ @@ -49,7 +41,7 @@ class Display { this.audioSystem = new AudioSystem({ audioUriBuilder: { getUri: async (definition, source, details) => { - return await apiAudioGetUri(definition, source, details); + return await api.audioGetUri(definition, source, details); } }, useCache: true @@ -212,7 +204,7 @@ class Display { url: this.context.get('url') }; - const definitions = await apiKanjiFind(link.textContent, this.getOptionsContext()); + const definitions = await api.kanjiFind(link.textContent, this.getOptionsContext()); this.setContent('kanji', {definitions, context}); } catch (error) { this.onError(error); @@ -290,7 +282,7 @@ class Display { try { textSource.setEndOffset(this.options.scanning.length); - ({definitions, length} = await apiTermsFind(textSource.text(), {}, this.getOptionsContext())); + ({definitions, length} = await api.termsFind(textSource.text(), {}, this.getOptionsContext())); if (definitions.length === 0) { return false; } @@ -334,7 +326,7 @@ class Display { onNoteView(e) { e.preventDefault(); const link = e.currentTarget; - apiNoteView(link.dataset.noteId); + api.noteView(link.dataset.noteId); } onKeyDown(e) { @@ -379,7 +371,7 @@ class Display { } async updateOptions() { - this.options = await apiOptionsGet(this.getOptionsContext()); + this.options = await api.optionsGet(this.getOptionsContext()); this.updateDocumentOptions(this.options); this.updateTheme(this.options.general.popupTheme); this.setCustomCss(this.options.general.customPopupCss); @@ -746,7 +738,7 @@ class Display { noteTryView() { const button = this.viewerButtonFind(this.index); if (button !== null && !button.classList.contains('disabled')) { - apiNoteView(button.dataset.noteId); + api.noteView(button.dataset.noteId); } } @@ -763,7 +755,7 @@ class Display { } const context = await this._getNoteContext(); - const noteId = await apiDefinitionAdd(definition, mode, context, details, this.getOptionsContext()); + const noteId = await api.definitionAdd(definition, mode, context, details, this.getOptionsContext()); if (noteId) { const index = this.definitions.indexOf(definition); const adderButton = this.adderButtonFind(index, mode); @@ -857,7 +849,7 @@ class Display { await promiseTimeout(1); // Wait for popup to be hidden. const {format, quality} = this.options.anki.screenshot; - const dataUrl = await apiScreenshotGet({format, quality}); + const dataUrl = await api.screenshotGet({format, quality}); if (!dataUrl || dataUrl.error) { return; } return {dataUrl, format}; @@ -871,7 +863,7 @@ class Display { } setPopupVisibleOverride(visible) { - return apiBroadcastTab('popupSetVisibleOverride', {visible}); + return api.broadcastTab('popupSetVisibleOverride', {visible}); } setSpinnerVisible(visible) { @@ -933,7 +925,7 @@ class Display { async getDefinitionsAddable(definitions, modes) { try { const context = await this._getNoteContext(); - return await apiDefinitionsAddable(definitions, modes, context, this.getOptionsContext()); + return await api.definitionsAddable(definitions, modes, context, this.getOptionsContext()); } catch (e) { return []; } diff --git a/ext/mixed/js/dynamic-loader.js b/ext/mixed/js/dynamic-loader.js index ce946109..37f85112 100644 --- a/ext/mixed/js/dynamic-loader.js +++ b/ext/mixed/js/dynamic-loader.js @@ -16,7 +16,7 @@ */ /* global - * apiInjectStylesheet + * api */ const dynamicLoader = (() => { @@ -45,7 +45,7 @@ const dynamicLoader = (() => { } injectedStylesheets.set(id, null); - await apiInjectStylesheet(type, value); + await api.injectStylesheet(type, value); return null; } diff --git a/ext/mixed/js/media-loader.js b/ext/mixed/js/media-loader.js index 64ccd715..fc6e93d1 100644 --- a/ext/mixed/js/media-loader.js +++ b/ext/mixed/js/media-loader.js @@ -16,7 +16,7 @@ */ /* global - * apiGetMedia + * api */ class MediaLoader { @@ -84,7 +84,7 @@ class MediaLoader { async _getMediaData(path, dictionaryName, cachedData) { const token = this._token; - const data = (await apiGetMedia([{path, dictionaryName}]))[0]; + const data = (await api.getMedia([{path, dictionaryName}]))[0]; if (token === this._token && data !== null) { const contentArrayBuffer = this._base64ToArrayBuffer(data.content); const blob = new Blob([contentArrayBuffer], {type: data.mediaType}); -- cgit v1.2.3 From 3089bb7908e42e9101241476f700033df82e685d Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 24 May 2020 13:38:48 -0400 Subject: Settings refactor (#541) * Remove debug info * Trigger onOptionsUpdated instead of formWrite when profile changes * Update how Anki field changes are observed * Update how general.enableClipboardPopups setting is changed * Change where ankiTemplatesUpdateValue occurs * Change where onDictionaryOptionsChanged occurs * Remove unused global declarations * Remove stray data attribute --- ext/bg/js/settings/anki.js | 38 ++++++++++++++++------ ext/bg/js/settings/main.js | 74 +++++++++++++++++------------------------- ext/bg/js/settings/profiles.js | 8 ++--- ext/bg/settings.html | 6 ++-- 4 files changed, 62 insertions(+), 64 deletions(-) (limited to 'ext/bg/js') diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js index ba83f994..8de97554 100644 --- a/ext/bg/js/settings/anki.js +++ b/ext/bg/js/settings/anki.js @@ -19,16 +19,12 @@ * api * getOptionsContext * getOptionsMutable - * onFormOptionsChanged * settingsSaveOptions * utilBackgroundIsolate */ // Private -let _ankiDataPopulated = false; - - function _ankiSpinnerShow(show) { const spinner = $('#anki-spinner'); if (show) { @@ -158,7 +154,7 @@ async function _ankiFieldsPopulate(tabId, options) { container.appendChild(fragment); for (const node of container.querySelectorAll('.anki-field-value')) { - node.addEventListener('change', onFormOptionsChanged, false); + node.addEventListener('change', _onAnkiFieldsChanged, false); } for (const node of container.querySelectorAll('.marker-link')) { node.addEventListener('click', _onAnkiMarkerClicked, false); @@ -203,6 +199,23 @@ async function _onAnkiModelChanged(e) { await _ankiFieldsPopulate(tabId, options); } +async function _onAnkiFieldsChanged() { + const optionsContext = getOptionsContext(); + const options = await getOptionsMutable(optionsContext); + + options.anki.terms.deck = $('#anki-terms-deck').val(); + options.anki.terms.model = $('#anki-terms-model').val(); + options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#terms .anki-field-value'))); + options.anki.kanji.deck = $('#anki-kanji-deck').val(); + options.anki.kanji.model = $('#anki-kanji-model').val(); + options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#kanji .anki-field-value'))); + + await settingsSaveOptions(); + + await onAnkiOptionsChanged(options); +} + + // Public @@ -272,20 +285,25 @@ function ankiGetFieldMarkers(type) { function ankiInitialize() { + $('#anki-fields-container input,#anki-fields-container select,#anki-fields-container textarea').change(_onAnkiFieldsChanged); + for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) { node.addEventListener('change', _onAnkiModelChanged, false); } + + onAnkiOptionsChanged(); } -async function onAnkiOptionsChanged(options) { +async function onAnkiOptionsChanged(options=null) { + if (options === null) { + const optionsContext = getOptionsContext(); + options = await getOptionsMutable(optionsContext); + } + if (!options.anki.enable) { - _ankiDataPopulated = false; return; } - if (_ankiDataPopulated) { return; } - await _ankiDeckAndModelPopulate(options); - _ankiDataPopulated = true; await Promise.all([_ankiFieldsPopulate('terms', options), _ankiFieldsPopulate('kanji', options)]); } diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index 94f7f8f5..60b9e008 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -16,8 +16,6 @@ */ /* global - * ankiErrorShown - * ankiFieldsToDict * ankiInitialize * ankiTemplatesInitialize * ankiTemplatesUpdateValue @@ -33,7 +31,6 @@ * storageInfoInitialize * utilBackend * utilBackgroundIsolate - * utilIsolate */ function getOptionsMutable(optionsContext) { @@ -48,22 +45,6 @@ function getOptionsFullMutable() { async function formRead(options) { options.general.enable = $('#enable').prop('checked'); - const enableClipboardPopups = $('#enable-clipboard-popups').prop('checked'); - if (enableClipboardPopups) { - options.general.enableClipboardPopups = await new Promise((resolve, _reject) => { - chrome.permissions.request( - {permissions: ['clipboardRead']}, - (granted) => { - if (!granted) { - $('#enable-clipboard-popups').prop('checked', false); - } - resolve(granted); - } - ); - }); - } else { - options.general.enableClipboardPopups = false; - } options.general.showGuide = $('#show-usage-guide').prop('checked'); options.general.compactTags = $('#compact-tags').prop('checked'); options.general.compactGlossaries = $('#compact-glossaries').prop('checked'); @@ -125,7 +106,6 @@ async function formRead(options) { options.parsing.termSpacing = $('#parsing-term-spacing').prop('checked'); options.parsing.readingMode = $('#parsing-reading-mode').val(); - const optionsAnkiEnableOld = options.anki.enable; options.anki.enable = $('#anki-enable').prop('checked'); options.anki.tags = utilBackgroundIsolate($('#card-tags').val().split(/[,; ]+/)); options.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10); @@ -133,20 +113,10 @@ async function formRead(options) { options.anki.duplicateScope = $('#duplicate-scope').val(); options.anki.screenshot.format = $('#screenshot-format').val(); options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10); - - if (optionsAnkiEnableOld && !ankiErrorShown()) { - options.anki.terms.deck = $('#anki-terms-deck').val(); - options.anki.terms.model = $('#anki-terms-model').val(); - options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#terms .anki-field-value'))); - options.anki.kanji.deck = $('#anki-kanji-deck').val(); - options.anki.kanji.model = $('#anki-kanji-model').val(); - options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#kanji .anki-field-value'))); - } } async function formWrite(options) { $('#enable').prop('checked', options.general.enable); - $('#enable-clipboard-popups').prop('checked', options.general.enableClipboardPopups); $('#show-usage-guide').prop('checked', options.general.showGuide); $('#compact-tags').prop('checked', options.general.compactTags); $('#compact-glossaries').prop('checked', options.general.compactGlossaries); @@ -216,14 +186,11 @@ async function formWrite(options) { $('#screenshot-format').val(options.anki.screenshot.format); $('#screenshot-quality').val(options.anki.screenshot.quality); - await ankiTemplatesUpdateValue(); - await onAnkiOptionsChanged(options); - await onDictionaryOptionsChanged(); - formUpdateVisibility(options); } function formSetupEventListeners() { + document.querySelector('#enable-clipboard-popups').addEventListener('change', onEnableClipboardPopupsChanged, false); $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(onFormOptionsChanged); } @@ -232,15 +199,6 @@ function formUpdateVisibility(options) { document.documentElement.dataset.optionsGeneralDebugInfo = `${!!options.general.debugInfo}`; document.documentElement.dataset.optionsGeneralShowAdvanced = `${!!options.general.showAdvanced}`; document.documentElement.dataset.optionsGeneralResultOutputMode = `${options.general.resultOutputMode}`; - - if (options.general.debugInfo) { - const temp = utilIsolate(options); - if (typeof temp.anki.fieldTemplates === 'string') { - temp.anki.fieldTemplates = '...'; - } - const text = JSON.stringify(temp, null, 4); - $('#debug').text(text); - } } async function onFormOptionsChanged() { @@ -250,8 +208,30 @@ async function onFormOptionsChanged() { await formRead(options); await settingsSaveOptions(); formUpdateVisibility(options); +} - await onAnkiOptionsChanged(options); +async function onEnableClipboardPopupsChanged(e) { + const optionsContext = getOptionsContext(); + const options = await getOptionsMutable(optionsContext); + + const enableClipboardPopups = e.target.checked; + if (enableClipboardPopups) { + options.general.enableClipboardPopups = await new Promise((resolve) => { + chrome.permissions.request( + {permissions: ['clipboardRead']}, + (granted) => { + if (!granted) { + $('#enable-clipboard-popups').prop('checked', false); + } + resolve(granted); + } + ); + }); + } else { + options.general.enableClipboardPopups = false; + } + + await settingsSaveOptions(); } @@ -272,6 +252,12 @@ async function onOptionsUpdated({source}) { const optionsContext = getOptionsContext(); const options = await getOptionsMutable(optionsContext); + + document.querySelector('#enable-clipboard-popups').checked = options.general.enableClipboardPopups; + ankiTemplatesUpdateValue(); + onDictionaryOptionsChanged(); + onAnkiOptionsChanged(); + await formWrite(options); } diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js index e32d5525..59f7fbb1 100644 --- a/ext/bg/js/settings/profiles.js +++ b/ext/bg/js/settings/profiles.js @@ -19,9 +19,8 @@ * ConditionsUI * api * conditionsClearCaches - * formWrite * getOptionsFullMutable - * getOptionsMutable + * onOptionsUpdated * profileConditionsDescriptor * profileConditionsDescriptorPromise * settingsSaveOptions @@ -131,10 +130,7 @@ function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndi async function profileOptionsUpdateTarget(optionsFull) { await profileFormWrite(optionsFull); - - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); - await formWrite(options); + await onOptionsUpdated({source: null}); } function profileOptionsCreateCopyName(name, profiles, maxUniqueAttempts) { diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 3ce91f12..7964ab90 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -138,7 +138,7 @@ -
+
@@ -854,7 +854,7 @@
  • Kanji
  • -
    +
    @@ -1111,8 +1111,6 @@

    -
    
    -
                 
    -- 
    cgit v1.2.3
    
    
    From 66e1185686f98f1cc4493298b5b1d4e0be7d826a Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Sun, 24 May 2020 13:50:34 -0400
    Subject: Settings binder (#542)
    
    * Fix _modifySetting being async
    
    * Return values for modifySettings's set and splice actions
    
    * Add apiGetSettings
    
    * Create a class which can accumulate tasks to run in bulk
    
    * Create a class which binds input elements to customizable sources
    
    * Create class which binds input elements to settings
    
    * Add support for value transforms
    
    * Remove redundant ObjectPropertyAccessor.getPathArray
    
    * Fix not using correct types for input.min/max/step
    
    * Fix wrong condition
    
    * Use api object
    ---
     ext/bg/js/backend.js                      |  90 +++++---
     ext/bg/js/settings/dom-settings-binder.js | 122 +++++++++++
     ext/mixed/js/api.js                       |   4 +
     ext/mixed/js/dom-data-binder.js           | 349 ++++++++++++++++++++++++++++++
     ext/mixed/js/task-accumulator.js          |  81 +++++++
     5 files changed, 612 insertions(+), 34 deletions(-)
     create mode 100644 ext/bg/js/settings/dom-settings-binder.js
     create mode 100644 ext/mixed/js/dom-data-binder.js
     create mode 100644 ext/mixed/js/task-accumulator.js
    
    (limited to 'ext/bg/js')
    
    diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
    index 8df4fd9d..90895737 100644
    --- a/ext/bg/js/backend.js
    +++ b/ext/bg/js/backend.js
    @@ -119,7 +119,8 @@ class Backend {
                 ['log',                          {async: false, contentScript: true,  handler: this._onApiLog.bind(this)}],
                 ['logIndicatorClear',            {async: false, contentScript: true,  handler: this._onApiLogIndicatorClear.bind(this)}],
                 ['createActionPort',             {async: false, contentScript: true,  handler: this._onApiCreateActionPort.bind(this)}],
    -            ['modifySettings',               {async: true,  contentScript: true,  handler: this._onApiModifySettings.bind(this)}]
    +            ['modifySettings',               {async: true,  contentScript: true,  handler: this._onApiModifySettings.bind(this)}],
    +            ['getSettings',                  {async: false, contentScript: true,  handler: this._onApiGetSettings.bind(this)}]
             ]);
             this._messageHandlersWithProgress = new Map([
                 ['importDictionaryArchive', {async: true,  contentScript: false, handler: this._onApiImportDictionaryArchive.bind(this)}],
    @@ -831,8 +832,8 @@ class Backend {
             const results = [];
             for (const target of targets) {
                 try {
    -                this._modifySetting(target);
    -                results.push({result: true});
    +                const result = this._modifySetting(target);
    +                results.push({result: utilIsolate(result)});
                 } catch (e) {
                     results.push({error: errorToJson(e)});
                 }
    @@ -841,6 +842,19 @@ class Backend {
             return results;
         }
     
    +    _onApiGetSettings({targets}) {
    +        const results = [];
    +        for (const target of targets) {
    +            try {
    +                const result = this._getSetting(target);
    +                results.push({result: utilIsolate(result)});
    +            } catch (e) {
    +                results.push({error: errorToJson(e)});
    +            }
    +        }
    +        return results;
    +    }
    +
         // Command handlers
     
         _createActionListenerPort(port, sender, handlers) {
    @@ -1017,45 +1031,53 @@ class Backend {
             }
         }
     
    -    async _modifySetting(target) {
    +    _getSetting(target) {
    +        const options = this._getModifySettingObject(target);
    +        const accessor = new ObjectPropertyAccessor(options);
    +        const {path} = target;
    +        if (typeof path !== 'string') { throw new Error('Invalid path'); }
    +        return accessor.get(ObjectPropertyAccessor.getPathArray(path));
    +    }
    +
    +    _modifySetting(target) {
             const options = this._getModifySettingObject(target);
             const accessor = new ObjectPropertyAccessor(options);
             const action = target.action;
             switch (action) {
                 case 'set':
    -                {
    -                    const {path, value} = target;
    -                    if (typeof path !== 'string') { throw new Error('Invalid path'); }
    -                    accessor.set(ObjectPropertyAccessor.getPathArray(path), value);
    -                }
    -                break;
    +            {
    +                const {path, value} = target;
    +                if (typeof path !== 'string') { throw new Error('Invalid path'); }
    +                const pathArray = ObjectPropertyAccessor.getPathArray(path);
    +                accessor.set(pathArray, value);
    +                return accessor.get(pathArray);
    +            }
                 case 'delete':
    -                {
    -                    const {path} = target;
    -                    if (typeof path !== 'string') { throw new Error('Invalid path'); }
    -                    accessor.delete(ObjectPropertyAccessor.getPathArray(path));
    -                }
    -                break;
    +            {
    +                const {path} = target;
    +                if (typeof path !== 'string') { throw new Error('Invalid path'); }
    +                accessor.delete(ObjectPropertyAccessor.getPathArray(path));
    +                return true;
    +            }
                 case 'swap':
    -                {
    -                    const {path1, path2} = target;
    -                    if (typeof path1 !== 'string') { throw new Error('Invalid path1'); }
    -                    if (typeof path2 !== 'string') { throw new Error('Invalid path2'); }
    -                    accessor.swap(ObjectPropertyAccessor.getPathArray(path1), ObjectPropertyAccessor.getPathArray(path2));
    -                }
    -                break;
    +            {
    +                const {path1, path2} = target;
    +                if (typeof path1 !== 'string') { throw new Error('Invalid path1'); }
    +                if (typeof path2 !== 'string') { throw new Error('Invalid path2'); }
    +                accessor.swap(ObjectPropertyAccessor.getPathArray(path1), ObjectPropertyAccessor.getPathArray(path2));
    +                return true;
    +            }
                 case 'splice':
    -                {
    -                    const {path, start, deleteCount, items} = target;
    -                    if (typeof path !== 'string') { throw new Error('Invalid path'); }
    -                    if (typeof start !== 'number' || Math.floor(start) !== start) { throw new Error('Invalid start'); }
    -                    if (typeof deleteCount !== 'number' || Math.floor(deleteCount) !== deleteCount) { throw new Error('Invalid deleteCount'); }
    -                    if (!Array.isArray(items)) { throw new Error('Invalid items'); }
    -                    const array = accessor.get(ObjectPropertyAccessor.getPathArray(path));
    -                    if (!Array.isArray(array)) { throw new Error('Invalid target type'); }
    -                    array.splice(start, deleteCount, ...items);
    -                }
    -                break;
    +            {
    +                const {path, start, deleteCount, items} = target;
    +                if (typeof path !== 'string') { throw new Error('Invalid path'); }
    +                if (typeof start !== 'number' || Math.floor(start) !== start) { throw new Error('Invalid start'); }
    +                if (typeof deleteCount !== 'number' || Math.floor(deleteCount) !== deleteCount) { throw new Error('Invalid deleteCount'); }
    +                if (!Array.isArray(items)) { throw new Error('Invalid items'); }
    +                const array = accessor.get(ObjectPropertyAccessor.getPathArray(path));
    +                if (!Array.isArray(array)) { throw new Error('Invalid target type'); }
    +                return array.splice(start, deleteCount, ...items);
    +            }
                 default:
                     throw new Error(`Unknown action: ${action}`);
             }
    diff --git a/ext/bg/js/settings/dom-settings-binder.js b/ext/bg/js/settings/dom-settings-binder.js
    new file mode 100644
    index 00000000..0441ec29
    --- /dev/null
    +++ b/ext/bg/js/settings/dom-settings-binder.js
    @@ -0,0 +1,122 @@
    +/*
    + * Copyright (C) 2020  Yomichan Authors
    + *
    + * This program is free software: you can redistribute it and/or modify
    + * it under the terms of the GNU General Public License as published by
    + * the Free Software Foundation, either version 3 of the License, or
    + * (at your option) any later version.
    + *
    + * This program is distributed in the hope that it will be useful,
    + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    + * GNU General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this program.  If not, see .
    + */
    +
    +/* global
    + * DOMDataBinder
    + * api
    + * getOptionsContext
    + */
    +
    +class DOMSettingsBinder {
    +    constructor({getOptionsContext, transforms=null}) {
    +        this._getOptionsContext = getOptionsContext;
    +        this._defaultScope = 'profile';
    +        this._dataBinder = new DOMDataBinder({
    +            selector: '[data-setting]',
    +            createElementMetadata: this._createElementMetadata.bind(this),
    +            compareElementMetadata: this._compareElementMetadata.bind(this),
    +            getValues: this._getValues.bind(this),
    +            setValues: this._setValues.bind(this)
    +        });
    +        this._transforms = new Map(transforms !== null ? transforms : []);
    +    }
    +
    +    observe(element) {
    +        this._dataBinder.observe(element);
    +    }
    +
    +    disconnect() {
    +        this._dataBinder.disconnect();
    +    }
    +
    +    refresh() {
    +        this._dataBinder.refresh();
    +    }
    +
    +    // Private
    +
    +    _createElementMetadata(element) {
    +        return {
    +            path: element.dataset.setting,
    +            scope: element.dataset.scope,
    +            transformPre: element.dataset.transformPre,
    +            transformPost: element.dataset.transformPost
    +        };
    +    }
    +
    +    _compareElementMetadata(metadata1, metadata2) {
    +        return (
    +            metadata1.path === metadata2.path &&
    +            metadata1.scope === metadata2.scope &&
    +            metadata1.transformPre === metadata2.transformPre &&
    +            metadata1.transformPost === metadata2.transformPost
    +        );
    +    }
    +
    +    async _getValues(targets) {
    +        const settingsTargets = [];
    +        for (const {metadata: {path, scope}} of targets) {
    +            const target = {
    +                path,
    +                scope: scope || this._defaultScope
    +            };
    +            if (target.scope === 'profile') {
    +                target.optionsContext = this._getOptionsContext();
    +            }
    +            settingsTargets.push(target);
    +        }
    +        return this._transformResults(await api.getSettings(settingsTargets), targets);
    +    }
    +
    +    async _setValues(targets) {
    +        const settingsTargets = [];
    +        for (const {metadata, value, element} of targets) {
    +            const {path, scope, transformPre} = metadata;
    +            const target = {
    +                path,
    +                scope: scope || this._defaultScope,
    +                action: 'set',
    +                value: this._transform(value, transformPre, metadata, element)
    +            };
    +            if (target.scope === 'profile') {
    +                target.optionsContext = this._getOptionsContext();
    +            }
    +            settingsTargets.push(target);
    +        }
    +        return this._transformResults(await api.modifySettings(settingsTargets), targets);
    +    }
    +
    +    _transform(value, transform, metadata, element) {
    +        if (typeof transform === 'string') {
    +            const transformFunction = this._transforms.get(transform);
    +            if (typeof transformFunction !== 'undefined') {
    +                value = transformFunction(value, metadata, element);
    +            }
    +        }
    +        return value;
    +    }
    +
    +    _transformResults(values, targets) {
    +        return values.map((value, i) => {
    +            const error = value.error;
    +            if (error) { return jsonToError(error); }
    +            const {metadata, element} = targets[i];
    +            const result = this._transform(value.result, metadata.transformPost, metadata, element);
    +            return {result};
    +        });
    +    }
    +}
    diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js
    index e09a0db6..2d5ad9e7 100644
    --- a/ext/mixed/js/api.js
    +++ b/ext/mixed/js/api.js
    @@ -172,6 +172,10 @@ const api = (() => {
                 return this._invoke('modifySettings', {targets, source});
             }
     
    +        getSettings(targets) {
    +            return this._invoke('getSettings', {targets});
    +        }
    +
             // Invoke functions with progress
     
             importDictionaryArchive(archiveContent, details, onProgress) {
    diff --git a/ext/mixed/js/dom-data-binder.js b/ext/mixed/js/dom-data-binder.js
    new file mode 100644
    index 00000000..05a84240
    --- /dev/null
    +++ b/ext/mixed/js/dom-data-binder.js
    @@ -0,0 +1,349 @@
    +/*
    + * Copyright (C) 2020  Yomichan Authors
    + *
    + * This program is free software: you can redistribute it and/or modify
    + * it under the terms of the GNU General Public License as published by
    + * the Free Software Foundation, either version 3 of the License, or
    + * (at your option) any later version.
    + *
    + * This program is distributed in the hope that it will be useful,
    + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    + * GNU General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this program.  If not, see .
    + */
    +
    +/* global
    + * TaskAccumulator
    + */
    +
    +class DOMDataBinder {
    +    constructor({selector, ignoreSelectors=[], createElementMetadata, compareElementMetadata, getValues, setValues, onError=null}) {
    +        this._selector = selector;
    +        this._ignoreSelectors = ignoreSelectors;
    +        this._createElementMetadata = createElementMetadata;
    +        this._compareElementMetadata = compareElementMetadata;
    +        this._getValues = getValues;
    +        this._setValues = setValues;
    +        this._onError = onError;
    +        this._updateTasks = new TaskAccumulator(this._onBulkUpdate.bind(this));
    +        this._assignTasks = new TaskAccumulator(this._onBulkAssign.bind(this));
    +        this._mutationObserver = new MutationObserver(this._onMutation.bind(this));
    +        this._observingElement = null;
    +        this._elementMap = new Map(); // Map([element => observer]...)
    +        this._elementAncestorMap = new Map(); // Map([element => Set([observer]...))
    +    }
    +
    +    observe(element) {
    +        if (this._isObserving) { return; }
    +
    +        this._observingElement = element;
    +        this._mutationObserver.observe(element, {
    +            attributes: true,
    +            attributeOldValue: true,
    +            childList: true,
    +            subtree: true
    +        });
    +        this._onMutation([{
    +            type: 'childList',
    +            target: element.parentNode,
    +            addedNodes: [element],
    +            removedNodes: []
    +        }]);
    +    }
    +
    +    disconnect() {
    +        if (!this._isObserving) { return; }
    +
    +        this._mutationObserver.disconnect();
    +        this._observingElement = null;
    +
    +        for (const observer of this._elementMap.values()) {
    +            this._removeObserver(observer);
    +        }
    +    }
    +
    +    async refresh() {
    +        await this._updateTasks.enqueue(null, {all: true});
    +    }
    +
    +    // Private
    +
    +    _onMutation(mutationList) {
    +        for (const mutation of mutationList) {
    +            switch (mutation.type) {
    +                case 'childList':
    +                    this._onChildListMutation(mutation);
    +                    break;
    +                case 'attributes':
    +                    this._onAttributeMutation(mutation);
    +                    break;
    +            }
    +        }
    +    }
    +
    +    _onChildListMutation({addedNodes, removedNodes, target}) {
    +        const selector = this._selector;
    +        const ELEMENT_NODE = Node.ELEMENT_NODE;
    +
    +        for (const node of removedNodes) {
    +            const observers = this._elementAncestorMap.get(node);
    +            if (typeof observers === 'undefined') { continue; }
    +            for (const observer of observers) {
    +                this._removeObserver(observer);
    +            }
    +        }
    +
    +        for (const node of addedNodes) {
    +            if (node.nodeType !== ELEMENT_NODE) { continue; }
    +            if (node.matches(selector)) {
    +                this._createObserver(node);
    +            }
    +            for (const childNode of node.querySelectorAll(selector)) {
    +                this._createObserver(childNode);
    +            }
    +        }
    +
    +        if (addedNodes.length !== 0 || addedNodes.length !== 0) {
    +            const observer = this._elementMap.get(target);
    +            if (typeof observer !== 'undefined') {
    +                observer.updateValue();
    +            }
    +        }
    +    }
    +
    +    _onAttributeMutation({target}) {
    +        const selector = this._selector;
    +        const observers = this._elementAncestorMap.get(target);
    +        if (typeof observers !== 'undefined') {
    +            for (const observer of observers) {
    +                const element = observer.element;
    +                if (
    +                    !element.matches(selector) ||
    +                    this._shouldIgnoreElement(element) ||
    +                    this._isObserverStale(observer)
    +                ) {
    +                    this._removeObserver(observer);
    +                }
    +            }
    +        }
    +
    +        if (target.matches(selector)) {
    +            this._createObserver(target);
    +        }
    +    }
    +
    +    async _onBulkUpdate(tasks) {
    +        let all = false;
    +        const targets = [];
    +        for (const [observer, task] of tasks) {
    +            if (observer === null) {
    +                if (task.data.all) {
    +                    all = true;
    +                    break;
    +                }
    +            } else {
    +                targets.push([observer, task]);
    +            }
    +        }
    +        if (all) {
    +            targets.length = 0;
    +            for (const observer of this._elementMap.values()) {
    +                targets.push([observer, null]);
    +            }
    +        }
    +
    +        const args = targets.map(([observer]) => ({
    +            element: observer.element,
    +            metadata: observer.metadata
    +        }));
    +        const responses = await this._getValues(args);
    +        this._applyValues(targets, responses, true);
    +    }
    +
    +    async _onBulkAssign(tasks) {
    +        const targets = tasks;
    +        const args = targets.map(([observer, task]) => ({
    +            element: observer.element,
    +            metadata: observer.metadata,
    +            value: task.data.value
    +        }));
    +        const responses = await this._setValues(args);
    +        this._applyValues(targets, responses, false);
    +    }
    +
    +    _onElementChange(observer) {
    +        const value = this._getElementValue(observer.element);
    +        observer.value = value;
    +        observer.hasValue = true;
    +        this._assignTasks.enqueue(observer, {value});
    +    }
    +
    +    _applyValues(targets, response, ignoreStale) {
    +        if (!Array.isArray(response)) { return; }
    +
    +        for (let i = 0, ii = targets.length; i < ii; ++i) {
    +            const [observer, task] = targets[i];
    +            const {error, result} = response[i];
    +            const stale = (task !== null && task.stale);
    +
    +            if (error) {
    +                if (typeof this._onError === 'function') {
    +                    this._onError(error, stale, observer.element, observer.metadata);
    +                }
    +                continue;
    +            }
    +
    +            if (stale && !ignoreStale) { continue; }
    +
    +            observer.value = result;
    +            observer.hasValue = true;
    +            this._setElementValue(observer.element, result);
    +        }
    +    }
    +
    +    _createObserver(element) {
    +        if (this._elementMap.has(element) || this._shouldIgnoreElement(element)) { return; }
    +
    +        const metadata = this._createElementMetadata(element);
    +        const nodeName = element.nodeName.toUpperCase();
    +        const ancestors = this._getAncestors(element);
    +        const observer = {
    +            element,
    +            ancestors,
    +            type: (nodeName === 'INPUT' ? element.type : null),
    +            value: null,
    +            hasValue: false,
    +            onChange: null,
    +            metadata
    +        };
    +        observer.onChange = this._onElementChange.bind(this, observer);
    +        this._elementMap.set(element, observer);
    +
    +        element.addEventListener('change', observer.onChange, false);
    +
    +        for (const ancestor of ancestors) {
    +            let observers = this._elementAncestorMap.get(ancestor);
    +            if (typeof observers === 'undefined') {
    +                observers = new Set();
    +                this._elementAncestorMap.set(ancestor, observers);
    +            }
    +            observers.add(observer);
    +        }
    +
    +        this._updateTasks.enqueue(observer);
    +    }
    +
    +    _removeObserver(observer) {
    +        const {element, ancestors} = observer;
    +
    +        element.removeEventListener('change', observer.onChange, false);
    +        observer.onChange = null;
    +
    +        this._elementMap.delete(element);
    +
    +        for (const ancestor of ancestors) {
    +            const observers = this._elementAncestorMap.get(ancestor);
    +            if (typeof observers === 'undefined') { continue; }
    +
    +            observers.delete(observer);
    +            if (observers.size === 0) {
    +                this._elementAncestorMap.delete(ancestor);
    +            }
    +        }
    +    }
    +
    +    _isObserverStale(observer) {
    +        const {element, type, metadata} = observer;
    +        const nodeName = element.nodeName.toUpperCase();
    +        return !(
    +            type === (nodeName === 'INPUT' ? element.type : null) &&
    +            this._compareElementMetadata(metadata, this._createElementMetadata(element))
    +        );
    +    }
    +
    +    _shouldIgnoreElement(element) {
    +        for (const selector of this._ignoreSelectors) {
    +            if (element.matches(selector)) {
    +                return true;
    +            }
    +        }
    +        return false;
    +    }
    +
    +    _getAncestors(node) {
    +        const root = this._observingElement;
    +        const results = [];
    +        while (true) {
    +            results.push(node);
    +            if (node === root) { break; }
    +            node = node.parentNode;
    +            if (node === null) { break; }
    +        }
    +        return results;
    +    }
    +
    +    _setElementValue(element, value) {
    +        switch (element.nodeName.toUpperCase()) {
    +            case 'INPUT':
    +                switch (element.type) {
    +                    case 'checkbox':
    +                        element.checked = value;
    +                        break;
    +                    case 'text':
    +                    case 'number':
    +                        element.value = value;
    +                        break;
    +                }
    +                break;
    +            case 'TEXTAREA':
    +            case 'SELECT':
    +                element.value = value;
    +                break;
    +        }
    +    }
    +
    +    _getElementValue(element) {
    +        switch (element.nodeName.toUpperCase()) {
    +            case 'INPUT':
    +                switch (element.type) {
    +                    case 'checkbox':
    +                        return !!element.checked;
    +                    case 'text':
    +                        return `${element.value}`;
    +                    case 'number':
    +                        return this._getInputNumberValue(element);
    +                }
    +                break;
    +            case 'TEXTAREA':
    +            case 'SELECT':
    +                return element.value;
    +        }
    +        return null;
    +    }
    +
    +    _getInputNumberValue(element) {
    +        let value = parseFloat(element.value);
    +        if (!Number.isFinite(value)) { return 0; }
    +
    +        let {min, max, step} = element;
    +        min = this._stringValueToNumberOrNull(min);
    +        max = this._stringValueToNumberOrNull(max);
    +        step = this._stringValueToNumberOrNull(step);
    +        if (typeof min === 'number') { value = Math.max(value, min); }
    +        if (typeof max === 'number') { value = Math.min(value, max); }
    +        if (typeof step === 'number' && step !== 0) { value = Math.round(value / step) * step; }
    +        return value;
    +    }
    +
    +    _stringValueToNumberOrNull(value) {
    +        if (typeof value !== 'string' || value.length === 0) {
    +            return null;
    +        }
    +
    +        const number = parseFloat(value);
    +        return !Number.isNaN(number) ? number : null;
    +    }
    +}
    diff --git a/ext/mixed/js/task-accumulator.js b/ext/mixed/js/task-accumulator.js
    new file mode 100644
    index 00000000..5c6fe312
    --- /dev/null
    +++ b/ext/mixed/js/task-accumulator.js
    @@ -0,0 +1,81 @@
    +/*
    + * Copyright (C) 2020  Yomichan Authors
    + *
    + * This program is free software: you can redistribute it and/or modify
    + * it under the terms of the GNU General Public License as published by
    + * the Free Software Foundation, either version 3 of the License, or
    + * (at your option) any later version.
    + *
    + * This program is distributed in the hope that it will be useful,
    + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    + * GNU General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this program.  If not, see .
    + */
    +
    +class TaskAccumulator {
    +    constructor(runTasks) {
    +        this._deferPromise = null;
    +        this._activePromise = null;
    +        this._tasks = [];
    +        this._tasksActive = [];
    +        this._uniqueTasks = new Map();
    +        this._uniqueTasksActive = new Map();
    +        this._runTasksBind = this._runTasks.bind(this);
    +        this._tasksCompleteBind = this._tasksComplete.bind(this);
    +        this._runTasks = runTasks;
    +    }
    +
    +    enqueue(key, data) {
    +        if (this._deferPromise === null) {
    +            const promise = this._activePromise !== null ? this._activePromise : Promise.resolve();
    +            this._deferPromise = promise.then(this._runTasksBind);
    +        }
    +
    +        const task = {data, stale: false};
    +        if (key !== null) {
    +            const activeTaskInfo = this._uniqueTasksActive.get(key);
    +            if (typeof activeTaskInfo !== 'undefined') {
    +                activeTaskInfo.stale = true;
    +            }
    +
    +            this._uniqueTasks.set(key, task);
    +        } else {
    +            this._tasks.push(task);
    +        }
    +
    +        return this._deferPromise;
    +    }
    +
    +    _runTasks() {
    +        this._deferPromise = null;
    +
    +        // Swap
    +        [this._tasks, this._tasksActive] = [this._tasksActive, this._tasks];
    +        [this._uniqueTasks, this._uniqueTasksActive] = [this._uniqueTasksActive, this._uniqueTasks];
    +
    +        const promise = this._runTasksAsync();
    +        this._activePromise = promise.then(this._tasksCompleteBind);
    +        return this._activePromise;
    +    }
    +
    +    async _runTasksAsync() {
    +        try {
    +            const allTasks = [
    +                ...this._tasksActive.map((taskInfo) => [null, taskInfo]),
    +                ...this._uniqueTasksActive.entries()
    +            ];
    +            await this._runTasks(allTasks);
    +        } catch (e) {
    +            yomichan.logError(e);
    +        }
    +    }
    +
    +    _tasksComplete() {
    +        this._tasksActive.length = 0;
    +        this._uniqueTasksActive.clear();
    +        this._activePromise = null;
    +    }
    +}
    -- 
    cgit v1.2.3
    
    
    From 13f57cccba5a29ff9e270a3fc2b2d7fee6e46b51 Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Sun, 24 May 2020 13:56:46 -0400
    Subject: Settings backup refactor (#551)
    
    * Update backup.js to be a class
    
    * Move utilReadFileArrayBuffer
    ---
     ext/bg/js/settings/backup.js | 569 ++++++++++++++++++++++---------------------
     ext/bg/js/settings/main.js   |   4 +-
     ext/bg/js/util.js            |   9 -
     3 files changed, 292 insertions(+), 290 deletions(-)
    
    (limited to 'ext/bg/js')
    
    diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js
    index 5eb55502..4e104e6f 100644
    --- a/ext/bg/js/settings/backup.js
    +++ b/ext/bg/js/settings/backup.js
    @@ -22,355 +22,366 @@
      * utilBackend
      * utilBackgroundIsolate
      * utilIsolate
    - * utilReadFileArrayBuffer
      */
     
    -// Exporting
    -
    -let _settingsExportToken = null;
    -let _settingsExportRevoke = null;
    -const SETTINGS_EXPORT_CURRENT_VERSION = 0;
    -
    -function _getSettingsExportDateString(date, dateSeparator, dateTimeSeparator, timeSeparator, resolution) {
    -    const values = [
    -        date.getUTCFullYear().toString(),
    -        dateSeparator,
    -        (date.getUTCMonth() + 1).toString().padStart(2, '0'),
    -        dateSeparator,
    -        date.getUTCDate().toString().padStart(2, '0'),
    -        dateTimeSeparator,
    -        date.getUTCHours().toString().padStart(2, '0'),
    -        timeSeparator,
    -        date.getUTCMinutes().toString().padStart(2, '0'),
    -        timeSeparator,
    -        date.getUTCSeconds().toString().padStart(2, '0')
    -    ];
    -    return values.slice(0, resolution * 2 - 1).join('');
    -}
    +class SettingsBackup {
    +    constructor() {
    +        this._settingsExportToken = null;
    +        this._settingsExportRevoke = null;
    +        this._currentVersion = 0;
    +    }
     
    -async function _getSettingsExportData(date) {
    -    const optionsFull = await api.optionsGetFull();
    -    const environment = await api.getEnvironmentInfo();
    -    const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates();
    +    prepare() {
    +        document.querySelector('#settings-export').addEventListener('click', this._onSettingsExportClick.bind(this), false);
    +        document.querySelector('#settings-import').addEventListener('click', this._onSettingsImportClick.bind(this), false);
    +        document.querySelector('#settings-import-file').addEventListener('change', this._onSettingsImportFileChange.bind(this), false);
    +        document.querySelector('#settings-reset').addEventListener('click', this._onSettingsResetClick.bind(this), false);
    +        document.querySelector('#settings-reset-modal-confirm').addEventListener('click', this._onSettingsResetConfirmClick.bind(this), false);
    +    }
     
    -    // Format options
    -    for (const {options} of optionsFull.profiles) {
    -        if (options.anki.fieldTemplates === fieldTemplatesDefault || !options.anki.fieldTemplates) {
    -            delete options.anki.fieldTemplates; // Default
    -        }
    +    // Private
    +
    +    _getSettingsExportDateString(date, dateSeparator, dateTimeSeparator, timeSeparator, resolution) {
    +        const values = [
    +            date.getUTCFullYear().toString(),
    +            dateSeparator,
    +            (date.getUTCMonth() + 1).toString().padStart(2, '0'),
    +            dateSeparator,
    +            date.getUTCDate().toString().padStart(2, '0'),
    +            dateTimeSeparator,
    +            date.getUTCHours().toString().padStart(2, '0'),
    +            timeSeparator,
    +            date.getUTCMinutes().toString().padStart(2, '0'),
    +            timeSeparator,
    +            date.getUTCSeconds().toString().padStart(2, '0')
    +        ];
    +        return values.slice(0, resolution * 2 - 1).join('');
         }
     
    -    const data = {
    -        version: SETTINGS_EXPORT_CURRENT_VERSION,
    -        date: _getSettingsExportDateString(date, '-', ' ', ':', 6),
    -        url: chrome.runtime.getURL('/'),
    -        manifest: chrome.runtime.getManifest(),
    -        environment,
    -        userAgent: navigator.userAgent,
    -        options: optionsFull
    -    };
    -
    -    return data;
    -}
    +    async _getSettingsExportData(date) {
    +        const optionsFull = await api.optionsGetFull();
    +        const environment = await api.getEnvironmentInfo();
    +        const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates();
     
    -function _saveBlob(blob, fileName) {
    -    if (typeof navigator === 'object' && typeof navigator.msSaveBlob === 'function') {
    -        if (navigator.msSaveBlob(blob)) {
    -            return;
    +        // Format options
    +        for (const {options} of optionsFull.profiles) {
    +            if (options.anki.fieldTemplates === fieldTemplatesDefault || !options.anki.fieldTemplates) {
    +                delete options.anki.fieldTemplates; // Default
    +            }
             }
    -    }
     
    -    const blobUrl = URL.createObjectURL(blob);
    +        const data = {
    +            version: this._currentVersion,
    +            date: this._getSettingsExportDateString(date, '-', ' ', ':', 6),
    +            url: chrome.runtime.getURL('/'),
    +            manifest: chrome.runtime.getManifest(),
    +            environment,
    +            userAgent: navigator.userAgent,
    +            options: optionsFull
    +        };
     
    -    const a = document.createElement('a');
    -    a.href = blobUrl;
    -    a.download = fileName;
    -    a.rel = 'noopener';
    -    a.target = '_blank';
    +        return data;
    +    }
     
    -    const revoke = () => {
    -        URL.revokeObjectURL(blobUrl);
    -        a.href = '';
    -        _settingsExportRevoke = null;
    -    };
    -    _settingsExportRevoke = revoke;
    +    _saveBlob(blob, fileName) {
    +        if (typeof navigator === 'object' && typeof navigator.msSaveBlob === 'function') {
    +            if (navigator.msSaveBlob(blob)) {
    +                return;
    +            }
    +        }
     
    -    a.dispatchEvent(new MouseEvent('click'));
    -    setTimeout(revoke, 60000);
    -}
    +        const blobUrl = URL.createObjectURL(blob);
     
    -async function _onSettingsExportClick() {
    -    if (_settingsExportRevoke !== null) {
    -        _settingsExportRevoke();
    -        _settingsExportRevoke = null;
    -    }
    +        const a = document.createElement('a');
    +        a.href = blobUrl;
    +        a.download = fileName;
    +        a.rel = 'noopener';
    +        a.target = '_blank';
     
    -    const date = new Date(Date.now());
    +        const revoke = () => {
    +            URL.revokeObjectURL(blobUrl);
    +            a.href = '';
    +            this._settingsExportRevoke = null;
    +        };
    +        this._settingsExportRevoke = revoke;
     
    -    const token = {};
    -    _settingsExportToken = token;
    -    const data = await _getSettingsExportData(date);
    -    if (_settingsExportToken !== token) {
    -        // A new export has been started
    -        return;
    +        a.dispatchEvent(new MouseEvent('click'));
    +        setTimeout(revoke, 60000);
         }
    -    _settingsExportToken = null;
     
    -    const fileName = `yomichan-settings-${_getSettingsExportDateString(date, '-', '-', '-', 6)}.json`;
    -    const blob = new Blob([JSON.stringify(data, null, 4)], {type: 'application/json'});
    -    _saveBlob(blob, fileName);
    -}
    -
    -
    -// Importing
    +    async _onSettingsExportClick() {
    +        if (this._settingsExportRevoke !== null) {
    +            this._settingsExportRevoke();
    +            this._settingsExportRevoke = null;
    +        }
     
    -async function _settingsImportSetOptionsFull(optionsFull) {
    -    return utilIsolate(utilBackend().setFullOptions(
    -        utilBackgroundIsolate(optionsFull)
    -    ));
    -}
    +        const date = new Date(Date.now());
     
    -function _showSettingsImportError(error) {
    -    yomichan.logError(error);
    -    document.querySelector('#settings-import-error-modal-message').textContent = `${error}`;
    -    $('#settings-import-error-modal').modal('show');
    -}
    +        const token = {};
    +        this._settingsExportToken = token;
    +        const data = await this._getSettingsExportData(date);
    +        if (this._settingsExportToken !== token) {
    +            // A new export has been started
    +            return;
    +        }
    +        this._settingsExportToken = null;
     
    -async function _showSettingsImportWarnings(warnings) {
    -    const modalNode = $('#settings-import-warning-modal');
    -    const buttons = document.querySelectorAll('.settings-import-warning-modal-import-button');
    -    const messageContainer = document.querySelector('#settings-import-warning-modal-message');
    -    if (modalNode.length === 0 || buttons.length === 0 || messageContainer === null) {
    -        return {result: false};
    +        const fileName = `yomichan-settings-${this._getSettingsExportDateString(date, '-', '-', '-', 6)}.json`;
    +        const blob = new Blob([JSON.stringify(data, null, 4)], {type: 'application/json'});
    +        this._saveBlob(blob, fileName);
         }
     
    -    // Set message
    -    const fragment = document.createDocumentFragment();
    -    for (const warning of warnings) {
    -        const node = document.createElement('li');
    -        node.textContent = `${warning}`;
    -        fragment.appendChild(node);
    +    _readFileArrayBuffer(file) {
    +        return new Promise((resolve, reject) => {
    +            const reader = new FileReader();
    +            reader.onload = () => resolve(reader.result);
    +            reader.onerror = () => reject(reader.error);
    +            reader.readAsArrayBuffer(file);
    +        });
         }
    -    messageContainer.textContent = '';
    -    messageContainer.appendChild(fragment);
    -
    -    // Show modal
    -    modalNode.modal('show');
    -
    -    // Wait for modal to close
    -    return new Promise((resolve) => {
    -        const onButtonClick = (e) => {
    -            e.preventDefault();
    -            complete({
    -                result: true,
    -                sanitize: e.currentTarget.dataset.importSanitize === 'true'
    -            });
    -            modalNode.modal('hide');
    -        };
    -        const onModalHide = () => {
    -            complete({result: false});
    -        };
     
    -        let completed = false;
    -        const complete = (result) => {
    -            if (completed) { return; }
    -            completed = true;
    +    // Importing
     
    -            modalNode.off('hide.bs.modal', onModalHide);
    -            for (const button of buttons) {
    -                button.removeEventListener('click', onButtonClick, false);
    -            }
    +    async _settingsImportSetOptionsFull(optionsFull) {
    +        return utilIsolate(utilBackend().setFullOptions(
    +            utilBackgroundIsolate(optionsFull)
    +        ));
    +    }
     
    -            resolve(result);
    -        };
    +    _showSettingsImportError(error) {
    +        yomichan.logError(error);
    +        document.querySelector('#settings-import-error-modal-message').textContent = `${error}`;
    +        $('#settings-import-error-modal').modal('show');
    +    }
     
    -        // Hook events
    -        modalNode.on('hide.bs.modal', onModalHide);
    -        for (const button of buttons) {
    -            button.addEventListener('click', onButtonClick, false);
    +    async _showSettingsImportWarnings(warnings) {
    +        const modalNode = $('#settings-import-warning-modal');
    +        const buttons = document.querySelectorAll('.settings-import-warning-modal-import-button');
    +        const messageContainer = document.querySelector('#settings-import-warning-modal-message');
    +        if (modalNode.length === 0 || buttons.length === 0 || messageContainer === null) {
    +            return {result: false};
             }
    -    });
    -}
     
    -function _isLocalhostUrl(urlString) {
    -    try {
    -        const url = new URL(urlString);
    -        switch (url.hostname.toLowerCase()) {
    -            case 'localhost':
    -            case '127.0.0.1':
    -            case '[::1]':
    -                switch (url.protocol.toLowerCase()) {
    -                    case 'http:':
    -                    case 'https:':
    -                        return true;
    -                }
    -                break;
    +        // Set message
    +        const fragment = document.createDocumentFragment();
    +        for (const warning of warnings) {
    +            const node = document.createElement('li');
    +            node.textContent = `${warning}`;
    +            fragment.appendChild(node);
             }
    -    } catch (e) {
    -        // NOP
    -    }
    -    return false;
    -}
    +        messageContainer.textContent = '';
    +        messageContainer.appendChild(fragment);
    +
    +        // Show modal
    +        modalNode.modal('show');
    +
    +        // Wait for modal to close
    +        return new Promise((resolve) => {
    +            const onButtonClick = (e) => {
    +                e.preventDefault();
    +                complete({
    +                    result: true,
    +                    sanitize: e.currentTarget.dataset.importSanitize === 'true'
    +                });
    +                modalNode.modal('hide');
    +            };
    +            const onModalHide = () => {
    +                complete({result: false});
    +            };
    +
    +            let completed = false;
    +            const complete = (result) => {
    +                if (completed) { return; }
    +                completed = true;
    +
    +                modalNode.off('hide.bs.modal', onModalHide);
    +                for (const button of buttons) {
    +                    button.removeEventListener('click', onButtonClick, false);
    +                }
     
    -function _settingsImportSanitizeProfileOptions(options, dryRun) {
    -    const warnings = [];
    +                resolve(result);
    +            };
     
    -    const anki = options.anki;
    -    if (isObject(anki)) {
    -        const fieldTemplates = anki.fieldTemplates;
    -        if (typeof fieldTemplates === 'string') {
    -            warnings.push('anki.fieldTemplates contains a non-default value');
    -            if (!dryRun) {
    -                delete anki.fieldTemplates;
    -            }
    -        }
    -        const server = anki.server;
    -        if (typeof server === 'string' && server.length > 0 && !_isLocalhostUrl(server)) {
    -            warnings.push('anki.server uses a non-localhost URL');
    -            if (!dryRun) {
    -                delete anki.server;
    +            // Hook events
    +            modalNode.on('hide.bs.modal', onModalHide);
    +            for (const button of buttons) {
    +                button.addEventListener('click', onButtonClick, false);
                 }
    -        }
    +        });
         }
     
    -    const audio = options.audio;
    -    if (isObject(audio)) {
    -        const customSourceUrl = audio.customSourceUrl;
    -        if (typeof customSourceUrl === 'string' && customSourceUrl.length > 0 && !_isLocalhostUrl(customSourceUrl)) {
    -            warnings.push('audio.customSourceUrl uses a non-localhost URL');
    -            if (!dryRun) {
    -                delete audio.customSourceUrl;
    +    _isLocalhostUrl(urlString) {
    +        try {
    +            const url = new URL(urlString);
    +            switch (url.hostname.toLowerCase()) {
    +                case 'localhost':
    +                case '127.0.0.1':
    +                case '[::1]':
    +                    switch (url.protocol.toLowerCase()) {
    +                        case 'http:':
    +                        case 'https:':
    +                            return true;
    +                    }
    +                    break;
                 }
    +        } catch (e) {
    +            // NOP
             }
    +        return false;
         }
     
    -    return warnings;
    -}
    -
    -function _settingsImportSanitizeOptions(optionsFull, dryRun) {
    -    const warnings = new Set();
    +    _settingsImportSanitizeProfileOptions(options, dryRun) {
    +        const warnings = [];
     
    -    const profiles = optionsFull.profiles;
    -    if (Array.isArray(profiles)) {
    -        for (const profile of profiles) {
    -            if (!isObject(profile)) { continue; }
    -            const options = profile.options;
    -            if (!isObject(options)) { continue; }
    -
    -            const warnings2 = _settingsImportSanitizeProfileOptions(options, dryRun);
    -            for (const warning of warnings2) {
    -                warnings.add(warning);
    +        const anki = options.anki;
    +        if (isObject(anki)) {
    +            const fieldTemplates = anki.fieldTemplates;
    +            if (typeof fieldTemplates === 'string') {
    +                warnings.push('anki.fieldTemplates contains a non-default value');
    +                if (!dryRun) {
    +                    delete anki.fieldTemplates;
    +                }
    +            }
    +            const server = anki.server;
    +            if (typeof server === 'string' && server.length > 0 && !this._isLocalhostUrl(server)) {
    +                warnings.push('anki.server uses a non-localhost URL');
    +                if (!dryRun) {
    +                    delete anki.server;
    +                }
                 }
             }
    -    }
     
    -    return warnings;
    -}
    +        const audio = options.audio;
    +        if (isObject(audio)) {
    +            const customSourceUrl = audio.customSourceUrl;
    +            if (typeof customSourceUrl === 'string' && customSourceUrl.length > 0 && !this._isLocalhostUrl(customSourceUrl)) {
    +                warnings.push('audio.customSourceUrl uses a non-localhost URL');
    +                if (!dryRun) {
    +                    delete audio.customSourceUrl;
    +                }
    +            }
    +        }
     
    -function _utf8Decode(arrayBuffer) {
    -    try {
    -        return new TextDecoder('utf-8').decode(arrayBuffer);
    -    } catch (e) {
    -        const binaryString = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer));
    -        return decodeURIComponent(escape(binaryString));
    +        return warnings;
         }
    -}
     
    -async function _importSettingsFile(file) {
    -    const dataString = _utf8Decode(await utilReadFileArrayBuffer(file));
    -    const data = JSON.parse(dataString);
    +    _settingsImportSanitizeOptions(optionsFull, dryRun) {
    +        const warnings = new Set();
     
    -    // Type check
    -    if (!isObject(data)) {
    -        throw new Error(`Invalid data type: ${typeof data}`);
    -    }
    +        const profiles = optionsFull.profiles;
    +        if (Array.isArray(profiles)) {
    +            for (const profile of profiles) {
    +                if (!isObject(profile)) { continue; }
    +                const options = profile.options;
    +                if (!isObject(options)) { continue; }
     
    -    // Version check
    -    const version = data.version;
    -    if (!(
    -        typeof version === 'number' &&
    -        Number.isFinite(version) &&
    -        version === Math.floor(version)
    -    )) {
    -        throw new Error(`Invalid version: ${version}`);
    -    }
    +                const warnings2 = this._settingsImportSanitizeProfileOptions(options, dryRun);
    +                for (const warning of warnings2) {
    +                    warnings.add(warning);
    +                }
    +            }
    +        }
     
    -    if (!(
    -        version >= 0 &&
    -        version <= SETTINGS_EXPORT_CURRENT_VERSION
    -    )) {
    -        throw new Error(`Unsupported version: ${version}`);
    +        return warnings;
         }
     
    -    // Verify options exists
    -    let optionsFull = data.options;
    -    if (!isObject(optionsFull)) {
    -        throw new Error(`Invalid options type: ${typeof optionsFull}`);
    +    _utf8Decode(arrayBuffer) {
    +        try {
    +            return new TextDecoder('utf-8').decode(arrayBuffer);
    +        } catch (e) {
    +            const binaryString = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer));
    +            return decodeURIComponent(escape(binaryString));
    +        }
         }
     
    -    // Upgrade options
    -    optionsFull = optionsUpdateVersion(optionsFull, {});
    +    async _importSettingsFile(file) {
    +        const dataString = this._utf8Decode(await this._readFileArrayBuffer(file));
    +        const data = JSON.parse(dataString);
     
    -    // Check for warnings
    -    const sanitizationWarnings = _settingsImportSanitizeOptions(optionsFull, true);
    +        // Type check
    +        if (!isObject(data)) {
    +            throw new Error(`Invalid data type: ${typeof data}`);
    +        }
     
    -    // Show sanitization warnings
    -    if (sanitizationWarnings.size > 0) {
    -        const {result, sanitize} = await _showSettingsImportWarnings(sanitizationWarnings);
    -        if (!result) { return; }
    +        // Version check
    +        const version = data.version;
    +        if (!(
    +            typeof version === 'number' &&
    +            Number.isFinite(version) &&
    +            version === Math.floor(version)
    +        )) {
    +            throw new Error(`Invalid version: ${version}`);
    +        }
     
    -        if (sanitize !== false) {
    -            _settingsImportSanitizeOptions(optionsFull, false);
    +        if (!(
    +            version >= 0 &&
    +            version <= this._currentVersion
    +        )) {
    +            throw new Error(`Unsupported version: ${version}`);
             }
    -    }
     
    -    // Assign options
    -    await _settingsImportSetOptionsFull(optionsFull);
    +        // Verify options exists
    +        let optionsFull = data.options;
    +        if (!isObject(optionsFull)) {
    +            throw new Error(`Invalid options type: ${typeof optionsFull}`);
    +        }
     
    -    // Reload settings page
    -    window.location.reload();
    -}
    +        // Upgrade options
    +        optionsFull = optionsUpdateVersion(optionsFull, {});
     
    -function _onSettingsImportClick() {
    -    document.querySelector('#settings-import-file').click();
    -}
    +        // Check for warnings
    +        const sanitizationWarnings = this._settingsImportSanitizeOptions(optionsFull, true);
     
    -function _onSettingsImportFileChange(e) {
    -    const files = e.target.files;
    -    if (files.length === 0) { return; }
    +        // Show sanitization warnings
    +        if (sanitizationWarnings.size > 0) {
    +            const {result, sanitize} = await this._showSettingsImportWarnings(sanitizationWarnings);
    +            if (!result) { return; }
     
    -    const file = files[0];
    -    e.target.value = null;
    -    _importSettingsFile(file).catch(_showSettingsImportError);
    -}
    +            if (sanitize !== false) {
    +                this._settingsImportSanitizeOptions(optionsFull, false);
    +            }
    +        }
     
    +        // Assign options
    +        await this._settingsImportSetOptionsFull(optionsFull);
     
    -// Resetting
    +        // Reload settings page
    +        window.location.reload();
    +    }
     
    -function _onSettingsResetClick() {
    -    $('#settings-reset-modal').modal('show');
    -}
    +    _onSettingsImportClick() {
    +        document.querySelector('#settings-import-file').click();
    +    }
     
    -async function _onSettingsResetConfirmClick() {
    -    $('#settings-reset-modal').modal('hide');
    +    async _onSettingsImportFileChange(e) {
    +        const files = e.target.files;
    +        if (files.length === 0) { return; }
     
    -    // Get default options
    -    const optionsFull = optionsGetDefault();
    +        const file = files[0];
    +        e.target.value = null;
    +        try {
    +            await this._importSettingsFile(file);
    +        } catch (error) {
    +            this._showSettingsImportError(error);
    +        }
    +    }
     
    -    // Assign options
    -    await _settingsImportSetOptionsFull(optionsFull);
    +    // Resetting
     
    -    // Reload settings page
    -    window.location.reload();
    -}
    +    _onSettingsResetClick() {
    +        $('#settings-reset-modal').modal('show');
    +    }
     
    +    async _onSettingsResetConfirmClick() {
    +        $('#settings-reset-modal').modal('hide');
     
    -// Setup
    +        // Get default options
    +        const optionsFull = optionsGetDefault();
     
    -function backupInitialize() {
    -    document.querySelector('#settings-export').addEventListener('click', _onSettingsExportClick, false);
    -    document.querySelector('#settings-import').addEventListener('click', _onSettingsImportClick, false);
    -    document.querySelector('#settings-import-file').addEventListener('change', _onSettingsImportFileChange, false);
    -    document.querySelector('#settings-reset').addEventListener('click', _onSettingsResetClick, false);
    -    document.querySelector('#settings-reset-modal-confirm').addEventListener('click', _onSettingsResetConfirmClick, false);
    +        // Assign options
    +        await this._settingsImportSetOptionsFull(optionsFull);
    +
    +        // Reload settings page
    +        window.location.reload();
    +    }
     }
    diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
    index 60b9e008..f96167af 100644
    --- a/ext/bg/js/settings/main.js
    +++ b/ext/bg/js/settings/main.js
    @@ -16,13 +16,13 @@
      */
     
     /* global
    + * SettingsBackup
      * ankiInitialize
      * ankiTemplatesInitialize
      * ankiTemplatesUpdateValue
      * api
      * appearanceInitialize
      * audioSettingsInitialize
    - * backupInitialize
      * dictSettingsInitialize
      * getOptionsContext
      * onAnkiOptionsChanged
    @@ -302,7 +302,7 @@ async function onReady() {
         await dictSettingsInitialize();
         ankiInitialize();
         ankiTemplatesInitialize();
    -    backupInitialize();
    +    new SettingsBackup().prepare();
     
         storageInfoInitialize();
     
    diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js
    index 8f86e47a..edc19c6e 100644
    --- a/ext/bg/js/util.js
    +++ b/ext/bg/js/util.js
    @@ -65,12 +65,3 @@ function utilBackend() {
         }
         return backend;
     }
    -
    -function utilReadFileArrayBuffer(file) {
    -    return new Promise((resolve, reject) => {
    -        const reader = new FileReader();
    -        reader.onload = () => resolve(reader.result);
    -        reader.onerror = () => reject(reader.error);
    -        reader.readAsArrayBuffer(file);
    -    });
    -}
    -- 
    cgit v1.2.3
    
    
    From 6dd6af05e1ed3e0da4091af073c38e1d8ec0268d Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Sun, 24 May 2020 14:01:21 -0400
    Subject: Update background global object usage (#556)
    
    * Omit global window object for scripts used on the background page
    
    * Validate document exists before using
    
    * Remove dom.js from background.html
    ---
     ext/bg/background.html         |  1 -
     ext/bg/js/anki-note-builder.js |  2 +-
     ext/bg/js/backend.js           | 22 +++++++++++-----------
     ext/bg/js/background-main.js   |  7 +++++--
     ext/bg/js/database.js          |  2 +-
     ext/mixed/js/core.js           | 20 +++++++++++---------
     6 files changed, 29 insertions(+), 25 deletions(-)
    
    (limited to 'ext/bg/js')
    
    diff --git a/ext/bg/background.html b/ext/bg/background.html
    index ca35a3c6..53e8b140 100644
    --- a/ext/bg/background.html
    +++ b/ext/bg/background.html
    @@ -20,7 +20,6 @@
             
     
             
    -        
             
             
     
    diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js
    index 76199db7..31e67394 100644
    --- a/ext/bg/js/anki-note-builder.js
    +++ b/ext/bg/js/anki-note-builder.js
    @@ -155,7 +155,7 @@ class AnkiNoteBuilder {
         }
     
         static arrayBufferToBase64(arrayBuffer) {
    -        return window.btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
    +        return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
         }
     
         static stringReplaceAsync(str, regex, replacer) {
    diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
    index 90895737..80b00d5f 100644
    --- a/ext/bg/js/backend.js
    +++ b/ext/bg/js/backend.js
    @@ -65,12 +65,14 @@ class Backend {
                 renderTemplate: this._renderTemplate.bind(this)
             });
     
    -        this.optionsContext = {
    -            depth: 0,
    -            url: window.location.href
    -        };
    +        const url = (typeof window === 'object' && window !== null ? window.location.href : '');
    +        this.optionsContext = {depth: 0, url};
     
    -        this.clipboardPasteTarget = document.querySelector('#clipboard-paste-target');
    +        this.clipboardPasteTarget = (
    +            typeof document === 'object' && document !== null ?
    +            document.querySelector('#clipboard-paste-target') :
    +            null
    +        );
     
             this.popupWindow = null;
     
    @@ -704,6 +706,9 @@ class Backend {
                 return await navigator.clipboard.readText();
             } else {
                 const clipboardPasteTarget = this.clipboardPasteTarget;
    +            if (clipboardPasteTarget === null) {
    +                throw new Error('Reading the clipboard is not supported in this context');
    +            }
                 clipboardPasteTarget.value = '';
                 clipboardPasteTarget.focus();
                 document.execCommand('paste');
    @@ -1005,13 +1010,8 @@ class Backend {
         }
     
         async _onCommandToggle() {
    -        const optionsContext = {
    -            depth: 0,
    -            url: window.location.href
    -        };
             const source = 'popup';
    -
    -        const options = this.getOptions(optionsContext);
    +        const options = this.getOptions(this.optionsContext);
             options.general.enable = !options.general.enable;
             await this._onApiOptionsSave({source});
         }
    diff --git a/ext/bg/js/background-main.js b/ext/bg/js/background-main.js
    index 24117f4e..345b4a77 100644
    --- a/ext/bg/js/background-main.js
    +++ b/ext/bg/js/background-main.js
    @@ -20,6 +20,9 @@
      */
     
     (async () => {
    -    window.yomichanBackend = new Backend();
    -    await window.yomichanBackend.prepare();
    +    const backend = new Backend();
    +    if (typeof window === 'object' && window !== null) {
    +        window.yomichanBackend = backend;
    +    }
    +    await backend.prepare();
     })();
    diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
    index 930cd0d0..65e267ab 100644
    --- a/ext/bg/js/database.js
    +++ b/ext/bg/js/database.js
    @@ -596,7 +596,7 @@ class Database {
     
         static _open(name, version, onUpgradeNeeded) {
             return new Promise((resolve, reject) => {
    -            const request = window.indexedDB.open(name, version * 10);
    +            const request = indexedDB.open(name, version * 10);
     
                 request.onupgradeneeded = (event) => {
                     try {
    diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js
    index 257c7edf..bf877e72 100644
    --- a/ext/mixed/js/core.js
    +++ b/ext/mixed/js/core.js
    @@ -177,7 +177,7 @@ function promiseTimeout(delay, resolveValue) {
         const complete = (callback, value) => {
             if (callback === null) { return; }
             if (timer !== null) {
    -            window.clearTimeout(timer);
    +            clearTimeout(timer);
                 timer = null;
             }
             promiseResolve = null;
    @@ -192,7 +192,7 @@ function promiseTimeout(delay, resolveValue) {
             promiseResolve = resolve2;
             promiseReject = reject2;
         });
    -    timer = window.setTimeout(() => {
    +    timer = setTimeout(() => {
             timer = null;
             resolve(resolveValue);
         }, delay);
    @@ -331,7 +331,7 @@ const yomichan = (() => {
     
             generateId(length) {
                 const array = new Uint8Array(length);
    -            window.crypto.getRandomValues(array);
    +            crypto.getRandomValues(array);
                 let id = '';
                 for (const value of array) {
                     id += value.toString(16).padStart(2, '0');
    @@ -364,7 +364,7 @@ const yomichan = (() => {
                     const runtimeMessageCallback = ({action, params}, sender, sendResponse) => {
                         let timeoutId = null;
                         if (timeout !== null) {
    -                        timeoutId = window.setTimeout(() => {
    +                        timeoutId = setTimeout(() => {
                                 timeoutId = null;
                                 eventHandler.removeListener(runtimeMessageCallback);
                                 reject(new Error(`Listener timed out in ${timeout} ms`));
    @@ -373,7 +373,7 @@ const yomichan = (() => {
     
                         const cleanupResolve = (value) => {
                             if (timeoutId !== null) {
    -                            window.clearTimeout(timeoutId);
    +                            clearTimeout(timeoutId);
                                 timeoutId = null;
                             }
                             eventHandler.removeListener(runtimeMessageCallback);
    @@ -453,10 +453,12 @@ const yomichan = (() => {
     
             // Private
     
    +        _getUrl() {
    +            return (typeof window === 'object' && window !== null ? window.location.href : '');
    +        }
    +
             _getLogContext() {
    -            return {
    -                url: window.location.href
    -            };
    +            return {url: this._getUrl()};
             }
     
             _onMessage({action, params}, sender, callback) {
    @@ -469,7 +471,7 @@ const yomichan = (() => {
             }
     
             _onMessageGetUrl() {
    -            return {url: window.location.href};
    +            return {url: this._getUrl()};
             }
     
             _onMessageOptionsUpdated({source}) {
    -- 
    cgit v1.2.3
    
    
    From 37f0396f1cf4f74833d9a4a364f503f4a7c3e34a Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Fri, 29 May 2020 19:44:53 -0400
    Subject: DOM binder fixes (#564)
    
    * Fix incorrect updateValue function
    
    * Add source
    ---
     ext/bg/js/settings/dom-settings-binder.js | 5 +++--
     ext/mixed/js/dom-data-binder.js           | 4 ++--
     2 files changed, 5 insertions(+), 4 deletions(-)
    
    (limited to 'ext/bg/js')
    
    diff --git a/ext/bg/js/settings/dom-settings-binder.js b/ext/bg/js/settings/dom-settings-binder.js
    index 0441ec29..4b63859f 100644
    --- a/ext/bg/js/settings/dom-settings-binder.js
    +++ b/ext/bg/js/settings/dom-settings-binder.js
    @@ -22,8 +22,9 @@
      */
     
     class DOMSettingsBinder {
    -    constructor({getOptionsContext, transforms=null}) {
    +    constructor({getOptionsContext, source=null, transforms=null}) {
             this._getOptionsContext = getOptionsContext;
    +        this._source = source;
             this._defaultScope = 'profile';
             this._dataBinder = new DOMDataBinder({
                 selector: '[data-setting]',
    @@ -97,7 +98,7 @@ class DOMSettingsBinder {
                 }
                 settingsTargets.push(target);
             }
    -        return this._transformResults(await api.modifySettings(settingsTargets), targets);
    +        return this._transformResults(await api.modifySettings(settingsTargets, this._source), targets);
         }
     
         _transform(value, transform, metadata, element) {
    diff --git a/ext/mixed/js/dom-data-binder.js b/ext/mixed/js/dom-data-binder.js
    index 05a84240..d46e8087 100644
    --- a/ext/mixed/js/dom-data-binder.js
    +++ b/ext/mixed/js/dom-data-binder.js
    @@ -108,8 +108,8 @@ class DOMDataBinder {
     
             if (addedNodes.length !== 0 || addedNodes.length !== 0) {
                 const observer = this._elementMap.get(target);
    -            if (typeof observer !== 'undefined') {
    -                observer.updateValue();
    +            if (typeof observer !== 'undefined' && observer.hasValue) {
    +                this._setElementValue(observer.element, observer.value);
                 }
             }
         }
    -- 
    cgit v1.2.3
    
    
    From 8537c8f386b7c04f21e62a6b82b179ec9a123ce1 Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Fri, 29 May 2020 19:45:54 -0400
    Subject: Create class to abstract access, mutation, and events for settings
     (#565)
    
    ---
     ext/bg/js/settings/main.js                |  4 ++
     ext/bg/js/settings/settings-controller.js | 83 +++++++++++++++++++++++++++++++
     ext/bg/settings.html                      |  1 +
     3 files changed, 88 insertions(+)
     create mode 100644 ext/bg/js/settings/settings-controller.js
    
    (limited to 'ext/bg/js')
    
    diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
    index f96167af..0cb1734e 100644
    --- a/ext/bg/js/settings/main.js
    +++ b/ext/bg/js/settings/main.js
    @@ -17,6 +17,7 @@
     
     /* global
      * SettingsBackup
    + * SettingsController
      * ankiInitialize
      * ankiTemplatesInitialize
      * ankiTemplatesUpdateValue
    @@ -292,6 +293,9 @@ async function onReady() {
         api.forwardLogsToBackend();
         await yomichan.prepare();
     
    +    const settingsController = new SettingsController();
    +    settingsController.prepare();
    +
         showExtensionInformation();
     
         await settingsPopulateModifierKeys();
    diff --git a/ext/bg/js/settings/settings-controller.js b/ext/bg/js/settings/settings-controller.js
    new file mode 100644
    index 00000000..61230226
    --- /dev/null
    +++ b/ext/bg/js/settings/settings-controller.js
    @@ -0,0 +1,83 @@
    +/*
    + * Copyright (C) 2020  Yomichan Authors
    + *
    + * This program is free software: you can redistribute it and/or modify
    + * it under the terms of the GNU General Public License as published by
    + * the Free Software Foundation, either version 3 of the License, or
    + * (at your option) any later version.
    + *
    + * This program is distributed in the hope that it will be useful,
    + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    + * GNU General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this program.  If not, see .
    + */
    +
    +/* global
    + * api
    + * utilBackend
    + * utilBackgroundIsolate
    + */
    +
    +class SettingsController extends EventDispatcher {
    +    constructor(profileIndex=0) {
    +        super();
    +        this._profileIndex = profileIndex;
    +        this._source = yomichan.generateId(16);
    +    }
    +
    +    get profileIndex() {
    +        return this._profileIndex;
    +    }
    +
    +    set profileIndex(value) {
    +        if (this._profileIndex === value) { return; }
    +        this._profileIndex = value;
    +        this.trigger('optionsContextChanged');
    +        this._onOptionsUpdatedInternal();
    +    }
    +
    +    prepare() {
    +        yomichan.on('optionsUpdated', this._onOptionsUpdated.bind(this));
    +    }
    +
    +    async save() {
    +        await api.optionsSave(this._source);
    +    }
    +
    +    async getOptions() {
    +        const optionsContext = this.getOptionsContext();
    +        return await api.optionsGet(optionsContext);
    +    }
    +
    +    async getOptionsFull() {
    +        return await api.optionsGetFull();
    +    }
    +
    +    async getOptionsMutable() {
    +        const optionsContext = this.getOptionsContext();
    +        return utilBackend().getOptions(utilBackgroundIsolate(optionsContext));
    +    }
    +
    +    async getOptionsFullMutable() {
    +        return utilBackend().getFullOptions();
    +    }
    +
    +    getOptionsContext() {
    +        return {index: this._profileIndex};
    +    }
    +
    +    // Private
    +
    +    _onOptionsUpdated({source}) {
    +        if (source === this._source) { return; }
    +        this._onOptionsUpdatedInternal();
    +    }
    +
    +    async _onOptionsUpdatedInternal() {
    +        const options = await this.getOptions();
    +        this.trigger('optionsChanged', {options});
    +    }
    +}
    diff --git a/ext/bg/settings.html b/ext/bg/settings.html
    index 7964ab90..c8f5b15c 100644
    --- a/ext/bg/settings.html
    +++ b/ext/bg/settings.html
    @@ -1147,6 +1147,7 @@
             
             
             
    +        
             
     
             
    -- 
    cgit v1.2.3
    
    
    From fde0072118128ea698e15472c9b61b17b4827c8a Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Fri, 29 May 2020 19:47:18 -0400
    Subject: Change profile.js into a class (#566)
    
    * Update how settings profile is stored and accessed
    
    * Convert profiles.js into a class
    
    * Rename members of ProfileController
    ---
     ext/bg/js/settings/main.js     |  20 +-
     ext/bg/js/settings/profiles.js | 422 +++++++++++++++++++++--------------------
     2 files changed, 233 insertions(+), 209 deletions(-)
    
    (limited to 'ext/bg/js')
    
    diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
    index 0cb1734e..64611ad5 100644
    --- a/ext/bg/js/settings/main.js
    +++ b/ext/bg/js/settings/main.js
    @@ -16,6 +16,7 @@
      */
     
     /* global
    + * ProfileController
      * SettingsBackup
      * SettingsController
      * ankiInitialize
    @@ -25,15 +26,28 @@
      * appearanceInitialize
      * audioSettingsInitialize
      * dictSettingsInitialize
    - * getOptionsContext
      * onAnkiOptionsChanged
      * onDictionaryOptionsChanged
    - * profileOptionsSetup
      * storageInfoInitialize
      * utilBackend
      * utilBackgroundIsolate
      */
     
    +let profileIndex = 0;
    +
    +function getOptionsContext() {
    +    return {index: getProfileIndex()};
    +}
    +
    +function getProfileIndex() {
    +    return profileIndex;
    +}
    +
    +function setProfileIndex(value) {
    +    profileIndex = value;
    +}
    +
    +
     function getOptionsMutable(optionsContext) {
         return utilBackend().getOptions(
             utilBackgroundIsolate(optionsContext)
    @@ -302,7 +316,7 @@ async function onReady() {
         formSetupEventListeners();
         appearanceInitialize();
         await audioSettingsInitialize();
    -    await profileOptionsSetup();
    +    await (new ProfileController()).prepare();
         await dictSettingsInitialize();
         ankiInitialize();
         ankiTemplatesInitialize();
    diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js
    index 59f7fbb1..e2c558e9 100644
    --- a/ext/bg/js/settings/profiles.js
    +++ b/ext/bg/js/settings/profiles.js
    @@ -20,281 +20,291 @@
      * api
      * conditionsClearCaches
      * getOptionsFullMutable
    + * getProfileIndex
      * onOptionsUpdated
      * profileConditionsDescriptor
      * profileConditionsDescriptorPromise
    + * setProfileIndex
      * settingsSaveOptions
      * utilBackgroundIsolate
      */
     
    -let currentProfileIndex = 0;
    -let profileConditionsContainer = null;
    -
    -function getOptionsContext() {
    -    return {
    -        index: currentProfileIndex
    -    };
    -}
    +class ProfileController {
    +    constructor() {
    +        this._conditionsContainer = null;
    +    }
     
    +    async prepare() {
    +        const optionsFull = await getOptionsFullMutable();
    +        setProfileIndex(optionsFull.profileCurrent);
     
    -async function profileOptionsSetup() {
    -    const optionsFull = await getOptionsFullMutable();
    -    currentProfileIndex = optionsFull.profileCurrent;
    +        this._setupEventListeners();
    +        await this._updateTarget(optionsFull);
    +    }
     
    -    profileOptionsSetupEventListeners();
    -    await profileOptionsUpdateTarget(optionsFull);
    -}
    +    // Private
    +
    +    _setupEventListeners() {
    +        $('#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));
    +    }
     
    -function profileOptionsSetupEventListeners() {
    -    $('#profile-target').change(onTargetProfileChanged);
    -    $('#profile-name').change(onProfileNameChanged);
    -    $('#profile-add').click(onProfileAdd);
    -    $('#profile-remove').click(onProfileRemove);
    -    $('#profile-remove-confirm').click(onProfileRemoveConfirm);
    -    $('#profile-copy').click(onProfileCopy);
    -    $('#profile-copy-confirm').click(onProfileCopyConfirm);
    -    $('#profile-move-up').click(() => onProfileMove(-1));
    -    $('#profile-move-down').click(() => onProfileMove(1));
    -    $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change(onProfileOptionsChanged);
    -}
    +    _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;
    +    }
     
    -function tryGetIntegerValue(selector, min, max) {
    -    const value = parseInt($(selector).val(), 10);
    -    return (
    -        typeof value === 'number' &&
    -        Number.isFinite(value) &&
    -        Math.floor(value) === value &&
    -        value >= min &&
    -        value < max
    -    ) ? value : null;
    -}
    +    async _formRead(optionsFull) {
    +        const currentProfileIndex = getProfileIndex();
    +        const profile = optionsFull.profiles[currentProfileIndex];
     
    -async function profileFormRead(optionsFull) {
    -    const profile = optionsFull.profiles[currentProfileIndex];
    +        // Current profile
    +        const index = this._tryGetIntegerValue('#profile-active', 0, optionsFull.profiles.length);
    +        if (index !== null) {
    +            optionsFull.profileCurrent = index;
    +        }
     
    -    // Current profile
    -    const index = tryGetIntegerValue('#profile-active', 0, optionsFull.profiles.length);
    -    if (index !== null) {
    -        optionsFull.profileCurrent = index;
    +        // Profile name
    +        profile.name = $('#profile-name').val();
         }
     
    -    // Profile name
    -    profile.name = $('#profile-name').val();
    -}
    +    async _formWrite(optionsFull) {
    +        const currentProfileIndex = getProfileIndex();
    +        const profile = optionsFull.profiles[currentProfileIndex];
     
    -async function profileFormWrite(optionsFull) {
    -    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);
     
    -    profileOptionsPopulateSelect($('#profile-active'), optionsFull.profiles, optionsFull.profileCurrent, null);
    -    profileOptionsPopulateSelect($('#profile-target'), optionsFull.profiles, currentProfileIndex, null);
    -    $('#profile-remove').prop('disabled', optionsFull.profiles.length <= 1);
    -    $('#profile-copy').prop('disabled', optionsFull.profiles.length <= 1);
    -    $('#profile-move-up').prop('disabled', currentProfileIndex <= 0);
    -    $('#profile-move-down').prop('disabled', currentProfileIndex >= optionsFull.profiles.length - 1);
    +        $('#profile-name').val(profile.name);
     
    -    $('#profile-name').val(profile.name);
    +        if (this._conditionsContainer !== null) {
    +            this._conditionsContainer.cleanup();
    +        }
     
    -    if (profileConditionsContainer !== null) {
    -        profileConditionsContainer.cleanup();
    +        await profileConditionsDescriptorPromise;
    +        this._conditionsContainer = new ConditionsUI.Container(
    +            profileConditionsDescriptor,
    +            'popupLevel',
    +            profile.conditionGroups,
    +            $('#profile-condition-groups'),
    +            $('#profile-add-condition-group')
    +        );
    +        this._conditionsContainer.save = () => {
    +            settingsSaveOptions();
    +            conditionsClearCaches(profileConditionsDescriptor);
    +        };
    +        this._conditionsContainer.isolate = utilBackgroundIsolate;
         }
     
    -    await profileConditionsDescriptorPromise;
    -    profileConditionsContainer = new ConditionsUI.Container(
    -        profileConditionsDescriptor,
    -        'popupLevel',
    -        profile.conditionGroups,
    -        $('#profile-condition-groups'),
    -        $('#profile-add-condition-group')
    -    );
    -    profileConditionsContainer.save = () => {
    -        settingsSaveOptions();
    -        conditionsClearCaches(profileConditionsDescriptor);
    -    };
    -    profileConditionsContainer.isolate = utilBackgroundIsolate;
    -}
    -
    -function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndices) {
    -    select.empty();
    +    _populateSelect(select, profiles, currentValue, ignoreIndices) {
    +        select.empty();
     
     
    -    for (let i = 0; i < profiles.length; ++i) {
    -        if (ignoreIndices !== null && ignoreIndices.indexOf(i) >= 0) {
    -            continue;
    +        for (let i = 0; i < profiles.length; ++i) {
    +            if (ignoreIndices !== null && ignoreIndices.indexOf(i) >= 0) {
    +                continue;
    +            }
    +            const profile = profiles[i];
    +            select.append($(``));
             }
    -        const profile = profiles[i];
    -        select.append($(``));
    -    }
     
    -    select.val(`${currentValue}`);
    -}
    +        select.val(`${currentValue}`);
    +    }
     
    -async function profileOptionsUpdateTarget(optionsFull) {
    -    await profileFormWrite(optionsFull);
    -    await onOptionsUpdated({source: null});
    -}
    +    async _updateTarget(optionsFull) {
    +        await this._formWrite(optionsFull);
    +        await onOptionsUpdated({source: null});
    +    }
     
    -function profileOptionsCreateCopyName(name, profiles, maxUniqueAttempts) {
    -    let space, index, prefix, suffix;
    -    const match = /^([\w\W]*\(Copy)((\s+)(\d+))?(\)\s*)$/.exec(name);
    -    if (match === null) {
    -        prefix = `${name} (Copy`;
    -        space = '';
    -        index = '';
    -        suffix = ')';
    -    } else {
    -        prefix = match[1];
    -        suffix = match[5];
    -        if (typeof match[2] === 'string') {
    -            space = match[3];
    -            index = parseInt(match[4], 10) + 1;
    +    _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 {
    -            space = ' ';
    -            index = 2;
    +            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;
    +        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 function onProfileOptionsChanged(e) {
    -    if (!e.originalEvent && !e.isTrigger) {
    -        return;
    +    async _onInputChanged(e) {
    +        if (!e.originalEvent && !e.isTrigger) {
    +            return;
    +        }
    +
    +        const optionsFull = await getOptionsFullMutable();
    +        await this._formRead(optionsFull);
    +        await settingsSaveOptions();
         }
     
    -    const optionsFull = await getOptionsFullMutable();
    -    await profileFormRead(optionsFull);
    -    await settingsSaveOptions();
    -}
    +    async _onTargetProfileChanged() {
    +        const optionsFull = await getOptionsFullMutable();
    +        const currentProfileIndex = getProfileIndex();
    +        const index = this._tryGetIntegerValue('#profile-target', 0, optionsFull.profiles.length);
    +        if (index === null || currentProfileIndex === index) {
    +            return;
    +        }
     
    -async function onTargetProfileChanged() {
    -    const optionsFull = await getOptionsFullMutable();
    -    const index = tryGetIntegerValue('#profile-target', 0, optionsFull.profiles.length);
    -    if (index === null || currentProfileIndex === index) {
    -        return;
    +        setProfileIndex(index);
    +
    +        await this._updateTarget(optionsFull);
    +
    +        yomichan.trigger('modifyingProfileChange');
         }
     
    -    currentProfileIndex = index;
    +    async _onAdd() {
    +        const optionsFull = await getOptionsFullMutable();
    +        const currentProfileIndex = getProfileIndex();
    +        const profile = utilBackgroundIsolate(optionsFull.profiles[currentProfileIndex]);
    +        profile.name = this._createCopyName(profile.name, optionsFull.profiles, 100);
    +        optionsFull.profiles.push(profile);
     
    -    await profileOptionsUpdateTarget(optionsFull);
    +        setProfileIndex(optionsFull.profiles.length - 1);
     
    -    yomichan.trigger('modifyingProfileChange');
    -}
    +        await this._updateTarget(optionsFull);
    +        await settingsSaveOptions();
     
    -async function onProfileAdd() {
    -    const optionsFull = await getOptionsFullMutable();
    -    const profile = utilBackgroundIsolate(optionsFull.profiles[currentProfileIndex]);
    -    profile.name = profileOptionsCreateCopyName(profile.name, optionsFull.profiles, 100);
    -    optionsFull.profiles.push(profile);
    +        yomichan.trigger('modifyingProfileChange');
    +    }
     
    -    currentProfileIndex = optionsFull.profiles.length - 1;
    +    async _onRemove(e) {
    +        if (e.shiftKey) {
    +            return await this._onRemoveConfirm();
    +        }
     
    -    await profileOptionsUpdateTarget(optionsFull);
    -    await settingsSaveOptions();
    +        const optionsFull = await api.optionsGetFull();
    +        if (optionsFull.profiles.length <= 1) {
    +            return;
    +        }
     
    -    yomichan.trigger('modifyingProfileChange');
    -}
    +        const currentProfileIndex = getProfileIndex();
    +        const profile = optionsFull.profiles[currentProfileIndex];
     
    -async function onProfileRemove(e) {
    -    if (e.shiftKey) {
    -        return await onProfileRemoveConfirm();
    +        $('#profile-remove-modal-profile-name').text(profile.name);
    +        $('#profile-remove-modal').modal('show');
         }
     
    -    const optionsFull = await api.optionsGetFull();
    -    if (optionsFull.profiles.length <= 1) {
    -        return;
    -    }
    +    async _onRemoveConfirm() {
    +        $('#profile-remove-modal').modal('hide');
     
    -    const profile = optionsFull.profiles[currentProfileIndex];
    +        const optionsFull = await getOptionsFullMutable();
    +        if (optionsFull.profiles.length <= 1) {
    +            return;
    +        }
     
    -    $('#profile-remove-modal-profile-name').text(profile.name);
    -    $('#profile-remove-modal').modal('show');
    -}
    +        const currentProfileIndex = getProfileIndex();
    +        optionsFull.profiles.splice(currentProfileIndex, 1);
     
    -async function onProfileRemoveConfirm() {
    -    $('#profile-remove-modal').modal('hide');
    +        if (currentProfileIndex >= optionsFull.profiles.length) {
    +            setProfileIndex(optionsFull.profiles.length - 1);
    +        }
     
    -    const optionsFull = await getOptionsFullMutable();
    -    if (optionsFull.profiles.length <= 1) {
    -        return;
    -    }
    +        if (optionsFull.profileCurrent >= optionsFull.profiles.length) {
    +            optionsFull.profileCurrent = optionsFull.profiles.length - 1;
    +        }
     
    -    optionsFull.profiles.splice(currentProfileIndex, 1);
    +        await this._updateTarget(optionsFull);
    +        await settingsSaveOptions();
     
    -    if (currentProfileIndex >= optionsFull.profiles.length) {
    -        --currentProfileIndex;
    +        yomichan.trigger('modifyingProfileChange');
         }
     
    -    if (optionsFull.profileCurrent >= optionsFull.profiles.length) {
    -        optionsFull.profileCurrent = optionsFull.profiles.length - 1;
    +    _onNameChanged() {
    +        const currentProfileIndex = getProfileIndex();
    +        $('#profile-active, #profile-target').find(`[value="${currentProfileIndex}"]`).text(this.value);
         }
     
    -    await profileOptionsUpdateTarget(optionsFull);
    -    await settingsSaveOptions();
    +    async _onMove(offset) {
    +        const optionsFull = await getOptionsFullMutable();
    +        const currentProfileIndex = getProfileIndex();
    +        const index = currentProfileIndex + offset;
    +        if (index < 0 || index >= optionsFull.profiles.length) {
    +            return;
    +        }
     
    -    yomichan.trigger('modifyingProfileChange');
    -}
    +        const profile = optionsFull.profiles[currentProfileIndex];
    +        optionsFull.profiles.splice(currentProfileIndex, 1);
    +        optionsFull.profiles.splice(index, 0, profile);
     
    -function onProfileNameChanged() {
    -    $('#profile-active, #profile-target').find(`[value="${currentProfileIndex}"]`).text(this.value);
    -}
    +        if (optionsFull.profileCurrent === currentProfileIndex) {
    +            optionsFull.profileCurrent = index;
    +        }
     
    -async function onProfileMove(offset) {
    -    const optionsFull = await getOptionsFullMutable();
    -    const index = currentProfileIndex + offset;
    -    if (index < 0 || index >= optionsFull.profiles.length) {
    -        return;
    -    }
    +        setProfileIndex(index);
     
    -    const profile = optionsFull.profiles[currentProfileIndex];
    -    optionsFull.profiles.splice(currentProfileIndex, 1);
    -    optionsFull.profiles.splice(index, 0, profile);
    +        await this._updateTarget(optionsFull);
    +        await settingsSaveOptions();
     
    -    if (optionsFull.profileCurrent === currentProfileIndex) {
    -        optionsFull.profileCurrent = index;
    +        yomichan.trigger('modifyingProfileChange');
         }
     
    -    currentProfileIndex = index;
    -
    -    await profileOptionsUpdateTarget(optionsFull);
    -    await settingsSaveOptions();
    -
    -    yomichan.trigger('modifyingProfileChange');
    -}
    +    async _onCopy() {
    +        const optionsFull = await api.optionsGetFull();
    +        if (optionsFull.profiles.length <= 1) {
    +            return;
    +        }
     
    -async function onProfileCopy() {
    -    const optionsFull = await api.optionsGetFull();
    -    if (optionsFull.profiles.length <= 1) {
    -        return;
    +        const currentProfileIndex = getProfileIndex();
    +        this._populateSelect($('#profile-copy-source'), optionsFull.profiles, currentProfileIndex === 0 ? 1 : 0, [currentProfileIndex]);
    +        $('#profile-copy-modal').modal('show');
         }
     
    -    profileOptionsPopulateSelect($('#profile-copy-source'), optionsFull.profiles, currentProfileIndex === 0 ? 1 : 0, [currentProfileIndex]);
    -    $('#profile-copy-modal').modal('show');
    -}
    -
    -async function onProfileCopyConfirm() {
    -    $('#profile-copy-modal').modal('hide');
    +    async _onCopyConfirm() {
    +        $('#profile-copy-modal').modal('hide');
     
    -    const optionsFull = await getOptionsFullMutable();
    -    const index = tryGetIntegerValue('#profile-copy-source', 0, optionsFull.profiles.length);
    -    if (index === null || index === currentProfileIndex) {
    -        return;
    -    }
    +        const optionsFull = await getOptionsFullMutable();
    +        const index = this._tryGetIntegerValue('#profile-copy-source', 0, optionsFull.profiles.length);
    +        const currentProfileIndex = getProfileIndex();
    +        if (index === null || index === currentProfileIndex) {
    +            return;
    +        }
     
    -    const profileOptions = utilBackgroundIsolate(optionsFull.profiles[index].options);
    -    optionsFull.profiles[currentProfileIndex].options = profileOptions;
    +        const profileOptions = utilBackgroundIsolate(optionsFull.profiles[index].options);
    +        optionsFull.profiles[currentProfileIndex].options = profileOptions;
     
    -    await profileOptionsUpdateTarget(optionsFull);
    -    await settingsSaveOptions();
    +        await this._updateTarget(optionsFull);
    +        await settingsSaveOptions();
    +    }
     }
    -- 
    cgit v1.2.3
    
    
    From 5f9889fd26f38396aa6ffaa5c669081b02467393 Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Fri, 29 May 2020 19:52:51 -0400
    Subject: Anki settings controllers (#567)
    
    * Convert anki-templates.js to a class
    
    * Convert anki.js to a class
    ---
     ext/bg/js/settings/anki-templates.js | 216 ++++++++--------
     ext/bg/js/settings/anki.js           | 464 +++++++++++++++++------------------
     ext/bg/js/settings/main.js           |  22 +-
     3 files changed, 354 insertions(+), 348 deletions(-)
    
    (limited to 'ext/bg/js')
    
    diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js
    index 0dadb433..dd128ab8 100644
    --- a/ext/bg/js/settings/anki-templates.js
    +++ b/ext/bg/js/settings/anki-templates.js
    @@ -17,141 +17,147 @@
     
     /* global
      * AnkiNoteBuilder
    - * ankiGetFieldMarkers
    - * ankiGetFieldMarkersHtml
      * api
      * getOptionsContext
      * getOptionsMutable
      * settingsSaveOptions
      */
     
    -function onAnkiFieldTemplatesReset(e) {
    -    e.preventDefault();
    -    $('#field-template-reset-modal').modal('show');
    -}
    -
    -async function onAnkiFieldTemplatesResetConfirm(e) {
    -    e.preventDefault();
    +class AnkiTemplatesController {
    +    constructor(ankiController) {
    +        this._ankiController = ankiController;
    +        this._cachedDefinitionValue = null;
    +        this._cachedDefinitionText = null;
    +    }
     
    -    $('#field-template-reset-modal').modal('hide');
    +    prepare() {
    +        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);
    +        }
     
    -    const value = await api.getDefaultAnkiFieldTemplates();
    +        $('#field-templates').on('change', this._onChanged.bind(this));
    +        $('#field-template-render').on('click', this._onRender.bind(this));
    +        $('#field-templates-reset').on('click', this._onReset.bind(this));
    +        $('#field-templates-reset-confirm').on('click', this._onResetConfirm.bind(this));
     
    -    const element = document.querySelector('#field-templates');
    -    element.value = value;
    -    element.dispatchEvent(new Event('change'));
    -}
    +        this.updateValue();
    +    }
     
    -function ankiTemplatesInitialize() {
    -    const markers = new Set(ankiGetFieldMarkers('terms').concat(ankiGetFieldMarkers('kanji')));
    -    const fragment = ankiGetFieldMarkersHtml(markers);
    +    async updateValue() {
    +        const optionsContext = getOptionsContext();
    +        const options = await api.optionsGet(optionsContext);
    +        let templates = options.anki.fieldTemplates;
    +        if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); }
    +        $('#field-templates').val(templates);
     
    -    const list = document.querySelector('#field-templates-list');
    -    list.appendChild(fragment);
    -    for (const node of list.querySelectorAll('.marker-link')) {
    -        node.addEventListener('click', onAnkiTemplateMarkerClicked, false);
    +        this._onValidateCompile();
         }
     
    -    $('#field-templates').on('change', onAnkiFieldTemplatesChanged);
    -    $('#field-template-render').on('click', onAnkiTemplateRender);
    -    $('#field-templates-reset').on('click', onAnkiFieldTemplatesReset);
    -    $('#field-templates-reset-confirm').on('click', onAnkiFieldTemplatesResetConfirm);
    +    // Private
     
    -    ankiTemplatesUpdateValue();
    -}
    +    _onReset(e) {
    +        e.preventDefault();
    +        $('#field-template-reset-modal').modal('show');
    +    }
     
    -async function ankiTemplatesUpdateValue() {
    -    const optionsContext = getOptionsContext();
    -    const options = await api.optionsGet(optionsContext);
    -    let templates = options.anki.fieldTemplates;
    -    if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); }
    -    $('#field-templates').val(templates);
    +    async _onResetConfirm(e) {
    +        e.preventDefault();
     
    -    onAnkiTemplatesValidateCompile();
    -}
    +        $('#field-template-reset-modal').modal('hide');
     
    -const ankiTemplatesValidateGetDefinition = (() => {
    -    let cachedValue = null;
    -    let cachedText = null;
    +        const value = await api.getDefaultAnkiFieldTemplates();
     
    -    return async (text, optionsContext) => {
    -        if (cachedText !== text) {
    -            const {definitions} = await api.termsFind(text, {}, optionsContext);
    -            if (definitions.length === 0) { return null; }
    +        const element = document.querySelector('#field-templates');
    +        element.value = value;
    +        element.dispatchEvent(new Event('change'));
    +    }
     
    -            cachedValue = definitions[0];
    -            cachedText = text;
    +    async _onChanged(e) {
    +        // Get value
    +        let templates = e.currentTarget.value;
    +        if (templates === await api.getDefaultAnkiFieldTemplates()) {
    +            // Default
    +            templates = null;
             }
    -        return cachedValue;
    -    };
    -})();
    -
    -async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, invalidateInput) {
    -    const text = document.querySelector('#field-templates-preview-text').value || '';
    -    const exceptions = [];
    -    let result = `No definition found for ${text}`;
    -    try {
    +
    +        // Overwrite
             const optionsContext = getOptionsContext();
    -        const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext);
    -        if (definition !== null) {
    -            const options = await api.optionsGet(optionsContext);
    -            const context = {
    -                document: {
    -                    title: document.title
    -                }
    -            };
    -            let templates = options.anki.fieldTemplates;
    -            if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); }
    -            const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: api.templateRender.bind(api)});
    -            result = await ankiNoteBuilder.formatField(field, definition, mode, context, options, templates, exceptions);
    -        }
    -    } catch (e) {
    -        exceptions.push(e);
    +        const options = await getOptionsMutable(optionsContext);
    +        options.anki.fieldTemplates = templates;
    +        await settingsSaveOptions();
    +
    +        // Compile
    +        this._onValidateCompile();
         }
     
    -    const hasException = exceptions.length > 0;
    -    infoNode.hidden = !(showSuccessResult || hasException);
    -    infoNode.textContent = hasException ? exceptions.map((e) => `${e}`).join('\n') : (showSuccessResult ? result : '');
    -    infoNode.classList.toggle('text-danger', hasException);
    -    if (invalidateInput) {
    -        const input = document.querySelector('#field-templates');
    -        input.classList.toggle('is-invalid', hasException);
    +    _onValidateCompile() {
    +        const infoNode = document.querySelector('#field-template-compile-result');
    +        this._validate(infoNode, '{expression}', 'term-kanji', false, true);
         }
    -}
     
    -async function onAnkiFieldTemplatesChanged(e) {
    -    // Get value
    -    let templates = e.currentTarget.value;
    -    if (templates === await api.getDefaultAnkiFieldTemplates()) {
    -        // Default
    -        templates = null;
    +    _onMarkerClicked(e) {
    +        e.preventDefault();
    +        document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`;
         }
     
    -    // Overwrite
    -    const optionsContext = getOptionsContext();
    -    const options = await getOptionsMutable(optionsContext);
    -    options.anki.fieldTemplates = templates;
    -    await settingsSaveOptions();
    +    _onRender(e) {
    +        e.preventDefault();
     
    -    // Compile
    -    onAnkiTemplatesValidateCompile();
    -}
    +        const field = document.querySelector('#field-template-render-text').value;
    +        const infoNode = document.querySelector('#field-template-render-result');
    +        infoNode.hidden = true;
    +        this._validate(infoNode, field, 'term-kanji', true, false);
    +    }
     
    -function onAnkiTemplatesValidateCompile() {
    -    const infoNode = document.querySelector('#field-template-compile-result');
    -    ankiTemplatesValidate(infoNode, '{expression}', 'term-kanji', false, true);
    -}
    +    async _getDefinition(text, optionsContext) {
    +        if (this._cachedDefinitionText !== text) {
    +            const {definitions} = await api.termsFind(text, {}, optionsContext);
    +            if (definitions.length === 0) { return null; }
     
    -function onAnkiTemplateMarkerClicked(e) {
    -    e.preventDefault();
    -    document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`;
    -}
    +            this._cachedDefinitionValue = definitions[0];
    +            this._cachedDefinitionText = text;
    +        }
    +        return this._cachedDefinitionValue;
    +    }
     
    -function onAnkiTemplateRender(e) {
    -    e.preventDefault();
    +    async _validate(infoNode, field, mode, showSuccessResult, invalidateInput) {
    +        const text = document.querySelector('#field-templates-preview-text').value || '';
    +        const exceptions = [];
    +        let result = `No definition found for ${text}`;
    +        try {
    +            const optionsContext = getOptionsContext();
    +            const definition = await this._getDefinition(text, optionsContext);
    +            if (definition !== null) {
    +                const options = await api.optionsGet(optionsContext);
    +                const context = {
    +                    document: {
    +                        title: document.title
    +                    }
    +                };
    +                let templates = options.anki.fieldTemplates;
    +                if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); }
    +                const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: api.templateRender.bind(api)});
    +                result = await ankiNoteBuilder.formatField(field, definition, mode, context, options, templates, exceptions);
    +            }
    +        } catch (e) {
    +            exceptions.push(e);
    +        }
     
    -    const field = document.querySelector('#field-template-render-text').value;
    -    const infoNode = document.querySelector('#field-template-render-result');
    -    infoNode.hidden = true;
    -    ankiTemplatesValidate(infoNode, field, 'term-kanji', true, false);
    +        const hasException = exceptions.length > 0;
    +        infoNode.hidden = !(showSuccessResult || hasException);
    +        infoNode.textContent = hasException ? exceptions.map((e) => `${e}`).join('\n') : (showSuccessResult ? result : '');
    +        infoNode.classList.toggle('text-danger', hasException);
    +        if (invalidateInput) {
    +            const input = document.querySelector('#field-templates');
    +            input.classList.toggle('is-invalid', hasException);
    +        }
    +    }
     }
    diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js
    index 8de97554..d110ef39 100644
    --- a/ext/bg/js/settings/anki.js
    +++ b/ext/bg/js/settings/anki.js
    @@ -23,287 +23,281 @@
      * utilBackgroundIsolate
      */
     
    -// Private
    -
    -function _ankiSpinnerShow(show) {
    -    const spinner = $('#anki-spinner');
    -    if (show) {
    -        spinner.show();
    -    } else {
    -        spinner.hide();
    -    }
    -}
    +class AnkiController {
    +    prepare() {
    +        $('#anki-fields-container input,#anki-fields-container select,#anki-fields-container textarea').change(this._onFieldsChanged.bind(this));
     
    -function _ankiSetError(error) {
    -    const node = document.querySelector('#anki-error');
    -    const node2 = document.querySelector('#anki-invalid-response-error');
    -    if (error) {
    -        const errorString = `${error}`;
    -        if (node !== null) {
    -            node.hidden = false;
    -            node.textContent = errorString;
    -            _ankiSetErrorData(node, error);
    +        for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) {
    +            node.addEventListener('change', this._onModelChanged.bind(this), false);
             }
     
    -        if (node2 !== null) {
    -            node2.hidden = (errorString.indexOf('Invalid response') < 0);
    -        }
    -    } else {
    -        if (node !== null) {
    -            node.hidden = true;
    -            node.textContent = '';
    +        this.optionsChanged();
    +    }
    +
    +    async optionsChanged(options=null) {
    +        if (options === null) {
    +            const optionsContext = getOptionsContext();
    +            options = await getOptionsMutable(optionsContext);
             }
     
    -        if (node2 !== null) {
    -            node2.hidden = true;
    +        if (!options.anki.enable) {
    +            return;
             }
    -    }
    -}
     
    -function _ankiSetErrorData(node, error) {
    -    const data = error.data;
    -    let message = '';
    -    if (typeof data !== 'undefined') {
    -        message += `${JSON.stringify(data, null, 4)}\n\n`;
    +        await this._deckAndModelPopulate(options);
    +        await Promise.all([
    +            this._fieldsPopulate('terms', options),
    +            this._fieldsPopulate('kanji', options)
    +        ]);
         }
    -    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;
    +    getFieldMarkers(type) {
    +        switch (type) {
    +            case 'terms':
    +                return [
    +                    'audio',
    +                    'cloze-body',
    +                    'cloze-prefix',
    +                    'cloze-suffix',
    +                    'dictionary',
    +                    'document-title',
    +                    'expression',
    +                    'furigana',
    +                    'furigana-plain',
    +                    'glossary',
    +                    'glossary-brief',
    +                    'reading',
    +                    'screenshot',
    +                    'sentence',
    +                    'tags',
    +                    'url'
    +                ];
    +            case 'kanji':
    +                return [
    +                    'character',
    +                    'dictionary',
    +                    'document-title',
    +                    'glossary',
    +                    'kunyomi',
    +                    'onyomi',
    +                    'screenshot',
    +                    'sentence',
    +                    'tags',
    +                    'url'
    +                ];
    +            default:
    +                return [];
    +        }
    +    }
     
    -    button.addEventListener('click', () => content.hidden = !content.hidden, false);
    +    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;
    +    }
     
    -    node.appendChild(button);
    -    node.appendChild(content);
    -}
    +    // Private
     
    -function _ankiSetDropdownOptions(dropdown, optionValues) {
    -    const fragment = document.createDocumentFragment();
    -    for (const optionValue of optionValues) {
    -        const option = document.createElement('option');
    -        option.value = optionValue;
    -        option.textContent = optionValue;
    -        fragment.appendChild(option);
    +    _fieldsToDict(elements) {
    +        const result = {};
    +        for (const element of elements) {
    +            result[element.dataset.field] = element.value;
    +        }
    +        return result;
         }
    -    dropdown.textContent = '';
    -    dropdown.appendChild(fragment);
    -}
     
    -async function _ankiDeckAndModelPopulate(options) {
    -    const termsDeck = {value: options.anki.terms.deck, selector: '#anki-terms-deck'};
    -    const kanjiDeck = {value: options.anki.kanji.deck, selector: '#anki-kanji-deck'};
    -    const termsModel = {value: options.anki.terms.model, selector: '#anki-terms-model'};
    -    const kanjiModel = {value: options.anki.kanji.model, selector: '#anki-kanji-model'};
    -    try {
    -        _ankiSpinnerShow(true);
    -        const [deckNames, modelNames] = await Promise.all([api.getAnkiDeckNames(), api.getAnkiModelNames()]);
    -        deckNames.sort();
    -        modelNames.sort();
    -        termsDeck.values = deckNames;
    -        kanjiDeck.values = deckNames;
    -        termsModel.values = modelNames;
    -        kanjiModel.values = modelNames;
    -        _ankiSetError(null);
    -    } catch (error) {
    -        _ankiSetError(error);
    -    } finally {
    -        _ankiSpinnerShow(false);
    +    _spinnerShow(show) {
    +        const spinner = $('#anki-spinner');
    +        if (show) {
    +            spinner.show();
    +        } else {
    +            spinner.hide();
    +        }
         }
     
    -    for (const {value, values, selector} of [termsDeck, kanjiDeck, termsModel, kanjiModel]) {
    -        const node = document.querySelector(selector);
    -        _ankiSetDropdownOptions(node, Array.isArray(values) ? values : [value]);
    -        node.value = value;
    +    _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;
    +            }
    +        }
         }
    -}
    -
    -function _ankiCreateFieldTemplate(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(ankiGetFieldMarkersHtml(markers));
    +    _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();
     
    -    return content;
    -}
    +        const button = document.createElement('a');
    +        button.className = 'error-data-show-button';
     
    -async function _ankiFieldsPopulate(tabId, options) {
    -    const tab = document.querySelector(`.tab-pane[data-anki-card-type=${tabId}]`);
    -    const container = tab.querySelector('tbody');
    -    const markers = ankiGetFieldMarkers(tabId);
    -
    -    const fragment = document.createDocumentFragment();
    -    const fields = options.anki[tabId].fields;
    -    for (const name of Object.keys(fields)) {
    -        const value = fields[name];
    -        const html = _ankiCreateFieldTemplate(name, value, markers);
    -        fragment.appendChild(html);
    -    }
    +        const content = document.createElement('div');
    +        content.className = 'error-data-container';
    +        content.textContent = message;
    +        content.hidden = true;
     
    -    container.textContent = '';
    -    container.appendChild(fragment);
    +        button.addEventListener('click', () => content.hidden = !content.hidden, false);
     
    -    for (const node of container.querySelectorAll('.anki-field-value')) {
    -        node.addEventListener('change', _onAnkiFieldsChanged, false);
    -    }
    -    for (const node of container.querySelectorAll('.marker-link')) {
    -        node.addEventListener('click', _onAnkiMarkerClicked, false);
    +        node.appendChild(button);
    +        node.appendChild(content);
         }
    -}
    -
    -function _onAnkiMarkerClicked(e) {
    -    e.preventDefault();
    -    const link = e.currentTarget;
    -    const input = $(link).closest('.input-group').find('.anki-field-value')[0];
    -    input.value = `{${link.textContent}}`;
    -    input.dispatchEvent(new Event('change'));
    -}
     
    -async function _onAnkiModelChanged(e) {
    -    const node = e.currentTarget;
    -    let fieldNames;
    -    try {
    -        const modelName = node.value;
    -        fieldNames = await api.getAnkiModelFieldNames(modelName);
    -        _ankiSetError(null);
    -    } catch (error) {
    -        _ankiSetError(error);
    -        return;
    -    } finally {
    -        _ankiSpinnerShow(false);
    +    _setDropdownOptions(dropdown, optionValues) {
    +        const fragment = document.createDocumentFragment();
    +        for (const optionValue of optionValues) {
    +            const option = document.createElement('option');
    +            option.value = optionValue;
    +            option.textContent = optionValue;
    +            fragment.appendChild(option);
    +        }
    +        dropdown.textContent = '';
    +        dropdown.appendChild(fragment);
         }
     
    -    const tabId = node.dataset.ankiCardType;
    -    if (tabId !== 'terms' && tabId !== 'kanji') { return; }
    +    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);
    +        }
     
    -    const fields = {};
    -    for (const name of fieldNames) {
    -        fields[name] = '';
    +        for (const {value, values, selector} of [termsDeck, kanjiDeck, termsModel, kanjiModel]) {
    +            const node = document.querySelector(selector);
    +            this._setDropdownOptions(node, Array.isArray(values) ? values : [value]);
    +            node.value = value;
    +        }
         }
     
    -    const optionsContext = getOptionsContext();
    -    const options = await getOptionsMutable(optionsContext);
    -    options.anki[tabId].fields = utilBackgroundIsolate(fields);
    -    await settingsSaveOptions();
    -
    -    await _ankiFieldsPopulate(tabId, options);
    -}
    -
    -async function _onAnkiFieldsChanged() {
    -    const optionsContext = getOptionsContext();
    -    const options = await getOptionsMutable(optionsContext);
    -
    -    options.anki.terms.deck = $('#anki-terms-deck').val();
    -    options.anki.terms.model = $('#anki-terms-model').val();
    -    options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#terms .anki-field-value')));
    -    options.anki.kanji.deck = $('#anki-kanji-deck').val();
    -    options.anki.kanji.model = $('#anki-kanji-model').val();
    -    options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#kanji .anki-field-value')));
    +    _createFieldTemplate(name, value, markers) {
    +        const template = document.querySelector('#anki-field-template').content;
    +        const content = document.importNode(template, true).firstChild;
     
    -    await settingsSaveOptions();
    +        content.querySelector('.anki-field-name').textContent = name;
     
    -    await onAnkiOptionsChanged(options);
    -}
    +        const field = content.querySelector('.anki-field-value');
    +        field.dataset.field = name;
    +        field.value = value;
     
    +        content.querySelector('.anki-field-marker-list').appendChild(this.getFieldMarkersHtml(markers));
     
    +        return content;
    +    }
     
    -// Public
    +    async _fieldsPopulate(tabId, options) {
    +        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();
    +        const fields = options.anki[tabId].fields;
    +        for (const name of Object.keys(fields)) {
    +            const value = fields[name];
    +            const html = this._createFieldTemplate(name, value, markers);
    +            fragment.appendChild(html);
    +        }
     
    -function ankiErrorShown() {
    -    const node = document.querySelector('#anki-error');
    -    return node && !node.hidden;
    -}
    +        container.textContent = '';
    +        container.appendChild(fragment);
     
    -function ankiFieldsToDict(elements) {
    -    const result = {};
    -    for (const element of elements) {
    -        result[element.dataset.field] = element.value;
    +        for (const node of container.querySelectorAll('.anki-field-value')) {
    +            node.addEventListener('change', this._onFieldsChanged.bind(this), false);
    +        }
    +        for (const node of container.querySelectorAll('.marker-link')) {
    +            node.addEventListener('click', this._onMarkerClicked.bind(this), false);
    +        }
         }
    -    return result;
    -}
     
    -
    -function ankiGetFieldMarkersHtml(markers) {
    -    const template = document.querySelector('#anki-field-marker-template').content;
    -    const fragment = document.createDocumentFragment();
    -    for (const marker of markers) {
    -        const markerNode = document.importNode(template, true).firstChild;
    -        markerNode.querySelector('.marker-link').textContent = marker;
    -        fragment.appendChild(markerNode);
    +    _onMarkerClicked(e) {
    +        e.preventDefault();
    +        const link = e.currentTarget;
    +        const input = $(link).closest('.input-group').find('.anki-field-value')[0];
    +        input.value = `{${link.textContent}}`;
    +        input.dispatchEvent(new Event('change'));
         }
    -    return fragment;
    -}
     
    -function ankiGetFieldMarkers(type) {
    -    switch (type) {
    -        case 'terms':
    -            return [
    -                'audio',
    -                'cloze-body',
    -                'cloze-prefix',
    -                'cloze-suffix',
    -                'dictionary',
    -                'document-title',
    -                'expression',
    -                'furigana',
    -                'furigana-plain',
    -                'glossary',
    -                'glossary-brief',
    -                'reading',
    -                'screenshot',
    -                'sentence',
    -                'tags',
    -                'url'
    -            ];
    -        case 'kanji':
    -            return [
    -                'character',
    -                'dictionary',
    -                'document-title',
    -                'glossary',
    -                'kunyomi',
    -                'onyomi',
    -                'screenshot',
    -                'sentence',
    -                'tags',
    -                'url'
    -            ];
    -        default:
    -            return [];
    -    }
    -}
    +    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; }
     
    -function ankiInitialize() {
    -    $('#anki-fields-container input,#anki-fields-container select,#anki-fields-container textarea').change(_onAnkiFieldsChanged);
    +        const fields = {};
    +        for (const name of fieldNames) {
    +            fields[name] = '';
    +        }
     
    -    for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) {
    -        node.addEventListener('change', _onAnkiModelChanged, false);
    -    }
    +        const optionsContext = getOptionsContext();
    +        const options = await getOptionsMutable(optionsContext);
    +        options.anki[tabId].fields = utilBackgroundIsolate(fields);
    +        await settingsSaveOptions();
     
    -    onAnkiOptionsChanged();
    -}
    +        await this._fieldsPopulate(tabId, options);
    +    }
     
    -async function onAnkiOptionsChanged(options=null) {
    -    if (options === null) {
    +    async _onFieldsChanged() {
             const optionsContext = getOptionsContext();
    -        options = await getOptionsMutable(optionsContext);
    -    }
    +        const options = await getOptionsMutable(optionsContext);
     
    -    if (!options.anki.enable) {
    -        return;
    -    }
    +        options.anki.terms.deck = $('#anki-terms-deck').val();
    +        options.anki.terms.model = $('#anki-terms-model').val();
    +        options.anki.terms.fields = utilBackgroundIsolate(this._fieldsToDict(document.querySelectorAll('#terms .anki-field-value')));
    +        options.anki.kanji.deck = $('#anki-kanji-deck').val();
    +        options.anki.kanji.model = $('#anki-kanji-model').val();
    +        options.anki.kanji.fields = utilBackgroundIsolate(this._fieldsToDict(document.querySelectorAll('#kanji .anki-field-value')));
    +
    +        await settingsSaveOptions();
     
    -    await _ankiDeckAndModelPopulate(options);
    -    await Promise.all([_ankiFieldsPopulate('terms', options), _ankiFieldsPopulate('kanji', options)]);
    +        await this.optionsChanged(options);
    +    }
     }
    diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
    index 64611ad5..872f8f05 100644
    --- a/ext/bg/js/settings/main.js
    +++ b/ext/bg/js/settings/main.js
    @@ -16,17 +16,15 @@
      */
     
     /* global
    + * AnkiController
    + * AnkiTemplatesController
      * ProfileController
      * SettingsBackup
      * SettingsController
    - * ankiInitialize
    - * ankiTemplatesInitialize
    - * ankiTemplatesUpdateValue
      * api
      * appearanceInitialize
      * audioSettingsInitialize
      * dictSettingsInitialize
    - * onAnkiOptionsChanged
      * onDictionaryOptionsChanged
      * storageInfoInitialize
      * utilBackend
    @@ -269,9 +267,13 @@ async function onOptionsUpdated({source}) {
         const options = await getOptionsMutable(optionsContext);
     
         document.querySelector('#enable-clipboard-popups').checked = options.general.enableClipboardPopups;
    -    ankiTemplatesUpdateValue();
    +    if (ankiTemplatesController !== null) {
    +        ankiTemplatesController.updateValue();
    +    }
         onDictionaryOptionsChanged();
    -    onAnkiOptionsChanged();
    +    if (ankiController !== null) {
    +        ankiController.optionsChanged();
    +    }
     
         await formWrite(options);
     }
    @@ -302,6 +304,8 @@ async function settingsPopulateModifierKeys() {
         }
     }
     
    +let ankiController = null;
    +let ankiTemplatesController = null;
     
     async function onReady() {
         api.forwardLogsToBackend();
    @@ -318,8 +322,10 @@ async function onReady() {
         await audioSettingsInitialize();
         await (new ProfileController()).prepare();
         await dictSettingsInitialize();
    -    ankiInitialize();
    -    ankiTemplatesInitialize();
    +    ankiController = new AnkiController();
    +    ankiController.prepare();
    +    ankiTemplatesController = new AnkiTemplatesController(ankiController);
    +    ankiTemplatesController.prepare();
         new SettingsBackup().prepare();
     
         storageInfoInitialize();
    -- 
    cgit v1.2.3
    
    
    From c62f980f37007743bed004ff43a82a8d7664dac6 Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Fri, 29 May 2020 19:56:38 -0400
    Subject: Audio controller (#569)
    
    * Convert audio.js into a class
    
    * Move audio-ui.js classes into audio.js
    
    * Rename fields
    
    * Merge classes
    
    * Remove audio-ui.js
    ---
     ext/bg/js/settings/audio-ui.js | 139 ----------------------
     ext/bg/js/settings/audio.js    | 255 ++++++++++++++++++++++++++++-------------
     ext/bg/js/settings/main.js     |   4 +-
     ext/bg/settings.html           |   1 -
     4 files changed, 178 insertions(+), 221 deletions(-)
     delete mode 100644 ext/bg/js/settings/audio-ui.js
    
    (limited to 'ext/bg/js')
    
    diff --git a/ext/bg/js/settings/audio-ui.js b/ext/bg/js/settings/audio-ui.js
    deleted file mode 100644
    index 73c64227..00000000
    --- a/ext/bg/js/settings/audio-ui.js
    +++ /dev/null
    @@ -1,139 +0,0 @@
    -/*
    - * Copyright (C) 2019-2020  Yomichan Authors
    - *
    - * This program is free software: you can redistribute it and/or modify
    - * it under the terms of the GNU General Public License as published by
    - * the Free Software Foundation, either version 3 of the License, or
    - * (at your option) any later version.
    - *
    - * This program is distributed in the hope that it will be useful,
    - * but WITHOUT ANY WARRANTY; without even the implied warranty of
    - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    - * GNU General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this program.  If not, see .
    - */
    -
    -class AudioSourceUI {
    -    static instantiateTemplate(templateSelector) {
    -        const template = document.querySelector(templateSelector);
    -        const content = document.importNode(template.content, true);
    -        return content.firstChild;
    -    }
    -}
    -
    -AudioSourceUI.Container = class Container {
    -    constructor(audioSources, container, addButton) {
    -        this.audioSources = audioSources;
    -        this.container = container;
    -        this.addButton = addButton;
    -        this.children = [];
    -
    -        this.container.textContent = '';
    -
    -        for (const audioSource of toIterable(audioSources)) {
    -            this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length));
    -        }
    -
    -        this._clickListener = this.onAddAudioSource.bind(this);
    -        this.addButton.addEventListener('click', this._clickListener, false);
    -    }
    -
    -    cleanup() {
    -        for (const child of this.children) {
    -            child.cleanup();
    -        }
    -
    -        this.addButton.removeEventListener('click', this._clickListener, false);
    -        this.container.textContent = '';
    -        this._clickListener = null;
    -    }
    -
    -    save() {
    -        // Override
    -    }
    -
    -    remove(child) {
    -        const index = this.children.indexOf(child);
    -        if (index < 0) {
    -            return;
    -        }
    -
    -        child.cleanup();
    -        this.children.splice(index, 1);
    -        this.audioSources.splice(index, 1);
    -
    -        for (let i = index; i < this.children.length; ++i) {
    -            this.children[i].index = i;
    -        }
    -    }
    -
    -    onAddAudioSource() {
    -        const audioSource = this.getUnusedAudioSource();
    -        this.audioSources.push(audioSource);
    -        this.save();
    -        this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length));
    -    }
    -
    -    getUnusedAudioSource() {
    -        const audioSourcesAvailable = [
    -            'jpod101',
    -            'jpod101-alternate',
    -            'jisho',
    -            'custom'
    -        ];
    -        for (const source of audioSourcesAvailable) {
    -            if (this.audioSources.indexOf(source) < 0) {
    -                return source;
    -            }
    -        }
    -        return audioSourcesAvailable[0];
    -    }
    -};
    -
    -AudioSourceUI.AudioSource = class AudioSource {
    -    constructor(parent, audioSource, index) {
    -        this.parent = parent;
    -        this.audioSource = audioSource;
    -        this.index = index;
    -
    -        this.container = AudioSourceUI.instantiateTemplate('#audio-source-template');
    -        this.select = this.container.querySelector('.audio-source-select');
    -        this.removeButton = this.container.querySelector('.audio-source-remove');
    -
    -        this.select.value = audioSource;
    -
    -        this._selectChangeListener = this.onSelectChanged.bind(this);
    -        this._removeClickListener = this.onRemoveClicked.bind(this);
    -
    -        this.select.addEventListener('change', this._selectChangeListener, false);
    -        this.removeButton.addEventListener('click', this._removeClickListener, false);
    -
    -        parent.container.appendChild(this.container);
    -    }
    -
    -    cleanup() {
    -        this.select.removeEventListener('change', this._selectChangeListener, false);
    -        this.removeButton.removeEventListener('click', this._removeClickListener, false);
    -
    -        if (this.container.parentNode !== null) {
    -            this.container.parentNode.removeChild(this.container);
    -        }
    -    }
    -
    -    save() {
    -        this.parent.save();
    -    }
    -
    -    onSelectChanged() {
    -        this.audioSource = this.select.value;
    -        this.parent.audioSources[this.index] = this.audioSource;
    -        this.save();
    -    }
    -
    -    onRemoveClicked() {
    -        this.parent.remove(this);
    -        this.save();
    -    }
    -};
    diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js
    index ac2d82f3..5c1cb131 100644
    --- a/ext/bg/js/settings/audio.js
    +++ b/ext/bg/js/settings/audio.js
    @@ -16,110 +16,207 @@
      */
     
     /* global
    - * AudioSourceUI
      * AudioSystem
      * getOptionsContext
      * getOptionsMutable
      * settingsSaveOptions
      */
     
    -let audioSourceUI = null;
    -let audioSystem = null;
    -
    -async function audioSettingsInitialize() {
    -    audioSystem = new AudioSystem({
    -        audioUriBuilder: null,
    -        useCache: true
    -    });
    -
    -    const optionsContext = getOptionsContext();
    -    const options = await getOptionsMutable(optionsContext);
    -    audioSourceUI = new AudioSourceUI.Container(
    -        options.audio.sources,
    -        document.querySelector('.audio-source-list'),
    -        document.querySelector('.audio-source-add')
    -    );
    -    audioSourceUI.save = settingsSaveOptions;
    -
    -    textToSpeechInitialize();
    -}
    +class AudioController {
    +    constructor() {
    +        this._audioSystem = null;
    +        this._settingsAudioSources = null;
    +        this._audioSourceContainer = null;
    +        this._audioSourceAddButton = null;
    +        this._audioSourceEntries = [];
    +    }
     
    -function textToSpeechInitialize() {
    -    if (typeof speechSynthesis === 'undefined') { return; }
    +    async prepare() {
    +        this._audioSystem = new AudioSystem({
    +            audioUriBuilder: null,
    +            useCache: true
    +        });
     
    -    speechSynthesis.addEventListener('voiceschanged', updateTextToSpeechVoices, false);
    -    updateTextToSpeechVoices();
    +        const optionsContext = getOptionsContext();
    +        const options = await getOptionsMutable(optionsContext);
     
    -    document.querySelector('#text-to-speech-voice').addEventListener('change', onTextToSpeechVoiceChange, false);
    -    document.querySelector('#text-to-speech-voice-test').addEventListener('click', textToSpeechTest, false);
    -}
    +        this._settingsAudioSources = options.audio.sources;
    +        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);
    +
    +        for (const audioSource of toIterable(this._settingsAudioSources)) {
    +            this._createAudioSourceEntry(audioSource);
    +        }
     
    -function updateTextToSpeechVoices() {
    -    const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index}));
    -    voices.sort(textToSpeechVoiceCompare);
    +        this._prepareTextToSpeech();
    +    }
    +
    +    // Private
     
    -    document.querySelector('#text-to-speech-voice-container').hidden = (voices.length === 0);
    +    async _save() {
    +        await settingsSaveOptions();
    +    }
     
    -    const fragment = document.createDocumentFragment();
    +    _prepareTextToSpeech() {
    +        if (typeof speechSynthesis === 'undefined') { return; }
     
    -    let option = document.createElement('option');
    -    option.value = '';
    -    option.textContent = 'None';
    -    fragment.appendChild(option);
    +        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);
    +    }
     
    -    for (const {voice} of voices) {
    -        option = document.createElement('option');
    -        option.value = voice.voiceURI;
    -        option.textContent = `${voice.name} (${voice.lang})`;
    +    _updateTextToSpeechVoices() {
    +        const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index}));
    +        voices.sort(this._textToSpeechVoiceCompare.bind(this));
    +
    +        document.querySelector('#text-to-speech-voice-container').hidden = (voices.length === 0);
    +
    +        const fragment = document.createDocumentFragment();
    +
    +        let option = document.createElement('option');
    +        option.value = '';
    +        option.textContent = 'None';
             fragment.appendChild(option);
    +
    +        for (const {voice} of voices) {
    +            option = document.createElement('option');
    +            option.value = voice.voiceURI;
    +            option.textContent = `${voice.name} (${voice.lang})`;
    +            fragment.appendChild(option);
    +        }
    +
    +        const select = document.querySelector('#text-to-speech-voice');
    +        select.textContent = '';
    +        select.appendChild(fragment);
    +        select.value = select.dataset.value;
         }
     
    -    const select = document.querySelector('#text-to-speech-voice');
    -    select.textContent = '';
    -    select.appendChild(fragment);
    -    select.value = select.dataset.value;
    -}
    +    _textToSpeechVoiceCompare(a, b) {
    +        const aIsJapanese = this._languageTagIsJapanese(a.voice.lang);
    +        const bIsJapanese = this._languageTagIsJapanese(b.voice.lang);
    +        if (aIsJapanese) {
    +            if (!bIsJapanese) { return -1; }
    +        } else {
    +            if (bIsJapanese) { return 1; }
    +        }
    +
    +        const aIsDefault = a.voice.default;
    +        const bIsDefault = b.voice.default;
    +        if (aIsDefault) {
    +            if (!bIsDefault) { return -1; }
    +        } else {
    +            if (bIsDefault) { return 1; }
    +        }
    +
    +        return a.index - b.index;
    +    }
     
    -function languageTagIsJapanese(languageTag) {
    -    return (
    -        languageTag.startsWith('ja-') ||
    -        languageTag.startsWith('jpn-')
    -    );
    -}
    +    _languageTagIsJapanese(languageTag) {
    +        return (
    +            languageTag.startsWith('ja-') ||
    +            languageTag.startsWith('jpn-')
    +        );
    +    }
     
    -function textToSpeechVoiceCompare(a, b) {
    -    const aIsJapanese = languageTagIsJapanese(a.voice.lang);
    -    const bIsJapanese = languageTagIsJapanese(b.voice.lang);
    -    if (aIsJapanese) {
    -        if (!bIsJapanese) { return -1; }
    -    } else {
    -        if (bIsJapanese) { return 1; }
    +    _testTextToSpeech() {
    +        try {
    +            const text = document.querySelector('#text-to-speech-voice-test').dataset.speechText || '';
    +            const voiceUri = document.querySelector('#text-to-speech-voice').value;
    +
    +            const audio = this._audioSystem.createTextToSpeechAudio(text, voiceUri);
    +            audio.volume = 1.0;
    +            audio.play();
    +        } catch (e) {
    +            // NOP
    +        }
         }
     
    -    const aIsDefault = a.voice.default;
    -    const bIsDefault = b.voice.default;
    -    if (aIsDefault) {
    -        if (!bIsDefault) { return -1; }
    -    } else {
    -        if (bIsDefault) { return 1; }
    +    _instantiateTemplate(templateSelector) {
    +        const template = document.querySelector(templateSelector);
    +        const content = document.importNode(template.content, true);
    +        return content.firstChild;
         }
     
    -    return a.index - b.index;
    -}
    +    _getUnusedAudioSource() {
    +        const audioSourcesAvailable = [
    +            'jpod101',
    +            'jpod101-alternate',
    +            'jisho',
    +            'custom'
    +        ];
    +        for (const source of audioSourcesAvailable) {
    +            if (this._settingsAudioSources.indexOf(source) < 0) {
    +                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;
     
    -function textToSpeechTest() {
    -    try {
    -        const text = document.querySelector('#text-to-speech-voice-test').dataset.speechText || '';
    -        const voiceUri = document.querySelector('#text-to-speech-voice').value;
    +        const entry = {
    +            container,
    +            eventListeners
    +        };
     
    -        const audio = audioSystem.createTextToSpeechAudio(text, voiceUri);
    -        audio.volume = 1.0;
    -        audio.play();
    -    } catch (e) {
    -        // NOP
    +        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);
    +    }
    +
    +    _removeAudioSourceEntry(entry) {
    +        const index = this._audioSourceEntries.indexOf(entry);
    +        if (index < 0) { return; }
    +
    +        const {container, eventListeners} = entry;
    +        if (container.parentNode !== null) {
    +            container.parentNode.removeChild(container);
    +        }
    +        eventListeners.removeAllEventListeners();
    +
    +        this._audioSourceEntries.splice(index, 1);
    +        this._settingsAudioSources.splice(index, 1);
    +
    +        for (let i = index, ii = this._audioSourceEntries.length; i < ii; ++i) {
    +            this._audioSourceEntries[i].index = i;
    +        }
    +    }
    +
    +    _onTextToSpeechVoiceChange(e) {
    +        e.currentTarget.dataset.value = e.currentTarget.value;
    +    }
    +
    +    _onAddAudioSource() {
    +        const audioSource = this._getUnusedAudioSource();
    +        this._settingsAudioSources.push(audioSource);
    +        this._createAudioSourceEntry(audioSource);
    +        this._save();
         }
    -}
     
    -function onTextToSpeechVoiceChange(e) {
    -    e.currentTarget.dataset.value = e.currentTarget.value;
    +    _onAudioSourceSelectChange(entry, event) {
    +        const index = this._audioSourceEntries.indexOf(entry);
    +        if (index < 0) { return; }
    +
    +        const value = event.currentTarget.value;
    +        this._settingsAudioSources[index] = value;
    +        this._save();
    +    }
    +
    +    _onAudioSourceRemoveClicked(entry) {
    +        this._removeAudioSourceEntry(entry);
    +        this._save();
    +    }
     }
    diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
    index 872f8f05..dddaef6c 100644
    --- a/ext/bg/js/settings/main.js
    +++ b/ext/bg/js/settings/main.js
    @@ -18,12 +18,12 @@
     /* global
      * AnkiController
      * AnkiTemplatesController
    + * AudioController
      * ProfileController
      * SettingsBackup
      * SettingsController
      * api
      * appearanceInitialize
    - * audioSettingsInitialize
      * dictSettingsInitialize
      * onDictionaryOptionsChanged
      * storageInfoInitialize
    @@ -319,7 +319,7 @@ async function onReady() {
         await settingsPopulateModifierKeys();
         formSetupEventListeners();
         appearanceInitialize();
    -    await audioSettingsInitialize();
    +    new AudioController().prepare();
         await (new ProfileController()).prepare();
         await dictSettingsInitialize();
         ankiController = new AnkiController();
    diff --git a/ext/bg/settings.html b/ext/bg/settings.html
    index c8f5b15c..5c7fde41 100644
    --- a/ext/bg/settings.html
    +++ b/ext/bg/settings.html
    @@ -1141,7 +1141,6 @@
             
             
             
    -        
             
             
             
    -- 
    cgit v1.2.3
    
    
    From 418e8a57bf7ea1def3e7b83270742d466e98e8cf Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Fri, 29 May 2020 20:25:22 -0400
    Subject: Convert dictionaries.js and storage.js to classes (#570)
    
    * Convert dictionaries.js to a class
    
    * Remove storage spinner
    
    * Convert storage.js to a class
    
    * Move dataset assignments into main.js
    ---
     ext/bg/css/settings.css            |   2 +-
     ext/bg/js/settings/dictionaries.js | 544 ++++++++++++++++++-------------------
     ext/bg/js/settings/main.js         |  25 +-
     ext/bg/js/settings/storage.js      | 199 +++++++-------
     ext/bg/settings.html               |   1 -
     5 files changed, 384 insertions(+), 387 deletions(-)
    
    (limited to 'ext/bg/js')
    
    diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css
    index f55082e7..eb11d77e 100644
    --- a/ext/bg/css/settings.css
    +++ b/ext/bg/css/settings.css
    @@ -18,7 +18,7 @@
     
     #anki-spinner,
     #dict-spinner, #dict-import-progress,
    -.storage-hidden, #storage-spinner {
    +.storage-hidden {
         display: none;
     }
     
    diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js
    index 4d307f0f..dd6dd1c1 100644
    --- a/ext/bg/js/settings/dictionaries.js
    +++ b/ext/bg/js/settings/dictionaries.js
    @@ -22,14 +22,9 @@
      * getOptionsFullMutable
      * getOptionsMutable
      * settingsSaveOptions
    - * storageEstimate
    - * storageUpdateStats
      * utilBackgroundIsolate
      */
     
    -let dictionaryUI = null;
    -
    -
     class SettingsDictionaryListUI {
         constructor(container, template, extraContainer, extraTemplate) {
             this.container = container;
    @@ -308,13 +303,13 @@ class SettingsDictionaryEntryUI {
     
                 await api.deleteDictionary(this.dictionaryInfo.title, onProgress);
             } catch (e) {
    -            dictionaryErrorsShow([e]);
    +            this.dictionaryErrorsShow([e]);
             } finally {
                 prevention.end();
                 this.isDeleting = false;
                 progress.hidden = true;
     
    -            onDatabaseUpdated();
    +            this.onDatabaseUpdated();
             }
         }
     
    @@ -388,340 +383,341 @@ class SettingsDictionaryExtraUI {
         }
     }
     
    +class DictionaryController {
    +    constructor(storageController) {
    +        this._storageController = storageController;
    +        this._dictionaryUI = null;
    +        this._dictionaryErrorToStringOverrides = [
    +            [
    +                'A mutation operation was attempted on a database that did not allow mutations.',
    +                'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.'
    +            ],
    +            [
    +                'The operation failed for reasons unrelated to the database itself and not covered by any other error code.',
    +                'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.'
    +            ],
    +            [
    +                'BulkError',
    +                'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.'
    +            ]
    +        ];
    +    }
     
    -async function dictSettingsInitialize() {
    -    dictionaryUI = new SettingsDictionaryListUI(
    -        document.querySelector('#dict-groups'),
    -        document.querySelector('#dict-template'),
    -        document.querySelector('#dict-groups-extra'),
    -        document.querySelector('#dict-extra-template')
    -    );
    -    dictionaryUI.save = settingsSaveOptions;
    -
    -    document.querySelector('#dict-purge-button').addEventListener('click', onDictionaryPurgeButtonClick, false);
    -    document.querySelector('#dict-purge-confirm').addEventListener('click', onDictionaryPurge, false);
    -    document.querySelector('#dict-file-button').addEventListener('click', onDictionaryImportButtonClick, false);
    -    document.querySelector('#dict-file').addEventListener('change', onDictionaryImport, false);
    -    document.querySelector('#dict-main').addEventListener('change', onDictionaryMainChanged, false);
    -    document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', onDatabaseEnablePrefixWildcardSearchesChanged, false);
    -
    -    await onDictionaryOptionsChanged();
    -    await onDatabaseUpdated();
    -}
    -
    -async function onDictionaryOptionsChanged() {
    -    if (dictionaryUI === null) { return; }
    -
    -    const optionsContext = getOptionsContext();
    -    const options = await getOptionsMutable(optionsContext);
    +    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 = settingsSaveOptions;
     
    -    dictionaryUI.setOptionsDictionaries(options.dictionaries);
    +        document.querySelector('#dict-purge-button').addEventListener('click', this._onPurgeButtonClick.bind(this), false);
    +        document.querySelector('#dict-purge-confirm').addEventListener('click', this._onPurgeConfirmButtonClick.bind(this), false);
    +        document.querySelector('#dict-file-button').addEventListener('click', this._onImportButtonClick.bind(this), false);
    +        document.querySelector('#dict-file').addEventListener('change', this._onImportFileChange.bind(this), false);
    +        document.querySelector('#dict-main').addEventListener('change', this._onDictionaryMainChanged.bind(this), false);
    +        document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', this._onDatabaseEnablePrefixWildcardSearchesChanged.bind(this), false);
     
    -    const optionsFull = await api.optionsGetFull();
    -    document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported;
    +        await this.optionsChanged();
    +        await this._onDatabaseUpdated();
    +    }
     
    -    await updateMainDictionarySelectValue();
    -}
    +    async optionsChanged() {
    +        if (this._dictionaryUI === null) { return; }
     
    -async function onDatabaseUpdated() {
    -    try {
    -        const dictionaries = await api.getDictionaryInfo();
    -        dictionaryUI.setDictionaries(dictionaries);
    +        const optionsContext = getOptionsContext();
    +        const options = await getOptionsMutable(optionsContext);
     
    -        document.querySelector('#dict-warning').hidden = (dictionaries.length > 0);
    +        this._dictionaryUI.setOptionsDictionaries(options.dictionaries);
     
    -        updateMainDictionarySelectOptions(dictionaries);
    -        await updateMainDictionarySelectValue();
    +        const optionsFull = await api.optionsGetFull();
    +        document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported;
     
    -        const {counts, total} = await api.getDictionaryCounts(dictionaries.map((v) => v.title), true);
    -        dictionaryUI.setCounts(counts, total);
    -    } catch (e) {
    -        dictionaryErrorsShow([e]);
    +        await this._updateMainDictionarySelectValue();
         }
    -}
    -
    -function updateMainDictionarySelectOptions(dictionaries) {
    -    const select = document.querySelector('#dict-main');
    -    select.textContent = ''; // Empty
     
    -    let option = document.createElement('option');
    -    option.className = 'text-muted';
    -    option.value = '';
    -    option.textContent = 'Not selected';
    -    select.appendChild(option);
    +    // Private
     
    -    for (const {title, sequenced} of toIterable(dictionaries)) {
    -        if (!sequenced) { continue; }
    +    _updateMainDictionarySelectOptions(dictionaries) {
    +        const select = document.querySelector('#dict-main');
    +        select.textContent = ''; // Empty
     
    -        option = document.createElement('option');
    -        option.value = title;
    -        option.textContent = title;
    +        let option = document.createElement('option');
    +        option.className = 'text-muted';
    +        option.value = '';
    +        option.textContent = 'Not selected';
             select.appendChild(option);
    -    }
    -}
    -
    -async function updateMainDictionarySelectValue() {
    -    const optionsContext = getOptionsContext();
    -    const options = await api.optionsGet(optionsContext);
     
    -    const value = options.general.mainDictionary;
    +        for (const {title, sequenced} of toIterable(dictionaries)) {
    +            if (!sequenced) { continue; }
     
    -    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;
    +            option = document.createElement('option');
    +            option.value = title;
    +            option.textContent = title;
    +            select.appendChild(option);
             }
         }
     
    -    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);
    +    async _updateMainDictionarySelectValue() {
    +        const optionsContext = getOptionsContext();
    +        const options = await api.optionsGet(optionsContext);
    +
    +        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;
    +            }
             }
    -    } else {
    -        if (missingNodeOption !== null) {
    -            missingNodeOption.parentNode.removeChild(missingNodeOption);
    +
    +        let missingNodeOption = select.querySelector('option[data-not-installed=true]');
    +        if (selectValue === null) {
    +            if (missingNodeOption === null) {
    +                missingNodeOption = document.createElement('option');
    +                missingNodeOption.className = 'text-muted';
    +                missingNodeOption.value = value;
    +                missingNodeOption.textContent = `${value} (Not installed)`;
    +                missingNodeOption.dataset.notInstalled = 'true';
    +                select.appendChild(missingNodeOption);
    +            }
    +        } else {
    +            if (missingNodeOption !== null) {
    +                missingNodeOption.parentNode.removeChild(missingNodeOption);
    +            }
             }
    +
    +        select.value = value;
         }
     
    -    select.value = value;
    -}
    +    _dictionaryErrorToString(error) {
    +        if (error.toString) {
    +            error = error.toString();
    +        } else {
    +            error = `${error}`;
    +        }
     
    -async function onDictionaryMainChanged(e) {
    -    const select = e.target;
    -    const value = select.value;
    +        for (const [match, subst] of this._dictionaryErrorToStringOverrides) {
    +            if (error.includes(match)) {
    +                error = subst;
    +                break;
    +            }
    +        }
     
    -    const missingNodeOption = select.querySelector('option[data-not-installed=true]');
    -    if (missingNodeOption !== null && missingNodeOption.value !== value) {
    -        missingNodeOption.parentNode.removeChild(missingNodeOption);
    +        return error;
         }
     
    -    const optionsContext = getOptionsContext();
    -    const options = await getOptionsMutable(optionsContext);
    -    options.general.mainDictionary = value;
    -    await settingsSaveOptions();
    -}
    +    _dictionaryErrorsShow(errors) {
    +        const dialog = document.querySelector('#dict-error');
    +        dialog.textContent = '';
     
    +        if (errors !== null && errors.length > 0) {
    +            const uniqueErrors = new Map();
    +            for (let e of errors) {
    +                yomichan.logError(e);
    +                e = this._dictionaryErrorToString(e);
    +                let count = uniqueErrors.get(e);
    +                if (typeof count === 'undefined') {
    +                    count = 0;
    +                }
    +                uniqueErrors.set(e, count + 1);
    +            }
     
    -function dictionaryErrorToString(error) {
    -    if (error.toString) {
    -        error = error.toString();
    -    } else {
    -        error = `${error}`;
    -    }
    +            for (const [e, count] of uniqueErrors.entries()) {
    +                const div = document.createElement('p');
    +                if (count > 1) {
    +                    div.textContent = `${e} `;
    +                    const em = document.createElement('em');
    +                    em.textContent = `(${count})`;
    +                    div.appendChild(em);
    +                } else {
    +                    div.textContent = `${e}`;
    +                }
    +                dialog.appendChild(div);
    +            }
     
    -    for (const [match, subst] of dictionaryErrorToString.overrides) {
    -        if (error.includes(match)) {
    -            error = subst;
    -            break;
    +            dialog.hidden = false;
    +        } else {
    +            dialog.hidden = true;
             }
         }
     
    -    return error;
    -}
    -dictionaryErrorToString.overrides = [
    -    [
    -        'A mutation operation was attempted on a database that did not allow mutations.',
    -        'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.'
    -    ],
    -    [
    -        'The operation failed for reasons unrelated to the database itself and not covered by any other error code.',
    -        'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.'
    -    ],
    -    [
    -        'BulkError',
    -        'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.'
    -    ]
    -];
    -
    -function dictionaryErrorsShow(errors) {
    -    const dialog = document.querySelector('#dict-error');
    -    dialog.textContent = '';
    -
    -    if (errors !== null && errors.length > 0) {
    -        const uniqueErrors = new Map();
    -        for (let e of errors) {
    -            yomichan.logError(e);
    -            e = dictionaryErrorToString(e);
    -            let count = uniqueErrors.get(e);
    -            if (typeof count === 'undefined') {
    -                count = 0;
    -            }
    -            uniqueErrors.set(e, count + 1);
    -        }
    -
    -        for (const [e, count] of uniqueErrors.entries()) {
    -            const div = document.createElement('p');
    -            if (count > 1) {
    -                div.textContent = `${e} `;
    -                const em = document.createElement('em');
    -                em.textContent = `(${count})`;
    -                div.appendChild(em);
    -            } else {
    -                div.textContent = `${e}`;
    -            }
    -            dialog.appendChild(div);
    +    _dictionarySpinnerShow(show) {
    +        const spinner = $('#dict-spinner');
    +        if (show) {
    +            spinner.show();
    +        } else {
    +            spinner.hide();
             }
    +    }
     
    -        dialog.hidden = false;
    -    } else {
    -        dialog.hidden = true;
    +    _dictReadFile(file) {
    +        return new Promise((resolve, reject) => {
    +            const reader = new FileReader();
    +            reader.onload = () => resolve(reader.result);
    +            reader.onerror = () => reject(reader.error);
    +            reader.readAsBinaryString(file);
    +        });
    +    }
    +
    +    async _onDatabaseUpdated() {
    +        try {
    +            const dictionaries = await api.getDictionaryInfo();
    +            this._dictionaryUI.setDictionaries(dictionaries);
    +
    +            document.querySelector('#dict-warning').hidden = (dictionaries.length > 0);
    +
    +            this._updateMainDictionarySelectOptions(dictionaries);
    +            await this._updateMainDictionarySelectValue();
    +
    +            const {counts, total} = await api.getDictionaryCounts(dictionaries.map((v) => v.title), true);
    +            this._dictionaryUI.setCounts(counts, total);
    +        } catch (e) {
    +            this._dictionaryErrorsShow([e]);
    +        }
         }
    -}
     
    +    async _onDictionaryMainChanged(e) {
    +        const select = e.target;
    +        const value = select.value;
    +
    +        const missingNodeOption = select.querySelector('option[data-not-installed=true]');
    +        if (missingNodeOption !== null && missingNodeOption.value !== value) {
    +            missingNodeOption.parentNode.removeChild(missingNodeOption);
    +        }
     
    -function dictionarySpinnerShow(show) {
    -    const spinner = $('#dict-spinner');
    -    if (show) {
    -        spinner.show();
    -    } else {
    -        spinner.hide();
    +        const optionsContext = getOptionsContext();
    +        const options = await getOptionsMutable(optionsContext);
    +        options.general.mainDictionary = value;
    +        await settingsSaveOptions();
         }
    -}
     
    -function onDictionaryImportButtonClick() {
    -    const dictFile = document.querySelector('#dict-file');
    -    dictFile.click();
    -}
    +    _onImportButtonClick() {
    +        const dictFile = document.querySelector('#dict-file');
    +        dictFile.click();
    +    }
     
    -function onDictionaryPurgeButtonClick(e) {
    -    e.preventDefault();
    -    $('#dict-purge-modal').modal('show');
    -}
    +    _onPurgeButtonClick(e) {
    +        e.preventDefault();
    +        $('#dict-purge-modal').modal('show');
    +    }
     
    -async function onDictionaryPurge(e) {
    -    e.preventDefault();
    +    async _onPurgeConfirmButtonClick(e) {
    +        e.preventDefault();
     
    -    $('#dict-purge-modal').modal('hide');
    +        $('#dict-purge-modal').modal('hide');
     
    -    const dictControls = $('#dict-importer, #dict-groups, #dict-groups-extra, #dict-main-group').hide();
    -    const dictProgress = document.querySelector('#dict-purge');
    -    dictProgress.hidden = false;
    +        const dictControls = $('#dict-importer, #dict-groups, #dict-groups-extra, #dict-main-group').hide();
    +        const dictProgress = document.querySelector('#dict-purge');
    +        dictProgress.hidden = false;
     
    -    const prevention = new PageExitPrevention();
    +        const prevention = new PageExitPrevention();
     
    -    try {
    -        prevention.start();
    -        dictionaryErrorsShow(null);
    -        dictionarySpinnerShow(true);
    +        try {
    +            prevention.start();
    +            this._dictionaryErrorsShow(null);
    +            this._dictionarySpinnerShow(true);
     
    -        await api.purgeDatabase();
    -        for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
    -            options.dictionaries = utilBackgroundIsolate({});
    -            options.general.mainDictionary = '';
    -        }
    -        await settingsSaveOptions();
    +            await api.purgeDatabase();
    +            for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
    +                options.dictionaries = utilBackgroundIsolate({});
    +                options.general.mainDictionary = '';
    +            }
    +            await settingsSaveOptions();
     
    -        onDatabaseUpdated();
    -    } catch (err) {
    -        dictionaryErrorsShow([err]);
    -    } finally {
    -        prevention.end();
    +            this._onDatabaseUpdated();
    +        } catch (err) {
    +            this._dictionaryErrorsShow([err]);
    +        } finally {
    +            prevention.end();
     
    -        dictionarySpinnerShow(false);
    +            this._dictionarySpinnerShow(false);
     
    -        dictControls.show();
    -        dictProgress.hidden = true;
    +            dictControls.show();
    +            dictProgress.hidden = true;
     
    -        if (storageEstimate.mostRecent !== null) {
    -            storageUpdateStats();
    +            this._storageController.updateStats();
             }
         }
    -}
     
    -async function onDictionaryImport(e) {
    -    const files = [...e.target.files];
    -    e.target.value = null;
    +    async _onImportFileChange(e) {
    +        const files = [...e.target.files];
    +        e.target.value = null;
     
    -    const dictFile = $('#dict-file');
    -    const dictControls = $('#dict-importer').hide();
    -    const dictProgress = $('#dict-import-progress').show();
    -    const dictImportInfo = document.querySelector('#dict-import-info');
    +        const dictFile = $('#dict-file');
    +        const dictControls = $('#dict-importer').hide();
    +        const dictProgress = $('#dict-import-progress').show();
    +        const dictImportInfo = document.querySelector('#dict-import-info');
     
    -    const prevention = new PageExitPrevention();
    +        const prevention = new PageExitPrevention();
     
    -    try {
    -        prevention.start();
    -        dictionaryErrorsShow(null);
    -        dictionarySpinnerShow(true);
    +        try {
    +            prevention.start();
    +            this._dictionaryErrorsShow(null);
    +            this._dictionarySpinnerShow(true);
     
    -        const setProgress = (percent) => dictProgress.find('.progress-bar').css('width', `${percent}%`);
    -        const updateProgress = (total, current) => {
    -            setProgress(current / total * 100.0);
    -            if (storageEstimate.mostRecent !== null && !storageUpdateStats.isUpdating) {
    -                storageUpdateStats();
    -            }
    -        };
    +            const setProgress = (percent) => dictProgress.find('.progress-bar').css('width', `${percent}%`);
    +            const updateProgress = (total, current) => {
    +                setProgress(current / total * 100.0);
    +                this._storageController.updateStats();
    +            };
     
    -        const optionsFull = await api.optionsGetFull();
    +            const optionsFull = await api.optionsGetFull();
     
    -        const importDetails = {
    -            prefixWildcardsSupported: optionsFull.global.database.prefixWildcardsSupported
    -        };
    +            const importDetails = {
    +                prefixWildcardsSupported: optionsFull.global.database.prefixWildcardsSupported
    +            };
     
    -        for (let i = 0, ii = files.length; i < ii; ++i) {
    -            setProgress(0.0);
    -            if (ii > 1) {
    -                dictImportInfo.hidden = false;
    -                dictImportInfo.textContent = `(${i + 1} of ${ii})`;
    -            }
    +            for (let i = 0, ii = files.length; i < ii; ++i) {
    +                setProgress(0.0);
    +                if (ii > 1) {
    +                    dictImportInfo.hidden = false;
    +                    dictImportInfo.textContent = `(${i + 1} of ${ii})`;
    +                }
     
    -            const archiveContent = await dictReadFile(files[i]);
    -            const {result, errors} = await api.importDictionaryArchive(archiveContent, importDetails, updateProgress);
    -            for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
    -                const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions();
    -                dictionaryOptions.enabled = true;
    -                options.dictionaries[result.title] = dictionaryOptions;
    -                if (result.sequenced && options.general.mainDictionary === '') {
    -                    options.general.mainDictionary = result.title;
    +                const archiveContent = await this._dictReadFile(files[i]);
    +                const {result, errors} = await api.importDictionaryArchive(archiveContent, importDetails, updateProgress);
    +                for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
    +                    const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions();
    +                    dictionaryOptions.enabled = true;
    +                    options.dictionaries[result.title] = dictionaryOptions;
    +                    if (result.sequenced && options.general.mainDictionary === '') {
    +                        options.general.mainDictionary = result.title;
    +                    }
                     }
    -            }
     
    -            await settingsSaveOptions();
    +                await settingsSaveOptions();
    +
    +                if (errors.length > 0) {
    +                    const errors2 = errors.map((error) => jsonToError(error));
    +                    errors2.push(`Dictionary may not have been imported properly: ${errors2.length} error${errors2.length === 1 ? '' : 's'} reported.`);
    +                    this._dictionaryErrorsShow(errors2);
    +                }
     
    -            if (errors.length > 0) {
    -                const errors2 = errors.map((error) => jsonToError(error));
    -                errors2.push(`Dictionary may not have been imported properly: ${errors2.length} error${errors2.length === 1 ? '' : 's'} reported.`);
    -                dictionaryErrorsShow(errors2);
    +                this._onDatabaseUpdated();
                 }
    +        } catch (err) {
    +            this._dictionaryErrorsShow([err]);
    +        } finally {
    +            prevention.end();
    +            this._dictionarySpinnerShow(false);
     
    -            onDatabaseUpdated();
    +            dictImportInfo.hidden = false;
    +            dictImportInfo.textContent = '';
    +            dictFile.val('');
    +            dictControls.show();
    +            dictProgress.hide();
             }
    -    } catch (err) {
    -        dictionaryErrorsShow([err]);
    -    } finally {
    -        prevention.end();
    -        dictionarySpinnerShow(false);
    -
    -        dictImportInfo.hidden = false;
    -        dictImportInfo.textContent = '';
    -        dictFile.val('');
    -        dictControls.show();
    -        dictProgress.hide();
         }
    -}
    -
    -function dictReadFile(file) {
    -    return new Promise((resolve, reject) => {
    -        const reader = new FileReader();
    -        reader.onload = () => resolve(reader.result);
    -        reader.onerror = () => reject(reader.error);
    -        reader.readAsBinaryString(file);
    -    });
    -}
     
    -
    -async function onDatabaseEnablePrefixWildcardSearchesChanged(e) {
    -    const optionsFull = await getOptionsFullMutable();
    -    const v = !!e.target.checked;
    -    if (optionsFull.global.database.prefixWildcardsSupported === v) { return; }
    -    optionsFull.global.database.prefixWildcardsSupported = !!e.target.checked;
    -    await settingsSaveOptions();
    +    async _onDatabaseEnablePrefixWildcardSearchesChanged(e) {
    +        const optionsFull = await getOptionsFullMutable();
    +        const v = !!e.target.checked;
    +        if (optionsFull.global.database.prefixWildcardsSupported === v) { return; }
    +        optionsFull.global.database.prefixWildcardsSupported = !!e.target.checked;
    +        await settingsSaveOptions();
    +    }
     }
    diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
    index dddaef6c..1d387749 100644
    --- a/ext/bg/js/settings/main.js
    +++ b/ext/bg/js/settings/main.js
    @@ -19,14 +19,13 @@
      * AnkiController
      * AnkiTemplatesController
      * AudioController
    + * DictionaryController
      * ProfileController
      * SettingsBackup
      * SettingsController
    + * StorageController
      * api
      * appearanceInitialize
    - * dictSettingsInitialize
    - * onDictionaryOptionsChanged
    - * storageInfoInitialize
      * utilBackend
      * utilBackgroundIsolate
      */
    @@ -270,7 +269,9 @@ async function onOptionsUpdated({source}) {
         if (ankiTemplatesController !== null) {
             ankiTemplatesController.updateValue();
         }
    -    onDictionaryOptionsChanged();
    +    if (dictionaryController !== null) {
    +        dictionaryController.optionsChanged();
    +    }
         if (ankiController !== null) {
             ankiController.optionsChanged();
         }
    @@ -304,8 +305,15 @@ async function settingsPopulateModifierKeys() {
         }
     }
     
    +async function setupEnvironmentInfo() {
    +    const {browser, platform} = await api.getEnvironmentInfo();
    +    document.documentElement.dataset.browser = browser;
    +    document.documentElement.dataset.operatingSystem = platform.os;
    +}
    +
     let ankiController = null;
     let ankiTemplatesController = null;
    +let dictionaryController = null;
     
     async function onReady() {
         api.forwardLogsToBackend();
    @@ -314,22 +322,25 @@ async function onReady() {
         const settingsController = new SettingsController();
         settingsController.prepare();
     
    +    setupEnvironmentInfo();
         showExtensionInformation();
     
    +    const storageController = new StorageController();
    +    storageController.prepare();
    +
         await settingsPopulateModifierKeys();
         formSetupEventListeners();
         appearanceInitialize();
         new AudioController().prepare();
         await (new ProfileController()).prepare();
    -    await dictSettingsInitialize();
    +    dictionaryController = new DictionaryController(storageController);
    +    dictionaryController.prepare();
         ankiController = new AnkiController();
         ankiController.prepare();
         ankiTemplatesController = new AnkiTemplatesController(ankiController);
         ankiTemplatesController.prepare();
         new SettingsBackup().prepare();
     
    -    storageInfoInitialize();
    -
         yomichan.on('optionsUpdated', onOptionsUpdated);
     }
     
    diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js
    index 73c93fa1..24c6d7ef 100644
    --- a/ext/bg/js/settings/storage.js
    +++ b/ext/bg/js/settings/storage.js
    @@ -15,126 +15,117 @@
      * along with this program.  If not, see .
      */
     
    -/* global
    - * api
    - */
    -
    -function storageBytesToLabeledString(size) {
    -    const base = 1000;
    -    const labels = [' bytes', 'KB', 'MB', 'GB'];
    -    let labelIndex = 0;
    -    while (size >= base) {
    -        size /= base;
    -        ++labelIndex;
    +class StorageController {
    +    constructor() {
    +        this._mostRecentStorageEstimate = null;
    +        this._storageEstimateFailed = false;
    +        this._isUpdating = false;
         }
    -    const label = labelIndex === 0 ? `${size}` : size.toFixed(1);
    -    return `${label}${labels[labelIndex]}`;
    -}
     
    -async function storageEstimate() {
    -    try {
    -        return (storageEstimate.mostRecent = await navigator.storage.estimate());
    -    } catch (e) {
    -        // NOP
    +    prepare() {
    +        this._preparePersistentStorage();
    +        this.updateStats();
    +        document.querySelector('#storage-refresh').addEventListener('click', this.updateStats.bind(this), false);
         }
    -    return null;
    -}
    -storageEstimate.mostRecent = null;
    -
    -async function isStoragePeristent() {
    -    try {
    -        return await navigator.storage.persisted();
    -    } catch (e) {
    -        // NOP
    -    }
    -    return false;
    -}
    -
    -async function storageInfoInitialize() {
    -    storagePersistInitialize();
    -    const {browser, platform} = await api.getEnvironmentInfo();
    -    document.documentElement.dataset.browser = browser;
    -    document.documentElement.dataset.operatingSystem = platform.os;
    -
    -    await storageShowInfo();
     
    -    document.querySelector('#storage-refresh').addEventListener('click', storageShowInfo, false);
    -}
    -
    -async function storageUpdateStats() {
    -    storageUpdateStats.isUpdating = true;
    -
    -    const estimate = await storageEstimate();
    -    const valid = (estimate !== null);
    -
    -    if (valid) {
    -        // Firefox reports usage as 0 when persistent storage is enabled.
    -        const finite = (estimate.usage > 0 || !(await isStoragePeristent()));
    -        if (finite) {
    -            document.querySelector('#storage-usage').textContent = storageBytesToLabeledString(estimate.usage);
    -            document.querySelector('#storage-quota').textContent = storageBytesToLabeledString(estimate.quota);
    +    async updateStats() {
    +        try {
    +            this._isUpdating = true;
    +
    +            const estimate = await this._storageEstimate();
    +            const valid = (estimate !== null);
    +
    +            if (valid) {
    +                // Firefox reports usage as 0 when persistent storage is enabled.
    +                const finite = (estimate.usage > 0 || !(await this._isStoragePeristent()));
    +                if (finite) {
    +                    document.querySelector('#storage-usage').textContent = this._bytesToLabeledString(estimate.usage);
    +                    document.querySelector('#storage-quota').textContent = this._bytesToLabeledString(estimate.quota);
    +                }
    +                document.querySelector('#storage-use-finite').classList.toggle('storage-hidden', !finite);
    +                document.querySelector('#storage-use-infinite').classList.toggle('storage-hidden', finite);
    +            }
    +
    +            document.querySelector('#storage-use').classList.toggle('storage-hidden', !valid);
    +            document.querySelector('#storage-error').classList.toggle('storage-hidden', valid);
    +
    +            return valid;
    +        } finally {
    +            this._isUpdating = false;
             }
    -        document.querySelector('#storage-use-finite').classList.toggle('storage-hidden', !finite);
    -        document.querySelector('#storage-use-infinite').classList.toggle('storage-hidden', finite);
         }
     
    -    storageUpdateStats.isUpdating = false;
    -    return valid;
    -}
    -storageUpdateStats.isUpdating = false;
    -
    -async function storageShowInfo() {
    -    storageSpinnerShow(true);
    -
    -    const valid = await storageUpdateStats();
    -    document.querySelector('#storage-use').classList.toggle('storage-hidden', !valid);
    -    document.querySelector('#storage-error').classList.toggle('storage-hidden', valid);
    +    // Private
     
    -    storageSpinnerShow(false);
    -}
    +    async _preparePersistentStorage() {
    +        if (!(navigator.storage && navigator.storage.persist)) {
    +            // Not supported
    +            return;
    +        }
     
    -function storageSpinnerShow(show) {
    -    const spinner = $('#storage-spinner');
    -    if (show) {
    -        spinner.show();
    -    } else {
    -        spinner.hide();
    +        const info = document.querySelector('#storage-persist-info');
    +        const button = document.querySelector('#storage-persist-button');
    +        const checkbox = document.querySelector('#storage-persist-button-checkbox');
    +
    +        info.classList.remove('storage-hidden');
    +        button.classList.remove('storage-hidden');
    +
    +        let persisted = await this._isStoragePeristent();
    +        checkbox.checked = persisted;
    +
    +        button.addEventListener('click', async () => {
    +            if (persisted) {
    +                return;
    +            }
    +            let result = false;
    +            try {
    +                result = await navigator.storage.persist();
    +            } catch (e) {
    +                // NOP
    +            }
    +
    +            if (result) {
    +                persisted = true;
    +                checkbox.checked = true;
    +                this.updateStats();
    +            } else {
    +                document.querySelector('.storage-persist-fail-warning').classList.remove('storage-hidden');
    +            }
    +        }, false);
         }
    -}
     
    -async function storagePersistInitialize() {
    -    if (!(navigator.storage && navigator.storage.persist)) {
    -        // Not supported
    -        return;
    +    async _storageEstimate() {
    +        if (this._storageEstimateFailed && this._mostRecentStorageEstimate === null) {
    +            return null;
    +        }
    +        try {
    +            const value = await navigator.storage.estimate();
    +            this._mostRecentStorageEstimate = value;
    +            return value;
    +        } catch (e) {
    +            this._storageEstimateFailed = true;
    +        }
    +        return null;
         }
     
    -    const info = document.querySelector('#storage-persist-info');
    -    const button = document.querySelector('#storage-persist-button');
    -    const checkbox = document.querySelector('#storage-persist-button-checkbox');
    -
    -    info.classList.remove('storage-hidden');
    -    button.classList.remove('storage-hidden');
    -
    -    let persisted = await isStoragePeristent();
    -    checkbox.checked = persisted;
    -
    -    button.addEventListener('click', async () => {
    -        if (persisted) {
    -            return;
    +    _bytesToLabeledString(size) {
    +        const base = 1000;
    +        const labels = [' bytes', 'KB', 'MB', 'GB'];
    +        let labelIndex = 0;
    +        while (size >= base) {
    +            size /= base;
    +            ++labelIndex;
             }
    -        let result = false;
    +        const label = labelIndex === 0 ? `${size}` : size.toFixed(1);
    +        return `${label}${labels[labelIndex]}`;
    +    }
    +
    +    async _isStoragePeristent() {
             try {
    -            result = await navigator.storage.persist();
    +            return await navigator.storage.persisted();
             } catch (e) {
                 // NOP
             }
    -
    -        if (result) {
    -            persisted = true;
    -            checkbox.checked = true;
    -            storageShowInfo();
    -        } else {
    -            $('.storage-persist-fail-warning').removeClass('storage-hidden');
    -        }
    -    }, false);
    +        return false;
    +    }
     }
    diff --git a/ext/bg/settings.html b/ext/bg/settings.html
    index 5c7fde41..4856b0b4 100644
    --- a/ext/bg/settings.html
    +++ b/ext/bg/settings.html
    @@ -711,7 +711,6 @@
     
                 
    -

    Storage

    -- cgit v1.2.3 From 9624566d2af853141ea5c6c781a019eeea212066 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 29 May 2020 20:28:12 -0400 Subject: Convert popup-preview.js to a class (#571) * Convert popup-preview.js to a class * Don't invoke 'prepare' --- ext/bg/js/settings/main.js | 4 +- ext/bg/js/settings/popup-preview.js | 138 ++++++++++++++++++++---------------- 2 files changed, 80 insertions(+), 62 deletions(-) (limited to 'ext/bg/js') diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index 1d387749..b84824e6 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -20,12 +20,12 @@ * AnkiTemplatesController * AudioController * DictionaryController + * PopupPreviewController * ProfileController * SettingsBackup * SettingsController * StorageController * api - * appearanceInitialize * utilBackend * utilBackgroundIsolate */ @@ -330,7 +330,7 @@ async function onReady() { await settingsPopulateModifierKeys(); formSetupEventListeners(); - appearanceInitialize(); + new PopupPreviewController().prepare(); new AudioController().prepare(); await (new ProfileController()).prepare(); dictionaryController = new DictionaryController(storageController); diff --git a/ext/bg/js/settings/popup-preview.js b/ext/bg/js/settings/popup-preview.js index fdc3dd94..d5519959 100644 --- a/ext/bg/js/settings/popup-preview.js +++ b/ext/bg/js/settings/popup-preview.js @@ -20,65 +20,83 @@ * wanakana */ -function appearanceInitialize() { - let previewVisible = false; - $('#settings-popup-preview-button').on('click', () => { - if (previewVisible) { return; } - showAppearancePreview(); - previewVisible = true; - }); -} +class PopupPreviewController { + constructor() { + 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); + yomichan.on('modifyingProfileChange', 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() { + this._invoke('updateOptionsContext', {optionsContext: getOptionsContext()}); + } + + _setText(text) { + this._invoke('setText', {text}); + } -function showAppearancePreview() { - const container = $('#settings-popup-preview-container'); - const buttonContainer = $('#settings-popup-preview-button-container'); - const settings = $('#settings-popup-preview-settings'); - const text = $('#settings-popup-preview-text'); - const customCss = $('#custom-popup-css'); - const customOuterCss = $('#custom-popup-outer-css'); - - const frame = document.createElement('iframe'); - frame.src = '/bg/settings-popup-preview.html'; - frame.id = 'settings-popup-preview-frame'; - - wanakana.bind(text[0]); - - const targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); - - text.on('input', () => { - const action = 'setText'; - const params = {text: text.val()}; - frame.contentWindow.postMessage({action, params}, targetOrigin); - }); - customCss.on('input', () => { - const action = 'setCustomCss'; - const params = {css: customCss.val()}; - frame.contentWindow.postMessage({action, params}, targetOrigin); - }); - customOuterCss.on('input', () => { - const action = 'setCustomOuterCss'; - const params = {css: customOuterCss.val()}; - frame.contentWindow.postMessage({action, params}, targetOrigin); - }); - - const updateOptionsContext = () => { - const action = 'updateOptionsContext'; - const params = { - optionsContext: getOptionsContext() - }; - frame.contentWindow.postMessage({action, params}, targetOrigin); - }; - yomichan.on('modifyingProfileChange', updateOptionsContext); - - frame.addEventListener('load', () => { - const action = 'prepare'; - const params = { - optionsContext: getOptionsContext() - }; - frame.contentWindow.postMessage({action, params}, targetOrigin); - }); - - container.append(frame); - buttonContainer.remove(); - settings.css('display', ''); + _invoke(action, params) { + if (this._frame === null || this._frame.contentWindow === null) { return; } + this._frame.contentWindow.postMessage({action, params}, this._targetOrigin); + } } -- cgit v1.2.3 From df040b104aa4a1d0ec15daf39fac4f237d598c2e Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 29 May 2020 20:29:19 -0400 Subject: Popup preview frame refactor (#572) * Use private variables * Use private methods * Update overrides * Make prepare called in the entry point function * Change format of handlers * Change where _apiOptionsGetOld is assigned * Rename class --- ext/bg/js/settings/popup-preview-frame-main.js | 7 +- ext/bg/js/settings/popup-preview-frame.js | 132 +++++++++++++------------ 2 files changed, 75 insertions(+), 64 deletions(-) (limited to 'ext/bg/js') diff --git a/ext/bg/js/settings/popup-preview-frame-main.js b/ext/bg/js/settings/popup-preview-frame-main.js index a362efa5..7c4e2eb9 100644 --- a/ext/bg/js/settings/popup-preview-frame-main.js +++ b/ext/bg/js/settings/popup-preview-frame-main.js @@ -16,11 +16,12 @@ */ /* global - * SettingsPopupPreview + * PopupPreviewFrame * api */ -(() => { +(async () => { api.forwardLogsToBackend(); - new SettingsPopupPreview(); + const preview = new PopupPreviewFrame(); + await preview.prepare(); })(); diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js index bd9357e9..28c95efb 100644 --- a/ext/bg/js/settings/popup-preview-frame.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -23,37 +23,36 @@ * api */ -class SettingsPopupPreview { +class PopupPreviewFrame { constructor() { - this.frontend = null; - this.apiOptionsGetOld = api.optionsGet.bind(api); - this.popup = null; - this.popupSetCustomOuterCssOld = null; - this.popupShown = false; - this.themeChangeTimeout = null; - this.textSource = null; - this.optionsContext = null; + this._frontend = null; + this._frontendGetOptionsContextOld = null; + this._apiOptionsGetOld = null; + this._popup = null; + this._popupSetCustomOuterCssOld = null; + this._popupShown = false; + this._themeChangeTimeout = null; + this._textSource = null; + this._optionsContext = null; this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); this._windowMessageHandlers = new Map([ - ['prepare', ({optionsContext}) => this.prepare(optionsContext)], - ['setText', ({text}) => this.setText(text)], - ['setCustomCss', ({css}) => this.setCustomCss(css)], - ['setCustomOuterCss', ({css}) => this.setCustomOuterCss(css)], - ['updateOptionsContext', ({optionsContext}) => this.updateOptionsContext(optionsContext)] + ['setText', this._setText.bind(this)], + ['setCustomCss', this._setCustomCss.bind(this)], + ['setCustomOuterCss', this._setCustomOuterCss.bind(this)], + ['updateOptionsContext', this._updateOptionsContext.bind(this)] ]); - - window.addEventListener('message', this.onMessage.bind(this), false); } - async prepare(optionsContext) { - this.optionsContext = optionsContext; + async prepare() { + window.addEventListener('message', this._onMessage.bind(this), false); // Setup events - document.querySelector('#theme-dark-checkbox').addEventListener('change', this.onThemeDarkCheckboxChanged.bind(this), false); + document.querySelector('#theme-dark-checkbox').addEventListener('change', this._onThemeDarkCheckboxChanged.bind(this), false); // Overwrite API functions - api.optionsGet = this.apiOptionsGet.bind(this); + this._apiOptionsGetOld = api.optionsGet.bind(api); + api.optionsGet = this._apiOptionsGet.bind(this); // Overwrite frontend const {frameId} = await api.frameInformationGet(); @@ -61,24 +60,35 @@ class SettingsPopupPreview { const popupFactory = new PopupFactory(frameId); await popupFactory.prepare(); - this.popup = popupFactory.getOrCreatePopup(); - this.popup.setChildrenSupported(false); + this._popup = popupFactory.getOrCreatePopup(); + this._popup.setChildrenSupported(false); - this.popupSetCustomOuterCssOld = this.popup.setCustomOuterCss; - this.popup.setCustomOuterCss = this.popupSetCustomOuterCss.bind(this); + this._popupSetCustomOuterCssOld = this._popup.setCustomOuterCss.bind(this._popup); + this._popup.setCustomOuterCss = this._popupSetCustomOuterCss.bind(this); - this.frontend = new Frontend(this.popup); - this.frontend.getOptionsContext = async () => this.optionsContext; - await this.frontend.prepare(); - this.frontend.setDisabledOverride(true); - this.frontend.canClearSelection = false; + this._frontend = new Frontend(this._popup); + this._frontendGetOptionsContextOld = this._frontend.getOptionsContext.bind(this._frontend); + this._frontend.getOptionsContext = this._getOptionsContext.bind(this); + await this._frontend.prepare(); + this._frontend.setDisabledOverride(true); + this._frontend.canClearSelection = false; // Update search - this.updateSearch(); + this._updateSearch(); + } + + // Private + + async _getOptionsContext() { + let optionsContext = this._optionsContext; + if (optionsContext === null) { + optionsContext = this._frontendGetOptionsContextOld(); + } + return optionsContext; } - async apiOptionsGet(...args) { - const options = await this.apiOptionsGetOld(...args); + async _apiOptionsGet(...args) { + const options = await this._apiOptionsGetOld(...args); options.general.enable = true; options.general.debugInfo = false; options.general.popupWidth = 400; @@ -93,9 +103,9 @@ class SettingsPopupPreview { return options; } - async popupSetCustomOuterCss(...args) { + async _popupSetCustomOuterCss(...args) { // This simulates the stylesheet priorities when injecting using the web extension API. - const result = await this.popupSetCustomOuterCssOld.call(this.popup, ...args); + const result = await this._popupSetCustomOuterCssOld(...args); const node = document.querySelector('#client-css'); if (node !== null && result !== null) { @@ -105,7 +115,7 @@ class SettingsPopupPreview { return result; } - onMessage(e) { + _onMessage(e) { if (e.origin !== this._targetOrigin) { return; } const {action, params} = e.data; @@ -115,49 +125,49 @@ class SettingsPopupPreview { handler(params); } - onThemeDarkCheckboxChanged(e) { + _onThemeDarkCheckboxChanged(e) { document.documentElement.classList.toggle('dark', e.target.checked); - if (this.themeChangeTimeout !== null) { - clearTimeout(this.themeChangeTimeout); + if (this._themeChangeTimeout !== null) { + clearTimeout(this._themeChangeTimeout); } - this.themeChangeTimeout = setTimeout(() => { - this.themeChangeTimeout = null; - this.popup.updateTheme(); + this._themeChangeTimeout = setTimeout(() => { + this._themeChangeTimeout = null; + this._popup.updateTheme(); }, 300); } - setText(text) { + _setText({text}) { const exampleText = document.querySelector('#example-text'); if (exampleText === null) { return; } exampleText.textContent = text; - this.updateSearch(); + this._updateSearch(); } - setInfoVisible(visible) { + _setInfoVisible(visible) { const node = document.querySelector('.placeholder-info'); if (node === null) { return; } node.classList.toggle('placeholder-info-visible', visible); } - setCustomCss(css) { - if (this.frontend === null) { return; } - this.popup.setCustomCss(css); + _setCustomCss({css}) { + if (this._frontend === null) { return; } + this._popup.setCustomCss(css); } - setCustomOuterCss(css) { - if (this.frontend === null) { return; } - this.popup.setCustomOuterCss(css, false); + _setCustomOuterCss({css}) { + if (this._frontend === null) { return; } + this._popup.setCustomOuterCss(css, false); } - async updateOptionsContext(optionsContext) { - this.optionsContext = optionsContext; - await this.frontend.updateOptions(); - await this.updateSearch(); + async _updateOptionsContext({optionsContext}) { + this._optionsContext = optionsContext; + await this._frontend.updateOptions(); + await this._updateSearch(); } - async updateSearch() { + async _updateSearch() { const exampleText = document.querySelector('#example-text'); if (exampleText === null) { return; } @@ -169,17 +179,17 @@ class SettingsPopupPreview { const source = new TextSourceRange(range, range.toString(), null, null); try { - await this.frontend.setTextSource(source); + await this._frontend.setTextSource(source); } finally { source.cleanup(); } - this.textSource = source; - await this.frontend.showContentCompleted(); + this._textSource = source; + await this._frontend.showContentCompleted(); - if (this.popup.isVisibleSync()) { - this.popupShown = true; + if (this._popup.isVisibleSync()) { + this._popupShown = true; } - this.setInfoVisible(!this.popupShown); + this._setInfoVisible(!this._popupShown); } } -- cgit v1.2.3 From 18f376358c4c8eec1e46fe1d9396861a42559918 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 29 May 2020 20:33:40 -0400 Subject: Generic settings controller + clipboard popups controller (#573) * Create GenericSettingController * Create ClipboardPopupsController --- ext/bg/js/settings/clipboard-popups-controller.js | 52 ++++++ ext/bg/js/settings/generic-setting-controller.js | 197 +++++++++++++++++++++ ext/bg/js/settings/main.js | 202 +--------------------- ext/bg/settings.html | 2 + 4 files changed, 260 insertions(+), 193 deletions(-) create mode 100644 ext/bg/js/settings/clipboard-popups-controller.js create mode 100644 ext/bg/js/settings/generic-setting-controller.js (limited to 'ext/bg/js') diff --git a/ext/bg/js/settings/clipboard-popups-controller.js b/ext/bg/js/settings/clipboard-popups-controller.js new file mode 100644 index 00000000..cb9e857f --- /dev/null +++ b/ext/bg/js/settings/clipboard-popups-controller.js @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* globals + * getOptionsContext + * getOptionsMutable + * settingsSaveOptions + */ + +class ClipboardPopupsController { + prepare() { + document.querySelector('#enable-clipboard-popups').addEventListener('change', this._onEnableClipboardPopupsChanged.bind(this), false); + } + + async _onEnableClipboardPopupsChanged(e) { + const optionsContext = getOptionsContext(); + const options = await getOptionsMutable(optionsContext); + + const enableClipboardPopups = e.target.checked; + if (enableClipboardPopups) { + options.general.enableClipboardPopups = await new Promise((resolve) => { + chrome.permissions.request( + {permissions: ['clipboardRead']}, + (granted) => { + if (!granted) { + $('#enable-clipboard-popups').prop('checked', false); + } + resolve(granted); + } + ); + }); + } else { + options.general.enableClipboardPopups = false; + } + + await settingsSaveOptions(); + } +} diff --git a/ext/bg/js/settings/generic-setting-controller.js b/ext/bg/js/settings/generic-setting-controller.js new file mode 100644 index 00000000..4a20bf65 --- /dev/null +++ b/ext/bg/js/settings/generic-setting-controller.js @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* globals + * getOptionsContext + * getOptionsMutable + * settingsSaveOptions + * utilBackgroundIsolate + */ + +class GenericSettingController { + prepare() { + $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(this._onFormOptionsChanged.bind(this)); + } + + optionsChanged(options) { + this._formWrite(options); + } + + // Private + + async _formWrite(options) { + $('#enable').prop('checked', options.general.enable); + $('#show-usage-guide').prop('checked', options.general.showGuide); + $('#compact-tags').prop('checked', options.general.compactTags); + $('#compact-glossaries').prop('checked', options.general.compactGlossaries); + $('#result-output-mode').val(options.general.resultOutputMode); + $('#show-debug-info').prop('checked', options.general.debugInfo); + $('#show-advanced-options').prop('checked', options.general.showAdvanced); + $('#max-displayed-results').val(options.general.maxResults); + $('#popup-display-mode').val(options.general.popupDisplayMode); + $('#popup-horizontal-text-position').val(options.general.popupHorizontalTextPosition); + $('#popup-vertical-text-position').val(options.general.popupVerticalTextPosition); + $('#popup-width').val(options.general.popupWidth); + $('#popup-height').val(options.general.popupHeight); + $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset); + $('#popup-vertical-offset').val(options.general.popupVerticalOffset); + $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2); + $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2); + $('#popup-scaling-factor').val(options.general.popupScalingFactor); + $('#popup-scale-relative-to-page-zoom').prop('checked', options.general.popupScaleRelativeToPageZoom); + $('#popup-scale-relative-to-visual-viewport').prop('checked', options.general.popupScaleRelativeToVisualViewport); + $('#show-pitch-accent-downstep-notation').prop('checked', options.general.showPitchAccentDownstepNotation); + $('#show-pitch-accent-position-notation').prop('checked', options.general.showPitchAccentPositionNotation); + $('#show-pitch-accent-graph').prop('checked', options.general.showPitchAccentGraph); + $('#show-iframe-popups-in-root-frame').prop('checked', options.general.showIframePopupsInRootFrame); + $('#popup-theme').val(options.general.popupTheme); + $('#popup-outer-theme').val(options.general.popupOuterTheme); + $('#custom-popup-css').val(options.general.customPopupCss); + $('#custom-popup-outer-css').val(options.general.customPopupOuterCss); + + $('#audio-playback-enabled').prop('checked', options.audio.enabled); + $('#auto-play-audio').prop('checked', options.audio.autoPlay); + $('#audio-playback-volume').val(options.audio.volume); + $('#audio-custom-source').val(options.audio.customSourceUrl); + $('#text-to-speech-voice').val(options.audio.textToSpeechVoice).attr('data-value', options.audio.textToSpeechVoice); + + $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse); + $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled); + $('#select-matched-text').prop('checked', options.scanning.selectText); + $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric); + $('#auto-hide-results').prop('checked', options.scanning.autoHideResults); + $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan); + $('#enable-search-within-first-popup').prop('checked', options.scanning.enablePopupSearch); + $('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions); + $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage); + $('#enable-search-tags').prop('checked', options.scanning.enableSearchTags); + $('#scan-delay').val(options.scanning.delay); + $('#scan-length').val(options.scanning.length); + $('#scan-modifier-key').val(options.scanning.modifier); + $('#popup-nesting-max-depth').val(options.scanning.popupNestingMaxDepth); + + $('#translation-convert-half-width-characters').val(options.translation.convertHalfWidthCharacters); + $('#translation-convert-numeric-characters').val(options.translation.convertNumericCharacters); + $('#translation-convert-alphabetic-characters').val(options.translation.convertAlphabeticCharacters); + $('#translation-convert-hiragana-to-katakana').val(options.translation.convertHiraganaToKatakana); + $('#translation-convert-katakana-to-hiragana').val(options.translation.convertKatakanaToHiragana); + $('#translation-collapse-emphatic-sequences').val(options.translation.collapseEmphaticSequences); + + $('#parsing-scan-enable').prop('checked', options.parsing.enableScanningParser); + $('#parsing-mecab-enable').prop('checked', options.parsing.enableMecabParser); + $('#parsing-term-spacing').prop('checked', options.parsing.termSpacing); + $('#parsing-reading-mode').val(options.parsing.readingMode); + + $('#anki-enable').prop('checked', options.anki.enable); + $('#card-tags').val(options.anki.tags.join(' ')); + $('#sentence-detection-extent').val(options.anki.sentenceExt); + $('#interface-server').val(options.anki.server); + $('#duplicate-scope').val(options.anki.duplicateScope); + $('#screenshot-format').val(options.anki.screenshot.format); + $('#screenshot-quality').val(options.anki.screenshot.quality); + + this._formUpdateVisibility(options); + } + + async _formRead(options) { + options.general.enable = $('#enable').prop('checked'); + options.general.showGuide = $('#show-usage-guide').prop('checked'); + options.general.compactTags = $('#compact-tags').prop('checked'); + options.general.compactGlossaries = $('#compact-glossaries').prop('checked'); + options.general.resultOutputMode = $('#result-output-mode').val(); + options.general.debugInfo = $('#show-debug-info').prop('checked'); + options.general.showAdvanced = $('#show-advanced-options').prop('checked'); + options.general.maxResults = parseInt($('#max-displayed-results').val(), 10); + options.general.popupDisplayMode = $('#popup-display-mode').val(); + options.general.popupHorizontalTextPosition = $('#popup-horizontal-text-position').val(); + options.general.popupVerticalTextPosition = $('#popup-vertical-text-position').val(); + options.general.popupWidth = parseInt($('#popup-width').val(), 10); + options.general.popupHeight = parseInt($('#popup-height').val(), 10); + options.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0); + options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10); + options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0); + options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10); + options.general.popupScalingFactor = parseFloat($('#popup-scaling-factor').val()); + options.general.popupScaleRelativeToPageZoom = $('#popup-scale-relative-to-page-zoom').prop('checked'); + options.general.popupScaleRelativeToVisualViewport = $('#popup-scale-relative-to-visual-viewport').prop('checked'); + options.general.showPitchAccentDownstepNotation = $('#show-pitch-accent-downstep-notation').prop('checked'); + options.general.showPitchAccentPositionNotation = $('#show-pitch-accent-position-notation').prop('checked'); + options.general.showPitchAccentGraph = $('#show-pitch-accent-graph').prop('checked'); + options.general.showIframePopupsInRootFrame = $('#show-iframe-popups-in-root-frame').prop('checked'); + options.general.popupTheme = $('#popup-theme').val(); + options.general.popupOuterTheme = $('#popup-outer-theme').val(); + options.general.customPopupCss = $('#custom-popup-css').val(); + options.general.customPopupOuterCss = $('#custom-popup-outer-css').val(); + + options.audio.enabled = $('#audio-playback-enabled').prop('checked'); + options.audio.autoPlay = $('#auto-play-audio').prop('checked'); + options.audio.volume = parseFloat($('#audio-playback-volume').val()); + options.audio.customSourceUrl = $('#audio-custom-source').val(); + options.audio.textToSpeechVoice = $('#text-to-speech-voice').val(); + + options.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked'); + options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked'); + options.scanning.selectText = $('#select-matched-text').prop('checked'); + options.scanning.alphanumeric = $('#search-alphanumeric').prop('checked'); + options.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); + options.scanning.deepDomScan = $('#deep-dom-scan').prop('checked'); + options.scanning.enablePopupSearch = $('#enable-search-within-first-popup').prop('checked'); + options.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked'); + options.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked'); + options.scanning.enableSearchTags = $('#enable-search-tags').prop('checked'); + options.scanning.delay = parseInt($('#scan-delay').val(), 10); + options.scanning.length = parseInt($('#scan-length').val(), 10); + options.scanning.modifier = $('#scan-modifier-key').val(); + options.scanning.popupNestingMaxDepth = parseInt($('#popup-nesting-max-depth').val(), 10); + + options.translation.convertHalfWidthCharacters = $('#translation-convert-half-width-characters').val(); + options.translation.convertNumericCharacters = $('#translation-convert-numeric-characters').val(); + options.translation.convertAlphabeticCharacters = $('#translation-convert-alphabetic-characters').val(); + options.translation.convertHiraganaToKatakana = $('#translation-convert-hiragana-to-katakana').val(); + options.translation.convertKatakanaToHiragana = $('#translation-convert-katakana-to-hiragana').val(); + options.translation.collapseEmphaticSequences = $('#translation-collapse-emphatic-sequences').val(); + + options.parsing.enableScanningParser = $('#parsing-scan-enable').prop('checked'); + options.parsing.enableMecabParser = $('#parsing-mecab-enable').prop('checked'); + options.parsing.termSpacing = $('#parsing-term-spacing').prop('checked'); + options.parsing.readingMode = $('#parsing-reading-mode').val(); + + options.anki.enable = $('#anki-enable').prop('checked'); + options.anki.tags = utilBackgroundIsolate($('#card-tags').val().split(/[,; ]+/)); + options.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10); + options.anki.server = $('#interface-server').val(); + options.anki.duplicateScope = $('#duplicate-scope').val(); + options.anki.screenshot.format = $('#screenshot-format').val(); + options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10); + } + + async _onFormOptionsChanged() { + const optionsContext = getOptionsContext(); + const options = await getOptionsMutable(optionsContext); + + await this._formRead(options); + await settingsSaveOptions(); + this._formUpdateVisibility(options); + } + + _formUpdateVisibility(options) { + document.documentElement.dataset.optionsAnkiEnable = `${!!options.anki.enable}`; + document.documentElement.dataset.optionsGeneralDebugInfo = `${!!options.general.debugInfo}`; + document.documentElement.dataset.optionsGeneralShowAdvanced = `${!!options.general.showAdvanced}`; + document.documentElement.dataset.optionsGeneralResultOutputMode = `${options.general.resultOutputMode}`; + } +} diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index b84824e6..d6f55bde 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -19,7 +19,9 @@ * AnkiController * AnkiTemplatesController * AudioController + * ClipboardPopupsController * DictionaryController + * GenericSettingController * PopupPreviewController * ProfileController * SettingsBackup @@ -55,197 +57,6 @@ function getOptionsFullMutable() { return utilBackend().getFullOptions(); } -async function formRead(options) { - options.general.enable = $('#enable').prop('checked'); - options.general.showGuide = $('#show-usage-guide').prop('checked'); - options.general.compactTags = $('#compact-tags').prop('checked'); - options.general.compactGlossaries = $('#compact-glossaries').prop('checked'); - options.general.resultOutputMode = $('#result-output-mode').val(); - options.general.debugInfo = $('#show-debug-info').prop('checked'); - options.general.showAdvanced = $('#show-advanced-options').prop('checked'); - options.general.maxResults = parseInt($('#max-displayed-results').val(), 10); - options.general.popupDisplayMode = $('#popup-display-mode').val(); - options.general.popupHorizontalTextPosition = $('#popup-horizontal-text-position').val(); - options.general.popupVerticalTextPosition = $('#popup-vertical-text-position').val(); - options.general.popupWidth = parseInt($('#popup-width').val(), 10); - options.general.popupHeight = parseInt($('#popup-height').val(), 10); - options.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0); - options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10); - options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0); - options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10); - options.general.popupScalingFactor = parseFloat($('#popup-scaling-factor').val()); - options.general.popupScaleRelativeToPageZoom = $('#popup-scale-relative-to-page-zoom').prop('checked'); - options.general.popupScaleRelativeToVisualViewport = $('#popup-scale-relative-to-visual-viewport').prop('checked'); - options.general.showPitchAccentDownstepNotation = $('#show-pitch-accent-downstep-notation').prop('checked'); - options.general.showPitchAccentPositionNotation = $('#show-pitch-accent-position-notation').prop('checked'); - options.general.showPitchAccentGraph = $('#show-pitch-accent-graph').prop('checked'); - options.general.showIframePopupsInRootFrame = $('#show-iframe-popups-in-root-frame').prop('checked'); - options.general.popupTheme = $('#popup-theme').val(); - options.general.popupOuterTheme = $('#popup-outer-theme').val(); - options.general.customPopupCss = $('#custom-popup-css').val(); - options.general.customPopupOuterCss = $('#custom-popup-outer-css').val(); - - options.audio.enabled = $('#audio-playback-enabled').prop('checked'); - options.audio.autoPlay = $('#auto-play-audio').prop('checked'); - options.audio.volume = parseFloat($('#audio-playback-volume').val()); - options.audio.customSourceUrl = $('#audio-custom-source').val(); - options.audio.textToSpeechVoice = $('#text-to-speech-voice').val(); - - options.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked'); - options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked'); - options.scanning.selectText = $('#select-matched-text').prop('checked'); - options.scanning.alphanumeric = $('#search-alphanumeric').prop('checked'); - options.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); - options.scanning.deepDomScan = $('#deep-dom-scan').prop('checked'); - options.scanning.enablePopupSearch = $('#enable-search-within-first-popup').prop('checked'); - options.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked'); - options.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked'); - options.scanning.enableSearchTags = $('#enable-search-tags').prop('checked'); - options.scanning.delay = parseInt($('#scan-delay').val(), 10); - options.scanning.length = parseInt($('#scan-length').val(), 10); - options.scanning.modifier = $('#scan-modifier-key').val(); - options.scanning.popupNestingMaxDepth = parseInt($('#popup-nesting-max-depth').val(), 10); - - options.translation.convertHalfWidthCharacters = $('#translation-convert-half-width-characters').val(); - options.translation.convertNumericCharacters = $('#translation-convert-numeric-characters').val(); - options.translation.convertAlphabeticCharacters = $('#translation-convert-alphabetic-characters').val(); - options.translation.convertHiraganaToKatakana = $('#translation-convert-hiragana-to-katakana').val(); - options.translation.convertKatakanaToHiragana = $('#translation-convert-katakana-to-hiragana').val(); - options.translation.collapseEmphaticSequences = $('#translation-collapse-emphatic-sequences').val(); - - options.parsing.enableScanningParser = $('#parsing-scan-enable').prop('checked'); - options.parsing.enableMecabParser = $('#parsing-mecab-enable').prop('checked'); - options.parsing.termSpacing = $('#parsing-term-spacing').prop('checked'); - options.parsing.readingMode = $('#parsing-reading-mode').val(); - - options.anki.enable = $('#anki-enable').prop('checked'); - options.anki.tags = utilBackgroundIsolate($('#card-tags').val().split(/[,; ]+/)); - options.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10); - options.anki.server = $('#interface-server').val(); - options.anki.duplicateScope = $('#duplicate-scope').val(); - options.anki.screenshot.format = $('#screenshot-format').val(); - options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10); -} - -async function formWrite(options) { - $('#enable').prop('checked', options.general.enable); - $('#show-usage-guide').prop('checked', options.general.showGuide); - $('#compact-tags').prop('checked', options.general.compactTags); - $('#compact-glossaries').prop('checked', options.general.compactGlossaries); - $('#result-output-mode').val(options.general.resultOutputMode); - $('#show-debug-info').prop('checked', options.general.debugInfo); - $('#show-advanced-options').prop('checked', options.general.showAdvanced); - $('#max-displayed-results').val(options.general.maxResults); - $('#popup-display-mode').val(options.general.popupDisplayMode); - $('#popup-horizontal-text-position').val(options.general.popupHorizontalTextPosition); - $('#popup-vertical-text-position').val(options.general.popupVerticalTextPosition); - $('#popup-width').val(options.general.popupWidth); - $('#popup-height').val(options.general.popupHeight); - $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset); - $('#popup-vertical-offset').val(options.general.popupVerticalOffset); - $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2); - $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2); - $('#popup-scaling-factor').val(options.general.popupScalingFactor); - $('#popup-scale-relative-to-page-zoom').prop('checked', options.general.popupScaleRelativeToPageZoom); - $('#popup-scale-relative-to-visual-viewport').prop('checked', options.general.popupScaleRelativeToVisualViewport); - $('#show-pitch-accent-downstep-notation').prop('checked', options.general.showPitchAccentDownstepNotation); - $('#show-pitch-accent-position-notation').prop('checked', options.general.showPitchAccentPositionNotation); - $('#show-pitch-accent-graph').prop('checked', options.general.showPitchAccentGraph); - $('#show-iframe-popups-in-root-frame').prop('checked', options.general.showIframePopupsInRootFrame); - $('#popup-theme').val(options.general.popupTheme); - $('#popup-outer-theme').val(options.general.popupOuterTheme); - $('#custom-popup-css').val(options.general.customPopupCss); - $('#custom-popup-outer-css').val(options.general.customPopupOuterCss); - - $('#audio-playback-enabled').prop('checked', options.audio.enabled); - $('#auto-play-audio').prop('checked', options.audio.autoPlay); - $('#audio-playback-volume').val(options.audio.volume); - $('#audio-custom-source').val(options.audio.customSourceUrl); - $('#text-to-speech-voice').val(options.audio.textToSpeechVoice).attr('data-value', options.audio.textToSpeechVoice); - - $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse); - $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled); - $('#select-matched-text').prop('checked', options.scanning.selectText); - $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric); - $('#auto-hide-results').prop('checked', options.scanning.autoHideResults); - $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan); - $('#enable-search-within-first-popup').prop('checked', options.scanning.enablePopupSearch); - $('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions); - $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage); - $('#enable-search-tags').prop('checked', options.scanning.enableSearchTags); - $('#scan-delay').val(options.scanning.delay); - $('#scan-length').val(options.scanning.length); - $('#scan-modifier-key').val(options.scanning.modifier); - $('#popup-nesting-max-depth').val(options.scanning.popupNestingMaxDepth); - - $('#translation-convert-half-width-characters').val(options.translation.convertHalfWidthCharacters); - $('#translation-convert-numeric-characters').val(options.translation.convertNumericCharacters); - $('#translation-convert-alphabetic-characters').val(options.translation.convertAlphabeticCharacters); - $('#translation-convert-hiragana-to-katakana').val(options.translation.convertHiraganaToKatakana); - $('#translation-convert-katakana-to-hiragana').val(options.translation.convertKatakanaToHiragana); - $('#translation-collapse-emphatic-sequences').val(options.translation.collapseEmphaticSequences); - - $('#parsing-scan-enable').prop('checked', options.parsing.enableScanningParser); - $('#parsing-mecab-enable').prop('checked', options.parsing.enableMecabParser); - $('#parsing-term-spacing').prop('checked', options.parsing.termSpacing); - $('#parsing-reading-mode').val(options.parsing.readingMode); - - $('#anki-enable').prop('checked', options.anki.enable); - $('#card-tags').val(options.anki.tags.join(' ')); - $('#sentence-detection-extent').val(options.anki.sentenceExt); - $('#interface-server').val(options.anki.server); - $('#duplicate-scope').val(options.anki.duplicateScope); - $('#screenshot-format').val(options.anki.screenshot.format); - $('#screenshot-quality').val(options.anki.screenshot.quality); - - formUpdateVisibility(options); -} - -function formSetupEventListeners() { - document.querySelector('#enable-clipboard-popups').addEventListener('change', onEnableClipboardPopupsChanged, false); - $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(onFormOptionsChanged); -} - -function formUpdateVisibility(options) { - document.documentElement.dataset.optionsAnkiEnable = `${!!options.anki.enable}`; - document.documentElement.dataset.optionsGeneralDebugInfo = `${!!options.general.debugInfo}`; - document.documentElement.dataset.optionsGeneralShowAdvanced = `${!!options.general.showAdvanced}`; - document.documentElement.dataset.optionsGeneralResultOutputMode = `${options.general.resultOutputMode}`; -} - -async function onFormOptionsChanged() { - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); - - await formRead(options); - await settingsSaveOptions(); - formUpdateVisibility(options); -} - -async function onEnableClipboardPopupsChanged(e) { - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); - - const enableClipboardPopups = e.target.checked; - if (enableClipboardPopups) { - options.general.enableClipboardPopups = await new Promise((resolve) => { - chrome.permissions.request( - {permissions: ['clipboardRead']}, - (granted) => { - if (!granted) { - $('#enable-clipboard-popups').prop('checked', false); - } - resolve(granted); - } - ); - }); - } else { - options.general.enableClipboardPopups = false; - } - - await settingsSaveOptions(); -} - function settingsGetSource() { return new Promise((resolve) => { @@ -276,7 +87,9 @@ async function onOptionsUpdated({source}) { ankiController.optionsChanged(); } - await formWrite(options); + if (genericSettingController !== null) { + genericSettingController.optionsChanged(options); + } } @@ -314,6 +127,7 @@ async function setupEnvironmentInfo() { let ankiController = null; let ankiTemplatesController = null; let dictionaryController = null; +let genericSettingController = null; async function onReady() { api.forwardLogsToBackend(); @@ -329,7 +143,9 @@ async function onReady() { storageController.prepare(); await settingsPopulateModifierKeys(); - formSetupEventListeners(); + genericSettingController = new GenericSettingController(); + genericSettingController.prepare(); + new ClipboardPopupsController().prepare(); new PopupPreviewController().prepare(); new AudioController().prepare(); await (new ProfileController()).prepare(); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 4856b0b4..bab62519 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1141,8 +1141,10 @@ + + -- cgit v1.2.3 From 1a5a37c9e47dc4d1f1e1b3ffaf990e792140b912 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 30 May 2020 09:31:46 -0400 Subject: Fix frontend being null if messages are received early (#575) --- ext/bg/js/settings/popup-preview-frame.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'ext/bg/js') diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js index 28c95efb..21fee7ee 100644 --- a/ext/bg/js/settings/popup-preview-frame.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -141,6 +141,7 @@ class PopupPreviewFrame { if (exampleText === null) { return; } exampleText.textContent = text; + if (this._frontend === null) { return; } this._updateSearch(); } @@ -163,6 +164,7 @@ class PopupPreviewFrame { async _updateOptionsContext({optionsContext}) { this._optionsContext = optionsContext; + if (this._frontend === null) { return; } await this._frontend.updateOptions(); await this._updateSearch(); } -- cgit v1.2.3 From 63a3e56367b95f7ea64a5701d17179de60ed8718 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 30 May 2020 09:33:13 -0400 Subject: Use SettingsController (#576) * Use settingsController internally in settings/main.js * Replace modifyingProfileChange with SettingsController.optionsContextChanged * Update ClipboardPopupsController to use SettingsController * Store reference to checkbox * Use this._settingsController for everything * Change where current profile is initially assigned from * Remove some unnecessary async calls * Move setup calls * Update AnkiTemplatesController to use SettingsController * Cache default field templates * Update AnkiController to use SettingsController * Update AudioController to use SettingsController * Update SettingsBackup to use SettingsController * Update DictionaryController to use SettingsController * Update GenericSettingController to use SettingsController * Update ProfileController to use SettingsController * Remove unused * Remove unused * Replace some uses of api.options* functions * Fix missing awaits * Fix invalid function --- ext/bg/js/settings/anki-templates.js | 35 ++++---- ext/bg/js/settings/anki.js | 53 +++++------ ext/bg/js/settings/audio.js | 33 ++++--- ext/bg/js/settings/backup.js | 12 +-- ext/bg/js/settings/clipboard-popups-controller.js | 34 ++++--- ext/bg/js/settings/dictionaries.js | 54 ++++++----- ext/bg/js/settings/dom-settings-binder.js | 1 - ext/bg/js/settings/generic-setting-controller.js | 28 +++--- ext/bg/js/settings/main.js | 97 +++----------------- ext/bg/js/settings/popup-preview.js | 9 +- ext/bg/js/settings/profiles.js | 105 +++++++++------------- ext/bg/js/settings/settings-controller.js | 5 ++ 12 files changed, 192 insertions(+), 274 deletions(-) (limited to 'ext/bg/js') diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js index dd128ab8..4ceff835 100644 --- a/ext/bg/js/settings/anki-templates.js +++ b/ext/bg/js/settings/anki-templates.js @@ -18,19 +18,20 @@ /* global * AnkiNoteBuilder * api - * getOptionsContext - * getOptionsMutable - * settingsSaveOptions */ class AnkiTemplatesController { - constructor(ankiController) { + constructor(settingsController, ankiController) { + this._settingsController = settingsController; this._ankiController = ankiController; this._cachedDefinitionValue = null; this._cachedDefinitionText = null; + this._defaultFieldTemplates = null; } - prepare() { + async prepare() { + this._defaultFieldTemplates = await api.getDefaultAnkiFieldTemplates(); + const markers = new Set([ ...this._ankiController.getFieldMarkers('terms'), ...this._ankiController.getFieldMarkers('kanji') @@ -48,21 +49,22 @@ class AnkiTemplatesController { $('#field-templates-reset').on('click', this._onReset.bind(this)); $('#field-templates-reset-confirm').on('click', this._onResetConfirm.bind(this)); - this.updateValue(); + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); } - async updateValue() { - const optionsContext = getOptionsContext(); - const options = await api.optionsGet(optionsContext); + // Private + + _onOptionsChanged({options}) { let templates = options.anki.fieldTemplates; - if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); } + if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; } $('#field-templates').val(templates); this._onValidateCompile(); } - // Private - _onReset(e) { e.preventDefault(); $('#field-template-reset-modal').modal('show'); @@ -89,10 +91,9 @@ class AnkiTemplatesController { } // Overwrite - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); + const options = await this._settingsController.getOptionsMutable(); options.anki.fieldTemplates = templates; - await settingsSaveOptions(); + await this._settingsController.save(); // Compile this._onValidateCompile(); @@ -133,10 +134,10 @@ class AnkiTemplatesController { const exceptions = []; let result = `No definition found for ${text}`; try { - const optionsContext = getOptionsContext(); + const optionsContext = this._settingsController.getOptionsContext(); const definition = await this._getDefinition(text, optionsContext); if (definition !== null) { - const options = await api.optionsGet(optionsContext); + const options = await this._settingsController.getOptions(); const context = { document: { title: document.title diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js index d110ef39..d099239d 100644 --- a/ext/bg/js/settings/anki.js +++ b/ext/bg/js/settings/anki.js @@ -17,38 +17,25 @@ /* global * api - * getOptionsContext - * getOptionsMutable - * settingsSaveOptions * utilBackgroundIsolate */ class AnkiController { - prepare() { + constructor(settingsController) { + this._settingsController = settingsController; + } + + async prepare() { $('#anki-fields-container input,#anki-fields-container select,#anki-fields-container textarea').change(this._onFieldsChanged.bind(this)); for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) { node.addEventListener('change', this._onModelChanged.bind(this), false); } - this.optionsChanged(); - } + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - async optionsChanged(options=null) { - if (options === null) { - const optionsContext = getOptionsContext(); - options = await getOptionsMutable(optionsContext); - } - - if (!options.anki.enable) { - return; - } - - await this._deckAndModelPopulate(options); - await Promise.all([ - this._fieldsPopulate('terms', options), - this._fieldsPopulate('kanji', options) - ]); + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); } getFieldMarkers(type) { @@ -103,6 +90,18 @@ class AnkiController { // Private + async _onOptionsChanged({options}) { + if (!options.anki.enable) { + return; + } + + await this._deckAndModelPopulate(options); + await Promise.all([ + this._fieldsPopulate('terms', options), + this._fieldsPopulate('kanji', options) + ]); + } + _fieldsToDict(elements) { const result = {}; for (const element of elements) { @@ -277,17 +276,15 @@ class AnkiController { fields[name] = ''; } - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); + const options = await this._settingsController.getOptionsMutable(); options.anki[tabId].fields = utilBackgroundIsolate(fields); - await settingsSaveOptions(); + await this._settingsController.save(); await this._fieldsPopulate(tabId, options); } async _onFieldsChanged() { - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); + const options = await this._settingsController.getOptionsMutable(); options.anki.terms.deck = $('#anki-terms-deck').val(); options.anki.terms.model = $('#anki-terms-model').val(); @@ -296,8 +293,6 @@ class AnkiController { options.anki.kanji.model = $('#anki-kanji-model').val(); options.anki.kanji.fields = utilBackgroundIsolate(this._fieldsToDict(document.querySelectorAll('#kanji .anki-field-value'))); - await settingsSaveOptions(); - - await this.optionsChanged(options); + await this._settingsController.save(); } } diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js index 5c1cb131..1a41a498 100644 --- a/ext/bg/js/settings/audio.js +++ b/ext/bg/js/settings/audio.js @@ -17,13 +17,11 @@ /* global * AudioSystem - * getOptionsContext - * getOptionsMutable - * settingsSaveOptions */ class AudioController { - constructor() { + constructor(settingsController) { + this._settingsController = settingsController; this._audioSystem = null; this._settingsAudioSources = null; this._audioSourceContainer = null; @@ -37,27 +35,36 @@ class AudioController { useCache: true }); - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); - - this._settingsAudioSources = options.audio.sources; 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); - for (const audioSource of toIterable(this._settingsAudioSources)) { - this._createAudioSourceEntry(audioSource); - } - this._prepareTextToSpeech(); + + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + this._onOptionsChanged(); } // Private + async _onOptionsChanged() { + const options = await this._settingsController.getOptionsMutable(); + + for (const entry of [...this._audioSourceEntries]) { + this._removeAudioSourceEntry(entry); + } + + this._settingsAudioSources = options.audio.sources; + for (const audioSource of toIterable(this._settingsAudioSources)) { + this._createAudioSourceEntry(audioSource); + } + } + async _save() { - await settingsSaveOptions(); + await this._settingsController.save(); } _prepareTextToSpeech() { diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js index 4e104e6f..e93e15bf 100644 --- a/ext/bg/js/settings/backup.js +++ b/ext/bg/js/settings/backup.js @@ -19,13 +19,11 @@ * api * optionsGetDefault * optionsUpdateVersion - * utilBackend - * utilBackgroundIsolate - * utilIsolate */ class SettingsBackup { - constructor() { + constructor(settingsController) { + this._settingsController = settingsController; this._settingsExportToken = null; this._settingsExportRevoke = null; this._currentVersion = 0; @@ -59,7 +57,7 @@ class SettingsBackup { } async _getSettingsExportData(date) { - const optionsFull = await api.optionsGetFull(); + const optionsFull = await this._settingsController.getOptionsFull(); const environment = await api.getEnvironmentInfo(); const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates(); @@ -143,9 +141,7 @@ class SettingsBackup { // Importing async _settingsImportSetOptionsFull(optionsFull) { - return utilIsolate(utilBackend().setFullOptions( - utilBackgroundIsolate(optionsFull) - )); + await this._settingsController.setOptionsFull(optionsFull); } _showSettingsImportError(error) { diff --git a/ext/bg/js/settings/clipboard-popups-controller.js b/ext/bg/js/settings/clipboard-popups-controller.js index cb9e857f..77fae305 100644 --- a/ext/bg/js/settings/clipboard-popups-controller.js +++ b/ext/bg/js/settings/clipboard-popups-controller.js @@ -15,29 +15,37 @@ * along with this program. If not, see . */ -/* globals - * getOptionsContext - * getOptionsMutable - * settingsSaveOptions - */ - class ClipboardPopupsController { - prepare() { - document.querySelector('#enable-clipboard-popups').addEventListener('change', this._onEnableClipboardPopupsChanged.bind(this), false); + constructor(settingsController) { + this._settingsController = settingsController; + this._checkbox = document.querySelector('#enable-clipboard-popups'); } - async _onEnableClipboardPopupsChanged(e) { - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); + async prepare() { + this._checkbox.addEventListener('change', this._onEnableClipboardPopupsChanged.bind(this), false); + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); + } + // Private + + _onOptionsChanged({options}) { + this._checkbox.checked = options.general.enableClipboardPopups; + } + + async _onEnableClipboardPopupsChanged(e) { const enableClipboardPopups = e.target.checked; + const options = await this._settingsController.getOptionsMutable(); + if (enableClipboardPopups) { options.general.enableClipboardPopups = await new Promise((resolve) => { chrome.permissions.request( {permissions: ['clipboardRead']}, (granted) => { if (!granted) { - $('#enable-clipboard-popups').prop('checked', false); + this._checkbox.checked = false; } resolve(granted); } @@ -47,6 +55,6 @@ class ClipboardPopupsController { options.general.enableClipboardPopups = false; } - await settingsSaveOptions(); + await this._settingsController.save(); } } diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js index dd6dd1c1..94a71233 100644 --- a/ext/bg/js/settings/dictionaries.js +++ b/ext/bg/js/settings/dictionaries.js @@ -18,15 +18,12 @@ /* global * PageExitPrevention * api - * getOptionsContext - * getOptionsFullMutable - * getOptionsMutable - * settingsSaveOptions * utilBackgroundIsolate */ -class SettingsDictionaryListUI { +class SettingsDictionaryListUI extends EventDispatcher { constructor(container, template, extraContainer, extraTemplate) { + super(); this.container = container; this.template = template; this.extraContainer = extraContainer; @@ -309,7 +306,7 @@ class SettingsDictionaryEntryUI { this.isDeleting = false; progress.hidden = true; - this.onDatabaseUpdated(); + this.parent.trigger('databaseUpdated'); } } @@ -384,7 +381,8 @@ class SettingsDictionaryExtraUI { } class DictionaryController { - constructor(storageController) { + constructor(settingsController, storageController) { + this._settingsController = settingsController; this._storageController = storageController; this._dictionaryUI = null; this._dictionaryErrorToStringOverrides = [ @@ -410,7 +408,8 @@ class DictionaryController { document.querySelector('#dict-groups-extra'), document.querySelector('#dict-extra-template') ); - this._dictionaryUI.save = settingsSaveOptions; + this._dictionaryUI.save = () => this._settingsController.save(); + this._dictionaryUI.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); document.querySelector('#dict-purge-button').addEventListener('click', this._onPurgeButtonClick.bind(this), false); document.querySelector('#dict-purge-confirm').addEventListener('click', this._onPurgeConfirmButtonClick.bind(this), false); @@ -419,26 +418,25 @@ class DictionaryController { document.querySelector('#dict-main').addEventListener('change', this._onDictionaryMainChanged.bind(this), false); document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', this._onDatabaseEnablePrefixWildcardSearchesChanged.bind(this), false); - await this.optionsChanged(); + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + await this._onOptionsChanged(); await this._onDatabaseUpdated(); } - async optionsChanged() { - if (this._dictionaryUI === null) { return; } + // Private - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); + async _onOptionsChanged() { + const options = await this._settingsController.getOptionsMutable(); this._dictionaryUI.setOptionsDictionaries(options.dictionaries); - const optionsFull = await api.optionsGetFull(); + const optionsFull = await this._settingsController.getOptionsFull(); document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported; await this._updateMainDictionarySelectValue(); } - // Private - _updateMainDictionarySelectOptions(dictionaries) { const select = document.querySelector('#dict-main'); select.textContent = ''; // Empty @@ -460,8 +458,7 @@ class DictionaryController { } async _updateMainDictionarySelectValue() { - const optionsContext = getOptionsContext(); - const options = await api.optionsGet(optionsContext); + const options = await this._settingsController.getOptions(); const value = options.general.mainDictionary; @@ -589,10 +586,9 @@ class DictionaryController { missingNodeOption.parentNode.removeChild(missingNodeOption); } - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); + const options = await this._settingsController.getOptionsMutable(); options.general.mainDictionary = value; - await settingsSaveOptions(); + await this._settingsController.save(); } _onImportButtonClick() { @@ -622,11 +618,12 @@ class DictionaryController { this._dictionarySpinnerShow(true); await api.purgeDatabase(); - for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) { + const optionsFull = await this._settingsController.getOptionsFullMutable(); + for (const {options} of toIterable(optionsFull.profiles)) { options.dictionaries = utilBackgroundIsolate({}); options.general.mainDictionary = ''; } - await settingsSaveOptions(); + await this._settingsController.save(); this._onDatabaseUpdated(); } catch (err) { @@ -665,7 +662,7 @@ class DictionaryController { this._storageController.updateStats(); }; - const optionsFull = await api.optionsGetFull(); + const optionsFull = await this._settingsController.getOptionsFull(); const importDetails = { prefixWildcardsSupported: optionsFull.global.database.prefixWildcardsSupported @@ -680,7 +677,8 @@ class DictionaryController { const archiveContent = await this._dictReadFile(files[i]); const {result, errors} = await api.importDictionaryArchive(archiveContent, importDetails, updateProgress); - for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) { + const optionsFull2 = await this._settingsController.getOptionsFullMutable(); + for (const {options} of toIterable(optionsFull2.profiles)) { const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions(); dictionaryOptions.enabled = true; options.dictionaries[result.title] = dictionaryOptions; @@ -689,7 +687,7 @@ class DictionaryController { } } - await settingsSaveOptions(); + await this._settingsController.save(); if (errors.length > 0) { const errors2 = errors.map((error) => jsonToError(error)); @@ -714,10 +712,10 @@ class DictionaryController { } async _onDatabaseEnablePrefixWildcardSearchesChanged(e) { - const optionsFull = await getOptionsFullMutable(); + 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 settingsSaveOptions(); + await this._settingsController.save(); } } diff --git a/ext/bg/js/settings/dom-settings-binder.js b/ext/bg/js/settings/dom-settings-binder.js index 4b63859f..07da4f37 100644 --- a/ext/bg/js/settings/dom-settings-binder.js +++ b/ext/bg/js/settings/dom-settings-binder.js @@ -18,7 +18,6 @@ /* global * DOMDataBinder * api - * getOptionsContext */ class DOMSettingsBinder { diff --git a/ext/bg/js/settings/generic-setting-controller.js b/ext/bg/js/settings/generic-setting-controller.js index 4a20bf65..d7d40c5d 100644 --- a/ext/bg/js/settings/generic-setting-controller.js +++ b/ext/bg/js/settings/generic-setting-controller.js @@ -16,24 +16,26 @@ */ /* globals - * getOptionsContext - * getOptionsMutable - * settingsSaveOptions * utilBackgroundIsolate */ class GenericSettingController { - prepare() { - $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(this._onFormOptionsChanged.bind(this)); + constructor(settingsController) { + this._settingsController = settingsController; } - optionsChanged(options) { - this._formWrite(options); + async prepare() { + $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(this._onFormOptionsChanged.bind(this)); + + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); } // Private - async _formWrite(options) { + _onOptionsChanged({options}) { $('#enable').prop('checked', options.general.enable); $('#show-usage-guide').prop('checked', options.general.showGuide); $('#compact-tags').prop('checked', options.general.compactTags); @@ -107,7 +109,7 @@ class GenericSettingController { this._formUpdateVisibility(options); } - async _formRead(options) { + _formRead(options) { options.general.enable = $('#enable').prop('checked'); options.general.showGuide = $('#show-usage-guide').prop('checked'); options.general.compactTags = $('#compact-tags').prop('checked'); @@ -180,12 +182,10 @@ class GenericSettingController { } async _onFormOptionsChanged() { - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); - - await this._formRead(options); - await settingsSaveOptions(); + const options = await this._settingsController.getOptionsMutable(); + this._formRead(options); this._formUpdateVisibility(options); + await this._settingsController.save(); } _formUpdateVisibility(options) { diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index d6f55bde..cf74c0fc 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -28,71 +28,8 @@ * SettingsController * StorageController * api - * utilBackend - * utilBackgroundIsolate */ -let profileIndex = 0; - -function getOptionsContext() { - return {index: getProfileIndex()}; -} - -function getProfileIndex() { - return profileIndex; -} - -function setProfileIndex(value) { - profileIndex = value; -} - - -function getOptionsMutable(optionsContext) { - return utilBackend().getOptions( - utilBackgroundIsolate(optionsContext) - ); -} - -function getOptionsFullMutable() { - return utilBackend().getFullOptions(); -} - - -function settingsGetSource() { - return new Promise((resolve) => { - chrome.tabs.getCurrent((tab) => resolve(`settings${tab ? tab.id : ''}`)); - }); -} - -async function settingsSaveOptions() { - const source = await settingsGetSource(); - await api.optionsSave(source); -} - -async function onOptionsUpdated({source}) { - const thisSource = await settingsGetSource(); - if (source === thisSource) { return; } - - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); - - document.querySelector('#enable-clipboard-popups').checked = options.general.enableClipboardPopups; - if (ankiTemplatesController !== null) { - ankiTemplatesController.updateValue(); - } - if (dictionaryController !== null) { - dictionaryController.optionsChanged(); - } - if (ankiController !== null) { - ankiController.optionsChanged(); - } - - if (genericSettingController !== null) { - genericSettingController.optionsChanged(options); - } -} - - function showExtensionInformation() { const node = document.getElementById('extension-info'); if (node === null) { return; } @@ -124,40 +61,34 @@ async function setupEnvironmentInfo() { document.documentElement.dataset.operatingSystem = platform.os; } -let ankiController = null; -let ankiTemplatesController = null; -let dictionaryController = null; -let genericSettingController = null; async function onReady() { api.forwardLogsToBackend(); await yomichan.prepare(); - const settingsController = new SettingsController(); - settingsController.prepare(); - setupEnvironmentInfo(); showExtensionInformation(); + settingsPopulateModifierKeys(); + + const optionsFull = await api.optionsGetFull(); + const settingsController = new SettingsController(optionsFull.profileCurrent); + settingsController.prepare(); const storageController = new StorageController(); storageController.prepare(); - await settingsPopulateModifierKeys(); - genericSettingController = new GenericSettingController(); + const genericSettingController = new GenericSettingController(settingsController); genericSettingController.prepare(); - new ClipboardPopupsController().prepare(); - new PopupPreviewController().prepare(); - new AudioController().prepare(); - await (new ProfileController()).prepare(); - dictionaryController = new DictionaryController(storageController); + new ClipboardPopupsController(settingsController).prepare(); + new PopupPreviewController(settingsController).prepare(); + new AudioController(settingsController).prepare(); + new ProfileController(settingsController).prepare(); + const dictionaryController = new DictionaryController(settingsController, storageController); dictionaryController.prepare(); - ankiController = new AnkiController(); + const ankiController = new AnkiController(settingsController); ankiController.prepare(); - ankiTemplatesController = new AnkiTemplatesController(ankiController); - ankiTemplatesController.prepare(); - new SettingsBackup().prepare(); - - yomichan.on('optionsUpdated', onOptionsUpdated); + new AnkiTemplatesController(settingsController, ankiController).prepare(); + new SettingsBackup(settingsController).prepare(); } $(document).ready(() => onReady()); diff --git a/ext/bg/js/settings/popup-preview.js b/ext/bg/js/settings/popup-preview.js index d5519959..d4145b76 100644 --- a/ext/bg/js/settings/popup-preview.js +++ b/ext/bg/js/settings/popup-preview.js @@ -16,12 +16,12 @@ */ /* global - * getOptionsContext * wanakana */ class PopupPreviewController { - constructor() { + constructor(settingsController) { + this._settingsController = settingsController; this._previewVisible = false; this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); this._frame = null; @@ -58,7 +58,7 @@ class PopupPreviewController { text.addEventListener('input', this._onTextChange.bind(this), false); customCss.addEventListener('input', this._onCustomCssChange.bind(this), false); customOuterCss.addEventListener('input', this._onCustomOuterCssChange.bind(this), false); - yomichan.on('modifyingProfileChange', this._onOptionsContextChange.bind(this)); + this._settingsController.on('optionsContextChanged', this._onOptionsContextChange.bind(this)); frame.src = '/bg/settings-popup-preview.html'; frame.id = 'settings-popup-preview-frame'; @@ -88,7 +88,8 @@ class PopupPreviewController { } _onOptionsContextChange() { - this._invoke('updateOptionsContext', {optionsContext: getOptionsContext()}); + const optionsContext = this._settingsController.getOptionsContext(); + this._invoke('updateOptionsContext', {optionsContext}); } _setText(text) { diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js index e2c558e9..2449ab44 100644 --- a/ext/bg/js/settings/profiles.js +++ b/ext/bg/js/settings/profiles.js @@ -17,34 +17,19 @@ /* global * ConditionsUI - * api * conditionsClearCaches - * getOptionsFullMutable - * getProfileIndex - * onOptionsUpdated * profileConditionsDescriptor * profileConditionsDescriptorPromise - * setProfileIndex - * settingsSaveOptions * utilBackgroundIsolate */ class ProfileController { - constructor() { + constructor(settingsController) { + this._settingsController = settingsController; this._conditionsContainer = null; } async prepare() { - const optionsFull = await getOptionsFullMutable(); - setProfileIndex(optionsFull.profileCurrent); - - this._setupEventListeners(); - await this._updateTarget(optionsFull); - } - - // Private - - _setupEventListeners() { $('#profile-target').change(this._onTargetProfileChanged.bind(this)); $('#profile-name').change(this._onNameChanged.bind(this)); $('#profile-add').click(this._onAdd.bind(this)); @@ -55,6 +40,17 @@ class ProfileController { $('#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(); + await this._formWrite(optionsFull); } _tryGetIntegerValue(selector, min, max) { @@ -69,7 +65,7 @@ class ProfileController { } async _formRead(optionsFull) { - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; const profile = optionsFull.profiles[currentProfileIndex]; // Current profile @@ -83,7 +79,7 @@ class ProfileController { } async _formWrite(optionsFull) { - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; const profile = optionsFull.profiles[currentProfileIndex]; this._populateSelect($('#profile-active'), optionsFull.profiles, optionsFull.profileCurrent, null); @@ -108,7 +104,7 @@ class ProfileController { $('#profile-add-condition-group') ); this._conditionsContainer.save = () => { - settingsSaveOptions(); + this._settingsController.save(); conditionsClearCaches(profileConditionsDescriptor); }; this._conditionsContainer.isolate = utilBackgroundIsolate; @@ -129,11 +125,6 @@ class ProfileController { select.val(`${currentValue}`); } - async _updateTarget(optionsFull) { - await this._formWrite(optionsFull); - await onOptionsUpdated({source: null}); - } - _createCopyName(name, profiles, maxUniqueAttempts) { let space, index, prefix, suffix; const match = /^([\w\W]*\(Copy)((\s+)(\d+))?(\)\s*)$/.exec(name); @@ -174,39 +165,32 @@ class ProfileController { return; } - const optionsFull = await getOptionsFullMutable(); + const optionsFull = await this._settingsController.getOptionsFullMutable(); await this._formRead(optionsFull); - await settingsSaveOptions(); + await this._settingsController.save(); } async _onTargetProfileChanged() { - const optionsFull = await getOptionsFullMutable(); - const currentProfileIndex = getProfileIndex(); + 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; } - setProfileIndex(index); - - await this._updateTarget(optionsFull); - - yomichan.trigger('modifyingProfileChange'); + this._settingsController.profileIndex = index; } async _onAdd() { - const optionsFull = await getOptionsFullMutable(); - const currentProfileIndex = getProfileIndex(); + 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); - setProfileIndex(optionsFull.profiles.length - 1); - - await this._updateTarget(optionsFull); - await settingsSaveOptions(); + this._settingsController.profileIndex = optionsFull.profiles.length - 1; - yomichan.trigger('modifyingProfileChange'); + await this._settingsController.save(); } async _onRemove(e) { @@ -214,12 +198,12 @@ class ProfileController { return await this._onRemoveConfirm(); } - const optionsFull = await api.optionsGetFull(); + const optionsFull = await this._settingsController.getOptionsFull(); if (optionsFull.profiles.length <= 1) { return; } - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; const profile = optionsFull.profiles[currentProfileIndex]; $('#profile-remove-modal-profile-name').text(profile.name); @@ -229,36 +213,33 @@ class ProfileController { async _onRemoveConfirm() { $('#profile-remove-modal').modal('hide'); - const optionsFull = await getOptionsFullMutable(); + const optionsFull = await this._settingsController.getOptionsFullMutable(); if (optionsFull.profiles.length <= 1) { return; } - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; optionsFull.profiles.splice(currentProfileIndex, 1); if (currentProfileIndex >= optionsFull.profiles.length) { - setProfileIndex(optionsFull.profiles.length - 1); + this._settingsController.profileIndex = optionsFull.profiles.length - 1; } if (optionsFull.profileCurrent >= optionsFull.profiles.length) { optionsFull.profileCurrent = optionsFull.profiles.length - 1; } - await this._updateTarget(optionsFull); - await settingsSaveOptions(); - - yomichan.trigger('modifyingProfileChange'); + await this._settingsController.save(); } _onNameChanged() { - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; $('#profile-active, #profile-target').find(`[value="${currentProfileIndex}"]`).text(this.value); } async _onMove(offset) { - const optionsFull = await getOptionsFullMutable(); - const currentProfileIndex = getProfileIndex(); + const optionsFull = await this._settingsController.getOptionsFullMutable(); + const currentProfileIndex = this._settingsController.profileIndex; const index = currentProfileIndex + offset; if (index < 0 || index >= optionsFull.profiles.length) { return; @@ -272,21 +253,18 @@ class ProfileController { optionsFull.profileCurrent = index; } - setProfileIndex(index); - - await this._updateTarget(optionsFull); - await settingsSaveOptions(); + this._settingsController.profileIndex = index; - yomichan.trigger('modifyingProfileChange'); + await this._settingsController.save(); } async _onCopy() { - const optionsFull = await api.optionsGetFull(); + const optionsFull = await this._settingsController.getOptionsFullMutable(); if (optionsFull.profiles.length <= 1) { return; } - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; this._populateSelect($('#profile-copy-source'), optionsFull.profiles, currentProfileIndex === 0 ? 1 : 0, [currentProfileIndex]); $('#profile-copy-modal').modal('show'); } @@ -294,9 +272,9 @@ class ProfileController { async _onCopyConfirm() { $('#profile-copy-modal').modal('hide'); - const optionsFull = await getOptionsFullMutable(); + const optionsFull = await this._settingsController.getOptionsFullMutable(); const index = this._tryGetIntegerValue('#profile-copy-source', 0, optionsFull.profiles.length); - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; if (index === null || index === currentProfileIndex) { return; } @@ -304,7 +282,6 @@ class ProfileController { const profileOptions = utilBackgroundIsolate(optionsFull.profiles[index].options); optionsFull.profiles[currentProfileIndex].options = profileOptions; - await this._updateTarget(optionsFull); - await settingsSaveOptions(); + await this._settingsController.save(); } } diff --git a/ext/bg/js/settings/settings-controller.js b/ext/bg/js/settings/settings-controller.js index 61230226..9f903f48 100644 --- a/ext/bg/js/settings/settings-controller.js +++ b/ext/bg/js/settings/settings-controller.js @@ -65,6 +65,11 @@ class SettingsController extends EventDispatcher { return utilBackend().getFullOptions(); } + async setOptionsFull(optionsFull) { + utilBackend().setFullOptions(utilBackgroundIsolate(optionsFull)); + await this.save(); + } + getOptionsContext() { return {index: this._profileIndex}; } -- cgit v1.2.3 From 789da0206b0a452605b49e9f72c4b294088b8046 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 30 May 2020 09:50:33 -0400 Subject: Organize settings/main.js (#577) --- ext/bg/js/settings/main.js | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) (limited to 'ext/bg/js') diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index cf74c0fc..e22c5e53 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -62,7 +62,7 @@ async function setupEnvironmentInfo() { } -async function onReady() { +(async () => { api.forwardLogsToBackend(); await yomichan.prepare(); @@ -71,6 +71,7 @@ async function onReady() { settingsPopulateModifierKeys(); const optionsFull = await api.optionsGetFull(); + const settingsController = new SettingsController(optionsFull.profileCurrent); settingsController.prepare(); @@ -79,16 +80,28 @@ async function onReady() { const genericSettingController = new GenericSettingController(settingsController); genericSettingController.prepare(); - new ClipboardPopupsController(settingsController).prepare(); - new PopupPreviewController(settingsController).prepare(); - new AudioController(settingsController).prepare(); - new ProfileController(settingsController).prepare(); + + const clipboardPopupsController = new ClipboardPopupsController(settingsController); + clipboardPopupsController.prepare(); + + const popupPreviewController = new PopupPreviewController(settingsController); + popupPreviewController.prepare(); + + const audioController = new AudioController(settingsController); + audioController.prepare(); + + const profileController = new ProfileController(settingsController); + profileController.prepare(); + const dictionaryController = new DictionaryController(settingsController, storageController); dictionaryController.prepare(); + const ankiController = new AnkiController(settingsController); ankiController.prepare(); - new AnkiTemplatesController(settingsController, ankiController).prepare(); - new SettingsBackup(settingsController).prepare(); -} -$(document).ready(() => onReady()); + const ankiTemplatesController = new AnkiTemplatesController(settingsController, ankiController); + ankiTemplatesController.prepare(); + + const settingsBackup = new SettingsBackup(settingsController); + settingsBackup.prepare(); +})(); -- cgit v1.2.3 From f22807861392a4fcada66f19784b63d66eace2dc Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 30 May 2020 11:24:34 -0400 Subject: SettingsController API update (#579) * Include optionsContext as part of optionsChanged event * Add get/modify functions --- ext/bg/js/settings/settings-controller.js | 37 ++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) (limited to 'ext/bg/js') diff --git a/ext/bg/js/settings/settings-controller.js b/ext/bg/js/settings/settings-controller.js index 9f903f48..0d7abaa9 100644 --- a/ext/bg/js/settings/settings-controller.js +++ b/ext/bg/js/settings/settings-controller.js @@ -70,6 +70,30 @@ class SettingsController extends EventDispatcher { await this.save(); } + async getGlobalSettings(targets) { + return await this._getSettings(targets, {scope: 'global'}); + } + + async getProfileSettings(targets) { + return await this._getSettings(targets, {scope: 'profile', optionsContext: this.getOptionsContext()}); + } + + async modifyGlobalSettings(targets) { + return await this._modifySettings(targets, {scope: 'global'}); + } + + async modifyProfileSettings(targets) { + return await this._modifySettings(targets, {scope: 'profile', optionsContext: this.getOptionsContext()}); + } + + async setGlobalSetting(path, value) { + return await this.modifyGlobalSettings([{action: 'set', path, value}]); + } + + async setProfileSetting(path, value) { + return await this.modifyProfileSettings([{action: 'set', path, value}]); + } + getOptionsContext() { return {index: this._profileIndex}; } @@ -82,7 +106,18 @@ class SettingsController extends EventDispatcher { } async _onOptionsUpdatedInternal() { + const optionsContext = this.getOptionsContext(); const options = await this.getOptions(); - this.trigger('optionsChanged', {options}); + this.trigger('optionsChanged', {options, optionsContext}); + } + + async _getSettings(targets, extraFields) { + targets = targets.map((target) => Object.assign({}, target, extraFields)); + return await api.getSettings(targets); + } + + async _modifySettings(targets, extraFields) { + targets = targets.map((target) => Object.assign({}, target, extraFields)); + return await api.modifySettings(targets, this._source); } } -- cgit v1.2.3 From 395a0f40965aac62389e2b7eea389d6b1672ae4a Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 30 May 2020 16:20:31 -0400 Subject: Update GenericSettingController to use DOMSettingsBinder (#578) --- ext/bg/js/settings/generic-setting-controller.js | 177 +++-------------------- ext/bg/js/settings/settings-controller.js | 4 + ext/bg/settings.html | 134 ++++++++--------- 3 files changed, 94 insertions(+), 221 deletions(-) (limited to 'ext/bg/js') diff --git a/ext/bg/js/settings/generic-setting-controller.js b/ext/bg/js/settings/generic-setting-controller.js index d7d40c5d..aa3118e5 100644 --- a/ext/bg/js/settings/generic-setting-controller.js +++ b/ext/bg/js/settings/generic-setting-controller.js @@ -16,182 +16,47 @@ */ /* globals + * DOMSettingsBinder * utilBackgroundIsolate */ class GenericSettingController { constructor(settingsController) { this._settingsController = settingsController; + this._settingsBinder = null; } async prepare() { - $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(this._onFormOptionsChanged.bind(this)); + this._settingsBinder = new DOMSettingsBinder({ + getOptionsContext: () => this._settingsController.getOptionsContext(), + source: this._settingsController.source, + transforms: [ + ['setDocumentAttribute', this._setDocumentAttribute.bind(this)], + ['splitTags', this._splitTags.bind(this)], + ['joinTags', this._joinTags.bind(this)] + ] + }); + this._settingsBinder.observe(document.body); this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - - const options = await this._settingsController.getOptions(); - this._onOptionsChanged({options}); } // Private - _onOptionsChanged({options}) { - $('#enable').prop('checked', options.general.enable); - $('#show-usage-guide').prop('checked', options.general.showGuide); - $('#compact-tags').prop('checked', options.general.compactTags); - $('#compact-glossaries').prop('checked', options.general.compactGlossaries); - $('#result-output-mode').val(options.general.resultOutputMode); - $('#show-debug-info').prop('checked', options.general.debugInfo); - $('#show-advanced-options').prop('checked', options.general.showAdvanced); - $('#max-displayed-results').val(options.general.maxResults); - $('#popup-display-mode').val(options.general.popupDisplayMode); - $('#popup-horizontal-text-position').val(options.general.popupHorizontalTextPosition); - $('#popup-vertical-text-position').val(options.general.popupVerticalTextPosition); - $('#popup-width').val(options.general.popupWidth); - $('#popup-height').val(options.general.popupHeight); - $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset); - $('#popup-vertical-offset').val(options.general.popupVerticalOffset); - $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2); - $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2); - $('#popup-scaling-factor').val(options.general.popupScalingFactor); - $('#popup-scale-relative-to-page-zoom').prop('checked', options.general.popupScaleRelativeToPageZoom); - $('#popup-scale-relative-to-visual-viewport').prop('checked', options.general.popupScaleRelativeToVisualViewport); - $('#show-pitch-accent-downstep-notation').prop('checked', options.general.showPitchAccentDownstepNotation); - $('#show-pitch-accent-position-notation').prop('checked', options.general.showPitchAccentPositionNotation); - $('#show-pitch-accent-graph').prop('checked', options.general.showPitchAccentGraph); - $('#show-iframe-popups-in-root-frame').prop('checked', options.general.showIframePopupsInRootFrame); - $('#popup-theme').val(options.general.popupTheme); - $('#popup-outer-theme').val(options.general.popupOuterTheme); - $('#custom-popup-css').val(options.general.customPopupCss); - $('#custom-popup-outer-css').val(options.general.customPopupOuterCss); - - $('#audio-playback-enabled').prop('checked', options.audio.enabled); - $('#auto-play-audio').prop('checked', options.audio.autoPlay); - $('#audio-playback-volume').val(options.audio.volume); - $('#audio-custom-source').val(options.audio.customSourceUrl); - $('#text-to-speech-voice').val(options.audio.textToSpeechVoice).attr('data-value', options.audio.textToSpeechVoice); - - $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse); - $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled); - $('#select-matched-text').prop('checked', options.scanning.selectText); - $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric); - $('#auto-hide-results').prop('checked', options.scanning.autoHideResults); - $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan); - $('#enable-search-within-first-popup').prop('checked', options.scanning.enablePopupSearch); - $('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions); - $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage); - $('#enable-search-tags').prop('checked', options.scanning.enableSearchTags); - $('#scan-delay').val(options.scanning.delay); - $('#scan-length').val(options.scanning.length); - $('#scan-modifier-key').val(options.scanning.modifier); - $('#popup-nesting-max-depth').val(options.scanning.popupNestingMaxDepth); - - $('#translation-convert-half-width-characters').val(options.translation.convertHalfWidthCharacters); - $('#translation-convert-numeric-characters').val(options.translation.convertNumericCharacters); - $('#translation-convert-alphabetic-characters').val(options.translation.convertAlphabeticCharacters); - $('#translation-convert-hiragana-to-katakana').val(options.translation.convertHiraganaToKatakana); - $('#translation-convert-katakana-to-hiragana').val(options.translation.convertKatakanaToHiragana); - $('#translation-collapse-emphatic-sequences').val(options.translation.collapseEmphaticSequences); - - $('#parsing-scan-enable').prop('checked', options.parsing.enableScanningParser); - $('#parsing-mecab-enable').prop('checked', options.parsing.enableMecabParser); - $('#parsing-term-spacing').prop('checked', options.parsing.termSpacing); - $('#parsing-reading-mode').val(options.parsing.readingMode); - - $('#anki-enable').prop('checked', options.anki.enable); - $('#card-tags').val(options.anki.tags.join(' ')); - $('#sentence-detection-extent').val(options.anki.sentenceExt); - $('#interface-server').val(options.anki.server); - $('#duplicate-scope').val(options.anki.duplicateScope); - $('#screenshot-format').val(options.anki.screenshot.format); - $('#screenshot-quality').val(options.anki.screenshot.quality); - - this._formUpdateVisibility(options); + _onOptionsChanged() { + this._settingsBinder.refresh(); } - _formRead(options) { - options.general.enable = $('#enable').prop('checked'); - options.general.showGuide = $('#show-usage-guide').prop('checked'); - options.general.compactTags = $('#compact-tags').prop('checked'); - options.general.compactGlossaries = $('#compact-glossaries').prop('checked'); - options.general.resultOutputMode = $('#result-output-mode').val(); - options.general.debugInfo = $('#show-debug-info').prop('checked'); - options.general.showAdvanced = $('#show-advanced-options').prop('checked'); - options.general.maxResults = parseInt($('#max-displayed-results').val(), 10); - options.general.popupDisplayMode = $('#popup-display-mode').val(); - options.general.popupHorizontalTextPosition = $('#popup-horizontal-text-position').val(); - options.general.popupVerticalTextPosition = $('#popup-vertical-text-position').val(); - options.general.popupWidth = parseInt($('#popup-width').val(), 10); - options.general.popupHeight = parseInt($('#popup-height').val(), 10); - options.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0); - options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10); - options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0); - options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10); - options.general.popupScalingFactor = parseFloat($('#popup-scaling-factor').val()); - options.general.popupScaleRelativeToPageZoom = $('#popup-scale-relative-to-page-zoom').prop('checked'); - options.general.popupScaleRelativeToVisualViewport = $('#popup-scale-relative-to-visual-viewport').prop('checked'); - options.general.showPitchAccentDownstepNotation = $('#show-pitch-accent-downstep-notation').prop('checked'); - options.general.showPitchAccentPositionNotation = $('#show-pitch-accent-position-notation').prop('checked'); - options.general.showPitchAccentGraph = $('#show-pitch-accent-graph').prop('checked'); - options.general.showIframePopupsInRootFrame = $('#show-iframe-popups-in-root-frame').prop('checked'); - options.general.popupTheme = $('#popup-theme').val(); - options.general.popupOuterTheme = $('#popup-outer-theme').val(); - options.general.customPopupCss = $('#custom-popup-css').val(); - options.general.customPopupOuterCss = $('#custom-popup-outer-css').val(); - - options.audio.enabled = $('#audio-playback-enabled').prop('checked'); - options.audio.autoPlay = $('#auto-play-audio').prop('checked'); - options.audio.volume = parseFloat($('#audio-playback-volume').val()); - options.audio.customSourceUrl = $('#audio-custom-source').val(); - options.audio.textToSpeechVoice = $('#text-to-speech-voice').val(); - - options.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked'); - options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked'); - options.scanning.selectText = $('#select-matched-text').prop('checked'); - options.scanning.alphanumeric = $('#search-alphanumeric').prop('checked'); - options.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); - options.scanning.deepDomScan = $('#deep-dom-scan').prop('checked'); - options.scanning.enablePopupSearch = $('#enable-search-within-first-popup').prop('checked'); - options.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked'); - options.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked'); - options.scanning.enableSearchTags = $('#enable-search-tags').prop('checked'); - options.scanning.delay = parseInt($('#scan-delay').val(), 10); - options.scanning.length = parseInt($('#scan-length').val(), 10); - options.scanning.modifier = $('#scan-modifier-key').val(); - options.scanning.popupNestingMaxDepth = parseInt($('#popup-nesting-max-depth').val(), 10); - - options.translation.convertHalfWidthCharacters = $('#translation-convert-half-width-characters').val(); - options.translation.convertNumericCharacters = $('#translation-convert-numeric-characters').val(); - options.translation.convertAlphabeticCharacters = $('#translation-convert-alphabetic-characters').val(); - options.translation.convertHiraganaToKatakana = $('#translation-convert-hiragana-to-katakana').val(); - options.translation.convertKatakanaToHiragana = $('#translation-convert-katakana-to-hiragana').val(); - options.translation.collapseEmphaticSequences = $('#translation-collapse-emphatic-sequences').val(); - - options.parsing.enableScanningParser = $('#parsing-scan-enable').prop('checked'); - options.parsing.enableMecabParser = $('#parsing-mecab-enable').prop('checked'); - options.parsing.termSpacing = $('#parsing-term-spacing').prop('checked'); - options.parsing.readingMode = $('#parsing-reading-mode').val(); - - options.anki.enable = $('#anki-enable').prop('checked'); - options.anki.tags = utilBackgroundIsolate($('#card-tags').val().split(/[,; ]+/)); - options.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10); - options.anki.server = $('#interface-server').val(); - options.anki.duplicateScope = $('#duplicate-scope').val(); - options.anki.screenshot.format = $('#screenshot-format').val(); - options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10); + _setDocumentAttribute(value, metadata, element) { + document.documentElement.setAttribute(element.dataset.documentAttribute, `${value}`); + return value; } - async _onFormOptionsChanged() { - const options = await this._settingsController.getOptionsMutable(); - this._formRead(options); - this._formUpdateVisibility(options); - await this._settingsController.save(); + _splitTags(value) { + return `${value}`.split(/[,; ]+/).filter((v) => !!v); } - _formUpdateVisibility(options) { - document.documentElement.dataset.optionsAnkiEnable = `${!!options.anki.enable}`; - document.documentElement.dataset.optionsGeneralDebugInfo = `${!!options.general.debugInfo}`; - document.documentElement.dataset.optionsGeneralShowAdvanced = `${!!options.general.showAdvanced}`; - document.documentElement.dataset.optionsGeneralResultOutputMode = `${options.general.resultOutputMode}`; + _joinTags(value) { + return value.join(' '); } } diff --git a/ext/bg/js/settings/settings-controller.js b/ext/bg/js/settings/settings-controller.js index 0d7abaa9..9224aedf 100644 --- a/ext/bg/js/settings/settings-controller.js +++ b/ext/bg/js/settings/settings-controller.js @@ -28,6 +28,10 @@ class SettingsController extends EventDispatcher { this._source = yomichan.generateId(16); } + get source() { + return this._source; + } + get profileIndex() { return this._profileIndex; } diff --git a/ext/bg/settings.html b/ext/bg/settings.html index bab62519..1baeeced 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -135,7 +135,7 @@

    General Options

    - +
    @@ -143,52 +143,52 @@
    - +
    - +
    - +
    - +
    - +
    - +
    - +
    - +
    - +
    - +
    - +
    - @@ -197,7 +197,7 @@
    - @@ -205,26 +205,26 @@
    - +
    - +
    -
    - @@ -239,11 +239,11 @@
    - +
    - +
    @@ -252,11 +252,11 @@
    - +
    - +
    @@ -265,11 +265,11 @@
    - +
    - +
    @@ -278,14 +278,14 @@
    -
    - @@ -298,11 +298,11 @@
    -
    +
    -
    +
    @@ -324,22 +324,22 @@

    Audio Options

    - +
    - +
    - +