From 8ee717cdf7c7701b9ec2d72799fcdcb621703e9e Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 9 Aug 2020 13:11:41 -0400 Subject: [PATCH] Persistent display mode (#714) * Simplify calls using chrome.tabs.sendMessage and getMessageResponseResult * Rename message handlers * Move onMessage handler into Display * Assign search popup mode which persists across refreshes * Update clipboard monitor on the search page * Remove mode param --- ext/bg/js/backend.js | 85 +++++++++++++++-------------- ext/bg/js/search.js | 117 +++++++++++++++++++++------------------- ext/fg/js/float.js | 2 +- ext/mixed/js/display.js | 51 ++++++++++++++++-- 4 files changed, 152 insertions(+), 103 deletions(-) diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 85b9b5e6..07920e01 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -899,24 +899,8 @@ class Backend { ); }); if (tab !== null) { - const isValidTab = await new Promise((resolve) => { - chrome.tabs.sendMessage( - tabId, - {action: 'getUrl', params: {}}, - {frameId: 0}, - (response) => { - let result = false; - try { - const {url} = yomichan.getMessageResponseResult(response); - result = url.startsWith(baseUrl); - } catch (e) { - // NOP - } - resolve(result); - } - ); - }); - // windowId + const url = await this._getTabUrl(tabId); + const isValidTab = (url !== null && url.startsWith(baseUrl)); if (isValidTab) { return {tab, created: false}; } @@ -935,7 +919,7 @@ class Backend { const popupWindow = await new Promise((resolve, reject) => { chrome.windows.create( { - url: `${baseUrl}?mode=popup`, + url: baseUrl, width: popupWidth, height: popupHeight, type: 'popup' @@ -959,23 +943,22 @@ class Backend { const tab = tabs[0]; await this._waitUntilTabFrameIsReady(tab.id, 0, 2000); + await this._sendMessageTab( + tab.id, + {action: 'setMode', params: {mode: 'popup'}}, + {frameId: 0} + ); + this._searchPopupTabId = tab.id; return {tab, created: true}; } _updateSearchQuery(tabId, text, animate) { - return new Promise((resolve, reject) => { - const callback = (response) => { - try { - resolve(yomichan.getMessageResponseResult(response)); - } catch (error) { - reject(error); - } - }; - - const message = {action: 'updateSearchQuery', params: {text, animate}}; - chrome.tabs.sendMessage(tabId, message, callback); - }); + return this._sendMessageTab( + tabId, + {action: 'updateSearchQuery', params: {text, animate}}, + {frameId: 0} + ); } _sendMessageAllTabs(action, params={}) { @@ -1381,18 +1364,20 @@ class Backend { return typeof templates === 'string' ? templates : this._defaultAnkiFieldTemplates; } - _getTabUrl(tab) { - return new Promise((resolve) => { - chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => { - let url = null; - try { - ({url} = yomichan.getMessageResponseResult(response)); - } catch (error) { - // NOP - } - resolve({tab, url}); - }); - }); + async _getTabUrl(tabId) { + try { + const {url} = await this._sendMessageTab( + tabId, + {action: 'getUrl', params: {}}, + {frameId: 0} + ); + if (typeof url === 'string') { + return url; + } + } catch (e) { + // NOP + } + return null; } async _findTab(timeout, checkUrl) { @@ -1538,4 +1523,18 @@ class Backend { } return await (json ? response.json() : response.text()); } + + _sendMessageTab(...args) { + return new Promise((resolve, reject) => { + const callback = (response) => { + try { + resolve(yomichan.getMessageResponseResult(response)); + } catch (error) { + reject(error); + } + }; + + chrome.tabs.sendMessage(...args, callback); + }); + } } diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index d95fd5e4..d2ec5090 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -37,6 +37,7 @@ class DisplaySearch extends Display { this._clipboardMonitor = new ClipboardMonitor({ getClipboard: api.clipboardGet.bind(api) }); + this._clipboardMonitorEnabled = false; this._onKeyDownIgnoreKeys = new Map([ ['ANY_MOD', new Set([ 'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageDown', 'PageUp', 'Home', 'End', @@ -51,9 +52,6 @@ class DisplaySearch extends Display { ['AltGraph', new Set()], ['Shift', new Set()] ]); - this._runtimeMessageHandlers = new Map([ - ['updateSearchQuery', {async: false, handler: this._onExternalSearchUpdate.bind(this)}] - ]); } async prepare() { @@ -62,18 +60,16 @@ class DisplaySearch extends Display { yomichan.on('optionsUpdated', () => this.updateOptions()); this.on('contentUpdating', this._onContentUpdating.bind(this)); + this.on('modeChange', this._onModeChange.bind(this)); + + this.registerMessageHandlers([ + ['updateSearchQuery', {async: false, handler: this._onExternalSearchUpdate.bind(this)}] + ]); this.queryParserVisible = true; this.setHistorySettings({useBrowserHistory: true}); const options = this.getOptions(); - - const urlSearchParams = new URLSearchParams(location.search); - let mode = urlSearchParams.get('mode'); - if (mode === null) { mode = ''; } - - document.documentElement.dataset.searchMode = mode; - if (options.general.enableWanakana === true) { this._wanakanaEnable.checked = true; wanakana.bind(this._query); @@ -81,25 +77,15 @@ class DisplaySearch extends Display { this._wanakanaEnable.checked = false; } - if (mode !== 'popup') { - if (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)); - 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('copy', this._onCopy.bind(this)); this._clipboardMonitor.on('change', this._onExternalSearchUpdate.bind(this)); + this._clipboardMonitorEnable.addEventListener('change', this._onClipboardMonitorEnableChange.bind(this)); this._updateSearchButton(); + this._onModeChange(); await this._prepareNestedPopups(); @@ -209,12 +195,6 @@ class DisplaySearch extends Display { this._onSearchQueryUpdated(query, true); } - _onRuntimeMessage({action, params}, sender, callback) { - const messageHandler = this._runtimeMessageHandlers.get(action); - if (typeof messageHandler === 'undefined') { return false; } - return yomichan.invokeMessageHandler(messageHandler, params, callback, sender); - } - _onCopy() { // ignore copy from search page this._clipboardMonitor.setPreviousText(window.getSelection().toString().trim()); @@ -261,34 +241,15 @@ class DisplaySearch extends Display { } _onClipboardMonitorEnableChange(e) { - if (e.target.checked) { - chrome.permissions.request( - {permissions: ['clipboardRead']}, - (granted) => { - if (granted) { - this._clipboardMonitor.start(); - api.modifySettings([{ - action: 'set', - path: 'general.enableClipboardMonitor', - value: true, - scope: 'profile', - optionsContext: this.getOptionsContext() - }], 'search'); - } else { - e.target.checked = false; - } - } - ); - } else { - this._clipboardMonitor.stop(); - api.modifySettings([{ - action: 'set', - path: 'general.enableClipboardMonitor', - value: false, - scope: 'profile', - optionsContext: this.getOptionsContext() - }], 'search'); - } + const enabled = e.target.checked; + this._setClipboardMonitorEnabled(enabled); + } + + _onModeChange() { + let mode = this.mode; + if (mode === null) { mode = ''; } + document.documentElement.dataset.searchMode = mode; + this._updateClipboardMonitorEnabled(); } _isWanakanaEnabled() { @@ -381,4 +342,48 @@ class DisplaySearch extends Display { await onOptionsUpdated(); } + + async _setClipboardMonitorEnabled(value) { + let modify = true; + if (value) { + value = await this._requestPermissions(['clipboardRead']); + modify = value; + } + + this._clipboardMonitorEnabled = value; + this._updateClipboardMonitorEnabled(); + + if (!modify) { return; } + + await api.modifySettings([{ + action: 'set', + path: 'general.enableClipboardMonitor', + value, + scope: 'profile', + optionsContext: this.getOptionsContext() + }], 'search'); + } + + _updateClipboardMonitorEnabled() { + const mode = this.mode; + const enabled = this._clipboardMonitorEnabled; + this._clipboardMonitorEnable.checked = enabled; + if (enabled && mode !== 'popup') { + this._clipboardMonitor.start(); + } else { + this._clipboardMonitor.stop(); + } + } + + _requestPermissions(permissions) { + return new Promise((resolve) => { + chrome.permissions.request( + {permissions}, + (granted) => { + const e = chrome.runtime.lastError; + resolve(!e && granted); + } + ); + }); + } } diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 794ccc6e..83c542d1 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -44,7 +44,7 @@ class DisplayFloat extends Display { async prepare() { await super.prepare(); - this.registerMessageHandlers([ + this.registerDirectMessageHandlers([ ['configure', {async: true, handler: this._onMessageConfigure.bind(this)}], ['setContentScale', {async: false, handler: this._onMessageSetContentScale.bind(this)}] ]); diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 93272666..418707ca 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -65,6 +65,7 @@ class Display extends EventDispatcher { this._hotkeys = new Map(); this._actions = new Map(); this._messageHandlers = new Map(); + this._directMessageHandlers = new Map(); this._history = new DisplayHistory({clearable: true, useBrowserHistory: false}); this._historyChangeIgnore = false; this._historyHasChanged = false; @@ -80,6 +81,7 @@ class Display extends EventDispatcher { getOptionsContext: this.getOptionsContext.bind(this), setSpinnerVisible: this.setSpinnerVisible.bind(this) }); + this._mode = null; this.registerActions([ ['close', () => { this.onEscape(); }], @@ -114,6 +116,9 @@ class Display extends EventDispatcher { {key: 'V', modifiers: ['alt'], action: 'viewNote'} ]); this.registerMessageHandlers([ + ['setMode', {async: false, handler: this._onMessageSetMode.bind(this)}] + ]); + this.registerDirectMessageHandlers([ ['setOptionsContext', {async: false, handler: this._onMessageSetOptionsContext.bind(this)}], ['setContent', {async: false, handler: this._onMessageSetContent.bind(this)}], ['clearAutoPlayTimer', {async: false, handler: this._onMessageClearAutoPlayTimer.bind(this)}], @@ -138,7 +143,12 @@ class Display extends EventDispatcher { this._updateQueryParserVisibility(); } + get mode() { + return this._mode; + } + async prepare() { + this._updateMode(); this._setInteractive(true); await this._displayGenerator.prepare(); await this._queryParser.prepare(); @@ -146,8 +156,9 @@ class Display extends EventDispatcher { this._history.on('stateChanged', this._onStateChanged.bind(this)); this._queryParser.on('searched', this._onQueryParserSearch.bind(this)); yomichan.on('extensionUnloaded', this._onExtensionUnloaded.bind(this)); + chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); api.crossFrame.registerHandlers([ - ['popupMessage', {async: 'dynamic', handler: this._onMessage.bind(this)}] + ['popupMessage', {async: 'dynamic', handler: this._onDirectMessage.bind(this)}] ]); } @@ -313,6 +324,12 @@ class Display extends EventDispatcher { } } + registerDirectMessageHandlers(handlers) { + for (const [name, handlerInfo] of handlers) { + this._directMessageHandlers.set(name, handlerInfo); + } + } + async setupNestedPopups(frontendInitializationData) { await dynamicLoader.loadScripts([ '/mixed/js/text-scanner.js', @@ -343,10 +360,16 @@ class Display extends EventDispatcher { // Message handlers - _onMessage(data) { + _onMessage({action, params}, sender, callback) { + const messageHandler = this._messageHandlers.get(action); + if (typeof messageHandler === 'undefined') { return false; } + return yomichan.invokeMessageHandler(messageHandler, params, callback, sender); + } + + _onDirectMessage(data) { data = this.authenticateMessageData(data); const {action, params} = data; - const handlerInfo = this._messageHandlers.get(action); + const handlerInfo = this._directMessageHandlers.get(action); if (typeof handlerInfo === 'undefined') { throw new Error(`Invalid action: ${action}`); } @@ -356,6 +379,10 @@ class Display extends EventDispatcher { return {async, result}; } + _onMessageSetMode({mode}) { + this._setMode(mode, true); + } + _onMessageSetOptionsContext({optionsContext}) { this.setOptionsContext(optionsContext); } @@ -1253,4 +1280,22 @@ class Display extends EventDispatcher { _closePopups() { yomichan.trigger('closePopups'); } + + _updateMode() { + const mode = sessionStorage.getItem('mode'); + this._setMode(mode, false); + } + + _setMode(mode, save) { + if (mode === this._mode) { return; } + if (save) { + if (mode === null) { + sessionStorage.removeItem('mode'); + } else { + sessionStorage.setItem('mode', mode); + } + } + this._mode = mode; + this.trigger('modeChange', {mode}); + } }