From 9a657214ad84f031c4c642cfa64ffa6b7d71ad77 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 23 May 2020 13:19:31 -0400 Subject: Add support for additional types of event listeners (#522) * Add support for additional types of event listeners * Fixes --- ext/mixed/js/core.js | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index 589425f2..fa66033d 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -255,15 +255,37 @@ class EventListenerCollection { return this._eventListeners.length; } - addEventListener(node, type, listener, options) { - node.addEventListener(type, listener, options); - this._eventListeners.push([node, type, listener, options]); + addEventListener(object, ...args) { + object.addEventListener(...args); + this._eventListeners.push(['removeEventListener', object, ...args]); + } + + addListener(object, ...args) { + object.addListener(args); + this._eventListeners.push(['removeListener', object, ...args]); + } + + on(object, ...args) { + object.on(args); + this._eventListeners.push(['off', object, ...args]); } removeAllEventListeners() { if (this._eventListeners.length === 0) { return; } - for (const [node, type, listener, options] of this._eventListeners) { - node.removeEventListener(type, listener, options); + for (const [removeFunctionName, object, ...args] of this._eventListeners) { + switch (removeFunctionName) { + case 'removeEventListener': + object.removeEventListener(...args); + break; + case 'removeListener': + object.removeListener(...args); + break; + case 'off': + object.off(...args); + break; + default: + throw new Error(`Unknown remove function: ${removeFunctionName}`); + } } this._eventListeners = []; } -- cgit v1.2.3 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 ++++++++ ext/mixed/js/comm.js | 282 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 322 insertions(+) create mode 100644 ext/mixed/js/comm.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}); } diff --git a/ext/mixed/js/comm.js b/ext/mixed/js/comm.js new file mode 100644 index 00000000..7787616e --- /dev/null +++ b/ext/mixed/js/comm.js @@ -0,0 +1,282 @@ +/* + * 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 CrossFrameAPIPort extends EventDispatcher { + constructor(otherFrameId, port, messageHandlers) { + super(); + this._otherFrameId = otherFrameId; + this._port = port; + this._messageHandlers = messageHandlers; + this._activeInvocations = new Map(); + this._invocationId = 0; + this._eventListeners = new EventListenerCollection(); + } + + get otherFrameId() { + return this._otherFrameId; + } + + prepare() { + this._eventListeners.addListener(this._port.onDisconnect, this._onDisconnect.bind(this)); + this._eventListeners.addListener(this._port.onMessage, this._onMessage.bind(this)); + } + + invoke(action, params, ackTimeout, responseTimeout) { + return new Promise((resolve, reject) => { + if (this._port === null) { + reject(new Error('Port is disconnected')); + return; + } + + const id = this._invocationId++; + const invocation = {id, resolve, reject, responseTimeout, ack: false, timer: null}; + this._activeInvocations.set(id, invocation); + + if (ackTimeout !== null) { + try { + invocation.timer = setTimeout(() => this._onError(id, new Error('Timeout (ack)')), ackTimeout); + } catch (e) { + this._onError(id, new Error('Failed to set timeout')); + return; + } + } + + try { + this._port.postMessage({type: 'invoke', id, data: {action, params}}); + } catch (e) { + this._onError(id, e); + } + }); + } + + disconnect() { + this._onDisconnect(); + } + + // Private + + _onDisconnect() { + if (this._port === null) { return; } + this._eventListeners.removeAllEventListeners(); + this._port = null; + for (const id of this._activeInvocations.keys()) { + this._onError(id, new Error('Disconnected')); + } + this.trigger('disconnect', this); + } + + _onMessage({type, id, data}) { + switch (type) { + case 'invoke': + this._onInvoke(id, data); + break; + case 'ack': + this._onAck(id); + break; + case 'result': + this._onResult(id, data); + break; + } + } + + // Response handlers + + _onAck(id) { + const invocation = this._activeInvocations.get(id); + if (typeof invocation === 'undefined') { + yomichan.logWarning(new Error(`Request ${id} not found for ack`)); + return; + } + + if (invocation.ack) { + this._onError(id, new Error(`Request ${id} already ack'd`)); + return; + } + + invocation.ack = true; + + if (invocation.timer !== null) { + clearTimeout(invocation.timer); + invocation.timer = null; + } + + const responseTimeout = invocation.responseTimeout; + if (responseTimeout !== null) { + try { + invocation.timer = setTimeout(() => this._onError(id, new Error('Timeout (response)')), responseTimeout); + } catch (e) { + this._onError(id, new Error('Failed to set timeout')); + } + } + } + + _onResult(id, data) { + const invocation = this._activeInvocations.get(id); + if (typeof invocation === 'undefined') { + yomichan.logWarning(new Error(`Request ${id} not found`)); + return; + } + + if (!invocation.ack) { + this._onError(id, new Error(`Request ${id} not ack'd`)); + return; + } + + this._activeInvocations.delete(id); + + if (invocation.timer !== null) { + clearTimeout(invocation.timer); + invocation.timer = null; + } + + const error = data.error; + if (typeof error !== 'undefined') { + invocation.reject(jsonToError(error)); + } else { + invocation.resolve(data.result); + } + } + + _onError(id, error) { + const invocation = this._activeInvocations.get(id); + if (typeof invocation === 'undefined') { return; } + + this._activeInvocations.delete(id); + if (invocation.timer !== null) { + clearTimeout(invocation.timer); + invocation.timer = null; + } + invocation.reject(error); + } + + // Invocation + + _onInvoke(id, {action, params}) { + const messageHandler = this._messageHandlers.get(action); + if (typeof messageHandler === 'undefined') { + this._sendError(id, new Error(`Unknown action: ${action}`)); + return; + } + + const {handler, async} = messageHandler; + + this._sendAck(id); + if (async) { + this._invokeHandlerAsync(id, handler, params); + } else { + this._invokeHandler(id, handler, params); + } + } + + _invokeHandler(id, handler, params) { + try { + const result = handler(params); + this._sendResult(id, result); + } catch (error) { + this._sendError(id, error); + } + } + + async _invokeHandlerAsync(id, handler, params) { + try { + const result = await handler(params); + this._sendResult(id, result); + } catch (error) { + this._sendError(id, error); + } + } + + _sendResponse(data) { + if (this._port === null) { return; } + try { + this._port.postMessage(data); + } catch (e) { + // NOP + } + } + + _sendAck(id) { + this._sendResponse({type: 'ack', id}); + } + + _sendResult(id, result) { + this._sendResponse({type: 'result', id, data: {result}}); + } + + _sendError(id, error) { + this._sendResponse({type: 'result', id, data: {error: errorToJson(error)}}); + } +} + +class CrossFrameAPI { + constructor() { + this._ackTimeout = 3000; // 3 seconds + this._responseTimeout = 10000; // 10 seconds + this._commPorts = new Map(); + this._messageHandlers = new Map(); + this._onDisconnectBind = this._onDisconnect.bind(this); + } + + prepare() { + chrome.runtime.onConnect.addListener(this._onConnect.bind(this)); + } + + async invoke(targetFrameId, action, params={}) { + const commPort = this._getOrCreateCommPort(targetFrameId); + return await commPort.invoke(action, params, this._ackTimeout, this._responseTimeout); + } + + registerHandlers(messageHandlers) { + for (const [key, value] of messageHandlers) { + if (this._messageHandlers.has(key)) { + throw new Error(`Handler ${key} is already registered`); + } + this._messageHandlers.set(key, value); + } + } + + _onConnect(port) { + const match = /^cross-frame-communication-port-(\d+)$/.exec(`${port.name}`); + if (match === null) { return; } + + const otherFrameId = parseInt(match[1], 10); + this._setupCommPort(otherFrameId, port); + } + + _onDisconnect(commPort) { + commPort.off('disconnect', this._onDisconnectBind); + this._commPorts.delete(commPort.otherFrameId); + } + + _getOrCreateCommPort(otherFrameId) { + const commPort = this._commPorts.get(otherFrameId); + return (typeof commPort !== 'undefined' ? commPort : this._createCommPort(otherFrameId)); + } + + _createCommPort(otherFrameId) { + const port = chrome.runtime.connect(null, {name: `background-cross-frame-communication-port-${otherFrameId}`}); + return this._setupCommPort(otherFrameId, port); + } + + _setupCommPort(otherFrameId, port) { + const commPort = new CrossFrameAPIPort(otherFrameId, port, this._messageHandlers); + this._commPorts.set(otherFrameId, commPort); + commPort.prepare(); + commPort.on('disconnect', this._onDisconnectBind); + return commPort; + } +} -- cgit v1.2.3 From 83a577fa569e5a6d468e3b304313106bba3e1e49 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 23 May 2020 14:18:02 -0400 Subject: Add missing spreads (#552) --- ext/mixed/js/core.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index fa66033d..4c22cae7 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -261,12 +261,12 @@ class EventListenerCollection { } addListener(object, ...args) { - object.addListener(args); + object.addListener(...args); this._eventListeners.push(['removeListener', object, ...args]); } on(object, ...args) { - object.on(args); + object.on(...args); this._eventListeners.push(['off', object, ...args]); } -- 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(-) 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 4d2e5b93f4634bd5dfe55c305e441f75f0e0690e Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 24 May 2020 13:37:23 -0400 Subject: Style adjustments (#537) * Remove newlines for term-definition-item-template * Update how action button icons are styled * Fix spacing * Group text colors together * Correct image styles * Fix missing --- ext/bg/search.html | 4 ++-- ext/fg/float.html | 4 ++-- ext/mixed/css/display.css | 46 ++++++++++++++++++++++++++++------------ ext/mixed/display-templates.html | 26 ++++++++++------------- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/ext/bg/search.html b/ext/bg/search.html index f3f156d8..c0721e5c 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -54,8 +54,8 @@
diff --git a/ext/fg/float.html b/ext/fg/float.html index 89952524..e9f5acae 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -17,8 +17,8 @@
diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css index 8b567173..a92775b8 100644 --- a/ext/mixed/css/display.css +++ b/ext/mixed/css/display.css @@ -27,13 +27,12 @@ --default-text-color: #333333; --light-text-color: #777777; --very-light-text-color: #999999; + --popuplar-kanji-text-color: #0275d8; --light-border-color: #eeeeee; --medium-border-color: #dddddd; --dark-border-color: #777777; - --popuplar-kanji-text-color: #0275d8; - --pitch-accent-annotation-color: #000000; --tag-text-color: #ffffff; @@ -58,13 +57,12 @@ --default-text-color: #d4d4d4; --light-text-color: #888888; --very-light-text-color: #666666; + --popuplar-kanji-text-color: #0275d8; --light-border-color: #2f2f2f; --medium-border-color: #3f3f3f; --dark-border-color: #888888; - --popuplar-kanji-text-color: #0275d8; - --pitch-accent-annotation-color: #ffffff; --tag-text-color: #e1e1e1; @@ -173,14 +171,14 @@ h2 { display: flex; } -.navigation-header:not([data-has-previous=true]) .navigation-header-actions .action-previous>img, -.navigation-header:not([data-has-next=true]) .navigation-header-actions .action-next>img { +.navigation-header:not([data-has-previous=true]) .navigation-header-actions .action-button.action-previous:before, +.navigation-header:not([data-has-next=true]) .navigation-header-actions .action-button.action-next:before { opacity: 0.25; -webkit-filter: grayscale(100%); filter: grayscale(100%); } -.action-next>img { +.action-button.action-next:before { transform: scaleX(-1); } @@ -234,18 +232,18 @@ h2 { padding-right: 0.72em; } -.actions .disabled { +.action-button.disabled { pointer-events: none; cursor: default; } -.actions .disabled img { +.action-button.disabled:before { -webkit-filter: grayscale(100%); filter: grayscale(100%); opacity: 0.25; } -.actions .pending { +.action-button.pending { visibility: hidden; } @@ -274,10 +272,32 @@ button.action-button { cursor: pointer; } -.icon-image { +.action-button[data-icon]:before { + content: ""; width: 1.14285714em; /* 14px => 16px */ height: 1.14285714em; /* 14px => 16px */ display: block; + background-color: transparent; + background-repeat: no-repeat; +} + +.action-button[data-icon=entry-current]:before { + background-image: url("/mixed/img/entry-current.svg"); +} +.action-button[data-icon=view-note]:before { + background-image: url("/mixed/img/view-note.svg"); +} +.action-button[data-icon=add-term-kanji]:before { + background-image: url("/mixed/img/add-term-kanji.svg"); +} +.action-button[data-icon=add-term-kana]:before { + background-image: url("/mixed/img/add-term-kana.svg"); +} +.action-button[data-icon=play-audio]:before { + background-image: url("/mixed/img/play-audio.svg"); +} +.action-button[data-icon=source-term]:before { + background-image: url("/mixed/img/source-term.svg"); } .term-expression .kanji-link { @@ -358,7 +378,7 @@ button.action-button { display: block; } -.tag-list>.tag:not(:last-child) { +.tag-list>.tag { margin-right: 0.375em; } @@ -505,7 +525,7 @@ button.action-button { } .term-definition-disambiguation-list:after { - content: " only)"; + content: " only) "; } .term-definition-disambiguation+.term-definition-disambiguation:before { diff --git a/ext/mixed/display-templates.html b/ext/mixed/display-templates.html index fc0558a9..fed252a1 100644 --- a/ext/mixed/display-templates.html +++ b/ext/mixed/display-templates.html @@ -5,11 +5,11 @@
- - - - - + + + + +
@@ -24,15 +24,11 @@

 
- + @@ -55,9 +51,9 @@
- - - + + +
@@ -92,7 +88,7 @@ - + -- cgit v1.2.3 From a595a0a48193f28f248191d146f5e476a2f04df6 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 24 May 2020 13:37:49 -0400 Subject: Add icons (#540) * Add icons * Update icons --- ext/mixed/img/backup.svg | 1 + ext/mixed/img/book.svg | 1 + ext/mixed/img/cog.svg | 1 + ext/mixed/img/hiragana-a.svg | 1 + ext/mixed/img/keyboard.svg | 1 + ext/mixed/img/left-chevron.svg | 1 + ext/mixed/img/magnifying-glass.svg | 1 + ext/mixed/img/note-card.svg | 1 + ext/mixed/img/palette.svg | 1 + ext/mixed/img/popup.svg | 1 + ext/mixed/img/profile.svg | 1 + ext/mixed/img/question-mark-circle.svg | 1 + ext/mixed/img/question-mark-thick.svg | 1 + ext/mixed/img/question-mark.svg | 1 + ext/mixed/img/right-chevron.svg | 1 + ext/mixed/img/scanning.svg | 1 + ext/mixed/img/speaker.svg | 1 + ext/mixed/img/text-parsing.svg | 1 + ext/mixed/img/translation.svg | 1 + resources/icons.svg | 325 ++++++++++++++++++++++++++++++--- 20 files changed, 318 insertions(+), 26 deletions(-) create mode 100644 ext/mixed/img/backup.svg create mode 100644 ext/mixed/img/book.svg create mode 100644 ext/mixed/img/cog.svg create mode 100644 ext/mixed/img/hiragana-a.svg create mode 100644 ext/mixed/img/keyboard.svg create mode 100644 ext/mixed/img/left-chevron.svg create mode 100644 ext/mixed/img/magnifying-glass.svg create mode 100644 ext/mixed/img/note-card.svg create mode 100644 ext/mixed/img/palette.svg create mode 100644 ext/mixed/img/popup.svg create mode 100644 ext/mixed/img/profile.svg create mode 100644 ext/mixed/img/question-mark-circle.svg create mode 100644 ext/mixed/img/question-mark-thick.svg create mode 100644 ext/mixed/img/question-mark.svg create mode 100644 ext/mixed/img/right-chevron.svg create mode 100644 ext/mixed/img/scanning.svg create mode 100644 ext/mixed/img/speaker.svg create mode 100644 ext/mixed/img/text-parsing.svg create mode 100644 ext/mixed/img/translation.svg diff --git a/ext/mixed/img/backup.svg b/ext/mixed/img/backup.svg new file mode 100644 index 00000000..081560c2 --- /dev/null +++ b/ext/mixed/img/backup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/book.svg b/ext/mixed/img/book.svg new file mode 100644 index 00000000..1b785296 --- /dev/null +++ b/ext/mixed/img/book.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/cog.svg b/ext/mixed/img/cog.svg new file mode 100644 index 00000000..7232d25d --- /dev/null +++ b/ext/mixed/img/cog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/hiragana-a.svg b/ext/mixed/img/hiragana-a.svg new file mode 100644 index 00000000..1a7d6a7f --- /dev/null +++ b/ext/mixed/img/hiragana-a.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/keyboard.svg b/ext/mixed/img/keyboard.svg new file mode 100644 index 00000000..b94afde5 --- /dev/null +++ b/ext/mixed/img/keyboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/left-chevron.svg b/ext/mixed/img/left-chevron.svg new file mode 100644 index 00000000..9dd012dc --- /dev/null +++ b/ext/mixed/img/left-chevron.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/magnifying-glass.svg b/ext/mixed/img/magnifying-glass.svg new file mode 100644 index 00000000..a8367d8d --- /dev/null +++ b/ext/mixed/img/magnifying-glass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/note-card.svg b/ext/mixed/img/note-card.svg new file mode 100644 index 00000000..fb00b074 --- /dev/null +++ b/ext/mixed/img/note-card.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/palette.svg b/ext/mixed/img/palette.svg new file mode 100644 index 00000000..4a615ef2 --- /dev/null +++ b/ext/mixed/img/palette.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/popup.svg b/ext/mixed/img/popup.svg new file mode 100644 index 00000000..ef528cfb --- /dev/null +++ b/ext/mixed/img/popup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/profile.svg b/ext/mixed/img/profile.svg new file mode 100644 index 00000000..52a1363d --- /dev/null +++ b/ext/mixed/img/profile.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/question-mark-circle.svg b/ext/mixed/img/question-mark-circle.svg new file mode 100644 index 00000000..0076f7cd --- /dev/null +++ b/ext/mixed/img/question-mark-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/question-mark-thick.svg b/ext/mixed/img/question-mark-thick.svg new file mode 100644 index 00000000..7f2214a6 --- /dev/null +++ b/ext/mixed/img/question-mark-thick.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/question-mark.svg b/ext/mixed/img/question-mark.svg new file mode 100644 index 00000000..bc3b9a1c --- /dev/null +++ b/ext/mixed/img/question-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/right-chevron.svg b/ext/mixed/img/right-chevron.svg new file mode 100644 index 00000000..e210057b --- /dev/null +++ b/ext/mixed/img/right-chevron.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/scanning.svg b/ext/mixed/img/scanning.svg new file mode 100644 index 00000000..9ac16c83 --- /dev/null +++ b/ext/mixed/img/scanning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/speaker.svg b/ext/mixed/img/speaker.svg new file mode 100644 index 00000000..4c9b8eba --- /dev/null +++ b/ext/mixed/img/speaker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/text-parsing.svg b/ext/mixed/img/text-parsing.svg new file mode 100644 index 00000000..dfa88af8 --- /dev/null +++ b/ext/mixed/img/text-parsing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ext/mixed/img/translation.svg b/ext/mixed/img/translation.svg new file mode 100644 index 00000000..fdb98b1d --- /dev/null +++ b/ext/mixed/img/translation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons.svg b/resources/icons.svg index f096947b..9980492e 100644 --- a/resources/icons.svg +++ b/resources/icons.svg @@ -10,16 +10,16 @@ xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="16" - height="16" - viewBox="0 0 16 16" - version="1.1" - id="svg8" - inkscape:version="0.92.4 (5da689c313, 2019-01-14)" - sodipodi:docname="icons.svg" - inkscape:export-xdpi="192" + inkscape:export-filename="../ext/mixed/img/icon32.png" inkscape:export-ydpi="192" - inkscape:export-filename="../ext/mixed/img/icon32.png"> + inkscape:export-xdpi="192" + sodipodi:docname="icons.svg" + inkscape:version="0.92.4 (5da689c313, 2019-01-14)" + id="svg8" + version="1.1" + viewBox="0 0 16 16" + height="16" + width="16"> + viewbox-height="16" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-intersection-paths="true" + inkscape:object-paths="true" + inkscape:snap-others="true" + inkscape:snap-nodes="true"> + empspacing="8" + spacingx="0.25" + spacingy="0.25" + dotted="false" /> @@ -490,6 +498,16 @@ id="linearGradient5227" xlink:href="#linearGradient5225" inkscape:collect="always" /> + @@ -499,7 +517,7 @@ image/svg+xml - + @@ -670,7 +688,7 @@ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAH0SURBVDjLxdPPS9tgGAfwgH/ATmPD0w5j MFa3IXOMFImsOKnbmCUTacW1WZM2Mf1ho6OBrohkIdJfWm9aLKhM6GF4Lz3No/+AMC/PYQXBXL1+ 95oxh1jGhsgOX/LywvN5n/fN+3IAuKuEuzagVFoO27b1/Z+BcrnUx4otx7FPLWsJvYpIM2SS9H4P qNWqfK1W8VKplHlW/G1zs4G9vS9YXPx4CaDkXOFES4Om4gceUK2WsbZWR72+gtXVFezsbKHVamF7 ewtm/sMFgBJZhd6pvm4kDndaAo2KOmt5Gfv7X9HpdNBut9FsNmFZFgPrMHKZc4DkjHyi6KC3MZNe hTOuGAH5Xx5ybK/Y3f0Mx3Fg2zaKxSIMw2DjT0inNQ84nogcUUQJHIfZquNT3hzx46DBALizg2o0 1qEoCqLRKERRRDAYhKYlWRK/AJdCMwH2BY28+Qk8fg667wdXKJjY2FiHaeaRzWYQCk1AEASGzSCZ jP/ewtik5r6eBD0dM+nRSMb1j4LuPDnkFhZymJ/PsmLdazmV0jxEkqKsK+niIQ69mKUBwdd9OAx3 SADdHtC53FyK12dVXlVlPpF4zytK7OgMyucNyHLs8m+8+2zJHRwG3fId9LxIbNU+OR6zWU57AR5y 84FKN+71//EqM2iapfv/HtPf5gcdtKR8VW88PgAAAABJRU5ErkJggg== " id="image4790" x="0" - y="-1.6125985e-007" /> + y="-1.6125985e-07" /> + y="-1.6125985e-07" /> + inkscape:label="Yomichan" + style="display:none"> + inkscape:label="Characters" + style="display:inline"> + d="M 2,2 V 4 H 5 V 7 H 2 v 2 h 3 v 3 H 2 v 2 H 7 V 2 Z m 7,0 v 2 h 5 V 2 Z m 0,5 v 2 h 5 V 7 Z m 0,5 v 2 h 5 v -2 z" + id="path3859" + inkscape:connector-curvature="0" /> + + + + + + + + + + + + + + + + + + + + + + -- 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(-) 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 c800444a0d4aa0177242da51e0f9716ebe882587 Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Sun, 24 May 2020 13:39:50 -0400
    Subject: Ensure the return value of promiseTimeout always has .resolve and
     .reject (#550)
    
    ---
     ext/mixed/js/core.js | 5 ++++-
     1 file changed, 4 insertions(+), 1 deletion(-)
    
    diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js
    index 4c22cae7..257c7edf 100644
    --- a/ext/mixed/js/core.js
    +++ b/ext/mixed/js/core.js
    @@ -164,7 +164,10 @@ function getSetDifference(set1, set2) {
     
     function promiseTimeout(delay, resolveValue) {
         if (delay <= 0) {
    -        return Promise.resolve(resolveValue);
    +        const promise = Promise.resolve(resolveValue);
    +        promise.resolve = () => {}; // NOP
    +        promise.reject = () => {}; // NOP
    +        return promise;
         }
     
         let timer = null;
    -- 
    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
    
    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(-)
    
    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 3c4c82dcfc66a1b24a3df3d4b15283235c72cf66 Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Sun, 24 May 2020 14:00:32 -0400
    Subject: Ensure single popup factory (#554)
    
    * Add createPopupFactory
    
    * Ensure only a single PopupFactory is generated
    ---
     ext/fg/js/content-script-main.js | 42 +++++++++++++++++++++++++---------------
     1 file changed, 26 insertions(+), 16 deletions(-)
    
    diff --git a/ext/fg/js/content-script-main.js b/ext/fg/js/content-script-main.js
    index b057ae3d..cebda2d7 100644
    --- a/ext/fg/js/content-script-main.js
    +++ b/ext/fg/js/content-script-main.js
    @@ -24,6 +24,19 @@
      * api
      */
     
    +async function createPopupFactory() {
    +    const {frameId} = await api.frameInformationGet();
    +    if (typeof frameId !== 'number') {
    +        const error = new Error('Failed to get frameId');
    +        yomichan.logError(error);
    +        throw error;
    +    }
    +
    +    const popupFactory = new PopupFactory(frameId);
    +    await popupFactory.prepare();
    +    return popupFactory;
    +}
    +
     async function createIframePopupProxy(frameOffsetForwarder, setDisabled) {
         const rootPopupInformationPromise = yomichan.getTemporaryListenerResult(
             chrome.runtime.onMessage,
    @@ -44,20 +57,8 @@ async function createIframePopupProxy(frameOffsetForwarder, setDisabled) {
         return popup;
     }
     
    -async function getOrCreatePopup(depth) {
    -    const {frameId} = await api.frameInformationGet();
    -    if (typeof frameId !== 'number') {
    -        const error = new Error('Failed to get frameId');
    -        yomichan.logError(error);
    -        throw error;
    -    }
    -
    -    const popupFactory = new PopupFactory(frameId);
    -    await popupFactory.prepare();
    -
    -    const popup = popupFactory.getOrCreatePopup(null, null, depth);
    -
    -    return popup;
    +async function getOrCreatePopup(depth, popupFactory) {
    +    return popupFactory.getOrCreatePopup(null, null, depth);
     }
     
     async function createPopupProxy(depth, id, parentFrameId) {
    @@ -85,6 +86,7 @@ async function createPopupProxy(depth, id, parentFrameId) {
         let frontend = null;
         let frontendPreparePromise = null;
         let frameOffsetForwarder = null;
    +    let popupFactoryPromise = null;
     
         let iframePopupsInRootFrameAvailable = true;
     
    @@ -124,8 +126,16 @@ async function createPopupProxy(depth, id, parentFrameId) {
                 popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId);
                 popups.proxy = popup;
             } else {
    -            popup = popups.normal || await getOrCreatePopup(depth);
    -            popups.normal = popup;
    +            popup = popups.normal;
    +            if (!popup) {
    +                if (popupFactoryPromise === null) {
    +                    popupFactoryPromise = createPopupFactory();
    +                }
    +                const popupFactory = await popupFactoryPromise;
    +                const popupNormal = await getOrCreatePopup(depth, popupFactory);
    +                popups.normal = popupNormal;
    +                popup = popupNormal;
    +            }
             }
     
             if (frontend === null) {
    -- 
    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(-)
    
    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 9d40955fc822f4cea141ecb9abf4a838ffe90eb9 Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Sun, 24 May 2020 22:10:50 -0400
    Subject: Update legal.html to include full licenses (#563)
    
    ---
     ext/bg/legal.html | 186 ++++++++++++++++++++++++++++++++++++++++++++----------
     1 file changed, 154 insertions(+), 32 deletions(-)
    
    diff --git a/ext/bg/legal.html b/ext/bg/legal.html
    index 1ee9a28c..8b4fe513 100644
    --- a/ext/bg/legal.html
    +++ b/ext/bg/legal.html
    @@ -1,22 +1,27 @@
     
     
    -    
    -        
    -        
    -        Yomichan Legal
    -        
    -        
    -        
    -        
    -        
    -        
    -        
    -        
    -        
    -    
    -    
    -        
    -

    Yomichan License

    + + + + Yomichan Legal + + + + + + + + + + + +
    + +

    Yomichan License

     Copyright (C) 2016-2020  Yomichan Authors
     
    @@ -33,21 +38,138 @@ GNU General Public License for more details.
     You should have received a copy of the GNU General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
     
    -

    EDRDG License

    + +

    EDRDG License

    -This package uses the EDICT and KANJIDIC dictionary files. These files are
    -the property of the Electronic Dictionary Research and Development Group,
    -and are used in conformance with the Group's licence.
    +This package uses the EDICT and KANJIDIC dictionary files. These files are
    +the property of the Electronic Dictionary Research and Development Group,
    +and are used in conformance with the Group's licence.
     
    -

    Third-Party Software Licenses

    - -
    -
    - + +

    Third-Party Software Licenses

    + +

    Bootstrap v3.3.7

    +
    +The MIT License (MIT)
    +
    +Copyright (c) 2011-2016 Twitter, Inc.
    +
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
    +
    +The above copyright notice and this permission notice shall be included in
    +all copies or substantial portions of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    +THE SOFTWARE.
    +
    + +

    Handlebars v4.0.6

    +
    +Copyright (C) 2011-2016 by Yehuda Katz
    +
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
    +
    +The above copyright notice and this permission notice shall be included in
    +all copies or substantial portions of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    +THE SOFTWARE.
    +
    + +

    jQuery v3.2.1

    +
    +Copyright JS Foundation and other contributors, https://js.foundation/
    +
    +This software consists of voluntary contributions made by many
    +individuals. For exact contribution history, see the revision history
    +available at https://github.com/jquery/jquery
    +
    +The following license applies to all parts of this software except as
    +documented below:
    +
    +====
    +
    +Permission is hereby granted, free of charge, to any person obtaining
    +a copy of this software and associated documentation files (the
    +"Software"), to deal in the Software without restriction, including
    +without limitation the rights to use, copy, modify, merge, publish,
    +distribute, sublicense, and/or sell copies of the Software, and to
    +permit persons to whom the Software is furnished to do so, subject to
    +the following conditions:
    +
    +The above copyright notice and this permission notice shall be
    +included in all copies or substantial portions of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
    +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
    +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    +
    +====
    +
    +All files located in the node_modules and external directories are
    +externally maintained libraries used by this software which have their
    +own licenses; we recommend you read them, as their terms may differ from
    +the terms above.
    +
    + +

    JSZip v3.1.3

    +
    +Copyright (c) 2009-2016 Stuart Knightley, David Duponchel, Franz Buchinger, António Afonso
    +
    +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
    +
    +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    +
    + +

    WanaKana v4.0.2

    +
    +The MIT License (MIT)
    +
    +Copyright (c) 2013 WaniKani Community Github
    +
    +Permission is hereby granted, free of charge, to any person obtaining a copy of
    +this software and associated documentation files (the "Software"), to deal in
    +the Software without restriction, including without limitation the rights to
    +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
    +the Software, and to permit persons to whom the Software is furnished to do so,
    +subject to the following conditions:
    +
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
    +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
    +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
    +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
    +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    +
    + +
    -- 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(-) 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 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(-) 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(-) 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 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(-) 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(-) 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(-) 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 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(+) 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(-) 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(-) 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(-) 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(-) 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

    - +
    - +
    - +