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(-) (limited to 'ext/mixed') 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 (limited to 'ext/mixed') 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(-) (limited to 'ext/mixed') 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(-) (limited to 'ext/mixed') 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(-) (limited to 'ext/mixed') 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 (limited to 'ext/mixed') 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 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(-) (limited to 'ext/mixed') 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 (limited to 'ext/mixed') 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 6dd6af05e1ed3e0da4091af073c38e1d8ec0268d Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 24 May 2020 14:01:21 -0400 Subject: Update background global object usage (#556) * Omit global window object for scripts used on the background page * Validate document exists before using * Remove dom.js from background.html --- ext/bg/background.html | 1 - ext/bg/js/anki-note-builder.js | 2 +- ext/bg/js/backend.js | 22 +++++++++++----------- ext/bg/js/background-main.js | 7 +++++-- ext/bg/js/database.js | 2 +- ext/mixed/js/core.js | 20 +++++++++++--------- 6 files changed, 29 insertions(+), 25 deletions(-) (limited to 'ext/mixed') diff --git a/ext/bg/background.html b/ext/bg/background.html index ca35a3c6..53e8b140 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -20,7 +20,6 @@ - diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index 76199db7..31e67394 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -155,7 +155,7 @@ class AnkiNoteBuilder { } static arrayBufferToBase64(arrayBuffer) { - return window.btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); + return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); } static stringReplaceAsync(str, regex, replacer) { diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 90895737..80b00d5f 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -65,12 +65,14 @@ class Backend { renderTemplate: this._renderTemplate.bind(this) }); - this.optionsContext = { - depth: 0, - url: window.location.href - }; + const url = (typeof window === 'object' && window !== null ? window.location.href : ''); + this.optionsContext = {depth: 0, url}; - this.clipboardPasteTarget = document.querySelector('#clipboard-paste-target'); + this.clipboardPasteTarget = ( + typeof document === 'object' && document !== null ? + document.querySelector('#clipboard-paste-target') : + null + ); this.popupWindow = null; @@ -704,6 +706,9 @@ class Backend { return await navigator.clipboard.readText(); } else { const clipboardPasteTarget = this.clipboardPasteTarget; + if (clipboardPasteTarget === null) { + throw new Error('Reading the clipboard is not supported in this context'); + } clipboardPasteTarget.value = ''; clipboardPasteTarget.focus(); document.execCommand('paste'); @@ -1005,13 +1010,8 @@ class Backend { } async _onCommandToggle() { - const optionsContext = { - depth: 0, - url: window.location.href - }; const source = 'popup'; - - const options = this.getOptions(optionsContext); + const options = this.getOptions(this.optionsContext); options.general.enable = !options.general.enable; await this._onApiOptionsSave({source}); } diff --git a/ext/bg/js/background-main.js b/ext/bg/js/background-main.js index 24117f4e..345b4a77 100644 --- a/ext/bg/js/background-main.js +++ b/ext/bg/js/background-main.js @@ -20,6 +20,9 @@ */ (async () => { - window.yomichanBackend = new Backend(); - await window.yomichanBackend.prepare(); + const backend = new Backend(); + if (typeof window === 'object' && window !== null) { + window.yomichanBackend = backend; + } + await backend.prepare(); })(); diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 930cd0d0..65e267ab 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -596,7 +596,7 @@ class Database { static _open(name, version, onUpgradeNeeded) { return new Promise((resolve, reject) => { - const request = window.indexedDB.open(name, version * 10); + const request = indexedDB.open(name, version * 10); request.onupgradeneeded = (event) => { try { diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index 257c7edf..bf877e72 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -177,7 +177,7 @@ function promiseTimeout(delay, resolveValue) { const complete = (callback, value) => { if (callback === null) { return; } if (timer !== null) { - window.clearTimeout(timer); + clearTimeout(timer); timer = null; } promiseResolve = null; @@ -192,7 +192,7 @@ function promiseTimeout(delay, resolveValue) { promiseResolve = resolve2; promiseReject = reject2; }); - timer = window.setTimeout(() => { + timer = setTimeout(() => { timer = null; resolve(resolveValue); }, delay); @@ -331,7 +331,7 @@ const yomichan = (() => { generateId(length) { const array = new Uint8Array(length); - window.crypto.getRandomValues(array); + crypto.getRandomValues(array); let id = ''; for (const value of array) { id += value.toString(16).padStart(2, '0'); @@ -364,7 +364,7 @@ const yomichan = (() => { const runtimeMessageCallback = ({action, params}, sender, sendResponse) => { let timeoutId = null; if (timeout !== null) { - timeoutId = window.setTimeout(() => { + timeoutId = setTimeout(() => { timeoutId = null; eventHandler.removeListener(runtimeMessageCallback); reject(new Error(`Listener timed out in ${timeout} ms`)); @@ -373,7 +373,7 @@ const yomichan = (() => { const cleanupResolve = (value) => { if (timeoutId !== null) { - window.clearTimeout(timeoutId); + clearTimeout(timeoutId); timeoutId = null; } eventHandler.removeListener(runtimeMessageCallback); @@ -453,10 +453,12 @@ const yomichan = (() => { // Private + _getUrl() { + return (typeof window === 'object' && window !== null ? window.location.href : ''); + } + _getLogContext() { - return { - url: window.location.href - }; + return {url: this._getUrl()}; } _onMessage({action, params}, sender, callback) { @@ -469,7 +471,7 @@ const yomichan = (() => { } _onMessageGetUrl() { - return {url: window.location.href}; + return {url: this._getUrl()}; } _onMessageOptionsUpdated({source}) { -- cgit v1.2.3 From 37f0396f1cf4f74833d9a4a364f503f4a7c3e34a Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 29 May 2020 19:44:53 -0400 Subject: DOM binder fixes (#564) * Fix incorrect updateValue function * Add source --- ext/bg/js/settings/dom-settings-binder.js | 5 +++-- ext/mixed/js/dom-data-binder.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'ext/mixed') 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 976a200ffc65e94f0246392f6b29505f1eb4f16c Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 30 May 2020 16:23:56 -0400 Subject: Backup update (#582) * Add function to assign all settings * Update how settings backups are restored * Remove page reload * Update profile index after importing --- ext/bg/js/backend.js | 17 +++++++---------- ext/bg/js/settings/backup.js | 8 +------- ext/bg/js/settings/settings-controller.js | 17 +++++++++++------ ext/mixed/js/api.js | 4 ++++ 4 files changed, 23 insertions(+), 23 deletions(-) (limited to 'ext/mixed') diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 80b00d5f..08ce82a2 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -122,7 +122,8 @@ class Backend { ['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)}], - ['getSettings', {async: false, contentScript: true, handler: this._onApiGetSettings.bind(this)}] + ['getSettings', {async: false, contentScript: true, handler: this._onApiGetSettings.bind(this)}], + ['setAllSettings', {async: true, contentScript: false, handler: this._onApiSetAllSettings.bind(this)}] ]); this._messageHandlersWithProgress = new Map([ ['importDictionaryArchive', {async: true, contentScript: false, handler: this._onApiImportDictionaryArchive.bind(this)}], @@ -317,15 +318,6 @@ class Backend { return useSchema ? JsonSchema.createProxy(options, this.optionsSchema) : options; } - setFullOptions(options) { - try { - this.options = JsonSchema.getValidValueOrDefault(this.optionsSchema, utilIsolate(options)); - } catch (e) { - // This shouldn't happen, but catch errors just in case of bugs - yomichan.logError(e); - } - } - getOptions(optionsContext, useSchema=false) { return this.getProfile(optionsContext, useSchema).options; } @@ -860,6 +852,11 @@ class Backend { return results; } + async _onApiSetAllSettings({value, source}) { + this.options = JsonSchema.getValidValueOrDefault(this.optionsSchema, value); + await this._onApiOptionsSave({source}); + } + // Command handlers _createActionListenerPort(port, sender, handlers) { diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js index e93e15bf..13f90886 100644 --- a/ext/bg/js/settings/backup.js +++ b/ext/bg/js/settings/backup.js @@ -141,7 +141,7 @@ class SettingsBackup { // Importing async _settingsImportSetOptionsFull(optionsFull) { - await this._settingsController.setOptionsFull(optionsFull); + await this._settingsController.setAllSettings(optionsFull); } _showSettingsImportError(error) { @@ -340,9 +340,6 @@ class SettingsBackup { // Assign options await this._settingsImportSetOptionsFull(optionsFull); - - // Reload settings page - window.location.reload(); } _onSettingsImportClick() { @@ -376,8 +373,5 @@ class SettingsBackup { // Assign options await this._settingsImportSetOptionsFull(optionsFull); - - // Reload settings page - window.location.reload(); } } diff --git a/ext/bg/js/settings/settings-controller.js b/ext/bg/js/settings/settings-controller.js index 9224aedf..4c902dff 100644 --- a/ext/bg/js/settings/settings-controller.js +++ b/ext/bg/js/settings/settings-controller.js @@ -38,9 +38,7 @@ class SettingsController extends EventDispatcher { set profileIndex(value) { if (this._profileIndex === value) { return; } - this._profileIndex = value; - this.trigger('optionsContextChanged'); - this._onOptionsUpdatedInternal(); + this._setProfileIndex(value); } prepare() { @@ -69,9 +67,10 @@ class SettingsController extends EventDispatcher { return utilBackend().getFullOptions(); } - async setOptionsFull(optionsFull) { - utilBackend().setFullOptions(utilBackgroundIsolate(optionsFull)); - await this.save(); + async setAllSettings(value) { + const profileIndex = value.profileCurrent; + await api.setAllSettings(value, this._source); + this._setProfileIndex(profileIndex); } async getGlobalSettings(targets) { @@ -104,6 +103,12 @@ class SettingsController extends EventDispatcher { // Private + _setProfileIndex(value) { + this._profileIndex = value; + this.trigger('optionsContextChanged'); + this._onOptionsUpdatedInternal(); + } + _onOptionsUpdated({source}) { if (source === this._source) { return; } this._onOptionsUpdatedInternal(); diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index 2d5ad9e7..075ea545 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -176,6 +176,10 @@ const api = (() => { return this._invoke('getSettings', {targets}); } + setAllSettings(value, source) { + return this._invoke('setAllSettings', {value, source}); + } + // Invoke functions with progress importDictionaryArchive(archiveContent, details, onProgress) { -- cgit v1.2.3 From 2c58b1c1091355e5e4f2a2c20d96051863549b8d Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 31 May 2020 18:17:12 -0400 Subject: Limit action port message size (#587) * Add onDisconnect handler * Update how error is posted * Update action ports to send long messages in fragments * Remove ack timer * Move message destructuring into try block --- ext/bg/js/backend.js | 47 +++++++++++++++++++++++++++++++++++++---------- ext/mixed/js/api.js | 26 +++++++++----------------- 2 files changed, 46 insertions(+), 27 deletions(-) (limited to 'ext/mixed') diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 08ce82a2..5eb7982d 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -861,6 +861,7 @@ class Backend { _createActionListenerPort(port, sender, handlers) { let hasStarted = false; + let messageString = ''; const onProgress = (...data) => { try { @@ -871,12 +872,34 @@ class Backend { } }; - const onMessage = async ({action, params}) => { + const onMessage = (message) => { if (hasStarted) { return; } - hasStarted = true; - port.onMessage.removeListener(onMessage); try { + const {action, data} = message; + switch (action) { + case 'fragment': + messageString += data; + break; + case 'invoke': + { + hasStarted = true; + port.onMessage.removeListener(onMessage); + + const messageData = JSON.parse(messageString); + messageString = null; + onMessageComplete(messageData); + } + break; + } + } catch (e) { + cleanup(e); + } + }; + + const onMessageComplete = async (message) => { + try { + const {action, params} = message; port.postMessage({type: 'ack'}); const messageHandler = handlers.get(action); @@ -893,25 +916,29 @@ class Backend { const result = async ? await promiseOrResult : promiseOrResult; port.postMessage({type: 'complete', data: result}); } catch (e) { - if (port !== null) { - port.postMessage({type: 'error', data: errorToJson(e)}); - } - cleanup(); + cleanup(e); } }; - const cleanup = () => { + const onDisconnect = () => { + cleanup(null); + }; + + const cleanup = (error) => { if (port === null) { return; } + if (error !== null) { + port.postMessage({type: 'error', data: errorToJson(error)}); + } if (!hasStarted) { port.onMessage.removeListener(onMessage); } - port.onDisconnect.removeListener(cleanup); + port.onDisconnect.removeListener(onDisconnect); port = null; handlers = null; }; port.onMessage.addListener(onMessage); - port.onDisconnect.addListener(cleanup); + port.onDisconnect.addListener(onDisconnect); } _getErrorLevelValue(errorLevel) { diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index 075ea545..5e3195d6 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -236,7 +236,6 @@ const api = (() => { _invokeWithProgress(action, params, onProgress, timeout=5000) { return new Promise((resolve, reject) => { - let timer = null; let port = null; if (typeof onProgress !== 'function') { @@ -245,12 +244,6 @@ const api = (() => { const onMessage = (message) => { switch (message.type) { - case 'ack': - if (timer !== null) { - clearTimeout(timer); - timer = null; - } - break; case 'progress': try { onProgress(...message.data); @@ -275,10 +268,6 @@ const api = (() => { }; const cleanup = () => { - if (timer !== null) { - clearTimeout(timer); - timer = null; - } if (port !== null) { port.onMessage.removeListener(onMessage); port.onDisconnect.removeListener(onDisconnect); @@ -288,17 +277,20 @@ const api = (() => { onProgress = null; }; - timer = setTimeout(() => { - cleanup(); - reject(new Error('Timeout')); - }, timeout); - (async () => { try { port = await this._createActionPort(timeout); port.onMessage.addListener(onMessage); port.onDisconnect.addListener(onDisconnect); - port.postMessage({action, params}); + + // Chrome has a maximum message size that can be sent, so longer messages must be fragmented. + const messageString = JSON.stringify({action, params}); + const fragmentSize = 1e7; // 10 MB + for (let i = 0, ii = messageString.length; i < ii; i += fragmentSize) { + const data = messageString.substring(i, i + fragmentSize); + port.postMessage({action: 'fragment', data}); + } + port.postMessage({action: 'invoke'}); } catch (e) { cleanup(); reject(e); -- cgit v1.2.3 From 0384e2afef68abd4236fc422f087b1a5f558da34 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sun, 7 Jun 2020 00:08:20 +0300 Subject: scale background icon when page is zoomed (#592) --- ext/mixed/css/display.css | 1 + 1 file changed, 1 insertion(+) (limited to 'ext/mixed') diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css index a92775b8..1f392de2 100644 --- a/ext/mixed/css/display.css +++ b/ext/mixed/css/display.css @@ -279,6 +279,7 @@ button.action-button { display: block; background-color: transparent; background-repeat: no-repeat; + background-size: contain; } .action-button[data-icon=entry-current]:before { -- cgit v1.2.3 From b614aca3ddd04b9d533959b2eabaa6db43b79f8f Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sun, 7 Jun 2020 00:08:46 +0300 Subject: fix css class name (#591) Broken in 90af55d4c84f545635f238178b30748a0e8093ee --- ext/mixed/css/display.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ext/mixed') diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css index 1f392de2..703cef1c 100644 --- a/ext/mixed/css/display.css +++ b/ext/mixed/css/display.css @@ -317,7 +317,7 @@ button.action-button { color: var(--very-light-text-color); } -.entry:not(.entry-current) .current { +.entry:not(.entry-current) .action-current-indicator { display: none; } -- cgit v1.2.3 From 9767b765536279023045ed4280b12d297ec78f0a Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 7 Jun 2020 21:40:11 -0400 Subject: Use cross frame API (#553) * Use new CrossFrameAPI for popup proxy communication * Remove use of old cross-frame communication classes * Remove use of old cross-frame communication files * Make the crossFrame object a member of the api object --- ext/bg/background.html | 1 - ext/bg/context.html | 1 + ext/bg/js/backend-api-forwarder.js | 44 ------------- ext/bg/js/backend.js | 4 -- ext/bg/js/search-main.js | 1 - ext/bg/search.html | 1 + ext/bg/settings-popup-preview.html | 2 +- ext/bg/settings.html | 1 + ext/fg/float.html | 1 + ext/fg/js/float-main.js | 1 - ext/fg/js/frontend-api-receiver.js | 76 ---------------------- ext/fg/js/frontend-api-sender.js | 128 ------------------------------------- ext/fg/js/popup-factory.js | 7 +- ext/fg/js/popup-proxy.js | 6 +- ext/manifest.json | 3 +- ext/mixed/js/api.js | 18 +++++- 16 files changed, 29 insertions(+), 266 deletions(-) delete mode 100644 ext/bg/js/backend-api-forwarder.js delete mode 100644 ext/fg/js/frontend-api-receiver.js delete mode 100644 ext/fg/js/frontend-api-sender.js (limited to 'ext/mixed') diff --git a/ext/bg/background.html b/ext/bg/background.html index 53e8b140..d51858a7 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -28,7 +28,6 @@ - diff --git a/ext/bg/context.html b/ext/bg/context.html index 93012d70..89695d0e 100644 --- a/ext/bg/context.html +++ b/ext/bg/context.html @@ -180,6 +180,7 @@
+ diff --git a/ext/bg/js/backend-api-forwarder.js b/ext/bg/js/backend-api-forwarder.js deleted file mode 100644 index 4ac12730..00000000 --- a/ext/bg/js/backend-api-forwarder.js +++ /dev/null @@ -1,44 +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 BackendApiForwarder { - prepare() { - chrome.runtime.onConnect.addListener(this._onConnect.bind(this)); - } - - _onConnect(port) { - if (port.name !== 'backend-api-forwarder') { return; } - - let tabId; - if (!( - port.sender && - port.sender.tab && - (typeof (tabId = port.sender.tab.id)) === 'number' - )) { - port.disconnect(); - return; - } - - const forwardPort = chrome.tabs.connect(tabId, {name: 'frontend-api-receiver'}); - - port.onMessage.addListener((message) => forwardPort.postMessage(message)); - forwardPort.onMessage.addListener((message) => port.postMessage(message)); - port.onDisconnect.addListener(() => forwardPort.disconnect()); - forwardPort.onDisconnect.addListener(() => port.disconnect()); - } -} diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 5eb7982d..7971d16f 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -20,7 +20,6 @@ * AnkiNoteBuilder * AudioSystem * AudioUriBuilder - * BackendApiForwarder * ClipboardMonitor * Database * DictionaryImporter @@ -76,9 +75,6 @@ class Backend { this.popupWindow = null; - const apiForwarder = new BackendApiForwarder(); - apiForwarder.prepare(); - this._defaultBrowserActionTitle = null; this._isPrepared = false; this._prepareError = false; diff --git a/ext/bg/js/search-main.js b/ext/bg/js/search-main.js index 3e089594..f18d6d88 100644 --- a/ext/bg/js/search-main.js +++ b/ext/bg/js/search-main.js @@ -24,7 +24,6 @@ async function injectSearchFrontend() { await dynamicLoader.loadScripts([ '/mixed/js/text-scanner.js', - '/fg/js/frontend-api-receiver.js', '/fg/js/frame-offset-forwarder.js', '/fg/js/popup.js', '/fg/js/popup-factory.js', diff --git a/ext/bg/search.html b/ext/bg/search.html index c0721e5c..de08cdae 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -71,6 +71,7 @@ + diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html index 2f0b841b..fe92f24f 100644 --- a/ext/bg/settings-popup-preview.html +++ b/ext/bg/settings-popup-preview.html @@ -119,13 +119,13 @@ + - diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 7c295f0d..a530534c 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1121,6 +1121,7 @@ + diff --git a/ext/fg/float.html b/ext/fg/float.html index e9f5acae..17dbcc6d 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -40,6 +40,7 @@ + diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js index 249b4dbe..2ec334c8 100644 --- a/ext/fg/js/float-main.js +++ b/ext/fg/js/float-main.js @@ -24,7 +24,6 @@ async function injectPopupNested() { await dynamicLoader.loadScripts([ '/mixed/js/text-scanner.js', - '/fg/js/frontend-api-sender.js', '/fg/js/popup.js', '/fg/js/popup-proxy.js', '/fg/js/frontend.js', diff --git a/ext/fg/js/frontend-api-receiver.js b/ext/fg/js/frontend-api-receiver.js deleted file mode 100644 index 3fa9e8b6..00000000 --- a/ext/fg/js/frontend-api-receiver.js +++ /dev/null @@ -1,76 +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 FrontendApiReceiver { - constructor(source, messageHandlers) { - this._source = source; - this._messageHandlers = messageHandlers; - } - - prepare() { - chrome.runtime.onConnect.addListener(this._onConnect.bind(this)); - } - - _onConnect(port) { - if (port.name !== 'frontend-api-receiver') { return; } - - port.onMessage.addListener(this._onMessage.bind(this, port)); - } - - _onMessage(port, {id, action, params, target, senderId}) { - if (target !== this._source) { return; } - - const messageHandler = this._messageHandlers.get(action); - if (typeof messageHandler === 'undefined') { return; } - - const {handler, async} = messageHandler; - - this._sendAck(port, id, senderId); - if (async) { - this._invokeHandlerAsync(handler, params, port, id, senderId); - } else { - this._invokeHandler(handler, params, port, id, senderId); - } - } - - _invokeHandler(handler, params, port, id, senderId) { - try { - const result = handler(params); - this._sendResult(port, id, senderId, {result}); - } catch (error) { - this._sendResult(port, id, senderId, {error: errorToJson(error)}); - } - } - - async _invokeHandlerAsync(handler, params, port, id, senderId) { - try { - const result = await handler(params); - this._sendResult(port, id, senderId, {result}); - } catch (error) { - this._sendResult(port, id, senderId, {error: errorToJson(error)}); - } - } - - _sendAck(port, id, senderId) { - port.postMessage({type: 'ack', id, senderId}); - } - - _sendResult(port, id, senderId, data) { - port.postMessage({type: 'result', id, senderId, data}); - } -} diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js deleted file mode 100644 index 4dcde638..00000000 --- a/ext/fg/js/frontend-api-sender.js +++ /dev/null @@ -1,128 +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 FrontendApiSender { - constructor(target) { - this._target = target; - this._senderId = yomichan.generateId(16); - this._ackTimeout = 3000; // 3 seconds - this._responseTimeout = 10000; // 10 seconds - this._callbacks = new Map(); - this._disconnected = false; - this._nextId = 0; - this._port = null; - } - - invoke(action, params) { - if (this._disconnected) { - // attempt to reconnect the next time - this._disconnected = false; - return Promise.reject(new Error('Disconnected')); - } - - if (this._port === null) { - this._createPort(); - } - - const id = `${this._nextId}`; - ++this._nextId; - - return new Promise((resolve, reject) => { - const info = {id, resolve, reject, ack: false, timer: null}; - this._callbacks.set(id, info); - info.timer = setTimeout(() => this._onError(id, 'Timeout (ack)'), this._ackTimeout); - - this._port.postMessage({id, action, params, target: this._target, senderId: this._senderId}); - }); - } - - _createPort() { - this._port = chrome.runtime.connect(null, {name: 'backend-api-forwarder'}); - this._port.onDisconnect.addListener(this._onDisconnect.bind(this)); - this._port.onMessage.addListener(this._onMessage.bind(this)); - } - - _onMessage({type, id, data, senderId}) { - if (senderId !== this._senderId) { return; } - switch (type) { - case 'ack': - this._onAck(id); - break; - case 'result': - this._onResult(id, data); - break; - } - } - - _onDisconnect() { - this._disconnected = true; - this._port = null; - - for (const id of this._callbacks.keys()) { - this._onError(id, 'Disconnected'); - } - } - - _onAck(id) { - const info = this._callbacks.get(id); - if (typeof info === 'undefined') { - yomichan.logWarning(new Error(`ID ${id} not found for ack`)); - return; - } - - if (info.ack) { - yomichan.logWarning(new Error(`Request ${id} already ack'd`)); - return; - } - - info.ack = true; - clearTimeout(info.timer); - info.timer = setTimeout(() => this._onError(id, 'Timeout (response)'), this._responseTimeout); - } - - _onResult(id, data) { - const info = this._callbacks.get(id); - if (typeof info === 'undefined') { - yomichan.logWarning(new Error(`ID ${id} not found`)); - return; - } - - if (!info.ack) { - yomichan.logWarning(new Error(`Request ${id} not ack'd`)); - return; - } - - this._callbacks.delete(id); - clearTimeout(info.timer); - info.timer = null; - - if (typeof data.error !== 'undefined') { - info.reject(jsonToError(data.error)); - } else { - info.resolve(data.result); - } - } - - _onError(id, reason) { - const info = this._callbacks.get(id); - if (typeof info === 'undefined') { return; } - this._callbacks.delete(id); - info.timer = null; - info.reject(new Error(reason)); - } -} diff --git a/ext/fg/js/popup-factory.js b/ext/fg/js/popup-factory.js index b10acbaf..b5997253 100644 --- a/ext/fg/js/popup-factory.js +++ b/ext/fg/js/popup-factory.js @@ -16,8 +16,8 @@ */ /* global - * FrontendApiReceiver * Popup + * api */ class PopupFactory { @@ -29,7 +29,7 @@ class PopupFactory { // Public functions async prepare() { - const apiReceiver = new FrontendApiReceiver(`popup-factory#${this._frameId}`, new Map([ + api.crossFrame.registerHandlers([ ['getOrCreatePopup', {async: false, handler: this._onApiGetOrCreatePopup.bind(this)}], ['setOptionsContext', {async: true, handler: this._onApiSetOptionsContext.bind(this)}], ['hide', {async: false, handler: this._onApiHide.bind(this)}], @@ -41,8 +41,7 @@ class PopupFactory { ['clearAutoPlayTimer', {async: false, handler: this._onApiClearAutoPlayTimer.bind(this)}], ['setContentScale', {async: false, handler: this._onApiSetContentScale.bind(this)}], ['getUrl', {async: false, handler: this._onApiGetUrl.bind(this)}] - ])); - apiReceiver.prepare(); + ]); } getOrCreatePopup(id=null, parentId=null, depth=null) { diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 82da839a..3387d941 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -16,7 +16,7 @@ */ /* global - * FrontendApiSender + * api */ class PopupProxy { @@ -24,7 +24,7 @@ class PopupProxy { this._id = id; this._depth = depth; this._parentPopupId = parentPopupId; - this._apiSender = new FrontendApiSender(`popup-factory#${parentFrameId}`); + this._parentFrameId = parentFrameId; this._getFrameOffset = getFrameOffset; this._setDisabled = setDisabled; @@ -111,7 +111,7 @@ class PopupProxy { // Private _invoke(action, params={}) { - return this._apiSender.invoke(action, params); + return api.crossFrame.invoke(this._parentFrameId, action, params); } async _updateFrameOffset() { diff --git a/ext/manifest.json b/ext/manifest.json index 6db257d7..75334675 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -36,13 +36,12 @@ "matches": ["http://*/*", "https://*/*", "file://*/*"], "js": [ "mixed/js/core.js", + "mixed/js/comm.js", "mixed/js/dom.js", "mixed/js/api.js", "mixed/js/dynamic-loader.js", "mixed/js/text-scanner.js", "fg/js/document.js", - "fg/js/frontend-api-sender.js", - "fg/js/frontend-api-receiver.js", "fg/js/popup.js", "fg/js/source.js", "fg/js/popup-factory.js", diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index 5e3195d6..c54196e2 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -15,10 +15,23 @@ * along with this program. If not, see . */ +/* global + * CrossFrameAPI + */ + const api = (() => { class API { constructor() { this._forwardLogsToBackendEnabled = false; + this._crossFrame = new CrossFrameAPI(); + } + + get crossFrame() { + return this._crossFrame; + } + + prepare() { + this._crossFrame.prepare(); } forwardLogsToBackend() { @@ -331,5 +344,8 @@ const api = (() => { } } - return new API(); + // eslint-disable-next-line no-shadow + const api = new API(); + api.prepare(); + return api; })(); -- cgit v1.2.3 From a84f188b737c616f6fc5172ef13af0b00fee03f2 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 10 Jun 2020 20:58:46 -0400 Subject: Handle cases where platform info is not available (#597) * Handle cases where platform info is not available * Safely return the correct os property --- ext/mixed/js/environment.js | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) (limited to 'ext/mixed') diff --git a/ext/mixed/js/environment.js b/ext/mixed/js/environment.js index e5bc20a7..5bd84010 100644 --- a/ext/mixed/js/environment.js +++ b/ext/mixed/js/environment.js @@ -32,17 +32,40 @@ class Environment { async _loadEnvironmentInfo() { const browser = await this._getBrowser(); - const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve)); - const modifierInfo = this._getModifierInfo(browser, platform.os); + const os = await this._getOperatingSystem(); + const modifierInfo = this._getModifierInfo(browser, os); return { browser, - platform: { - os: platform.os - }, + platform: {os}, modifiers: modifierInfo }; } + async _getOperatingSystem() { + try { + const {os} = await this._getPlatformInfo(); + if (typeof os === 'string') { + return os; + } + } catch (e) { + // NOP + } + return 'unknown'; + } + + _getPlatformInfo() { + return new Promise((resolve, reject) => { + chrome.runtime.getPlatformInfo((result) => { + const error = chrome.runtime.lastError; + if (error) { + reject(error); + } else { + resolve(result); + } + }); + }); + } + async _getBrowser() { if (EXTENSION_IS_BROWSER_EDGE) { return 'edge'; @@ -96,8 +119,15 @@ class Environment { ['meta', 'Super'] ]; break; - default: - throw new Error(`Invalid OS: ${os}`); + default: // 'unknown', etc + separator = ' + '; + osKeys = [ + ['alt', 'Alt'], + ['ctrl', 'Ctrl'], + ['shift', 'Shift'], + ['meta', 'Meta'] + ]; + break; } const isFirefox = (browser === 'firefox' || browser === 'firefox-mobile'); -- cgit v1.2.3 From 8a7ff6a18c78bbc2048dd4017597ccc4f5ee4106 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 13 Jun 2020 10:23:04 -0400 Subject: Replace XMLHttpRequest (#562) * Replace XMLHttpRequest with fetch * Implement fetch placeholder for tests --- ext/bg/js/audio-uri-builder.js | 41 ++++++++++++++++---------- ext/bg/js/request.js | 42 +++++++++++++-------------- ext/mixed/js/audio-system.js | 30 +++++++++---------- test/test-database.js | 66 +++++++----------------------------------- 4 files changed, 73 insertions(+), 106 deletions(-) (limited to 'ext/mixed') diff --git a/ext/bg/js/audio-uri-builder.js b/ext/bg/js/audio-uri-builder.js index 27e97680..7dd5e4c6 100644 --- a/ext/bg/js/audio-uri-builder.js +++ b/ext/bg/js/audio-uri-builder.js @@ -82,16 +82,24 @@ class AudioUriBuilder { } async _getUriJpod101Alternate(definition) { - const response = await new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post'); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.addEventListener('error', () => reject(new Error('Failed to scrape audio data'))); - xhr.addEventListener('load', () => resolve(xhr.responseText)); - xhr.send(`post=dictionary_reference&match_type=exact&search_query=${encodeURIComponent(definition.expression)}`); + const fetchUrl = 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post'; + const data = `post=dictionary_reference&match_type=exact&search_query=${encodeURIComponent(definition.expression)}`; + const response = await fetch(fetchUrl, { + method: 'POST', + mode: 'no-cors', + cache: 'default', + credentials: 'omit', + redirect: 'follow', + referrerPolicy: 'no-referrer', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: data }); + const responseText = await response.text(); + console.log(responseText); - const dom = new DOMParser().parseFromString(response, 'text/html'); + const dom = new DOMParser().parseFromString(responseText, 'text/html'); for (const row of dom.getElementsByClassName('dc-result-row')) { try { const url = row.querySelector('audio>source[src]').getAttribute('src'); @@ -108,15 +116,18 @@ class AudioUriBuilder { } async _getUriJisho(definition) { - const response = await new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('GET', `https://jisho.org/search/${definition.expression}`); - xhr.addEventListener('error', () => reject(new Error('Failed to scrape audio data'))); - xhr.addEventListener('load', () => resolve(xhr.responseText)); - xhr.send(); + const fetchUrl = `https://jisho.org/search/${definition.expression}`; + const response = await fetch(fetchUrl, { + method: 'GET', + mode: 'no-cors', + cache: 'default', + credentials: 'omit', + redirect: 'follow', + referrerPolicy: 'no-referrer' }); + const responseText = await response.text(); - const dom = new DOMParser().parseFromString(response, 'text/html'); + const dom = new DOMParser().parseFromString(responseText, 'text/html'); try { const audio = dom.getElementById(`audio_${definition.expression}:${definition.reading}`); if (audio !== null) { diff --git a/ext/bg/js/request.js b/ext/bg/js/request.js index 957ac0f5..d1c6ed4e 100644 --- a/ext/bg/js/request.js +++ b/ext/bg/js/request.js @@ -16,28 +16,28 @@ */ -function requestText(url, action, params) { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.overrideMimeType('text/plain'); - xhr.addEventListener('load', () => resolve(xhr.responseText)); - xhr.addEventListener('error', () => reject(new Error('Failed to connect'))); - xhr.open(action, url); - if (params) { - xhr.send(JSON.stringify(params)); - } else { - xhr.send(); - } +async function requestText(url, method, data) { + const response = await fetch(url, { + method, + mode: 'no-cors', + cache: 'default', + credentials: 'omit', + redirect: 'follow', + referrerPolicy: 'no-referrer', + body: (data ? JSON.stringify(data) : void 0) }); + return await response.text(); } -async function requestJson(url, action, params) { - const responseText = await requestText(url, action, params); - try { - return JSON.parse(responseText); - } catch (e) { - const error = new Error(`Invalid response (${e.message || e})`); - error.data = {url, action, params, responseText}; - throw error; - } +async function requestJson(url, method, data) { + const response = await fetch(url, { + method, + mode: 'no-cors', + cache: 'default', + credentials: 'omit', + redirect: 'follow', + referrerPolicy: 'no-referrer', + body: (data ? JSON.stringify(data) : void 0) + }); + return await response.json(); } diff --git a/ext/mixed/js/audio-system.js b/ext/mixed/js/audio-system.js index fdfb0b10..c590b909 100644 --- a/ext/mixed/js/audio-system.js +++ b/ext/mixed/js/audio-system.js @@ -169,22 +169,22 @@ class AudioSystem { }); } - _createAudioBinaryFromUrl(url) { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.responseType = 'arraybuffer'; - xhr.addEventListener('load', async () => { - const arrayBuffer = xhr.response; - if (!await this._isAudioBinaryValid(arrayBuffer)) { - reject(new Error('Could not retrieve audio')); - } else { - resolve(arrayBuffer); - } - }); - xhr.addEventListener('error', () => reject(new Error('Failed to connect'))); - xhr.open('GET', url); - xhr.send(); + async _createAudioBinaryFromUrl(url) { + const response = await fetch(url, { + method: 'GET', + mode: 'no-cors', + cache: 'default', + credentials: 'omit', + redirect: 'follow', + referrerPolicy: 'no-referrer' }); + const arrayBuffer = await response.arrayBuffer(); + + if (!await this._isAudioBinaryValid(arrayBuffer)) { + throw new Error('Could not retrieve audio'); + } + + return arrayBuffer; } _isAudioValid(audio) { diff --git a/test/test-database.js b/test/test-database.js index e8a4a343..63989857 100644 --- a/test/test-database.js +++ b/test/test-database.js @@ -38,60 +38,6 @@ const chrome = { } }; -class XMLHttpRequest { - constructor() { - this._eventCallbacks = new Map(); - this._url = ''; - this._responseText = null; - } - - overrideMimeType() { - // NOP - } - - addEventListener(eventName, callback) { - let callbacks = this._eventCallbacks.get(eventName); - if (typeof callbacks === 'undefined') { - callbacks = []; - this._eventCallbacks.set(eventName, callbacks); - } - callbacks.push(callback); - } - - open(action, url2) { - this._url = url2; - } - - send() { - const filePath = url.fileURLToPath(this._url); - Promise.resolve() - .then(() => { - let source; - try { - source = fs.readFileSync(filePath, {encoding: 'utf8'}); - } catch (e) { - this._trigger('error'); - return; - } - this._responseText = source; - this._trigger('load'); - }); - } - - get responseText() { - return this._responseText; - } - - _trigger(eventName, ...args) { - const callbacks = this._eventCallbacks.get(eventName); - if (typeof callbacks === 'undefined') { return; } - - for (let i = 0, ii = callbacks.length; i < ii; ++i) { - callbacks[i](...args); - } - } -} - class Image { constructor() { this._src = ''; @@ -138,11 +84,21 @@ class Image { } } +async function fetch(url2) { + const filePath = url.fileURLToPath(url2); + await Promise.resolve(); + const content = fs.readFileSync(filePath, {encoding: null}); + return { + text: async () => Promise.resolve(content.toString('utf8')), + json: async () => Promise.resolve(JSON.parse(content.toString('utf8'))) + }; +} + const vm = new VM({ chrome, Image, - XMLHttpRequest, + fetch, indexedDB: global.indexedDB, IDBKeyRange: global.IDBKeyRange, JSZip: yomichanTest.JSZip, -- cgit v1.2.3 From 9e28db6ef7df990ee035b5e191727f8c0d3d3139 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 21 Jun 2020 15:51:36 -0400 Subject: Safely handle volume values that are out of range (#617) --- ext/mixed/js/display.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'ext/mixed') diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 380134ad..90fd1037 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -807,9 +807,10 @@ class Display { this._stopPlayingAudio(); + const volume = Math.max(0.0, Math.min(1.0, this.options.audio.volume / 100.0)); this.audioPlaying = audio; audio.currentTime = 0; - audio.volume = this.options.audio.volume / 100.0; + audio.volume = Number.isFinite(volume) ? volume : 1.0; const playPromise = audio.play(); if (typeof playPromise !== 'undefined') { try { -- cgit v1.2.3 From e23504613f8526b90a497512c086ed48e66cde95 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 21 Jun 2020 16:07:51 -0400 Subject: Use DOMTextScanner (#536) * Use DOMTextScanner instead of TextSourceRange.seek* * Move getNodesInRange to dom.js * Move anyNodeMatchesSelector to dom.js * Remove unused functions * Update tests * Add layoutAwareScan option * Use layoutAwareScan for source and sentence scanning * Remove unused IGNORE_TEXT_PATTERN --- ext/bg/data/options-schema.json | 7 +- ext/bg/js/options.js | 3 +- ext/bg/js/search-query-parser.js | 8 +- ext/bg/search.html | 1 + ext/bg/settings-popup-preview.html | 1 + ext/bg/settings.html | 4 + ext/fg/float.html | 1 + ext/fg/js/document.js | 11 +- ext/fg/js/frontend.js | 14 ++- ext/fg/js/source.js | 224 ++----------------------------------- ext/manifest.json | 1 + ext/mixed/js/display.js | 11 +- ext/mixed/js/dom.js | 38 +++++++ ext/mixed/js/text-scanner.js | 11 +- test/test-document.js | 14 ++- 15 files changed, 102 insertions(+), 247 deletions(-) (limited to 'ext/mixed') diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json index 0379fa75..5885e036 100644 --- a/ext/bg/data/options-schema.json +++ b/ext/bg/data/options-schema.json @@ -321,7 +321,8 @@ "enablePopupSearch", "enableOnPopupExpressions", "enableOnSearchPage", - "enableSearchTags" + "enableSearchTags", + "layoutAwareScan" ], "properties": { "middleMouse": { @@ -383,6 +384,10 @@ "enableSearchTags": { "type": "boolean", "default": false + }, + "layoutAwareScan": { + "type": "boolean", + "default": false } } }, diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 97368a0b..170e4799 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -203,7 +203,8 @@ function profileOptionsCreateDefaults() { enablePopupSearch: false, enableOnPopupExpressions: false, enableOnSearchPage: true, - enableSearchTags: false + enableSearchTags: false, + layoutAwareScan: false }, translation: { diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index addfc686..97e98b40 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -75,15 +75,17 @@ class QueryParser { async _search(textSource, cause) { if (textSource === null) { return null; } - const searchText = this._textScanner.getTextSourceContent(textSource, this._options.scanning.length); + const {length: scanLength, layoutAwareScan} = this._options.scanning; + const searchText = this._textScanner.getTextSourceContent(textSource, scanLength, layoutAwareScan); if (searchText.length === 0) { return null; } const {definitions, length} = await api.termsFind(searchText, {}, this._getOptionsContext()); if (definitions.length === 0) { return null; } - const sentence = docSentenceExtract(textSource, this._options.anki.sentenceExt); + const sentenceExtent = this._options.anki.sentenceExt; + const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan); - textSource.setEndOffset(length); + textSource.setEndOffset(length, layoutAwareScan); this._setContent('terms', {definitions, context: { focus: false, diff --git a/ext/bg/search.html b/ext/bg/search.html index de08cdae..4a28dd88 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -79,6 +79,7 @@ + diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html index fe92f24f..5eecd005 100644 --- a/ext/bg/settings-popup-preview.html +++ b/ext/bg/settings-popup-preview.html @@ -126,6 +126,7 @@ + diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 118a13b9..77b61aef 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -400,6 +400,10 @@ +
+ +
+
diff --git a/ext/fg/float.html b/ext/fg/float.html index 17dbcc6d..3e41cde5 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -46,6 +46,7 @@ + diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index d639bc86..c288502c 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -17,6 +17,7 @@ /* global * DOM + * DOMTextScanner * TextSourceElement * TextSourceRange */ @@ -152,14 +153,14 @@ function docRangeFromPoint(x, y, deepDomScan) { } } -function docSentenceExtract(source, extent) { +function docSentenceExtract(source, extent, layoutAwareScan) { const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'}; const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'}; const terminators = '…。..??!!'; const sourceLocal = source.clone(); - const position = sourceLocal.setStartOffset(extent); - sourceLocal.setEndOffset(extent * 2 - position, true); + const position = sourceLocal.setStartOffset(extent, layoutAwareScan); + sourceLocal.setEndOffset(extent * 2 - position, layoutAwareScan, true); const content = sourceLocal.text(); let quoteStack = []; @@ -232,7 +233,7 @@ function isPointInRange(x, y, range) { const nodePre = range.endContainer; const offsetPre = range.endOffset; try { - const {node, offset, content} = TextSourceRange.seekForward(range.endContainer, range.endOffset, 1); + const {node, offset, content} = new DOMTextScanner(range.endContainer, range.endOffset, true, false).seek(1); range.setEnd(node, offset); if (!isWhitespace(content) && DOM.isPointInAnyRect(x, y, range.getClientRects())) { @@ -243,7 +244,7 @@ function isPointInRange(x, y, range) { } // Scan backward - const {node, offset, content} = TextSourceRange.seekBackward(range.startContainer, range.startOffset, 1); + const {node, offset, content} = new DOMTextScanner(range.startContainer, range.startOffset, true, false).seek(-1); range.setStart(node, offset); if (!isWhitespace(content) && DOM.isPointInAnyRect(x, y, range.getClientRects())) { diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 70bd8a48..ab455c09 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -258,32 +258,36 @@ class Frontend { } async _findTerms(textSource, optionsContext) { - const searchText = this._textScanner.getTextSourceContent(textSource, this._options.scanning.length); + const {length: scanLength, layoutAwareScan} = this._options.scanning; + const searchText = this._textScanner.getTextSourceContent(textSource, scanLength, layoutAwareScan); if (searchText.length === 0) { return null; } const {definitions, length} = await api.termsFind(searchText, {}, optionsContext); if (definitions.length === 0) { return null; } - textSource.setEndOffset(length); + textSource.setEndOffset(length, layoutAwareScan); return {definitions, type: 'terms'}; } async _findKanji(textSource, optionsContext) { - const searchText = this._textScanner.getTextSourceContent(textSource, 1); + const layoutAwareScan = this._options.scanning.layoutAwareScan; + const searchText = this._textScanner.getTextSourceContent(textSource, 1, layoutAwareScan); if (searchText.length === 0) { return null; } const definitions = await api.kanjiFind(searchText, optionsContext); if (definitions.length === 0) { return null; } - textSource.setEndOffset(1); + textSource.setEndOffset(1, layoutAwareScan); return {definitions, type: 'kanji'}; } _showContent(textSource, focus, definitions, type, optionsContext) { const {url} = optionsContext; - const sentence = docSentenceExtract(textSource, this._options.anki.sentenceExt); + const sentenceExtent = this._options.anki.sentenceExt; + const layoutAwareScan = this._options.scanning.layoutAwareScan; + const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan); this._showPopupContent( textSource, optionsContext, diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index fa4706f2..38810f07 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -15,9 +15,9 @@ * along with this program. If not, see . */ -// \u200c (Zero-width non-joiner) appears on Google Docs from Chrome 76 onwards -const IGNORE_TEXT_PATTERN = /\u200c/; - +/* global + * DOMTextScanner + */ /* * TextSourceRange @@ -46,19 +46,19 @@ class TextSourceRange { return this.content; } - setEndOffset(length, fromEnd=false) { + setEndOffset(length, layoutAwareScan, fromEnd=false) { const state = ( fromEnd ? - TextSourceRange.seekForward(this.range.endContainer, this.range.endOffset, length) : - TextSourceRange.seekForward(this.range.startContainer, this.range.startOffset, length) + new DOMTextScanner(this.range.endContainer, this.range.endOffset, !layoutAwareScan, layoutAwareScan).seek(length) : + new DOMTextScanner(this.range.startContainer, this.range.startOffset, !layoutAwareScan, layoutAwareScan).seek(length) ); this.range.setEnd(state.node, state.offset); this.content = (fromEnd ? this.content + state.content : state.content); return length - state.remainder; } - setStartOffset(length) { - const state = TextSourceRange.seekBackward(this.range.startContainer, this.range.startOffset, length); + setStartOffset(length, layoutAwareScan) { + const state = new DOMTextScanner(this.range.startContainer, this.range.startOffset, !layoutAwareScan, layoutAwareScan).seek(-length); this.range.setStart(state.node, state.offset); this.rangeStartOffset = this.range.startOffset; this.content = state.content + this.content; @@ -110,154 +110,6 @@ class TextSourceRange { } } - static shouldEnter(node) { - switch (node.nodeName.toUpperCase()) { - case 'RT': - case 'SCRIPT': - case 'STYLE': - return false; - } - - const style = window.getComputedStyle(node); - return !( - style.visibility === 'hidden' || - style.display === 'none' || - parseFloat(style.fontSize) === 0 - ); - } - - static getRubyElement(node) { - node = TextSourceRange.getParentElement(node); - if (node !== null && node.nodeName.toUpperCase() === 'RT') { - node = node.parentNode; - return (node !== null && node.nodeName.toUpperCase() === 'RUBY') ? node : null; - } - return null; - } - - static seekForward(node, offset, length) { - const state = {node, offset, remainder: length, content: ''}; - if (length <= 0) { - return state; - } - - const TEXT_NODE = Node.TEXT_NODE; - const ELEMENT_NODE = Node.ELEMENT_NODE; - let resetOffset = false; - - const ruby = TextSourceRange.getRubyElement(node); - if (ruby !== null) { - node = ruby; - resetOffset = true; - } - - while (node !== null) { - let visitChildren = true; - const nodeType = node.nodeType; - - if (nodeType === TEXT_NODE) { - state.node = node; - if (TextSourceRange.seekForwardTextNode(state, resetOffset)) { - break; - } - resetOffset = true; - } else if (nodeType === ELEMENT_NODE) { - visitChildren = TextSourceRange.shouldEnter(node); - } - - node = TextSourceRange.getNextNode(node, visitChildren); - } - - return state; - } - - static seekForwardTextNode(state, resetOffset) { - const nodeValue = state.node.nodeValue; - const nodeValueLength = nodeValue.length; - let content = state.content; - let offset = resetOffset ? 0 : state.offset; - let remainder = state.remainder; - let result = false; - - for (; offset < nodeValueLength; ++offset) { - const c = nodeValue[offset]; - if (!IGNORE_TEXT_PATTERN.test(c)) { - content += c; - if (--remainder <= 0) { - result = true; - ++offset; - break; - } - } - } - - state.offset = offset; - state.content = content; - state.remainder = remainder; - return result; - } - - static seekBackward(node, offset, length) { - const state = {node, offset, remainder: length, content: ''}; - if (length <= 0) { - return state; - } - - const TEXT_NODE = Node.TEXT_NODE; - const ELEMENT_NODE = Node.ELEMENT_NODE; - let resetOffset = false; - - const ruby = TextSourceRange.getRubyElement(node); - if (ruby !== null) { - node = ruby; - resetOffset = true; - } - - while (node !== null) { - let visitChildren = true; - const nodeType = node.nodeType; - - if (nodeType === TEXT_NODE) { - state.node = node; - if (TextSourceRange.seekBackwardTextNode(state, resetOffset)) { - break; - } - resetOffset = true; - } else if (nodeType === ELEMENT_NODE) { - visitChildren = TextSourceRange.shouldEnter(node); - } - - node = TextSourceRange.getPreviousNode(node, visitChildren); - } - - return state; - } - - static seekBackwardTextNode(state, resetOffset) { - const nodeValue = state.node.nodeValue; - let content = state.content; - let offset = resetOffset ? nodeValue.length : state.offset; - let remainder = state.remainder; - let result = false; - - for (; offset > 0; --offset) { - const c = nodeValue[offset - 1]; - if (!IGNORE_TEXT_PATTERN.test(c)) { - content = c + content; - if (--remainder <= 0) { - result = true; - --offset; - break; - } - } - } - - state.offset = offset; - state.content = content; - state.remainder = remainder; - return result; - } - static getParentElement(node) { while (node !== null && node.nodeType !== Node.ELEMENT_NODE) { node = node.parentNode; @@ -290,66 +142,6 @@ class TextSourceRange { return writingMode; } } - - static getNodesInRange(range) { - const end = range.endContainer; - const nodes = []; - for (let node = range.startContainer; node !== null; node = TextSourceRange.getNextNode(node, true)) { - nodes.push(node); - if (node === end) { break; } - } - return nodes; - } - - static getNextNode(node, visitChildren) { - let next = visitChildren ? node.firstChild : null; - if (next === null) { - while (true) { - next = node.nextSibling; - if (next !== null) { break; } - - next = node.parentNode; - if (next === null) { break; } - - node = next; - } - } - return next; - } - - static getPreviousNode(node, visitChildren) { - let next = visitChildren ? node.lastChild : null; - if (next === null) { - while (true) { - next = node.previousSibling; - if (next !== null) { break; } - - next = node.parentNode; - if (next === null) { break; } - - node = next; - } - } - return next; - } - - static anyNodeMatchesSelector(nodeList, selector) { - for (const node of nodeList) { - if (TextSourceRange.nodeMatchesSelector(node, selector)) { - return true; - } - } - return false; - } - - static nodeMatchesSelector(node, selector) { - for (; node !== null; node = node.parentNode) { - if (node.nodeType === Node.ELEMENT_NODE) { - return node.matches(selector); - } - } - return false; - } } diff --git a/ext/manifest.json b/ext/manifest.json index 75334675..4d4f0c06 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -42,6 +42,7 @@ "mixed/js/dynamic-loader.js", "mixed/js/text-scanner.js", "fg/js/document.js", + "fg/js/dom-text-scanner.js", "fg/js/popup.js", "fg/js/source.js", "fg/js/popup-factory.js", diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 90fd1037..1d699706 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -236,7 +236,9 @@ class Display { const {textSource, definitions} = termLookupResults; const scannedElement = e.target; - const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); + const sentenceExtent = this.options.anki.sentenceExt; + const layoutAwareScan = this.options.scanning.layoutAwareScan; + const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan); this.context.update({ index: this.entryIndexFind(scannedElement), @@ -273,21 +275,22 @@ class Display { try { e.preventDefault(); - const textSource = docRangeFromPoint(e.clientX, e.clientY, this.options.scanning.deepDomScan); + const {length: scanLength, deepDomScan: deepScan, layoutAwareScan} = this.options.scanning; + const textSource = docRangeFromPoint(e.clientX, e.clientY, deepScan); if (textSource === null) { return false; } let definitions, length; try { - textSource.setEndOffset(this.options.scanning.length); + textSource.setEndOffset(scanLength, layoutAwareScan); ({definitions, length} = await api.termsFind(textSource.text(), {}, this.getOptionsContext())); if (definitions.length === 0) { return false; } - textSource.setEndOffset(length); + textSource.setEndOffset(length, layoutAwareScan); } finally { textSource.cleanup(); } diff --git a/ext/mixed/js/dom.js b/ext/mixed/js/dom.js index 0e8f4462..05764443 100644 --- a/ext/mixed/js/dom.js +++ b/ext/mixed/js/dom.js @@ -86,4 +86,42 @@ class DOM { null ); } + + static getNodesInRange(range) { + const end = range.endContainer; + const nodes = []; + for (let node = range.startContainer; node !== null; node = DOM.getNextNode(node)) { + nodes.push(node); + if (node === end) { break; } + } + return nodes; + } + + static getNextNode(node) { + let next = node.firstChild; + if (next === null) { + while (true) { + next = node.nextSibling; + if (next !== null) { break; } + + next = node.parentNode; + if (next === null) { break; } + + node = next; + } + } + return next; + } + + static anyNodeMatchesSelector(nodes, selector) { + const ELEMENT_NODE = Node.ELEMENT_NODE; + for (let node of nodes) { + for (; node !== null; node = node.parentNode) { + if (node.nodeType !== ELEMENT_NODE) { continue; } + if (node.matches(selector)) { return true; } + break; + } + } + return false; + } } diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index b8688b08..fb275452 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -17,7 +17,6 @@ /* global * DOM - * TextSourceRange * docRangeFromPoint */ @@ -119,20 +118,20 @@ class TextScanner extends EventDispatcher { } } - getTextSourceContent(textSource, length) { + getTextSourceContent(textSource, length, layoutAwareScan) { const clonedTextSource = textSource.clone(); - clonedTextSource.setEndOffset(length); + clonedTextSource.setEndOffset(length, layoutAwareScan); if (this._ignoreNodes !== null && clonedTextSource.range) { length = clonedTextSource.text().length; while (clonedTextSource.range && length > 0) { - const nodes = TextSourceRange.getNodesInRange(clonedTextSource.range); - if (!TextSourceRange.anyNodeMatchesSelector(nodes, this._ignoreNodes)) { + const nodes = DOM.getNodesInRange(clonedTextSource.range); + if (!DOM.anyNodeMatchesSelector(nodes, this._ignoreNodes)) { break; } --length; - clonedTextSource.setEndOffset(length); + clonedTextSource.setEndOffset(length, layoutAwareScan); } } diff --git a/test/test-document.js b/test/test-document.js index 0d9026db..ba7acc49 100644 --- a/test/test-document.js +++ b/test/test-document.js @@ -94,10 +94,12 @@ async function testDocument1() { const vm = new VM({document, window, Range, Node}); vm.execute([ 'mixed/js/dom.js', + 'fg/js/dom-text-scanner.js', 'fg/js/source.js', 'fg/js/document.js' ]); - const [TextSourceRange, TextSourceElement, docRangeFromPoint, docSentenceExtract] = vm.get([ + const [DOMTextScanner, TextSourceRange, TextSourceElement, docRangeFromPoint, docSentenceExtract] = vm.get([ + 'DOMTextScanner', 'TextSourceRange', 'TextSourceElement', 'docRangeFromPoint', @@ -106,7 +108,7 @@ async function testDocument1() { try { await testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSentenceExtract, TextSourceRange, TextSourceElement}); - await testTextSourceRangeSeekFunctions(dom, {TextSourceRange}); + await testTextSourceRangeSeekFunctions(dom, {DOMTextScanner}); } finally { window.close(); } @@ -179,7 +181,7 @@ async function testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSen if (source === null) { continue; } // Test docSentenceExtract - const sentenceActual = docSentenceExtract(source, sentenceExtent).text; + const sentenceActual = docSentenceExtract(source, sentenceExtent, false).text; assert.strictEqual(sentenceActual, sentence); // Clean @@ -187,7 +189,7 @@ async function testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSen } } -async function testTextSourceRangeSeekFunctions(dom, {TextSourceRange}) { +async function testTextSourceRangeSeekFunctions(dom, {DOMTextScanner}) { const document = dom.window.document; for (const testElement of document.querySelectorAll('.test[data-test-type=text-source-range-seek]')) { @@ -220,8 +222,8 @@ async function testTextSourceRangeSeekFunctions(dom, {TextSourceRange}) { const {node, offset, content} = ( seekDirection === 'forward' ? - TextSourceRange.seekForward(seekNode, seekOffset, seekLength) : - TextSourceRange.seekBackward(seekNode, seekOffset, seekLength) + new DOMTextScanner(seekNode, seekOffset, true, false).seek(seekLength) : + new DOMTextScanner(seekNode, seekOffset, true, false).seek(-seekLength) ); assert.strictEqual(node, expectedResultNode); -- cgit v1.2.3 From f2991fb9ee8e83738b726eb558af992f4bb5d9dc Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 21 Jun 2020 16:14:05 -0400 Subject: Frontend initialization refactor (#610) * Create member functions for ignoreElements and ignorePoint * Create addFullscreenChangeEventListener utility * Move popup creation management into Frontend * Move getUrl implementation * Remove old setup * Remove try/catch block * Error wrap * Add prepare call to TextScanner * Update depth when popup changes * Refactor how Frontend gets PopupFactory and frameId * Update popup preview to work * Update popup preview frame to use the frontend's popup * Update how nested popups are set up * Error wrap * Update how popups are set up on the search page * Error wrap * Error unwrap * Add missing prepare * Remove use of frontendInitializationData * Catch and log errors --- ext/bg/js/search-main.js | 44 +---- ext/bg/js/search-query-parser.js | 1 + ext/bg/js/search.js | 120 +++++++++---- ext/bg/js/settings/popup-preview-frame-main.js | 17 +- ext/bg/js/settings/popup-preview-frame.js | 46 +++-- ext/bg/settings-popup-preview.html | 1 + ext/fg/js/content-script-main.js | 152 ++-------------- ext/fg/js/float-main.js | 43 +---- ext/fg/js/float.js | 65 ++++++- ext/fg/js/frontend.js | 231 ++++++++++++++++++++----- ext/fg/js/popup-factory.js | 7 +- ext/fg/js/popup-proxy.js | 4 - ext/fg/js/popup.js | 11 +- ext/mixed/js/dom.js | 18 ++ ext/mixed/js/text-scanner.js | 8 +- 15 files changed, 437 insertions(+), 331 deletions(-) (limited to 'ext/mixed') diff --git a/ext/bg/js/search-main.js b/ext/bg/js/search-main.js index f18d6d88..13bd8767 100644 --- a/ext/bg/js/search-main.js +++ b/ext/bg/js/search-main.js @@ -18,42 +18,16 @@ /* global * DisplaySearch * api - * dynamicLoader */ -async function injectSearchFrontend() { - await dynamicLoader.loadScripts([ - '/mixed/js/text-scanner.js', - '/fg/js/frame-offset-forwarder.js', - '/fg/js/popup.js', - '/fg/js/popup-factory.js', - '/fg/js/frontend.js', - '/fg/js/content-script-main.js' - ]); -} - (async () => { - api.forwardLogsToBackend(); - await yomichan.prepare(); - - const displaySearch = new DisplaySearch(); - await displaySearch.prepare(); - - let optionsApplied = false; - - const applyOptions = async () => { - const optionsContext = {depth: 0, url: window.location.href}; - const options = await api.optionsGet(optionsContext); - if (!options.scanning.enableOnSearchPage || optionsApplied) { return; } - - optionsApplied = true; - yomichan.off('optionsUpdated', applyOptions); - - window.frontendInitializationData = {depth: 1, proxy: false, isSearchPage: true}; - await injectSearchFrontend(); - }; - - yomichan.on('optionsUpdated', applyOptions); - - await applyOptions(); + try { + api.forwardLogsToBackend(); + await yomichan.prepare(); + + const displaySearch = new DisplaySearch(); + await displaySearch.prepare(); + } catch (e) { + yomichan.logError(e); + } })(); diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index 97e98b40..86524b66 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -42,6 +42,7 @@ class QueryParser { async prepare() { await this._queryParserGenerator.prepare(); + this._textScanner.prepare(); this._queryParser.addEventListener('click', this._onClick.bind(this)); } diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index 08c02624..88be335f 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -19,8 +19,11 @@ * ClipboardMonitor * DOM * Display + * Frontend + * PopupFactory * QueryParser * api + * dynamicLoader * wanakana */ @@ -73,51 +76,49 @@ class DisplaySearch extends Display { } async prepare() { - try { - await super.prepare(); - await this.updateOptions(); - yomichan.on('optionsUpdated', () => this.updateOptions()); - await this.queryParser.prepare(); + await super.prepare(); + await this.updateOptions(); + yomichan.on('optionsUpdated', () => this.updateOptions()); + await this.queryParser.prepare(); + + const {queryParams: {query='', mode=''}} = parseUrl(window.location.href); + + document.documentElement.dataset.searchMode = mode; - const {queryParams: {query='', mode=''}} = parseUrl(window.location.href); + if (this.options.general.enableWanakana === true) { + this.wanakanaEnable.checked = true; + wanakana.bind(this.query); + } else { + this.wanakanaEnable.checked = false; + } - document.documentElement.dataset.searchMode = mode; + this.setQuery(query); + this.onSearchQueryUpdated(this.query.value, false); - if (this.options.general.enableWanakana === true) { - this.wanakanaEnable.checked = true; - wanakana.bind(this.query); + if (mode !== 'popup') { + if (this.options.general.enableClipboardMonitor === true) { + this.clipboardMonitorEnable.checked = true; + this.clipboardMonitor.start(); } else { - this.wanakanaEnable.checked = false; + this.clipboardMonitorEnable.checked = false; } + this.clipboardMonitorEnable.addEventListener('change', this.onClipboardMonitorEnableChange.bind(this)); + } - this.setQuery(query); - this.onSearchQueryUpdated(this.query.value, false); - - if (mode !== 'popup') { - if (this.options.general.enableClipboardMonitor === true) { - this.clipboardMonitorEnable.checked = true; - this.clipboardMonitor.start(); - } else { - this.clipboardMonitorEnable.checked = false; - } - this.clipboardMonitorEnable.addEventListener('change', this.onClipboardMonitorEnableChange.bind(this)); - } + chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); - chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); + this.search.addEventListener('click', this.onSearch.bind(this), false); + this.query.addEventListener('input', this.onSearchInput.bind(this), false); + this.wanakanaEnable.addEventListener('change', this.onWanakanaEnableChange.bind(this)); + window.addEventListener('popstate', this.onPopState.bind(this)); + window.addEventListener('copy', this.onCopy.bind(this)); + this.clipboardMonitor.on('change', this.onExternalSearchUpdate.bind(this)); - this.search.addEventListener('click', this.onSearch.bind(this), false); - this.query.addEventListener('input', this.onSearchInput.bind(this), false); - this.wanakanaEnable.addEventListener('change', this.onWanakanaEnableChange.bind(this)); - window.addEventListener('popstate', this.onPopState.bind(this)); - window.addEventListener('copy', this.onCopy.bind(this)); - this.clipboardMonitor.on('change', this.onExternalSearchUpdate.bind(this)); + this.updateSearchButton(); - this.updateSearchButton(); + await this._prepareNestedPopups(); - this._isPrepared = true; - } catch (e) { - this.onError(e); - } + this._isPrepared = true; } onError(error) { @@ -401,4 +402,53 @@ class DisplaySearch extends Display { document.title = `${text} - Yomichan Search`; } } + + async _prepareNestedPopups() { + let complete = false; + + const onOptionsUpdated = async () => { + const optionsContext = this.getOptionsContext(); + const options = await api.optionsGet(optionsContext); + if (!options.scanning.enableOnSearchPage || complete) { return; } + + complete = true; + yomichan.off('optionsUpdated', onOptionsUpdated); + + try { + await this._setupNestedPopups(); + } catch (e) { + yomichan.logError(e); + } + }; + + yomichan.on('optionsUpdated', onOptionsUpdated); + + await onOptionsUpdated(); + } + + async _setupNestedPopups() { + await dynamicLoader.loadScripts([ + '/mixed/js/text-scanner.js', + '/fg/js/frame-offset-forwarder.js', + '/fg/js/popup.js', + '/fg/js/popup-factory.js', + '/fg/js/frontend.js' + ]); + + const {frameId} = await api.frameInformationGet(); + + const popupFactory = new PopupFactory(frameId); + await popupFactory.prepare(); + + const frontend = new Frontend( + frameId, + popupFactory, + { + depth: 1, + proxy: false, + isSearchPage: true + } + ); + await frontend.prepare(); + } } diff --git a/ext/bg/js/settings/popup-preview-frame-main.js b/ext/bg/js/settings/popup-preview-frame-main.js index 7c4e2eb9..4c6096ec 100644 --- a/ext/bg/js/settings/popup-preview-frame-main.js +++ b/ext/bg/js/settings/popup-preview-frame-main.js @@ -16,12 +16,23 @@ */ /* global + * PopupFactory * PopupPreviewFrame * api */ (async () => { - api.forwardLogsToBackend(); - const preview = new PopupPreviewFrame(); - await preview.prepare(); + try { + api.forwardLogsToBackend(); + + const {frameId} = await api.frameInformationGet(); + + const popupFactory = new PopupFactory(frameId); + await popupFactory.prepare(); + + const preview = new PopupPreviewFrame(frameId, popupFactory); + await preview.prepare(); + } catch (e) { + yomichan.logError(e); + } })(); diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js index 21fee7ee..98630503 100644 --- a/ext/bg/js/settings/popup-preview-frame.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -18,17 +18,17 @@ /* global * Frontend * Popup - * PopupFactory * TextSourceRange * api */ class PopupPreviewFrame { - constructor() { + constructor(frameId, popupFactory) { + this._frameId = frameId; + this._popupFactory = popupFactory; this._frontend = null; this._frontendGetOptionsContextOld = null; this._apiOptionsGetOld = null; - this._popup = null; this._popupSetCustomOuterCssOld = null; this._popupShown = false; this._themeChangeTimeout = null; @@ -55,24 +55,25 @@ class PopupPreviewFrame { api.optionsGet = this._apiOptionsGet.bind(this); // Overwrite frontend - const {frameId} = await api.frameInformationGet(); - - const popupFactory = new PopupFactory(frameId); - await popupFactory.prepare(); - - this._popup = popupFactory.getOrCreatePopup(); - this._popup.setChildrenSupported(false); - - this._popupSetCustomOuterCssOld = this._popup.setCustomOuterCss.bind(this._popup); - this._popup.setCustomOuterCss = this._popupSetCustomOuterCss.bind(this); - - this._frontend = new Frontend(this._popup); + this._frontend = new Frontend( + this._frameId, + this._popupFactory, + { + allowRootFramePopupProxy: false + } + ); this._frontendGetOptionsContextOld = this._frontend.getOptionsContext.bind(this._frontend); this._frontend.getOptionsContext = this._getOptionsContext.bind(this); await this._frontend.prepare(); this._frontend.setDisabledOverride(true); this._frontend.canClearSelection = false; + const popup = this._frontend.popup; + popup.setChildrenSupported(false); + + this._popupSetCustomOuterCssOld = popup.setCustomOuterCss.bind(popup); + popup.setCustomOuterCss = this._popupSetCustomOuterCss.bind(this); + // Update search this._updateSearch(); } @@ -132,7 +133,9 @@ class PopupPreviewFrame { } this._themeChangeTimeout = setTimeout(() => { this._themeChangeTimeout = null; - this._popup.updateTheme(); + const popup = this._frontend.popup; + if (popup === null) { return; } + popup.updateTheme(); }, 300); } @@ -154,12 +157,16 @@ class PopupPreviewFrame { _setCustomCss({css}) { if (this._frontend === null) { return; } - this._popup.setCustomCss(css); + const popup = this._frontend.popup; + if (popup === null) { return; } + popup.setCustomCss(css); } _setCustomOuterCss({css}) { if (this._frontend === null) { return; } - this._popup.setCustomOuterCss(css, false); + const popup = this._frontend.popup; + if (popup === null) { return; } + popup.setCustomOuterCss(css, false); } async _updateOptionsContext({optionsContext}) { @@ -188,7 +195,8 @@ class PopupPreviewFrame { this._textSource = source; await this._frontend.showContentCompleted(); - if (this._popup.isVisibleSync()) { + const popup = this._frontend.popup; + if (popup !== null && popup.isVisibleSync()) { this._popupShown = true; } diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html index 5eecd005..75bf06c8 100644 --- a/ext/bg/settings-popup-preview.html +++ b/ext/bg/settings-popup-preview.html @@ -131,6 +131,7 @@ + diff --git a/ext/fg/js/content-script-main.js b/ext/fg/js/content-script-main.js index c31cde3f..c4aa1bca 100644 --- a/ext/fg/js/content-script-main.js +++ b/ext/fg/js/content-script-main.js @@ -16,147 +16,31 @@ */ /* global - * DOM - * FrameOffsetForwarder * Frontend * PopupFactory - * PopupProxy * 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, - ({action, params}, {resolve}) => { - if (action === 'rootPopupInformation') { - resolve(params); - } - } - ); - api.broadcastTab('rootPopupRequestInformationBroadcast'); - const {popupId, frameId: parentFrameId} = await rootPopupInformationPromise; - - const popup = new PopupProxy(popupId, 0, null, parentFrameId, frameOffsetForwarder); - popup.on('offsetNotFound', setDisabled); - await popup.prepare(); - - return popup; -} - -async function getOrCreatePopup(depth, popupFactory) { - return popupFactory.getOrCreatePopup(null, null, depth); -} - -async function createPopupProxy(depth, id, parentFrameId) { - const popup = new PopupProxy(null, depth + 1, id, parentFrameId); - await popup.prepare(); - - return popup; -} - (async () => { - api.forwardLogsToBackend(); - await yomichan.prepare(); - - const data = window.frontendInitializationData || {}; - const {id, depth=0, parentFrameId, url=window.location.href, proxy=false, isSearchPage=false} = data; - - const isIframe = !proxy && (window !== window.parent); - - const popups = { - iframe: null, - proxy: null, - normal: null - }; + try { + api.forwardLogsToBackend(); + await yomichan.prepare(); - let frontend = null; - let frontendPreparePromise = null; - let frameOffsetForwarder = null; - let popupFactoryPromise = null; - - let iframePopupsInRootFrameAvailable = true; - - const disableIframePopupsInRootFrame = () => { - iframePopupsInRootFrameAvailable = false; - applyOptions(); - }; - - let urlUpdatedAt = 0; - let popupProxyUrlCached = url; - const getPopupProxyUrl = async () => { - const now = Date.now(); - if (popups.proxy !== null && now - urlUpdatedAt > 500) { - popupProxyUrlCached = await popups.proxy.getUrl(); - urlUpdatedAt = now; + const {frameId} = await api.frameInformationGet(); + if (typeof frameId !== 'number') { + throw new Error('Failed to get frameId'); } - return popupProxyUrlCached; - }; - - const applyOptions = async () => { - const optionsContext = { - depth: isSearchPage ? 0 : depth, - url: proxy ? await getPopupProxyUrl() : window.location.href - }; - const options = await api.optionsGet(optionsContext); - if (!proxy && frameOffsetForwarder === null) { - frameOffsetForwarder = new FrameOffsetForwarder(); - frameOffsetForwarder.prepare(); - } - - let popup; - if (isIframe && options.general.showIframePopupsInRootFrame && DOM.getFullscreenElement() === null && iframePopupsInRootFrameAvailable) { - popup = popups.iframe || await createIframePopupProxy(frameOffsetForwarder, disableIframePopupsInRootFrame); - popups.iframe = popup; - } else if (proxy) { - popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId); - popups.proxy = popup; - } else { - 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) { - const getUrl = proxy ? getPopupProxyUrl : null; - frontend = new Frontend(popup, getUrl); - frontendPreparePromise = frontend.prepare(); - await frontendPreparePromise; - } else { - await frontendPreparePromise; - if (isSearchPage) { - const disabled = !options.scanning.enableOnSearchPage; - frontend.setDisabledOverride(disabled); - } - - if (isIframe) { - await frontend.setPopup(popup); - } - } - }; - - yomichan.on('optionsUpdated', applyOptions); - window.addEventListener('fullscreenchange', applyOptions, false); - - await applyOptions(); + const popupFactory = new PopupFactory(frameId); + await popupFactory.prepare(); + + const frontend = new Frontend( + frameId, + popupFactory, + {} + ); + await frontend.prepare(); + } catch (e) { + yomichan.logError(e); + } })(); diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js index 2ec334c8..3bedfe58 100644 --- a/ext/fg/js/float-main.js +++ b/ext/fg/js/float-main.js @@ -18,42 +18,15 @@ /* global * DisplayFloat * api - * dynamicLoader */ -async function injectPopupNested() { - await dynamicLoader.loadScripts([ - '/mixed/js/text-scanner.js', - '/fg/js/popup.js', - '/fg/js/popup-proxy.js', - '/fg/js/frontend.js', - '/fg/js/content-script-main.js' - ]); -} - -async function popupNestedInitialize(id, depth, parentFrameId, url) { - let optionsApplied = false; - - const applyOptions = async () => { - const optionsContext = {depth, url}; - const options = await api.optionsGet(optionsContext); - const maxPopupDepthExceeded = !(typeof depth === 'number' && depth < options.scanning.popupNestingMaxDepth); - if (maxPopupDepthExceeded || optionsApplied) { return; } - - optionsApplied = true; - yomichan.off('optionsUpdated', applyOptions); - - window.frontendInitializationData = {id, depth, parentFrameId, url, proxy: true}; - await injectPopupNested(); - }; - - yomichan.on('optionsUpdated', applyOptions); - - await applyOptions(); -} - (async () => { - api.forwardLogsToBackend(); - const display = new DisplayFloat(); - await display.prepare(); + try { + api.forwardLogsToBackend(); + + const display = new DisplayFloat(); + await display.prepare(); + } catch (e) { + yomichan.logError(e); + } })(); diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 12d27a9f..199990e5 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -17,8 +17,10 @@ /* global * Display + * Frontend + * PopupFactory * api - * popupNestedInitialize + * dynamicLoader */ class DisplayFloat extends Display { @@ -30,7 +32,7 @@ class DisplayFloat extends Display { this._token = null; this._orphaned = false; - this._initializedNestedPopups = false; + this._nestedPopupsPrepared = false; this._onKeyDownHandlers = new Map([ ['C', (e) => { @@ -183,10 +185,10 @@ class DisplayFloat extends Display { await this.updateOptions(); - if (childrenSupported && !this._initializedNestedPopups) { + if (childrenSupported && !this._nestedPopupsPrepared) { const {depth, url} = optionsContext; - popupNestedInitialize(popupId, depth, frameId, url); - this._initializedNestedPopups = true; + this._prepareNestedPopups(popupId, depth, frameId, url); + this._nestedPopupsPrepared = true; } this.setContentScale(scale); @@ -201,4 +203,57 @@ class DisplayFloat extends Display { this._secret === message.secret ); } + + async _prepareNestedPopups(id, depth, parentFrameId, url) { + let complete = false; + + const onOptionsUpdated = async () => { + const optionsContext = this.optionsContext; + const options = await api.optionsGet(optionsContext); + const maxPopupDepthExceeded = !(typeof depth === 'number' && depth < options.scanning.popupNestingMaxDepth); + if (maxPopupDepthExceeded || complete) { return; } + + complete = true; + yomichan.off('optionsUpdated', onOptionsUpdated); + + try { + await this._setupNestedPopups(id, depth, parentFrameId, url); + } catch (e) { + yomichan.logError(e); + } + }; + + yomichan.on('optionsUpdated', onOptionsUpdated); + + await onOptionsUpdated(); + } + + async _setupNestedPopups(id, depth, parentFrameId, url) { + await dynamicLoader.loadScripts([ + '/mixed/js/text-scanner.js', + '/fg/js/popup.js', + '/fg/js/popup-proxy.js', + '/fg/js/popup-factory.js', + '/fg/js/frame-offset-forwarder.js', + '/fg/js/frontend.js' + ]); + + const {frameId} = await api.frameInformationGet(); + + const popupFactory = new PopupFactory(frameId); + await popupFactory.prepare(); + + const frontend = new Frontend( + frameId, + popupFactory, + { + id, + depth, + parentFrameId, + url, + proxy: true + } + ); + await frontend.prepare(); + } } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index ab455c09..71e53b03 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -16,16 +16,18 @@ */ /* global + * DOM + * FrameOffsetForwarder + * PopupProxy * TextScanner * api * docSentenceExtract */ class Frontend { - constructor(popup, getUrl=null) { + constructor(frameId, popupFactory, frontendInitializationData) { this._id = yomichan.generateId(16); - this._popup = popup; - this._getUrl = getUrl; + this._popup = null; this._disabledOverride = false; this._options = null; this._pageZoomFactor = 1.0; @@ -37,11 +39,31 @@ class Frontend { this._optionsUpdatePending = false; this._textScanner = new TextScanner({ node: window, - ignoreElements: () => this._popup.isProxy() ? [] : [this._popup.getFrame()], - ignorePoint: (x, y) => this._popup.containsPoint(x, y), + ignoreElements: this._ignoreElements.bind(this), + ignorePoint: this._ignorePoint.bind(this), search: this._search.bind(this) }); + const { + depth=0, + id: proxyPopupId, + parentFrameId, + proxy: useProxyPopup=false, + isSearchPage=false, + allowRootFramePopupProxy=true + } = frontendInitializationData; + this._proxyPopupId = proxyPopupId; + this._parentFrameId = parentFrameId; + this._useProxyPopup = useProxyPopup; + this._isSearchPage = isSearchPage; + this._depth = depth; + this._frameId = frameId; + this._frameOffsetForwarder = new FrameOffsetForwarder(); + this._popupFactory = popupFactory; + this._allowRootFramePopupProxy = allowRootFramePopupProxy; + this._popupCache = new Map(); + this._updatePopupToken = null; + this._windowMessageHandlers = new Map([ ['popupClose', this._onMessagePopupClose.bind(this)], ['selectionCopy', this._onMessageSelectionCopy.bind()] @@ -62,43 +84,46 @@ class Frontend { this._textScanner.canClearSelection = value; } + get popup() { + return this._popup; + } + async prepare() { + this._frameOffsetForwarder.prepare(); + + await this.updateOptions(); try { - await this.updateOptions(); - try { - const {zoomFactor} = await api.getZoom(); - this._pageZoomFactor = zoomFactor; - } catch (e) { - // Ignore exceptions which may occur due to being on an unsupported page (e.g. about:blank) - } + const {zoomFactor} = await api.getZoom(); + this._pageZoomFactor = zoomFactor; + } catch (e) { + // Ignore exceptions which may occur due to being on an unsupported page (e.g. about:blank) + } - window.addEventListener('resize', this._onResize.bind(this), false); + this._textScanner.prepare(); - const visualViewport = window.visualViewport; - if (visualViewport !== null && typeof visualViewport === 'object') { - window.visualViewport.addEventListener('scroll', this._onVisualViewportScroll.bind(this)); - window.visualViewport.addEventListener('resize', this._onVisualViewportResize.bind(this)); - } + window.addEventListener('resize', this._onResize.bind(this), false); + DOM.addFullscreenChangeEventListener(this._updatePopup.bind(this)); - yomichan.on('orphaned', this._onOrphaned.bind(this)); - yomichan.on('optionsUpdated', this.updateOptions.bind(this)); - yomichan.on('zoomChanged', this._onZoomChanged.bind(this)); - chrome.runtime.onMessage.addListener(this._onRuntimeMessage.bind(this)); + const visualViewport = window.visualViewport; + if (visualViewport !== null && typeof visualViewport === 'object') { + window.visualViewport.addEventListener('scroll', this._onVisualViewportScroll.bind(this)); + window.visualViewport.addEventListener('resize', this._onVisualViewportResize.bind(this)); + } - this._textScanner.on('clearSelection', this._onClearSelection.bind(this)); - this._textScanner.on('activeModifiersChanged', this._onActiveModifiersChanged.bind(this)); + yomichan.on('orphaned', this._onOrphaned.bind(this)); + yomichan.on('optionsUpdated', this.updateOptions.bind(this)); + yomichan.on('zoomChanged', this._onZoomChanged.bind(this)); + chrome.runtime.onMessage.addListener(this._onRuntimeMessage.bind(this)); - this._updateContentScale(); - this._broadcastRootPopupInformation(); - } catch (e) { - yomichan.logError(e); - } - } + this._textScanner.on('clearSelection', this._onClearSelection.bind(this)); + this._textScanner.on('activeModifiersChanged', this._onActiveModifiersChanged.bind(this)); - async setPopup(popup) { - this._textScanner.clearSelection(true); - this._popup = popup; - await popup.setOptionsContext(await this.getOptionsContext(), this._id); + api.crossFrame.registerHandlers([ + ['getUrl', {async: false, handler: this._onApiGetUrl.bind(this)}] + ]); + + this._updateContentScale(); + this._broadcastRootPopupInformation(); } setDisabledOverride(disabled) { @@ -112,8 +137,16 @@ class Frontend { } async getOptionsContext() { - const url = this._getUrl !== null ? await this._getUrl() : window.location.href; - const depth = this._popup.depth; + let url = window.location.href; + if (this._useProxyPopup) { + try { + url = await api.crossFrame.invoke(this._parentFrameId, 'getUrl', {}); + } catch (e) { + // NOP + } + } + + const depth = this._depth; const modifierKeys = [...this._activeModifiers]; return {depth, url, modifierKeys}; } @@ -121,6 +154,9 @@ class Frontend { async updateOptions() { const optionsContext = await this.getOptionsContext(); this._options = await api.optionsGet(optionsContext); + + await this._updatePopup(); + this._textScanner.setOptions(this._options); this._updateTextScannerEnabled(); @@ -130,8 +166,6 @@ class Frontend { } this._textScanner.ignoreNodes = ignoreNodes.join(','); - await this._popup.setOptionsContext(optionsContext, this._id); - this._updateContentScale(); const textSourceCurrent = this._textScanner.getCurrentTextSource(); @@ -167,6 +201,12 @@ class Frontend { this._broadcastDocumentInformation(uniqueId); } + // API message handlers + + _onApiGetUrl() { + return window.location.href; + } + // Private _onResize() { @@ -223,6 +263,95 @@ class Frontend { await this.updateOptions(); } + async _updatePopup() { + const showIframePopupsInRootFrame = this._options.general.showIframePopupsInRootFrame; + const isIframe = !this._useProxyPopup && (window !== window.parent); + + let popupPromise; + if ( + isIframe && + showIframePopupsInRootFrame && + DOM.getFullscreenElement() === null && + this._allowRootFramePopupProxy + ) { + popupPromise = this._popupCache.get('iframe'); + if (typeof popupPromise === 'undefined') { + popupPromise = this._getIframeProxyPopup(); + this._popupCache.set('iframe', popupPromise); + } + } else if (this._useProxyPopup) { + popupPromise = this._popupCache.get('proxy'); + if (typeof popupPromise === 'undefined') { + popupPromise = this._getProxyPopup(); + this._popupCache.set('proxy', popupPromise); + } + } else { + popupPromise = this._popupCache.get('default'); + if (typeof popupPromise === 'undefined') { + popupPromise = this._getDefaultPopup(); + this._popupCache.set('default', popupPromise); + } + } + + // The token below is used as a unique identifier to ensure that a new _updatePopup call + // hasn't been started during the await. + const token = {}; + this._updatePopupToken = token; + const popup = await popupPromise; + const optionsContext = await this.getOptionsContext(); + if (this._updatePopupToken !== token) { return; } + await popup.setOptionsContext(optionsContext, this._id); + if (this._updatePopupToken !== token) { return; } + + if (this._isSearchPage) { + this.setDisabledOverride(!this._options.scanning.enableOnSearchPage); + } + + this._textScanner.clearSelection(true); + this._popup = popup; + this._depth = popup.depth; + } + + async _getDefaultPopup() { + return this._popupFactory.getOrCreatePopup(null, null, this._depth); + } + + async _getProxyPopup() { + const popup = new PopupProxy(null, this._depth + 1, this._proxyPopupId, this._parentFrameId); + await popup.prepare(); + return popup; + } + + async _getIframeProxyPopup() { + const rootPopupInformationPromise = yomichan.getTemporaryListenerResult( + chrome.runtime.onMessage, + ({action, params}, {resolve}) => { + if (action === 'rootPopupInformation') { + resolve(params); + } + } + ); + api.broadcastTab('rootPopupRequestInformationBroadcast'); + const {popupId, frameId: parentFrameId} = await rootPopupInformationPromise; + + const popup = new PopupProxy(popupId, 0, null, parentFrameId, this._frameOffsetForwarder); + popup.on('offsetNotFound', () => { + this._allowRootFramePopupProxy = false; + this._updatePopup(); + }); + await popup.prepare(); + + return popup; + } + + _ignoreElements() { + return this._popup === null || this._popup.isProxy() ? [] : [this._popup.getFrame()]; + } + + _ignorePoint(x, y) { + return this._popup !== null && this._popup.containsPoint(x, y); + } + async _search(textSource, cause) { await this._updatePendingOptions(); @@ -318,7 +447,7 @@ class Frontend { _updateTextScannerEnabled() { const enabled = ( this._options.general.enable && - this._popup.depth <= this._options.scanning.popupNestingMaxDepth && + this._depth <= this._options.scanning.popupNestingMaxDepth && !this._disabledOverride ); this._enabledEventListeners.removeAllEventListeners(); @@ -342,27 +471,41 @@ class Frontend { if (contentScale === this._contentScale) { return; } this._contentScale = contentScale; - this._popup.setContentScale(this._contentScale); + if (this._popup !== null) { + this._popup.setContentScale(this._contentScale); + } this._updatePopupPosition(); } async _updatePopupPosition() { const textSource = this._textScanner.getCurrentTextSource(); - if (textSource !== null && await this._popup.isVisible()) { + if ( + textSource !== null && + this._popup !== null && + await this._popup.isVisible() + ) { this._showPopupContent(textSource, await this.getOptionsContext()); } } _broadcastRootPopupInformation() { - if (!this._popup.isProxy() && this._popup.depth === 0 && this._popup.frameId === 0) { - api.broadcastTab('rootPopupInformation', {popupId: this._popup.id, frameId: this._popup.frameId}); + if ( + this._popup !== null && + !this._popup.isProxy() && + this._depth === 0 && + this._frameId === 0 + ) { + api.broadcastTab('rootPopupInformation', { + popupId: this._popup.id, + frameId: this._frameId + }); } } _broadcastDocumentInformation(uniqueId) { api.broadcastTab('documentInformationBroadcast', { uniqueId, - frameId: this._popup.frameId, + frameId: this._frameId, title: document.title }); } diff --git a/ext/fg/js/popup-factory.js b/ext/fg/js/popup-factory.js index b5997253..4edda91f 100644 --- a/ext/fg/js/popup-factory.js +++ b/ext/fg/js/popup-factory.js @@ -39,8 +39,7 @@ class PopupFactory { ['showContent', {async: true, handler: this._onApiShowContent.bind(this)}], ['setCustomCss', {async: false, handler: this._onApiSetCustomCss.bind(this)}], ['clearAutoPlayTimer', {async: false, handler: this._onApiClearAutoPlayTimer.bind(this)}], - ['setContentScale', {async: false, handler: this._onApiSetContentScale.bind(this)}], - ['getUrl', {async: false, handler: this._onApiGetUrl.bind(this)}] + ['setContentScale', {async: false, handler: this._onApiSetContentScale.bind(this)}] ]); } @@ -147,10 +146,6 @@ class PopupFactory { return popup.setContentScale(scale); } - _onApiGetUrl() { - return window.location.href; - } - // Private functions _getPopup(id) { diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 14ddcafb..a6602eae 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -104,10 +104,6 @@ class PopupProxy extends EventDispatcher { this._invoke('setContentScale', {id: this._id, scale}); } - async getUrl() { - return await this._invoke('getUrl', {}); - } - // Private _invoke(action, params={}) { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index af24989f..4394a965 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -415,16 +415,7 @@ class Popup { return; } - const fullscreenEvents = [ - 'fullscreenchange', - 'MSFullscreenChange', - 'mozfullscreenchange', - 'webkitfullscreenchange' - ]; - const onFullscreenChanged = this._onFullscreenChanged.bind(this); - for (const eventName of fullscreenEvents) { - this._fullscreenEventListeners.addEventListener(document, eventName, onFullscreenChanged, false); - } + DOM.addFullscreenChangeEventListener(this._onFullscreenChanged.bind(this), this._fullscreenEventListeners); } _onFullscreenChanged() { diff --git a/ext/mixed/js/dom.js b/ext/mixed/js/dom.js index 05764443..59fea9f6 100644 --- a/ext/mixed/js/dom.js +++ b/ext/mixed/js/dom.js @@ -77,6 +77,24 @@ class DOM { return (typeof key === 'string' ? (key.length === 1 ? key.toUpperCase() : key) : ''); } + static addFullscreenChangeEventListener(onFullscreenChanged, eventListenerCollection=null) { + const target = document; + const options = false; + const fullscreenEventNames = [ + 'fullscreenchange', + 'MSFullscreenChange', + 'mozfullscreenchange', + 'webkitfullscreenchange' + ]; + for (const eventName of fullscreenEventNames) { + if (eventListenerCollection === null) { + target.addEventListener(eventName, onFullscreenChanged, options); + } else { + eventListenerCollection.addEventListener(target, eventName, onFullscreenChanged, options); + } + } + } + static getFullscreenElement() { return ( document.fullscreenElement || diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index fb275452..7c705fc8 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -28,6 +28,7 @@ class TextScanner extends EventDispatcher { this._ignorePoint = ignorePoint; this._search = search; + this._isPrepared = false; this._ignoreNodes = null; this._causeCurrent = null; @@ -69,10 +70,15 @@ class TextScanner extends EventDispatcher { return this._causeCurrent; } + prepare() { + this._isPrepared = true; + this.setEnabled(this._enabled); + } + setEnabled(enabled) { this._eventListeners.removeAllEventListeners(); this._enabled = enabled; - if (this._enabled) { + if (this._enabled && this._isPrepared) { this._hookEvents(); } else { this.clearSelection(true); -- cgit v1.2.3 From 3e68af8666bdf9a6d8d605f7a3bb0432c8d6cb33 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 24 Jun 2020 21:46:13 -0400 Subject: Shadow DOM container for popup iframes (#623) * Add support for injecting stylesheets into a custom parent node * Add api.getStylesheetContent * Add support for injecting a CSS file's content * Add usePopupShadowDom option * Use a per-parentNode cache * Add support for using a shadow DOM wrapper around popup iframes * Ignore the popup container instead of the frame --- ext/bg/data/options-schema.json | 7 ++++- ext/bg/js/backend.js | 8 ++++++ ext/bg/js/options.js | 3 +- ext/bg/settings.html | 4 +++ ext/fg/js/frontend.js | 2 +- ext/fg/js/popup.js | 62 +++++++++++++++++++++++++++++++++++------ ext/mixed/js/api.js | 4 +++ ext/mixed/js/dynamic-loader.js | 49 ++++++++++++++++++++++++++------ 8 files changed, 119 insertions(+), 20 deletions(-) (limited to 'ext/mixed') diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json index f8791433..b56017bc 100644 --- a/ext/bg/data/options-schema.json +++ b/ext/bg/data/options-schema.json @@ -110,7 +110,8 @@ "showPitchAccentPositionNotation", "showPitchAccentGraph", "showIframePopupsInRootFrame", - "useSecurePopupFrameUrl" + "useSecurePopupFrameUrl", + "usePopupShadowDom" ], "properties": { "enable": { @@ -252,6 +253,10 @@ "useSecurePopupFrameUrl": { "type": "boolean", "default": true + }, + "usePopupShadowDom": { + "type": "boolean", + "default": true } } }, diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index b89cb641..344706d1 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -108,6 +108,7 @@ class Backend { ['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}], ['frameInformationGet', {async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this)}], ['injectStylesheet', {async: true, contentScript: true, handler: this._onApiInjectStylesheet.bind(this)}], + ['getStylesheetContent', {async: true, contentScript: true, handler: this._onApiGetStylesheetContent.bind(this)}], ['getEnvironmentInfo', {async: false, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}], ['clipboardGet', {async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this)}], ['getDisplayTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this)}], @@ -719,6 +720,13 @@ class Backend { }); } + async _onApiGetStylesheetContent({url}) { + if (!url.startsWith('/') || url.startsWith('//') || !url.endsWith('.css')) { + throw new Error('Invalid URL'); + } + return await requestText(url, 'GET'); + } + _onApiGetEnvironmentInfo() { return this.environment.getInfo(); } diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 151c945b..ccc56848 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -177,7 +177,8 @@ function profileOptionsCreateDefaults() { showPitchAccentPositionNotation: true, showPitchAccentGraph: false, showIframePopupsInRootFrame: false, - useSecurePopupFrameUrl: true + useSecurePopupFrameUrl: true, + usePopupShadowDom: true }, audio: { diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 4de70b7e..51cb14e7 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -186,6 +186,10 @@ +
+ +
+
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index ae0953f9..f6b0d236 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -345,7 +345,7 @@ class Frontend { } _ignoreElements() { - return this._popup === null || this._popup.isProxy() ? [] : [this._popup.getFrame()]; + return this._popup === null || this._popup.isProxy() ? [] : [this._popup.getContainer()]; } _ignorePoint(x, y) { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 3b14d3d0..5ee62c9b 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -47,6 +47,9 @@ class Popup { this._frame.style.width = '0'; this._frame.style.height = '0'; + this._container = this._frame; + this._shadow = null; + this._fullscreenEventListeners = new EventListenerCollection(); } @@ -180,7 +183,12 @@ class Popup { } async setCustomOuterCss(css, useWebExtensionApi) { - return await dynamicLoader.loadStyle('yomichan-popup-outer-user-stylesheet', 'code', css, useWebExtensionApi); + let parentNode = null; + if (this._shadow !== null) { + useWebExtensionApi = false; + parentNode = this._shadow; + } + return await dynamicLoader.loadStyle('yomichan-popup-outer-user-stylesheet', 'code', css, useWebExtensionApi, parentNode); } setChildrenSupported(value) { @@ -195,6 +203,10 @@ class Popup { return this._frame.getBoundingClientRect(); } + getContainer() { + return this._container; + } + // Private functions _inject() { @@ -330,9 +342,9 @@ class Popup { throw new Error('Options not initialized'); } - const {useSecurePopupFrameUrl} = this._options.general; + const {useSecurePopupFrameUrl, usePopupShadowDom} = this._options.general; - this._injectStyles(); + await this._setUpContainer(usePopupShadowDom); const {secret, token} = await this._initializeFrame(this._frame, this._targetOrigin, this._frameId, (frame) => { frame.removeAttribute('src'); @@ -382,9 +394,9 @@ class Popup { } _resetFrame() { - const parent = this._frame.parentNode; + const parent = this._container.parentNode; if (parent !== null) { - parent.removeChild(this._frame); + parent.removeChild(this._container); } this._frame.removeAttribute('src'); this._frame.removeAttribute('srcdoc'); @@ -395,9 +407,31 @@ class Popup { this._injectPromiseComplete = false; } + async _setUpContainer(usePopupShadowDom) { + if (usePopupShadowDom && typeof this._frame.attachShadow === 'function') { + const container = document.createElement('div'); + container.style.setProperty('all', 'initial', 'important'); + const shadow = container.attachShadow({mode: 'closed', delegatesFocus: true}); + shadow.appendChild(this._frame); + + this._container = container; + this._shadow = shadow; + } else { + const frameParentNode = this._frame.parentNode; + if (frameParentNode !== null) { + frameParentNode.removeChild(this._frame); + } + + this._container = this._frame; + this._shadow = null; + } + + await this._injectStyles(); + } + async _injectStyles() { try { - await dynamicLoader.loadStyle('yomichan-popup-outer-stylesheet', 'file', '/fg/css/client.css', true); + await this._injectPopupOuterStylesheet(); } catch (e) { // NOP } @@ -409,6 +443,18 @@ class Popup { } } + async _injectPopupOuterStylesheet() { + let fileType = 'file'; + let useWebExtensionApi = true; + let parentNode = null; + if (this._shadow !== null) { + fileType = 'file-content'; + useWebExtensionApi = false; + parentNode = this._shadow; + } + await dynamicLoader.loadStyle('yomichan-popup-outer-stylesheet', fileType, '/fg/css/client.css', useWebExtensionApi, parentNode); + } + _observeFullscreen(observe) { if (!observe) { this._fullscreenEventListeners.removeAllEventListeners(); @@ -425,8 +471,8 @@ class Popup { _onFullscreenChanged() { const parent = this._getFrameParentElement(); - if (parent !== null && this._frame.parentNode !== parent) { - parent.appendChild(this._frame); + if (parent !== null && this._container.parentNode !== parent) { + parent.appendChild(this._container); } } diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index c54196e2..5c17d50e 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -121,6 +121,10 @@ const api = (() => { return this._invoke('injectStylesheet', {type, value}); } + getStylesheetContent(url) { + return this._invoke('getStylesheetContent', {url}); + } + getEnvironmentInfo() { return this._invoke('getEnvironmentInfo'); } diff --git a/ext/mixed/js/dynamic-loader.js b/ext/mixed/js/dynamic-loader.js index 37f85112..981d1ee5 100644 --- a/ext/mixed/js/dynamic-loader.js +++ b/ext/mixed/js/dynamic-loader.js @@ -21,14 +21,36 @@ const dynamicLoader = (() => { const injectedStylesheets = new Map(); + const injectedStylesheetsWithParent = new WeakMap(); - async function loadStyle(id, type, value, useWebExtensionApi=false) { + function getInjectedStylesheet(id, parentNode) { + if (parentNode === null) { + return injectedStylesheets.get(id); + } + const map = injectedStylesheetsWithParent.get(parentNode); + return typeof map !== 'undefined' ? map.get(id) : void 0; + } + + function setInjectedStylesheet(id, parentNode, value) { + if (parentNode === null) { + injectedStylesheets.set(id, value); + return; + } + let map = injectedStylesheetsWithParent.get(parentNode); + if (typeof map === 'undefined') { + map = new Map(); + injectedStylesheetsWithParent.set(parentNode, map); + } + map.set(id, value); + } + + async function loadStyle(id, type, value, useWebExtensionApi=false, parentNode=null) { if (useWebExtensionApi && yomichan.isExtensionUrl(window.location.href)) { // Permissions error will occur if trying to use the WebExtension API to inject into an extension page useWebExtensionApi = false; } - let styleNode = injectedStylesheets.get(id); + let styleNode = getInjectedStylesheet(id, parentNode); if (typeof styleNode !== 'undefined') { if (styleNode === null) { // Previously injected via WebExtension API @@ -38,21 +60,30 @@ const dynamicLoader = (() => { styleNode = null; } + if (type === 'file-content') { + value = await api.getStylesheetContent(value); + type = 'code'; + useWebExtensionApi = false; + } + if (useWebExtensionApi) { // Inject via WebExtension API if (styleNode !== null && styleNode.parentNode !== null) { styleNode.parentNode.removeChild(styleNode); } - injectedStylesheets.set(id, null); + setInjectedStylesheet(id, parentNode, null); await api.injectStylesheet(type, value); return null; } // Create node in document - const parentNode = document.head; - if (parentNode === null) { - throw new Error('No parent node'); + let parentNode2 = parentNode; + if (parentNode2 === null) { + parentNode2 = document.head; + if (parentNode2 === null) { + throw new Error('No parent node'); + } } // Create or reuse node @@ -74,12 +105,12 @@ const dynamicLoader = (() => { } // Update parent - if (styleNode.parentNode !== parentNode) { - parentNode.appendChild(styleNode); + if (styleNode.parentNode !== parentNode2) { + parentNode2.appendChild(styleNode); } // Add to map - injectedStylesheets.set(id, styleNode); + setInjectedStylesheet(id, parentNode, styleNode); return styleNode; } -- cgit v1.2.3 From 22d70c9e22657400b02f7a851b1a2402e1c30595 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 26 Jun 2020 17:22:29 -0400 Subject: Add lock icon (#628) --- ext/mixed/img/lock.svg | 1 + resources/icons.svg | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 ext/mixed/img/lock.svg (limited to 'ext/mixed') diff --git a/ext/mixed/img/lock.svg b/ext/mixed/img/lock.svg new file mode 100644 index 00000000..7707ba98 --- /dev/null +++ b/ext/mixed/img/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons.svg b/resources/icons.svg index 9980492e..86c25825 100644 --- a/resources/icons.svg +++ b/resources/icons.svg @@ -31,7 +31,7 @@ inkscape:cx="12.059712" inkscape:cy="6.3977551" inkscape:document-units="px" - inkscape:current-layer="layer32" + inkscape:current-layer="layer37" showgrid="true" units="px" inkscape:snap-center="true" @@ -1026,7 +1026,7 @@ inkscape:connector-curvature="0" /> + + + -- cgit v1.2.3