/* * Copyright (C) 2016-2021 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 <https://www.gnu.org/licenses/>. */ /* global * CrossFrameAPI */ const api = (() => { class API { constructor() { this._forwardLogsToBackendEnabled = false; this._crossFrame = new CrossFrameAPI(); } get crossFrame() { return this._crossFrame; } prepare() { this._crossFrame.prepare(); } forwardLogsToBackend() { if (this._forwardLogsToBackendEnabled) { return; } this._forwardLogsToBackendEnabled = true; yomichan.on('log', async ({error, level, context}) => { try { await this.log(serializeError(error), level, context); } catch (e) { // NOP } }); } // 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}); } 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 { 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 } } // eslint-disable-next-line no-shadow const api = new API(); api.prepare(); return api; })();