From 286534e648af350d24fbf3c7892a7ec81aaeb4bd Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 14 Feb 2021 15:53:35 -0500 Subject: [PATCH] Move api to yomichan object (#1392) * Move cross frame API from API to Yomichan * Add API instance to Yomichan * Move api global to yomichan.api * Pass yomichan to API * Remove IIFE --- ext/js/app/content-script-main.js | 4 +- ext/js/app/frontend.js | 17 +- ext/js/app/popup-factory.js | 5 +- ext/js/app/popup-proxy.js | 6 +- ext/js/app/popup-window.js | 12 +- ext/js/app/popup.js | 5 +- ext/js/comm/api.js | 600 +++++++++--------- ext/js/comm/frame-ancestry-handler.js | 10 +- ext/js/comm/frame-endpoint.js | 8 +- ext/js/comm/frame-offset-forwarder.js | 5 +- ext/js/display/display-audio.js | 3 +- ext/js/display/display-generator.js | 3 +- ext/js/display/display-profile-selection.js | 5 +- ext/js/display/display.js | 31 +- ext/js/display/popup-main.js | 4 +- ext/js/display/query-parser.js | 5 +- ext/js/display/search-display-controller.js | 7 +- ext/js/display/search-main.js | 4 +- ext/js/input/hotkey-handler.js | 5 +- ext/js/input/hotkey-help-controller.js | 3 +- ext/js/language/text-scanner.js | 5 +- ext/js/media/media-loader.js | 6 +- ext/js/pages/action-popup-main.js | 16 +- ext/js/pages/info-main.js | 8 +- ext/js/pages/permissions-main.js | 4 +- ext/js/pages/welcome-main.js | 6 +- ext/js/script/dynamic-loader.js | 8 +- ext/js/settings/anki-templates-controller.js | 5 +- ext/js/settings/backup-controller.js | 5 +- ext/js/settings/dictionary-controller.js | 5 +- .../settings/dictionary-import-controller.js | 5 +- ...extension-keyboard-shortcuts-controller.js | 3 +- .../settings/keyboard-shortcuts-controller.js | 3 +- ext/js/settings/main.js | 6 +- ext/js/settings/mecab-controller.js | 6 +- ext/js/settings/pitch-accents-preview-main.js | 2 - ext/js/settings/popup-preview-frame-main.js | 4 +- ext/js/settings/popup-preview-frame.js | 5 +- ext/js/settings/popup-window-controller.js | 6 +- ext/js/settings/profile-controller.js | 3 +- ext/js/settings/scan-inputs-controller.js | 3 +- .../settings/scan-inputs-simple-controller.js | 3 +- ext/js/settings/settings-controller.js | 13 +- ext/js/settings/settings-main.js | 6 +- .../templates/template-renderer-frame-main.js | 4 +- ext/js/yomichan.js | 534 ++++++++-------- 46 files changed, 672 insertions(+), 744 deletions(-) diff --git a/ext/js/app/content-script-main.js b/ext/js/app/content-script-main.js index ee05034e..a09e52ea 100644 --- a/ext/js/app/content-script-main.js +++ b/ext/js/app/content-script-main.js @@ -19,15 +19,13 @@ * Frontend * HotkeyHandler * PopupFactory - * api */ (async () => { try { - api.prepare(); await yomichan.prepare(); - const {tabId, frameId} = await api.frameInformationGet(); + const {tabId, frameId} = await yomichan.api.frameInformationGet(); if (typeof frameId !== 'number') { throw new Error('Failed to get frameId'); } diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js index a62b06bf..74cc63d2 100644 --- a/ext/js/app/frontend.js +++ b/ext/js/app/frontend.js @@ -20,7 +20,6 @@ * TextScanner * TextSourceElement * TextSourceRange - * api */ class Frontend { @@ -99,7 +98,7 @@ class Frontend { async prepare() { await this.updateOptions(); try { - const {zoomFactor} = await api.getZoom(); + const {zoomFactor} = await yomichan.api.getZoom(); this._pageZoomFactor = zoomFactor; } catch (e) { // Ignore exceptions which may occur due to being on an unsupported page (e.g. about:blank) @@ -124,7 +123,7 @@ class Frontend { this._textScanner.on('clearSelection', this._onClearSelection.bind(this)); this._textScanner.on('searched', this._onSearched.bind(this)); - api.crossFrame.registerHandlers([ + yomichan.crossFrame.registerHandlers([ ['closePopup', {async: false, handler: this._onApiClosePopup.bind(this)}], ['copySelection', {async: false, handler: this._onApiCopySelection.bind(this)}], ['getSelectionText', {async: false, handler: this._onApiGetSelectionText.bind(this)}], @@ -332,7 +331,7 @@ class Frontend { async _updateOptionsInternal() { const optionsContext = await this._getOptionsContext(); - const options = await api.optionsGet(optionsContext); + const options = await yomichan.api.optionsGet(optionsContext); const {scanning: scanningOptions, sentenceParsing: sentenceParsingOptions} = options; this._options = options; @@ -462,7 +461,7 @@ class Frontend { return await this._getDefaultPopup(); } - const {popupId} = await api.crossFrame.invoke(targetFrameId, 'getPopupInfo'); + const {popupId} = await yomichan.crossFrame.invoke(targetFrameId, 'getPopupInfo'); if (popupId === null) { return null; } @@ -608,9 +607,9 @@ class Frontend { _signalFrontendReady(targetFrameId=null) { const params = {frameId: this._frameId}; if (targetFrameId === null) { - api.broadcastTab('frontendReady', params); + yomichan.api.broadcastTab('frontendReady', params); } else { - api.sendMessageToFrame(targetFrameId, 'frontendReady', params); + yomichan.api.sendMessageToFrame(targetFrameId, 'frontendReady', params); } } @@ -627,7 +626,7 @@ class Frontend { }, 10000 ); - api.broadcastTab('requestFrontendReadyBroadcast', {frameId: this._frameId}); + yomichan.api.broadcastTab('requestFrontendReadyBroadcast', {frameId: this._frameId}); await promise; } @@ -653,7 +652,7 @@ class Frontend { let documentTitle = document.title; if (this._useProxyPopup) { try { - ({url, documentTitle} = await api.crossFrame.invoke(this._parentFrameId, 'getPageInfo', {})); + ({url, documentTitle} = await yomichan.crossFrame.invoke(this._parentFrameId, 'getPageInfo', {})); } catch (e) { // NOP } diff --git a/ext/js/app/popup-factory.js b/ext/js/app/popup-factory.js index 7571d7ab..8f0c2a6e 100644 --- a/ext/js/app/popup-factory.js +++ b/ext/js/app/popup-factory.js @@ -20,7 +20,6 @@ * Popup * PopupProxy * PopupWindow - * api */ class PopupFactory { @@ -35,7 +34,7 @@ class PopupFactory { prepare() { this._frameOffsetForwarder.prepare(); - api.crossFrame.registerHandlers([ + yomichan.crossFrame.registerHandlers([ ['getOrCreatePopup', {async: true, handler: this._onApiGetOrCreatePopup.bind(this)}], ['setOptionsContext', {async: true, handler: this._onApiSetOptionsContext.bind(this)}], ['hide', {async: false, handler: this._onApiHide.bind(this)}], @@ -132,7 +131,7 @@ class PopupFactory { throw new Error('Invalid frameId'); } const useFrameOffsetForwarder = (parentPopupId === null); - ({id, depth, frameId} = await api.crossFrame.invoke(frameId, 'getOrCreatePopup', { + ({id, depth, frameId} = await yomichan.crossFrame.invoke(frameId, 'getOrCreatePopup', { id, parentPopupId, frameId, diff --git a/ext/js/app/popup-proxy.js b/ext/js/app/popup-proxy.js index b2e81824..19856e3f 100644 --- a/ext/js/app/popup-proxy.js +++ b/ext/js/app/popup-proxy.js @@ -15,10 +15,6 @@ * along with this program. If not, see . */ -/* global - * api - */ - class PopupProxy extends EventDispatcher { constructor({ id, @@ -158,7 +154,7 @@ class PopupProxy extends EventDispatcher { // Private _invoke(action, params={}) { - return api.crossFrame.invoke(this._frameId, action, params); + return yomichan.crossFrame.invoke(this._frameId, action, params); } async _invokeSafe(action, params={}, defaultReturnValue) { diff --git a/ext/js/app/popup-window.js b/ext/js/app/popup-window.js index 5fa0c647..d0826775 100644 --- a/ext/js/app/popup-window.js +++ b/ext/js/app/popup-window.js @@ -15,10 +15,6 @@ * along with this program. If not, see . */ -/* global - * api - */ - class PopupWindow extends EventDispatcher { constructor({ id, @@ -82,7 +78,7 @@ class PopupWindow extends EventDispatcher { } async isVisible() { - return (this._popupTabId !== null && await api.isTabSearchPopup(this._popupTabId)); + return (this._popupTabId !== null && await yomichan.api.isTabSearchPopup(this._popupTabId)); } async setVisibleOverride(_value, _priority) { @@ -148,7 +144,7 @@ class PopupWindow extends EventDispatcher { const frameId = 0; if (this._popupTabId !== null) { try { - return await api.crossFrame.invokeTab(this._popupTabId, frameId, 'popupMessage', {action, params}); + return await yomichan.crossFrame.invokeTab(this._popupTabId, frameId, 'popupMessage', {action, params}); } catch (e) { if (yomichan.isExtensionUnloaded) { open = false; @@ -161,9 +157,9 @@ class PopupWindow extends EventDispatcher { return defaultReturnValue; } - const {tabId} = await api.getOrCreateSearchPopup({focus: 'ifCreated'}); + const {tabId} = await yomichan.api.getOrCreateSearchPopup({focus: 'ifCreated'}); this._popupTabId = tabId; - return await api.crossFrame.invokeTab(this._popupTabId, frameId, 'popupMessage', {action, params}); + return await yomichan.crossFrame.invokeTab(this._popupTabId, frameId, 'popupMessage', {action, params}); } } diff --git a/ext/js/app/popup.js b/ext/js/app/popup.js index 75b74257..44cca14a 100644 --- a/ext/js/app/popup.js +++ b/ext/js/app/popup.js @@ -18,7 +18,6 @@ /* global * DocumentUtil * FrameClient - * api * dynamicLoader */ @@ -460,7 +459,7 @@ class Popup extends EventDispatcher { if (this._frameClient === null || !this._frameClient.isConnected() || contentWindow === null) { return; } const message = this._frameClient.createMessage({action, params}); - return await api.crossFrame.invoke(this._frameClient.frameId, 'popupMessage', message); + return await yomichan.crossFrame.invoke(this._frameClient.frameId, 'popupMessage', message); } async _invokeSafe(action, params={}, defaultReturnValue) { @@ -676,7 +675,7 @@ class Popup extends EventDispatcher { async _setOptionsContext(optionsContext) { this._optionsContext = optionsContext; - this._options = await api.optionsGet(optionsContext); + this._options = await yomichan.api.optionsGet(optionsContext); this.updateTheme(); } diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index 26397d1f..472e464b 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -15,319 +15,291 @@ * along with this program. If not, see . */ -/* global - * CrossFrameAPI - */ - -const api = (() => { - class API { - constructor() { - this._prepared = false; - this._crossFrame = null; - } - - get crossFrame() { - return this._crossFrame; - } - - prepare() { - if (this._prepared) { return; } - this._crossFrame = new CrossFrameAPI(); - this._crossFrame.prepare(); - yomichan.on('log', this._onLog.bind(this)); - this._prepared = true; - } - - // Invoke functions - - optionsGet(optionsContext) { - return this._invoke('optionsGet', {optionsContext}); - } - - optionsGetFull() { - return this._invoke('optionsGetFull'); - } - - 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}); - } - - isAnkiConnected() { - return this._invoke('isAnkiConnected'); - } - - getAnkiConnectVersion() { - return this._invoke('getAnkiConnectVersion'); - } - - addAnkiNote(note) { - return this._invoke('addAnkiNote', {note}); - } - - getAnkiNoteInfo(notes) { - return this._invoke('getAnkiNoteInfo', {notes}); - } - - injectAnkiNoteMedia(timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails) { - return this._invoke('injectAnkiNoteMedia', {timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails}); - } - - noteView(noteId) { - return this._invoke('noteView', {noteId}); - } - - suspendAnkiCardsForNote(noteId) { - return this._invoke('suspendAnkiCardsForNote', {noteId}); - } - - getExpressionAudioInfoList(source, expression, reading, details) { - return this._invoke('getExpressionAudioInfoList', {source, expression, reading, details}); - } - - commandExec(command, params) { - return this._invoke('commandExec', {command, params}); - } - - sendMessageToFrame(frameId, action, params) { - return this._invoke('sendMessageToFrame', {frameId, action, params}); - } - - broadcastTab(action, params) { - return this._invoke('broadcastTab', {action, params}); - } - - frameInformationGet() { - return this._invoke('frameInformationGet'); - } - - injectStylesheet(type, value) { - return this._invoke('injectStylesheet', {type, value}); - } - - getStylesheetContent(url) { - return this._invoke('getStylesheetContent', {url}); - } - - getEnvironmentInfo() { - return this._invoke('getEnvironmentInfo'); - } - - clipboardGet() { - return this._invoke('clipboardGet'); - } - - getDisplayTemplatesHtml() { - return this._invoke('getDisplayTemplatesHtml'); - } - - getZoom() { - return this._invoke('getZoom'); - } - - getDefaultAnkiFieldTemplates() { - return this._invoke('getDefaultAnkiFieldTemplates'); - } - - getDictionaryInfo() { - return this._invoke('getDictionaryInfo'); - } - - getDictionaryCounts(dictionaryNames, getTotal) { - return this._invoke('getDictionaryCounts', {dictionaryNames, getTotal}); - } - - purgeDatabase() { - return this._invoke('purgeDatabase'); - } - - getMedia(targets) { - return this._invoke('getMedia', {targets}); - } - - logIndicatorClear() { - return this._invoke('logIndicatorClear'); - } - - modifySettings(targets, source) { - return this._invoke('modifySettings', {targets, source}); - } - - getSettings(targets) { - return this._invoke('getSettings', {targets}); - } - - setAllSettings(value, source) { - return this._invoke('setAllSettings', {value, source}); - } - - getOrCreateSearchPopup(details) { - return this._invoke('getOrCreateSearchPopup', isObject(details) ? details : {}); - } - - isTabSearchPopup(tabId) { - return this._invoke('isTabSearchPopup', {tabId}); - } - - triggerDatabaseUpdated(type, cause) { - return this._invoke('triggerDatabaseUpdated', {type, cause}); - } - - testMecab() { - return this._invoke('testMecab', {}); - } - - // Utilities - - _createActionPort(timeout=5000) { - return new Promise((resolve, reject) => { - let timer = null; - const portDetails = deferPromise(); - - const onConnect = async (port) => { - try { - const {name: expectedName, id: expectedId} = await portDetails.promise; - const {name, id} = JSON.parse(port.name); - if (name !== expectedName || id !== expectedId || 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); - portDetails.reject(e); - reject(e); - }; - - timer = setTimeout(() => onError(new Error('Timeout')), timeout); - - chrome.runtime.onConnect.addListener(onConnect); - this._invoke('createActionPort').then(portDetails.resolve, onError); - }); - } - - _invokeWithProgress(action, params, onProgress, timeout=5000) { - return new Promise((resolve, reject) => { - let port = null; - - if (typeof onProgress !== 'function') { - onProgress = () => {}; - } - - const onMessage = (message) => { - switch (message.type) { - case 'progress': - try { - onProgress(...message.data); - } catch (e) { - // NOP - } - break; - case 'complete': - cleanup(); - resolve(message.data); - break; - case 'error': - cleanup(); - reject(deserializeError(message.data)); - break; - } - }; - - const onDisconnect = () => { - cleanup(); - reject(new Error('Disconnected')); - }; - - const cleanup = () => { - if (port !== null) { - port.onMessage.removeListener(onMessage); - port.onDisconnect.removeListener(onDisconnect); - port.disconnect(); - port = null; - } - onProgress = null; - }; - - (async () => { - try { - port = await this._createActionPort(timeout); - port.onMessage.addListener(onMessage); - port.onDisconnect.addListener(onDisconnect); - - // 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); - } finally { - action = null; - params = null; - } - })(); - }); - } - - _invoke(action, params={}) { - const data = {action, params}; - return new Promise((resolve, reject) => { - try { - yomichan.sendMessage(data, (response) => { - this._checkLastError(chrome.runtime.lastError); - if (response !== null && typeof response === 'object') { - if (typeof response.error !== 'undefined') { - reject(deserializeError(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); - } - }); - } - - _checkLastError() { - // NOP - } - - async _onLog({error, level, context}) { - try { - error = serializeError(error); - await this._invoke('log', {error, level, context}); - } catch (e) { - // NOP - } - } +class API { + constructor(yomichan) { + this._yomichan = yomichan; } - return new API(); -})(); + optionsGet(optionsContext) { + return this._invoke('optionsGet', {optionsContext}); + } + + optionsGetFull() { + return this._invoke('optionsGetFull'); + } + + 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}); + } + + isAnkiConnected() { + return this._invoke('isAnkiConnected'); + } + + getAnkiConnectVersion() { + return this._invoke('getAnkiConnectVersion'); + } + + addAnkiNote(note) { + return this._invoke('addAnkiNote', {note}); + } + + getAnkiNoteInfo(notes) { + return this._invoke('getAnkiNoteInfo', {notes}); + } + + injectAnkiNoteMedia(timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails) { + return this._invoke('injectAnkiNoteMedia', {timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails}); + } + + noteView(noteId) { + return this._invoke('noteView', {noteId}); + } + + suspendAnkiCardsForNote(noteId) { + return this._invoke('suspendAnkiCardsForNote', {noteId}); + } + + getExpressionAudioInfoList(source, expression, reading, details) { + return this._invoke('getExpressionAudioInfoList', {source, expression, reading, details}); + } + + commandExec(command, params) { + return this._invoke('commandExec', {command, params}); + } + + sendMessageToFrame(frameId, action, params) { + return this._invoke('sendMessageToFrame', {frameId, action, params}); + } + + broadcastTab(action, params) { + return this._invoke('broadcastTab', {action, params}); + } + + frameInformationGet() { + return this._invoke('frameInformationGet'); + } + + injectStylesheet(type, value) { + return this._invoke('injectStylesheet', {type, value}); + } + + getStylesheetContent(url) { + return this._invoke('getStylesheetContent', {url}); + } + + getEnvironmentInfo() { + return this._invoke('getEnvironmentInfo'); + } + + clipboardGet() { + return this._invoke('clipboardGet'); + } + + getDisplayTemplatesHtml() { + return this._invoke('getDisplayTemplatesHtml'); + } + + getZoom() { + return this._invoke('getZoom'); + } + + getDefaultAnkiFieldTemplates() { + return this._invoke('getDefaultAnkiFieldTemplates'); + } + + getDictionaryInfo() { + return this._invoke('getDictionaryInfo'); + } + + 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}); + } + + getSettings(targets) { + return this._invoke('getSettings', {targets}); + } + + setAllSettings(value, source) { + return this._invoke('setAllSettings', {value, source}); + } + + getOrCreateSearchPopup(details) { + return this._invoke('getOrCreateSearchPopup', isObject(details) ? details : {}); + } + + isTabSearchPopup(tabId) { + return this._invoke('isTabSearchPopup', {tabId}); + } + + triggerDatabaseUpdated(type, cause) { + return this._invoke('triggerDatabaseUpdated', {type, cause}); + } + + testMecab() { + return this._invoke('testMecab', {}); + } + + // Utilities + + _createActionPort(timeout=5000) { + return new Promise((resolve, reject) => { + let timer = null; + const portDetails = deferPromise(); + + const onConnect = async (port) => { + try { + const {name: expectedName, id: expectedId} = await portDetails.promise; + const {name, id} = JSON.parse(port.name); + if (name !== expectedName || id !== expectedId || 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); + portDetails.reject(e); + reject(e); + }; + + timer = setTimeout(() => onError(new Error('Timeout')), timeout); + + chrome.runtime.onConnect.addListener(onConnect); + this._invoke('createActionPort').then(portDetails.resolve, onError); + }); + } + + _invokeWithProgress(action, params, onProgress, timeout=5000) { + return new Promise((resolve, reject) => { + let port = null; + + if (typeof onProgress !== 'function') { + onProgress = () => {}; + } + + const onMessage = (message) => { + switch (message.type) { + case 'progress': + try { + onProgress(...message.data); + } catch (e) { + // NOP + } + break; + case 'complete': + cleanup(); + resolve(message.data); + break; + case 'error': + cleanup(); + reject(deserializeError(message.data)); + break; + } + }; + + const onDisconnect = () => { + cleanup(); + reject(new Error('Disconnected')); + }; + + const cleanup = () => { + if (port !== null) { + port.onMessage.removeListener(onMessage); + port.onDisconnect.removeListener(onDisconnect); + port.disconnect(); + port = null; + } + onProgress = null; + }; + + (async () => { + try { + port = await this._createActionPort(timeout); + port.onMessage.addListener(onMessage); + port.onDisconnect.addListener(onDisconnect); + + // 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); + } finally { + action = null; + params = null; + } + })(); + }); + } + + _invoke(action, params={}) { + const data = {action, params}; + return new Promise((resolve, reject) => { + try { + this._yomichan.sendMessage(data, (response) => { + this._checkLastError(chrome.runtime.lastError); + if (response !== null && typeof response === 'object') { + if (typeof response.error !== 'undefined') { + reject(deserializeError(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); + } + }); + } + + _checkLastError() { + // NOP + } +} diff --git a/ext/js/comm/frame-ancestry-handler.js b/ext/js/comm/frame-ancestry-handler.js index b1ed7114..d53334e1 100644 --- a/ext/js/comm/frame-ancestry-handler.js +++ b/ext/js/comm/frame-ancestry-handler.js @@ -15,10 +15,6 @@ * along with this program. If not, see . */ -/* global - * api - */ - /** * This class is used to return the ancestor frame IDs for the current frame. * This is a workaround to using the `webNavigation.getAllFrames` API, which @@ -118,7 +114,7 @@ class FrameAncestryHandler { clearTimeout(timer); timer = null; } - api.crossFrame.unregisterHandler(responseMessageId); + yomichan.crossFrame.unregisterHandler(responseMessageId); }; const onMessage = (params) => { if (params.nonce !== nonce) { return null; } @@ -148,7 +144,7 @@ class FrameAncestryHandler { }; // Start - api.crossFrame.registerHandlers([[responseMessageId, {async: false, handler: onMessage}]]); + yomichan.crossFrame.registerHandlers([[responseMessageId, {async: false, handler: onMessage}]]); resetTimeout(); const frameId = this._frameId; this._requestFrameInfo(targetWindow, frameId, frameId, uniqueId, nonce); @@ -187,7 +183,7 @@ class FrameAncestryHandler { const responseMessageId = `${this._responseMessageIdBase}${uniqueId}`; try { - const response = await api.crossFrame.invoke(originFrameId, responseMessageId, responseParams); + const response = await yomichan.crossFrame.invoke(originFrameId, responseMessageId, responseParams); if (response === null) { return; } nonce = response.nonce; } catch (e) { diff --git a/ext/js/comm/frame-endpoint.js b/ext/js/comm/frame-endpoint.js index 27af9cf3..bc3c50f8 100644 --- a/ext/js/comm/frame-endpoint.js +++ b/ext/js/comm/frame-endpoint.js @@ -15,10 +15,6 @@ * along with this program. If not, see . */ -/* global - * api - */ - class FrameEndpoint { constructor() { this._secret = generateId(16); @@ -32,7 +28,7 @@ class FrameEndpoint { this._eventListeners.addEventListener(window, 'message', this._onMessage.bind(this), false); this._eventListenersSetup = true; } - api.broadcastTab('frameEndpointReady', {secret: this._secret}); + yomichan.api.broadcastTab('frameEndpointReady', {secret: this._secret}); } authenticate(message) { @@ -60,6 +56,6 @@ class FrameEndpoint { this._token = token; this._eventListeners.removeAllEventListeners(); - api.sendMessageToFrame(hostFrameId, 'frameEndpointConnected', {secret, token}); + yomichan.api.sendMessageToFrame(hostFrameId, 'frameEndpointConnected', {secret, token}); } } diff --git a/ext/js/comm/frame-offset-forwarder.js b/ext/js/comm/frame-offset-forwarder.js index 0a0b4a18..2382a9fa 100644 --- a/ext/js/comm/frame-offset-forwarder.js +++ b/ext/js/comm/frame-offset-forwarder.js @@ -17,7 +17,6 @@ /* global * FrameAncestryHandler - * api */ class FrameOffsetForwarder { @@ -28,7 +27,7 @@ class FrameOffsetForwarder { prepare() { this._frameAncestryHandler.prepare(); - api.crossFrame.registerHandlers([ + yomichan.crossFrame.registerHandlers([ ['FrameOffsetForwarder.getChildFrameRect', {async: false, handler: this._onMessageGetChildFrameRect.bind(this)}] ]); } @@ -43,7 +42,7 @@ class FrameOffsetForwarder { let childFrameId = this._frameId; const promises = []; for (const frameId of ancestorFrameIds) { - promises.push(api.crossFrame.invoke(frameId, 'FrameOffsetForwarder.getChildFrameRect', {frameId: childFrameId})); + promises.push(yomichan.crossFrame.invoke(frameId, 'FrameOffsetForwarder.getChildFrameRect', {frameId: childFrameId})); childFrameId = frameId; } diff --git a/ext/js/display/display-audio.js b/ext/js/display/display-audio.js index f624d85b..24f2dd7b 100644 --- a/ext/js/display/display-audio.js +++ b/ext/js/display/display-audio.js @@ -18,7 +18,6 @@ /* global * AudioSystem * PopupMenu - * api */ class DisplayAudio { @@ -314,7 +313,7 @@ class DisplayAudio { } async _getExpressionAudioInfoList(source, expression, reading, details) { - const infoList = await api.getExpressionAudioInfoList(source, expression, reading, details); + const infoList = await yomichan.api.getExpressionAudioInfoList(source, expression, reading, details); return infoList.map((info) => ({info, audioPromise: null, audioResolved: false, audio: null})); } diff --git a/ext/js/display/display-generator.js b/ext/js/display/display-generator.js index 05376ee5..18159f68 100644 --- a/ext/js/display/display-generator.js +++ b/ext/js/display/display-generator.js @@ -18,7 +18,6 @@ /* global * DictionaryDataUtil * HtmlTemplateCollection - * api */ class DisplayGenerator { @@ -31,7 +30,7 @@ class DisplayGenerator { } async prepare() { - const html = await api.getDisplayTemplatesHtml(); + const html = await yomichan.api.getDisplayTemplatesHtml(); this._templates = new HtmlTemplateCollection(html); this.updateHotkeys(); } diff --git a/ext/js/display/display-profile-selection.js b/ext/js/display/display-profile-selection.js index 0a44392e..d858da28 100644 --- a/ext/js/display/display-profile-selection.js +++ b/ext/js/display/display-profile-selection.js @@ -17,7 +17,6 @@ /* global * PanelElement - * api */ class DisplayProfileSelection { @@ -67,7 +66,7 @@ class DisplayProfileSelection { async _updateProfileList() { this._profileListNeedsUpdate = false; - const options = await api.optionsGetFull(); + const options = await yomichan.api.optionsGetFull(); this._eventListeners.removeAllEventListeners(); const displayGenerator = this._display.displayGenerator; @@ -95,7 +94,7 @@ class DisplayProfileSelection { } async _setProfileCurrent(index) { - await api.modifySettings([{ + await yomichan.api.modifySettings([{ action: 'set', path: 'profileCurrent', value: index, diff --git a/ext/js/display/display.js b/ext/js/display/display.js index fe4160a6..553c17e7 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -31,7 +31,6 @@ * QueryParser * ScrollElement * TextScanner - * api * dynamicLoader */ @@ -206,7 +205,7 @@ class Display extends EventDispatcher { async prepare() { // State setup const {documentElement} = document; - const {browser} = await api.getEnvironmentInfo(); + const {browser} = await yomichan.api.getEnvironmentInfo(); this._browser = browser; // Prepare @@ -221,7 +220,7 @@ class Display extends EventDispatcher { this._queryParser.on('searched', this._onQueryParserSearch.bind(this)); this._progressIndicatorVisible.on('change', this._onProgressIndicatorVisibleChanged.bind(this)); yomichan.on('extensionUnloaded', this._onExtensionUnloaded.bind(this)); - api.crossFrame.registerHandlers([ + yomichan.crossFrame.registerHandlers([ ['popupMessage', {async: 'dynamic', handler: this._onDirectMessage.bind(this)}] ]); window.addEventListener('message', this._onWindowMessage.bind(this), false); @@ -290,7 +289,7 @@ class Display extends EventDispatcher { } async updateOptions() { - const options = await api.optionsGet(this.getOptionsContext()); + const options = await yomichan.api.optionsGet(this.getOptionsContext()); const templates = await this._getAnkiFieldTemplates(options); const {scanning: scanningOptions, sentenceParsing: sentenceParsingOptions} = options; this._options = options; @@ -674,7 +673,7 @@ class Display extends EventDispatcher { if (typeof documentTitle !== 'string') { documentTitle = document.title; } const optionsContext = this.getOptionsContext(); const query = e.currentTarget.textContent; - const definitions = await api.kanjiFind(query, optionsContext); + const definitions = await yomichan.api.kanjiFind(query, optionsContext); const details = { focus: false, history: true, @@ -707,7 +706,7 @@ class Display extends EventDispatcher { _onNoteView(e) { e.preventDefault(); const link = e.currentTarget; - api.noteView(link.dataset.noteId); + yomichan.api.noteView(link.dataset.noteId); } _onWheel(e) { @@ -839,10 +838,10 @@ class Display extends EventDispatcher { } } - const {definitions} = await api.termsFind(source, findDetails, optionsContext); + const {definitions} = await yomichan.api.termsFind(source, findDetails, optionsContext); return definitions; } else { - const definitions = await api.kanjiFind(source, optionsContext); + const definitions = await yomichan.api.kanjiFind(source, optionsContext); return definitions; } } @@ -1059,7 +1058,7 @@ class Display extends EventDispatcher { const noteContext = this._getNoteContext(); states = await this._areDefinitionsAddable(definitions, modes, noteContext); } else { - if (!await api.isAnkiConnected()) { + if (!await yomichan.api.isAnkiConnected()) { throw new Error('Anki not connected'); } states = this._areDefinitionsAddableForcedValue(definitions, modes, true); @@ -1183,7 +1182,7 @@ class Display extends EventDispatcher { _tryViewAnkiNoteForSelectedDefinition() { const button = this._viewerButtonFind(this._index); if (button !== null && !button.disabled) { - api.noteView(button.dataset.noteId); + yomichan.api.noteView(button.dataset.noteId); } } @@ -1206,7 +1205,7 @@ class Display extends EventDispatcher { let noteId = null; let addNoteOkay = false; try { - noteId = await api.addAnkiNote(note); + noteId = await yomichan.api.addAnkiNote(note); addNoteOkay = true; } catch (e) { errors.length = 0; @@ -1219,7 +1218,7 @@ class Display extends EventDispatcher { } else { if (suspendNewCards) { try { - await api.suspendAnkiCardsForNote(noteId); + await yomichan.api.suspendAnkiCardsForNote(noteId); } catch (e) { errors.push(e); } @@ -1400,7 +1399,7 @@ class Display extends EventDispatcher { templates = this._ankiFieldTemplatesDefault; if (typeof templates === 'string') { return templates; } - templates = await api.getDefaultAnkiFieldTemplates(); + templates = await yomichan.api.getDefaultAnkiFieldTemplates(); this._ankiFieldTemplatesDefault = templates; return templates; } @@ -1416,7 +1415,7 @@ class Display extends EventDispatcher { } const notes = await Promise.all(notePromises); - const infos = await api.getAnkiNoteInfo(notes); + const infos = await yomichan.api.getAnkiNoteInfo(notes); const results = []; for (let i = 0, ii = infos.length; i < ii; i += modeCount) { results.push(infos.slice(i, i + modeCount)); @@ -1494,7 +1493,7 @@ class Display extends EventDispatcher { image: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image'), text: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-text') }; - return await api.injectAnkiNoteMedia( + return await yomichan.api.injectAnkiNoteMedia( timestamp, definitionDetails, audioDetails, @@ -1605,7 +1604,7 @@ class Display extends EventDispatcher { if (this._contentOriginTabId === this._tabId && this._contentOriginFrameId === this._frameId) { throw new Error('Content origin is same page'); } - return await api.crossFrame.invokeTab(this._contentOriginTabId, this._contentOriginFrameId, action, params); + return await yomichan.crossFrame.invokeTab(this._contentOriginTabId, this._contentOriginFrameId, action, params); } _copyHostSelection() { diff --git a/ext/js/display/popup-main.js b/ext/js/display/popup-main.js index f1228aa6..24be6c01 100644 --- a/ext/js/display/popup-main.js +++ b/ext/js/display/popup-main.js @@ -21,7 +21,6 @@ * DocumentFocusController * HotkeyHandler * JapaneseUtil - * api */ (async () => { @@ -29,10 +28,9 @@ const documentFocusController = new DocumentFocusController(); documentFocusController.prepare(); - api.prepare(); await yomichan.prepare(); - const {tabId, frameId} = await api.frameInformationGet(); + const {tabId, frameId} = await yomichan.api.frameInformationGet(); const japaneseUtil = new JapaneseUtil(null); diff --git a/ext/js/display/query-parser.js b/ext/js/display/query-parser.js index 05ebfa27..29401657 100644 --- a/ext/js/display/query-parser.js +++ b/ext/js/display/query-parser.js @@ -17,7 +17,6 @@ /* global * TextScanner - * api */ class QueryParser extends EventDispatcher { @@ -76,7 +75,7 @@ class QueryParser extends EventDispatcher { const token = {}; this._setTextToken = token; - this._parseResults = await api.textParse(text, this._getOptionsContext()); + this._parseResults = await yomichan.api.textParse(text, this._getOptionsContext()); if (this._setTextToken !== token) { return; } this._refreshSelectedParser(); @@ -116,7 +115,7 @@ class QueryParser extends EventDispatcher { _setSelectedParser(value) { const optionsContext = this._getOptionsContext(); - api.modifySettings([{ + yomichan.api.modifySettings([{ action: 'set', path: 'parsing.selectedParser', value, diff --git a/ext/js/display/search-display-controller.js b/ext/js/display/search-display-controller.js index a295346d..3b48af44 100644 --- a/ext/js/display/search-display-controller.js +++ b/ext/js/display/search-display-controller.js @@ -17,7 +17,6 @@ /* global * ClipboardMonitor - * api * wanakana */ @@ -40,7 +39,7 @@ class SearchDisplayController { this._clipboardMonitor = new ClipboardMonitor({ japaneseUtil, clipboardReader: { - getText: async () => (await api.clipboardGet()) + getText: async () => (await yomichan.api.clipboardGet()) } }); this._messageHandlers = new Map(); @@ -201,7 +200,7 @@ class SearchDisplayController { _onWanakanaEnableChange(e) { const value = e.target.checked; this._setWanakanaEnabled(value); - api.modifySettings([{ + yomichan.api.modifySettings([{ action: 'set', path: 'general.enableWanakana', value, @@ -301,7 +300,7 @@ class SearchDisplayController { if (!modify) { return; } - await api.modifySettings([{ + await yomichan.api.modifySettings([{ action: 'set', path: 'clipboard.enableSearchPageMonitor', value, diff --git a/ext/js/display/search-main.js b/ext/js/display/search-main.js index d3e8af0b..08645833 100644 --- a/ext/js/display/search-main.js +++ b/ext/js/display/search-main.js @@ -21,7 +21,6 @@ * HotkeyHandler * JapaneseUtil * SearchDisplayController - * api * wanakana */ @@ -30,10 +29,9 @@ const documentFocusController = new DocumentFocusController(); documentFocusController.prepare(); - api.prepare(); await yomichan.prepare(); - const {tabId, frameId} = await api.frameInformationGet(); + const {tabId, frameId} = await yomichan.api.frameInformationGet(); const japaneseUtil = new JapaneseUtil(wanakana); diff --git a/ext/js/input/hotkey-handler.js b/ext/js/input/hotkey-handler.js index 423410b7..6db3e0f0 100644 --- a/ext/js/input/hotkey-handler.js +++ b/ext/js/input/hotkey-handler.js @@ -17,7 +17,6 @@ /* global * DocumentUtil - * api */ /** @@ -59,7 +58,7 @@ class HotkeyHandler extends EventDispatcher { prepare() { this._isPrepared = true; this._updateEventHandlers(); - api.crossFrame.registerHandlers([ + yomichan.crossFrame.registerHandlers([ ['hotkeyHandler.forwardHotkey', {async: false, handler: this._onMessageForwardHotkey.bind(this)}] ]); } @@ -259,7 +258,7 @@ class HotkeyHandler extends EventDispatcher { const frameId = this._forwardFrameId; if (frameId === null) { throw new Error('No forwarding target'); } try { - await api.crossFrame.invoke(frameId, 'hotkeyHandler.forwardHotkey', {key, modifiers}); + await yomichan.crossFrame.invoke(frameId, 'hotkeyHandler.forwardHotkey', {key, modifiers}); } catch (e) { // NOP } diff --git a/ext/js/input/hotkey-help-controller.js b/ext/js/input/hotkey-help-controller.js index 8137b50b..9bf95c77 100644 --- a/ext/js/input/hotkey-help-controller.js +++ b/ext/js/input/hotkey-help-controller.js @@ -17,7 +17,6 @@ /* global * HotkeyUtil - * api */ class HotkeyHelpController { @@ -29,7 +28,7 @@ class HotkeyHelpController { } async prepare() { - const {platform: {os}} = await api.getEnvironmentInfo(); + const {platform: {os}} = await yomichan.api.getEnvironmentInfo(); this._hotkeyUtil.os = os; await this._setupGlobalCommands(this._globalActionHotkeys); } diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js index 7672b69d..e91498ba 100644 --- a/ext/js/language/text-scanner.js +++ b/ext/js/language/text-scanner.js @@ -17,7 +17,6 @@ /* global * DocumentUtil - * api */ class TextScanner extends EventDispatcher { @@ -762,7 +761,7 @@ class TextScanner extends EventDispatcher { const searchText = this.getTextSourceContent(textSource, scanLength, layoutAwareScan); if (searchText.length === 0) { return null; } - const {definitions, length} = await api.termsFind(searchText, {}, optionsContext); + const {definitions, length} = await yomichan.api.termsFind(searchText, {}, optionsContext); if (definitions.length === 0) { return null; } textSource.setEndOffset(length, layoutAwareScan); @@ -787,7 +786,7 @@ class TextScanner extends EventDispatcher { const searchText = this.getTextSourceContent(textSource, 1, layoutAwareScan); if (searchText.length === 0) { return null; } - const definitions = await api.kanjiFind(searchText, optionsContext); + const definitions = await yomichan.api.kanjiFind(searchText, optionsContext); if (definitions.length === 0) { return null; } textSource.setEndOffset(1, layoutAwareScan); diff --git a/ext/js/media/media-loader.js b/ext/js/media/media-loader.js index 5974e31a..d9d40a36 100644 --- a/ext/js/media/media-loader.js +++ b/ext/js/media/media-loader.js @@ -15,10 +15,6 @@ * along with this program. If not, see . */ -/* global - * api - */ - class MediaLoader { constructor() { this._token = {}; @@ -84,7 +80,7 @@ class MediaLoader { async _getMediaData(path, dictionaryName, cachedData) { const token = this._token; - const data = (await api.getMedia([{path, dictionaryName}]))[0]; + const data = (await yomichan.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}); diff --git a/ext/js/pages/action-popup-main.js b/ext/js/pages/action-popup-main.js index 99a76bdf..75dfb641 100644 --- a/ext/js/pages/action-popup-main.js +++ b/ext/js/pages/action-popup-main.js @@ -18,7 +18,6 @@ /* global * HotkeyHelpController * PermissionsUtil - * api */ class DisplayController { @@ -35,7 +34,7 @@ class DisplayController { this._setupButtonEvents('.action-open-search', 'openSearchPage', chrome.runtime.getURL('/search.html')); this._setupButtonEvents('.action-open-info', 'openInfoPage', chrome.runtime.getURL('/info.html')); - const optionsFull = await api.optionsGetFull(); + const optionsFull = await yomichan.api.optionsGetFull(); this._optionsFull = optionsFull; this._setupHotkeys(); @@ -74,12 +73,12 @@ class DisplayController { if (typeof command === 'string') { node.addEventListener('click', (e) => { if (e.button !== 0) { return; } - api.commandExec(command, {mode: e.ctrlKey ? 'newTab' : 'existingOrNewTab'}); + yomichan.api.commandExec(command, {mode: e.ctrlKey ? 'newTab' : 'existingOrNewTab'}); e.preventDefault(); }, false); node.addEventListener('auxclick', (e) => { if (e.button !== 1) { return; } - api.commandExec(command, {mode: 'newTab'}); + yomichan.api.commandExec(command, {mode: 'newTab'}); e.preventDefault(); }, false); } @@ -130,7 +129,7 @@ class DisplayController { _setupOptions({options}) { const extensionEnabled = options.general.enable; - const onToggleChanged = () => api.commandExec('toggleTextScanning'); + const onToggleChanged = () => yomichan.api.commandExec('toggleTextScanning'); for (const toggle of document.querySelectorAll('#enable-search,#enable-search2')) { toggle.checked = extensionEnabled; toggle.addEventListener('change', onToggleChanged, false); @@ -178,7 +177,7 @@ class DisplayController { } async _setPrimaryProfileIndex(value) { - return await api.modifySettings( + return await yomichan.api.modifySettings( [{ action: 'set', path: 'profileCurrent', @@ -190,7 +189,7 @@ class DisplayController { async _updateDictionariesEnabledWarnings(options) { const noDictionariesEnabledWarnings = document.querySelectorAll('.no-dictionaries-enabled-warning'); - const dictionaries = await api.getDictionaryInfo(); + const dictionaries = await yomichan.api.getDictionaryInfo(); let enabledCount = 0; for (const {title} of dictionaries) { @@ -221,10 +220,9 @@ class DisplayController { } (async () => { - api.prepare(); await yomichan.prepare(); - api.logIndicatorClear(); + yomichan.api.logIndicatorClear(); const displayController = new DisplayController(); displayController.prepare(); diff --git a/ext/js/pages/info-main.js b/ext/js/pages/info-main.js index 7d34d47a..45c28d25 100644 --- a/ext/js/pages/info-main.js +++ b/ext/js/pages/info-main.js @@ -19,7 +19,6 @@ * BackupController * DocumentFocusController * SettingsController - * api */ function getBrowserDisplayName(browser) { @@ -54,12 +53,11 @@ function getOperatingSystemDisplayName(os) { const manifest = chrome.runtime.getManifest(); const language = chrome.i18n.getUILanguage(); - api.prepare(); await yomichan.prepare(); const {userAgent} = navigator; const {name, version} = manifest; - const {browser, platform: {os}} = await api.getEnvironmentInfo(); + const {browser, platform: {os}} = await yomichan.api.getEnvironmentInfo(); const thisVersionLink = document.querySelector('#release-notes-this-version-link'); thisVersionLink.href = thisVersionLink.dataset.hrefFormat.replace(/\{version\}/g, version); @@ -73,7 +71,7 @@ function getOperatingSystemDisplayName(os) { (async () => { let ankiConnectVersion = null; try { - ankiConnectVersion = await api.getAnkiConnectVersion(); + ankiConnectVersion = await yomichan.api.getAnkiConnectVersion(); } catch (e) { // NOP } @@ -86,7 +84,7 @@ function getOperatingSystemDisplayName(os) { (async () => { let dictionaryInfos; try { - dictionaryInfos = await api.getDictionaryInfo(); + dictionaryInfos = await yomichan.api.getDictionaryInfo(); } catch (e) { return; } diff --git a/ext/js/pages/permissions-main.js b/ext/js/pages/permissions-main.js index 7c0427b9..0cb37e93 100644 --- a/ext/js/pages/permissions-main.js +++ b/ext/js/pages/permissions-main.js @@ -19,12 +19,11 @@ * DocumentFocusController * PermissionsToggleController * SettingsController - * api */ async function setupEnvironmentInfo() { const {manifest_version: manifestVersion} = chrome.runtime.getManifest(); - const {browser, platform} = await api.getEnvironmentInfo(); + const {browser, platform} = await yomichan.api.getEnvironmentInfo(); document.documentElement.dataset.browser = browser; document.documentElement.dataset.os = platform.os; document.documentElement.dataset.manifestVersion = `${manifestVersion}`; @@ -69,7 +68,6 @@ function setupPermissionsToggles() { node.textContent = chrome.runtime.getURL('/'); } - api.prepare(); await yomichan.prepare(); setupEnvironmentInfo(); diff --git a/ext/js/pages/welcome-main.js b/ext/js/pages/welcome-main.js index 626cbd3a..5a6bb2a5 100644 --- a/ext/js/pages/welcome-main.js +++ b/ext/js/pages/welcome-main.js @@ -25,12 +25,11 @@ * SettingsController * SettingsDisplayController * StatusFooter - * api */ async function setupEnvironmentInfo() { const {manifest_version: manifestVersion} = chrome.runtime.getManifest(); - const {browser, platform} = await api.getEnvironmentInfo(); + const {browser, platform} = await yomichan.api.getEnvironmentInfo(); document.documentElement.dataset.browser = browser; document.documentElement.dataset.os = platform.os; document.documentElement.dataset.manifestVersion = `${manifestVersion}`; @@ -49,12 +48,11 @@ async function setupGenericSettingsController(genericSettingController) { const statusFooter = new StatusFooter(document.querySelector('.status-footer-container')); statusFooter.prepare(); - api.prepare(); await yomichan.prepare(); setupEnvironmentInfo(); - const optionsFull = await api.optionsGetFull(); + const optionsFull = await yomichan.api.optionsGetFull(); const preparePromises = []; diff --git a/ext/js/script/dynamic-loader.js b/ext/js/script/dynamic-loader.js index 0464f151..a2cfb77a 100644 --- a/ext/js/script/dynamic-loader.js +++ b/ext/js/script/dynamic-loader.js @@ -15,10 +15,6 @@ * along with this program. If not, see . */ -/* global - * api - */ - const dynamicLoader = (() => { const injectedStylesheets = new Map(); const injectedStylesheetsWithParent = new WeakMap(); @@ -61,7 +57,7 @@ const dynamicLoader = (() => { } if (type === 'file-content') { - value = await api.getStylesheetContent(value); + value = await yomichan.api.getStylesheetContent(value); type = 'code'; useWebExtensionApi = false; } @@ -73,7 +69,7 @@ const dynamicLoader = (() => { } setInjectedStylesheet(id, parentNode, null); - await api.injectStylesheet(type, value); + await yomichan.api.injectStylesheet(type, value); return null; } diff --git a/ext/js/settings/anki-templates-controller.js b/ext/js/settings/anki-templates-controller.js index 31bd1e92..8e3a1a70 100644 --- a/ext/js/settings/anki-templates-controller.js +++ b/ext/js/settings/anki-templates-controller.js @@ -17,7 +17,6 @@ /* global * AnkiNoteBuilder - * api */ class AnkiTemplatesController { @@ -37,7 +36,7 @@ class AnkiTemplatesController { } async prepare() { - this._defaultFieldTemplates = await api.getDefaultAnkiFieldTemplates(); + this._defaultFieldTemplates = await yomichan.api.getDefaultAnkiFieldTemplates(); this._fieldTemplatesTextarea = document.querySelector('#anki-card-templates-textarea'); this._compileResultInfo = document.querySelector('#anki-card-templates-compile-result'); @@ -154,7 +153,7 @@ class AnkiTemplatesController { async _getDefinition(text, optionsContext) { if (this._cachedDefinitionText !== text) { - const {definitions} = await api.termsFind(text, {}, optionsContext); + const {definitions} = await yomichan.api.termsFind(text, {}, optionsContext); if (definitions.length === 0) { return null; } this._cachedDefinitionValue = definitions[0]; diff --git a/ext/js/settings/backup-controller.js b/ext/js/settings/backup-controller.js index 8837b927..117f2422 100644 --- a/ext/js/settings/backup-controller.js +++ b/ext/js/settings/backup-controller.js @@ -18,7 +18,6 @@ /* global * DictionaryController * OptionsUtil - * api */ class BackupController { @@ -85,8 +84,8 @@ class BackupController { async _getSettingsExportData(date) { const optionsFull = await this._settingsController.getOptionsFull(); - const environment = await api.getEnvironmentInfo(); - const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates(); + const environment = await yomichan.api.getEnvironmentInfo(); + const fieldTemplatesDefault = await yomichan.api.getDefaultAnkiFieldTemplates(); const permissions = await this._settingsController.permissionsUtil.getAllPermissions(); // Format options diff --git a/ext/js/settings/dictionary-controller.js b/ext/js/settings/dictionary-controller.js index ea9f7503..78173202 100644 --- a/ext/js/settings/dictionary-controller.js +++ b/ext/js/settings/dictionary-controller.js @@ -18,7 +18,6 @@ /* global * DictionaryDatabase * ObjectPropertyAccessor - * api */ class DictionaryEntry { @@ -362,7 +361,7 @@ class DictionaryController { const token = this._databaseStateToken; const dictionaryTitles = this._dictionaries.map(({title}) => title); - const {counts, total} = await api.getDictionaryCounts(dictionaryTitles, true); + const {counts, total} = await yomichan.api.getDictionaryCounts(dictionaryTitles, true); if (this._databaseStateToken !== token) { return; } for (let i = 0, ii = Math.min(counts.length, this._dictionaryEntries.length); i < ii; ++i) { @@ -499,7 +498,7 @@ class DictionaryController { const dictionaryDatabase = await this._getPreparedDictionaryDatabase(); try { await dictionaryDatabase.deleteDictionary(dictionaryTitle, {rate: 1000}, onProgress); - api.triggerDatabaseUpdated('dictionary', 'delete'); + yomichan.api.triggerDatabaseUpdated('dictionary', 'delete'); } finally { dictionaryDatabase.close(); } diff --git a/ext/js/settings/dictionary-import-controller.js b/ext/js/settings/dictionary-import-controller.js index c4ad9e59..00eb7b95 100644 --- a/ext/js/settings/dictionary-import-controller.js +++ b/ext/js/settings/dictionary-import-controller.js @@ -19,7 +19,6 @@ * DictionaryDatabase * DictionaryImporter * ObjectPropertyAccessor - * api */ class DictionaryImportController { @@ -102,7 +101,7 @@ class DictionaryImportController { this._setSpinnerVisible(true); if (purgeNotification !== null) { purgeNotification.hidden = false; } - await api.purgeDatabase(); + await yomichan.api.purgeDatabase(); const errors = await this._clearDictionarySettings(); if (errors.length > 0) { @@ -197,7 +196,7 @@ class DictionaryImportController { const dictionaryImporter = new DictionaryImporter(); const archiveContent = await this._readFile(file); const {result, errors} = await dictionaryImporter.importDictionary(dictionaryDatabase, archiveContent, importDetails, onProgress); - api.triggerDatabaseUpdated('dictionary', 'import'); + yomichan.api.triggerDatabaseUpdated('dictionary', 'import'); const errors2 = await this._addDictionarySettings(result.sequenced, result.title); if (errors.length > 0) { diff --git a/ext/js/settings/extension-keyboard-shortcuts-controller.js b/ext/js/settings/extension-keyboard-shortcuts-controller.js index 9c930703..032f9dcc 100644 --- a/ext/js/settings/extension-keyboard-shortcuts-controller.js +++ b/ext/js/settings/extension-keyboard-shortcuts-controller.js @@ -18,7 +18,6 @@ /* global * HotkeyUtil * KeyboardMouseInputField - * api */ class ExtensionKeyboardShortcutController { @@ -53,7 +52,7 @@ class ExtensionKeyboardShortcutController { this._clearButton.addEventListener('click', this._onClearClick.bind(this)); } - const {platform: {os}} = await api.getEnvironmentInfo(); + const {platform: {os}} = await yomichan.api.getEnvironmentInfo(); this._os = os; this._hotkeyUtil.os = os; diff --git a/ext/js/settings/keyboard-shortcuts-controller.js b/ext/js/settings/keyboard-shortcuts-controller.js index 0dcfa2ee..99b16f06 100644 --- a/ext/js/settings/keyboard-shortcuts-controller.js +++ b/ext/js/settings/keyboard-shortcuts-controller.js @@ -17,7 +17,6 @@ /* global * KeyboardMouseInputField - * api */ class KeyboardShortcutController { @@ -38,7 +37,7 @@ class KeyboardShortcutController { } async prepare() { - const {platform: {os}} = await api.getEnvironmentInfo(); + const {platform: {os}} = await yomichan.api.getEnvironmentInfo(); this._os = os; this._addButton = document.querySelector('#hotkey-list-add'); diff --git a/ext/js/settings/main.js b/ext/js/settings/main.js index 0707ea3c..51cd5f27 100644 --- a/ext/js/settings/main.js +++ b/ext/js/settings/main.js @@ -31,7 +31,6 @@ * ScanInputsSimpleController * SettingsController * StorageController - * api */ function showExtensionInformation() { @@ -43,7 +42,7 @@ function showExtensionInformation() { } async function setupEnvironmentInfo() { - const {browser, platform} = await api.getEnvironmentInfo(); + const {browser, platform} = await yomichan.api.getEnvironmentInfo(); document.documentElement.dataset.browser = browser; document.documentElement.dataset.operatingSystem = platform.os; } @@ -51,13 +50,12 @@ async function setupEnvironmentInfo() { (async () => { try { - api.prepare(); await yomichan.prepare(); setupEnvironmentInfo(); showExtensionInformation(); - const optionsFull = await api.optionsGetFull(); + const optionsFull = await yomichan.api.optionsGetFull(); const modalController = new ModalController(); modalController.prepare(); diff --git a/ext/js/settings/mecab-controller.js b/ext/js/settings/mecab-controller.js index ff2a4a66..122f82f9 100644 --- a/ext/js/settings/mecab-controller.js +++ b/ext/js/settings/mecab-controller.js @@ -15,10 +15,6 @@ * along with this program. If not, see . */ -/* global - * api - */ - class MecabController { constructor(settingsController) { this._settingsController = settingsController; @@ -49,7 +45,7 @@ class MecabController { this._testButton.disabled = true; this._resultsContainer.textContent = ''; this._resultsContainer.hidden = true; - await api.testMecab(); + await yomichan.api.testMecab(); this._setStatus('Connection was successful', false); } catch (e) { this._setStatus(e.message, true); diff --git a/ext/js/settings/pitch-accents-preview-main.js b/ext/js/settings/pitch-accents-preview-main.js index 7bc995a2..fe3bd36d 100644 --- a/ext/js/settings/pitch-accents-preview-main.js +++ b/ext/js/settings/pitch-accents-preview-main.js @@ -17,12 +17,10 @@ /* global * DisplayGenerator - * api */ (async () => { try { - api.prepare(); await yomichan.prepare(); const displayGenerator = new DisplayGenerator({ diff --git a/ext/js/settings/popup-preview-frame-main.js b/ext/js/settings/popup-preview-frame-main.js index f61b26dc..a62e8d28 100644 --- a/ext/js/settings/popup-preview-frame-main.js +++ b/ext/js/settings/popup-preview-frame-main.js @@ -19,15 +19,13 @@ * HotkeyHandler * PopupFactory * PopupPreviewFrame - * api */ (async () => { try { - api.prepare(); await yomichan.prepare(); - const {tabId, frameId} = await api.frameInformationGet(); + const {tabId, frameId} = await yomichan.api.frameInformationGet(); const hotkeyHandler = new HotkeyHandler(); hotkeyHandler.prepare(); diff --git a/ext/js/settings/popup-preview-frame.js b/ext/js/settings/popup-preview-frame.js index 56100fb3..638dd414 100644 --- a/ext/js/settings/popup-preview-frame.js +++ b/ext/js/settings/popup-preview-frame.js @@ -18,7 +18,6 @@ /* global * Frontend * TextSourceRange - * api * wanakana */ @@ -63,8 +62,8 @@ class PopupPreviewFrame { this._exampleTextInput.addEventListener('input', this._onExampleTextInputInput.bind(this), false); // Overwrite API functions - this._apiOptionsGetOld = api.optionsGet.bind(api); - api.optionsGet = this._apiOptionsGet.bind(this); + this._apiOptionsGetOld = yomichan.api.optionsGet.bind(yomichan.api); + yomichan.api.optionsGet = this._apiOptionsGet.bind(this); // Overwrite frontend this._frontend = new Frontend({ diff --git a/ext/js/settings/popup-window-controller.js b/ext/js/settings/popup-window-controller.js index cc83db68..403c060c 100644 --- a/ext/js/settings/popup-window-controller.js +++ b/ext/js/settings/popup-window-controller.js @@ -15,10 +15,6 @@ * along with this program. If not, see . */ -/* global - * api - */ - class PopupWindowController { prepare() { const testLink = document.querySelector('#test-window-open-link'); @@ -33,6 +29,6 @@ class PopupWindowController { } async _testWindowOpen() { - await api.getOrCreateSearchPopup({focus: true}); + await yomichan.api.getOrCreateSearchPopup({focus: true}); } } diff --git a/ext/js/settings/profile-controller.js b/ext/js/settings/profile-controller.js index 914fc679..3883e80a 100644 --- a/ext/js/settings/profile-controller.js +++ b/ext/js/settings/profile-controller.js @@ -17,7 +17,6 @@ /* global * ProfileConditionsUI - * api */ class ProfileController { @@ -58,7 +57,7 @@ class ProfileController { } async prepare() { - const {platform: {os}} = await api.getEnvironmentInfo(); + const {platform: {os}} = await yomichan.api.getEnvironmentInfo(); this._profileConditionsUI.os = os; this._profileActiveSelect = document.querySelector('#profile-active-select'); diff --git a/ext/js/settings/scan-inputs-controller.js b/ext/js/settings/scan-inputs-controller.js index eb179c6a..79b2bdf4 100644 --- a/ext/js/settings/scan-inputs-controller.js +++ b/ext/js/settings/scan-inputs-controller.js @@ -17,7 +17,6 @@ /* global * KeyboardMouseInputField - * api */ class ScanInputsController { @@ -31,7 +30,7 @@ class ScanInputsController { } async prepare() { - const {platform: {os}} = await api.getEnvironmentInfo(); + const {platform: {os}} = await yomichan.api.getEnvironmentInfo(); this._os = os; this._container = document.querySelector('#scan-input-list'); diff --git a/ext/js/settings/scan-inputs-simple-controller.js b/ext/js/settings/scan-inputs-simple-controller.js index 01f044c2..b011af5d 100644 --- a/ext/js/settings/scan-inputs-simple-controller.js +++ b/ext/js/settings/scan-inputs-simple-controller.js @@ -18,7 +18,6 @@ /* global * HotkeyUtil * ScanInputsController - * api */ class ScanInputsSimpleController { @@ -34,7 +33,7 @@ class ScanInputsSimpleController { this._middleMouseButtonScan = document.querySelector('#middle-mouse-button-scan'); this._mainScanModifierKeyInput = document.querySelector('#main-scan-modifier-key'); - const {platform: {os}} = await api.getEnvironmentInfo(); + const {platform: {os}} = await yomichan.api.getEnvironmentInfo(); this._hotkeyUtil.os = os; this._mainScanModifierKeyInputHasOther = false; diff --git a/ext/js/settings/settings-controller.js b/ext/js/settings/settings-controller.js index 11a9435c..4a86470d 100644 --- a/ext/js/settings/settings-controller.js +++ b/ext/js/settings/settings-controller.js @@ -19,7 +19,6 @@ * HtmlTemplateCollection * OptionsUtil * PermissionsUtil - * api */ class SettingsController extends EventDispatcher { @@ -62,16 +61,16 @@ class SettingsController extends EventDispatcher { async getOptions() { const optionsContext = this.getOptionsContext(); - return await api.optionsGet(optionsContext); + return await yomichan.api.optionsGet(optionsContext); } async getOptionsFull() { - return await api.optionsGetFull(); + return await yomichan.api.optionsGetFull(); } async setAllSettings(value) { const profileIndex = value.profileCurrent; - await api.setAllSettings(value, this._source); + await yomichan.api.setAllSettings(value, this._source); this._setProfileIndex(profileIndex); } @@ -108,7 +107,7 @@ class SettingsController extends EventDispatcher { } async getDictionaryInfo() { - return await api.getDictionaryInfo(); + return await yomichan.api.getDictionaryInfo(); } getOptionsContext() { @@ -171,12 +170,12 @@ class SettingsController extends EventDispatcher { async _getSettings(targets, extraFields) { targets = this._setupTargets(targets, extraFields); - return await api.getSettings(targets); + return await yomichan.api.getSettings(targets); } async _modifySettings(targets, extraFields) { targets = this._setupTargets(targets, extraFields); - return await api.modifySettings(targets, this._source); + return await yomichan.api.modifySettings(targets, this._source); } _onBeforeUnload(e) { diff --git a/ext/js/settings/settings-main.js b/ext/js/settings/settings-main.js index a7e6b7b0..a5bb642c 100644 --- a/ext/js/settings/settings-main.js +++ b/ext/js/settings/settings-main.js @@ -42,12 +42,11 @@ * StatusFooter * StorageController * TranslationTextReplacementsController - * api */ async function setupEnvironmentInfo() { const {manifest_version: manifestVersion} = chrome.runtime.getManifest(); - const {browser, platform} = await api.getEnvironmentInfo(); + const {browser, platform} = await yomichan.api.getEnvironmentInfo(); document.documentElement.dataset.browser = browser; document.documentElement.dataset.os = platform.os; document.documentElement.dataset.manifestVersion = `${manifestVersion}`; @@ -66,12 +65,11 @@ async function setupGenericSettingsController(genericSettingController) { const statusFooter = new StatusFooter(document.querySelector('.status-footer-container')); statusFooter.prepare(); - api.prepare(); await yomichan.prepare(); setupEnvironmentInfo(); - const optionsFull = await api.optionsGetFull(); + const optionsFull = await yomichan.api.optionsGetFull(); const preparePromises = []; diff --git a/ext/js/templates/template-renderer-frame-main.js b/ext/js/templates/template-renderer-frame-main.js index d25eb56d..a3892002 100644 --- a/ext/js/templates/template-renderer-frame-main.js +++ b/ext/js/templates/template-renderer-frame-main.js @@ -28,6 +28,6 @@ templateRenderer.registerDataType('ankiNote', { modifier: ({data, marker}) => new AnkiNoteData(data, marker).createPublic() }); - const api = new TemplateRendererFrameApi(templateRenderer); - api.prepare(); + const templateRendererFrameApi = new TemplateRendererFrameApi(templateRenderer); + templateRendererFrameApi.prepare(); })(); diff --git a/ext/js/yomichan.js b/ext/js/yomichan.js index 7d101a4c..73deeab9 100644 --- a/ext/js/yomichan.js +++ b/ext/js/yomichan.js @@ -15,6 +15,11 @@ * along with this program. If not, see . */ +/* global + * API + * CrossFrameAPI + */ + // Set up chrome alias if it's not available (Edge Legacy) if ((() => { let hasChrome = false; @@ -34,271 +39,300 @@ if ((() => { chrome = browser; } -const yomichan = (() => { - class Yomichan extends EventDispatcher { - constructor() { - super(); +class Yomichan extends EventDispatcher { + constructor() { + super(); - this._extensionName = 'Yomichan'; - try { - const manifest = chrome.runtime.getManifest(); - this._extensionName = `${manifest.name} v${manifest.version}`; - } catch (e) { - // NOP - } - - this._isExtensionUnloaded = false; - this._isTriggeringExtensionUnloaded = false; - this._isReady = false; - - const {promise, resolve} = deferPromise(); - this._isBackendReadyPromise = promise; - this._isBackendReadyPromiseResolve = resolve; - - this._messageHandlers = new Map([ - ['isReady', {async: false, handler: this._onMessageIsReady.bind(this)}], - ['backendReady', {async: false, handler: this._onMessageBackendReady.bind(this)}], - ['getUrl', {async: false, handler: this._onMessageGetUrl.bind(this)}], - ['optionsUpdated', {async: false, handler: this._onMessageOptionsUpdated.bind(this)}], - ['databaseUpdated', {async: false, handler: this._onMessageDatabaseUpdated.bind(this)}], - ['zoomChanged', {async: false, handler: this._onMessageZoomChanged.bind(this)}] - ]); + this._extensionName = 'Yomichan'; + try { + const manifest = chrome.runtime.getManifest(); + this._extensionName = `${manifest.name} v${manifest.version}`; + } catch (e) { + // NOP } - // Public + this._isBackground = null; + this._api = null; + this._crossFrame = null; + this._isExtensionUnloaded = false; + this._isTriggeringExtensionUnloaded = false; + this._isReady = false; - get isExtensionUnloaded() { - return this._isExtensionUnloaded; - } + const {promise, resolve} = deferPromise(); + this._isBackendReadyPromise = promise; + this._isBackendReadyPromiseResolve = resolve; - async prepare(isBackground=false) { - chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); + this._messageHandlers = new Map([ + ['isReady', {async: false, handler: this._onMessageIsReady.bind(this)}], + ['backendReady', {async: false, handler: this._onMessageBackendReady.bind(this)}], + ['getUrl', {async: false, handler: this._onMessageGetUrl.bind(this)}], + ['optionsUpdated', {async: false, handler: this._onMessageOptionsUpdated.bind(this)}], + ['databaseUpdated', {async: false, handler: this._onMessageDatabaseUpdated.bind(this)}], + ['zoomChanged', {async: false, handler: this._onMessageZoomChanged.bind(this)}] + ]); + } - if (!isBackground) { - this.sendMessage({action: 'requestBackendReadySignal'}); - await this._isBackendReadyPromise; - } - } + // Public - ready() { - this._isReady = true; - this.sendMessage({action: 'yomichanReady'}); - } + get isBackground() { + return this._isBackground; + } - isExtensionUrl(url) { - try { - return url.startsWith(chrome.runtime.getURL('/')); - } catch (e) { - return false; - } - } + get isExtensionUnloaded() { + return this._isExtensionUnloaded; + } - getTemporaryListenerResult(eventHandler, userCallback, timeout=null) { - if (!( - typeof eventHandler.addListener === 'function' && - typeof eventHandler.removeListener === 'function' - )) { - throw new Error('Event handler type not supported'); - } + get api() { + return this._api; + } - return new Promise((resolve, reject) => { - const runtimeMessageCallback = ({action, params}, sender, sendResponse) => { - let timeoutId = null; - if (timeout !== null) { - timeoutId = setTimeout(() => { - timeoutId = null; - eventHandler.removeListener(runtimeMessageCallback); - reject(new Error(`Listener timed out in ${timeout} ms`)); - }, timeout); - } + get crossFrame() { + return this._crossFrame; + } - const cleanupResolve = (value) => { - if (timeoutId !== null) { - clearTimeout(timeoutId); - timeoutId = null; - } - eventHandler.removeListener(runtimeMessageCallback); - sendResponse(); - resolve(value); - }; + async prepare(isBackground=false) { + this._isBackground = isBackground; + chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); - userCallback({action, params}, {resolve: cleanupResolve, sender}); - }; + if (!isBackground) { + this._api = new API(this); - eventHandler.addListener(runtimeMessageCallback); - }); - } + this._crossFrame = new CrossFrameAPI(); + this._crossFrame.prepare(); - logWarning(error) { - this.log(error, 'warn'); - } + this.sendMessage({action: 'requestBackendReadySignal'}); + await this._isBackendReadyPromise; - logError(error) { - this.log(error, 'error'); - } - - log(error, level, context=null) { - if (!isObject(context)) { - context = this._getLogContext(); - } - - let errorString; - try { - errorString = error.toString(); - if (/^\[object \w+\]$/.test(errorString)) { - errorString = JSON.stringify(error); - } - } catch (e) { - errorString = `${error}`; - } - - let errorStack; - try { - errorStack = (typeof error.stack === 'string' ? error.stack.trimRight() : ''); - } catch (e) { - errorStack = ''; - } - - let errorData; - try { - errorData = error.data; - } catch (e) { - // NOP - } - - if (errorStack.startsWith(errorString)) { - errorString = errorStack; - } else if (errorStack.length > 0) { - errorString += `\n${errorStack}`; - } - - let message = `${this._extensionName} has encountered a problem.`; - message += `\nOriginating URL: ${context.url}\n`; - message += errorString; - if (typeof errorData !== 'undefined') { - message += `\nData: ${JSON.stringify(errorData, null, 4)}`; - } - message += '\n\nIssues can be reported at https://github.com/FooSoft/yomichan/issues'; - - switch (level) { - case 'info': console.info(message); break; - case 'debug': console.debug(message); break; - case 'warn': console.warn(message); break; - case 'error': console.error(message); break; - default: console.log(message); break; - } - - this.trigger('log', {error, level, context}); - } - - sendMessage(...args) { - try { - return chrome.runtime.sendMessage(...args); - } catch (e) { - this.triggerExtensionUnloaded(); - throw e; - } - } - - connect(...args) { - try { - return chrome.runtime.connect(...args); - } catch (e) { - this.triggerExtensionUnloaded(); - throw e; - } - } - - getMessageResponseResult(response) { - let error = chrome.runtime.lastError; - if (error) { - throw new Error(error.message); - } - if (!isObject(response)) { - throw new Error('Tab did not respond'); - } - error = response.error; - if (error) { - throw deserializeError(error); - } - return response.result; - } - - invokeMessageHandler({handler, async}, params, callback, ...extraArgs) { - try { - let promiseOrResult = handler(params, ...extraArgs); - if (async === 'dynamic') { - ({async, result: promiseOrResult} = promiseOrResult); - } - if (async) { - promiseOrResult.then( - (result) => { callback({result}); }, - (error) => { callback({error: serializeError(error)}); } - ); - return true; - } else { - callback({result: promiseOrResult}); - return false; - } - } catch (error) { - callback({error: serializeError(error)}); - return false; - } - } - - triggerExtensionUnloaded() { - this._isExtensionUnloaded = true; - if (this._isTriggeringExtensionUnloaded) { return; } - try { - this._isTriggeringExtensionUnloaded = true; - this.trigger('extensionUnloaded'); - } finally { - this._isTriggeringExtensionUnloaded = false; - } - } - - // Private - - _getUrl() { - return location.href; - } - - _getLogContext() { - return {url: this._getUrl()}; - } - - _onMessage({action, params}, sender, callback) { - const messageHandler = this._messageHandlers.get(action); - if (typeof messageHandler === 'undefined') { return false; } - return this.invokeMessageHandler(messageHandler, params, callback, sender); - } - - _onMessageIsReady() { - return this._isReady; - } - - _onMessageBackendReady() { - if (this._isBackendReadyPromiseResolve === null) { return; } - this._isBackendReadyPromiseResolve(); - this._isBackendReadyPromiseResolve = null; - } - - _onMessageGetUrl() { - return {url: this._getUrl()}; - } - - _onMessageOptionsUpdated({source}) { - this.trigger('optionsUpdated', {source}); - } - - _onMessageDatabaseUpdated({type, cause}) { - this.trigger('databaseUpdated', {type, cause}); - } - - _onMessageZoomChanged({oldZoomFactor, newZoomFactor}) { - this.trigger('zoomChanged', {oldZoomFactor, newZoomFactor}); + this.on('log', this._onForwardLog.bind(this)); } } - return new Yomichan(); -})(); + ready() { + this._isReady = true; + this.sendMessage({action: 'yomichanReady'}); + } + + isExtensionUrl(url) { + try { + return url.startsWith(chrome.runtime.getURL('/')); + } catch (e) { + return false; + } + } + + getTemporaryListenerResult(eventHandler, userCallback, timeout=null) { + if (!( + typeof eventHandler.addListener === 'function' && + typeof eventHandler.removeListener === 'function' + )) { + throw new Error('Event handler type not supported'); + } + + return new Promise((resolve, reject) => { + const runtimeMessageCallback = ({action, params}, sender, sendResponse) => { + let timeoutId = null; + if (timeout !== null) { + timeoutId = setTimeout(() => { + timeoutId = null; + eventHandler.removeListener(runtimeMessageCallback); + reject(new Error(`Listener timed out in ${timeout} ms`)); + }, timeout); + } + + const cleanupResolve = (value) => { + if (timeoutId !== null) { + clearTimeout(timeoutId); + timeoutId = null; + } + eventHandler.removeListener(runtimeMessageCallback); + sendResponse(); + resolve(value); + }; + + userCallback({action, params}, {resolve: cleanupResolve, sender}); + }; + + eventHandler.addListener(runtimeMessageCallback); + }); + } + + logWarning(error) { + this.log(error, 'warn'); + } + + logError(error) { + this.log(error, 'error'); + } + + log(error, level, context=null) { + if (!isObject(context)) { + context = this._getLogContext(); + } + + let errorString; + try { + errorString = error.toString(); + if (/^\[object \w+\]$/.test(errorString)) { + errorString = JSON.stringify(error); + } + } catch (e) { + errorString = `${error}`; + } + + let errorStack; + try { + errorStack = (typeof error.stack === 'string' ? error.stack.trimRight() : ''); + } catch (e) { + errorStack = ''; + } + + let errorData; + try { + errorData = error.data; + } catch (e) { + // NOP + } + + if (errorStack.startsWith(errorString)) { + errorString = errorStack; + } else if (errorStack.length > 0) { + errorString += `\n${errorStack}`; + } + + let message = `${this._extensionName} has encountered a problem.`; + message += `\nOriginating URL: ${context.url}\n`; + message += errorString; + if (typeof errorData !== 'undefined') { + message += `\nData: ${JSON.stringify(errorData, null, 4)}`; + } + message += '\n\nIssues can be reported at https://github.com/FooSoft/yomichan/issues'; + + switch (level) { + case 'info': console.info(message); break; + case 'debug': console.debug(message); break; + case 'warn': console.warn(message); break; + case 'error': console.error(message); break; + default: console.log(message); break; + } + + this.trigger('log', {error, level, context}); + } + + sendMessage(...args) { + try { + return chrome.runtime.sendMessage(...args); + } catch (e) { + this.triggerExtensionUnloaded(); + throw e; + } + } + + connect(...args) { + try { + return chrome.runtime.connect(...args); + } catch (e) { + this.triggerExtensionUnloaded(); + throw e; + } + } + + getMessageResponseResult(response) { + let error = chrome.runtime.lastError; + if (error) { + throw new Error(error.message); + } + if (!isObject(response)) { + throw new Error('Tab did not respond'); + } + error = response.error; + if (error) { + throw deserializeError(error); + } + return response.result; + } + + invokeMessageHandler({handler, async}, params, callback, ...extraArgs) { + try { + let promiseOrResult = handler(params, ...extraArgs); + if (async === 'dynamic') { + ({async, result: promiseOrResult} = promiseOrResult); + } + if (async) { + promiseOrResult.then( + (result) => { callback({result}); }, + (error) => { callback({error: serializeError(error)}); } + ); + return true; + } else { + callback({result: promiseOrResult}); + return false; + } + } catch (error) { + callback({error: serializeError(error)}); + return false; + } + } + + triggerExtensionUnloaded() { + this._isExtensionUnloaded = true; + if (this._isTriggeringExtensionUnloaded) { return; } + try { + this._isTriggeringExtensionUnloaded = true; + this.trigger('extensionUnloaded'); + } finally { + this._isTriggeringExtensionUnloaded = false; + } + } + + // Private + + _getUrl() { + return location.href; + } + + _getLogContext() { + return {url: this._getUrl()}; + } + + _onMessage({action, params}, sender, callback) { + const messageHandler = this._messageHandlers.get(action); + if (typeof messageHandler === 'undefined') { return false; } + return this.invokeMessageHandler(messageHandler, params, callback, sender); + } + + _onMessageIsReady() { + return this._isReady; + } + + _onMessageBackendReady() { + if (this._isBackendReadyPromiseResolve === null) { return; } + this._isBackendReadyPromiseResolve(); + this._isBackendReadyPromiseResolve = null; + } + + _onMessageGetUrl() { + return {url: this._getUrl()}; + } + + _onMessageOptionsUpdated({source}) { + this.trigger('optionsUpdated', {source}); + } + + _onMessageDatabaseUpdated({type, cause}) { + this.trigger('databaseUpdated', {type, cause}); + } + + _onMessageZoomChanged({oldZoomFactor, newZoomFactor}) { + this.trigger('zoomChanged', {oldZoomFactor, newZoomFactor}); + } + + async _onForwardLog({error, level, context}) { + try { + await this._api.log(serializeError(error), level, context); + } catch (e) { + // NOP + } + } +} + +const yomichan = new Yomichan();