Something went wrong. Try again.
Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294/* * Copyright (C) 2023-2025 Yomitan Authors * Copyright (C) 2020-2022 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/>. */
import {API} from './comm/api.js';import {CrossFrameAPI} from './comm/cross-frame-api.js';import {createApiMap, invokeApiMapHandler} from './core/api-map.js';import {EventDispatcher} from './core/event-dispatcher.js';import {ExtensionError} from './core/extension-error.js';import {log} from './core/log.js';import {deferPromise} from './core/utilities.js';import {WebExtension} from './extension/web-extension.js';
/** * @returns {boolean} */function checkChromeNotAvailable() { let hasChrome = false; let hasBrowser = false; try { hasChrome = (typeof chrome === 'object' && chrome !== null && typeof chrome.runtime !== 'undefined'); } catch (e) { // NOP } try { hasBrowser = (typeof browser === 'object' && browser !== null && typeof browser.runtime !== 'undefined'); } catch (e) { // NOP } return (hasBrowser && !hasChrome);}
// Set up chrome alias if it's not available (Edge Legacy)if (checkChromeNotAvailable()) { // @ts-expect-error - objects should have roughly the same interface // eslint-disable-next-line no-global-assign chrome = browser;}
/** * @param {WebExtension} webExtension */async function waitForBackendReady(webExtension) { const {promise, resolve} = /** @type {import('core').DeferredPromiseDetails<void>} */ (deferPromise()); /** @type {import('application').ApiMap} */ const apiMap = createApiMap([['applicationBackendReady', () => { resolve(); }]]); /** @type {import('extension').ChromeRuntimeOnMessageCallback<import('application').ApiMessageAny>} */ const onMessage = ({action, params}, _sender, callback) => invokeApiMapHandler(apiMap, action, params, [], callback); chrome.runtime.onMessage.addListener(onMessage); try { await webExtension.sendMessagePromise({action: 'requestBackendReadySignal'}); await promise; } finally { chrome.runtime.onMessage.removeListener(onMessage); }}
/** * @returns {Promise<void>} */function waitForDomContentLoaded() { return new Promise((resolve) => { if (document.readyState !== 'loading') { resolve(); return; } const onDomContentLoaded = () => { document.removeEventListener('DOMContentLoaded', onDomContentLoaded); resolve(); }; document.addEventListener('DOMContentLoaded', onDomContentLoaded); });}
/** * The Yomitan class is a core component through which various APIs are handled and invoked. * @augments EventDispatcher<import('application').Events> */export class Application extends EventDispatcher { /** * Creates a new instance. The instance should not be used until it has been fully prepare()'d. * @param {API} api * @param {CrossFrameAPI} crossFrameApi */ constructor(api, crossFrameApi) { super(); /** @type {WebExtension} */ this._webExtension = new WebExtension(); /** @type {?boolean} */ this._isBackground = null; /** @type {API} */ this._api = api; /** @type {CrossFrameAPI} */ this._crossFrame = crossFrameApi; /** @type {boolean} */ this._isReady = false; /* eslint-disable @stylistic/no-multi-spaces */ /** @type {import('application').ApiMap} */ this._apiMap = createApiMap([ ['applicationIsReady', this._onMessageIsReady.bind(this)], ['applicationGetUrl', this._onMessageGetUrl.bind(this)], ['applicationOptionsUpdated', this._onMessageOptionsUpdated.bind(this)], ['applicationDatabaseUpdated', this._onMessageDatabaseUpdated.bind(this)], ['applicationZoomChanged', this._onMessageZoomChanged.bind(this)], ]); /* eslint-enable @stylistic/no-multi-spaces */ }
/** @type {WebExtension} */ get webExtension() { return this._webExtension; }
/** * Gets the API instance for communicating with the backend. * This value will be null on the background page/service worker. * @type {API} */ get api() { return this._api; }
/** * Gets the CrossFrameAPI instance for communicating with different frames. * This value will be null on the background page/service worker. * @type {CrossFrameAPI} */ get crossFrame() { return this._crossFrame; }
/** * @type {?number} */ get tabId() { return this._crossFrame.tabId; }
/** * @type {?number} */ get frameId() { return this._crossFrame.frameId; }
/** * Prepares the instance for use. */ prepare() { chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); log.on('logGenericError', this._onLogGenericError.bind(this)); }
/** * Sends a message to the backend indicating that the frame is ready and all script * setup has completed. */ ready() { if (this._isReady) { return; } this._isReady = true; void this._webExtension.sendMessagePromise({action: 'applicationReady'}); }
/** */ triggerStorageChanged() { this.trigger('storageChanged', {}); }
/** */ triggerClosePopups() { this.trigger('closePopups', {}); }
/** * @param {boolean} waitForDom * @param {(application: Application) => (Promise<void>)} mainFunction */ static async main(waitForDom, mainFunction) { const supportsServiceWorker = 'serviceWorker' in navigator; // Basically, all browsers except Firefox. But it's possible Firefox will support it in the future, so we check in this fashion to be future-proof. const inExtensionContext = window.location.protocol === new URL(import.meta.url).protocol; // This code runs both in content script as well as in the iframe, so we need to differentiate the situation /** @type {MessagePort | null} */ // If this is Firefox, we don't have a service worker and can't postMessage, // so we temporarily create a SharedWorker in order to establish a MessageChannel // which we can use to postMessage with the backend. // This can only be done in the extension context (aka iframe within popup), // not in the content script context. const backendPort = !supportsServiceWorker && inExtensionContext ? (() => { const sharedWorkerBridge = new SharedWorker(new URL('comm/shared-worker-bridge.js', import.meta.url), {type: 'module'}); const backendChannel = new MessageChannel(); sharedWorkerBridge.port.postMessage({action: 'connectToBackend1'}, [backendChannel.port1]); sharedWorkerBridge.port.close(); return backendChannel.port2; })() : null;
const webExtension = new WebExtension(); log.configure(webExtension.extensionName);
const mediaDrawingWorkerToBackendChannel = new MessageChannel(); const mediaDrawingWorker = inExtensionContext ? new Worker(new URL('display/media-drawing-worker.js', import.meta.url), {type: 'module'}) : null; mediaDrawingWorker?.postMessage({action: 'connectToDatabaseWorker'}, [mediaDrawingWorkerToBackendChannel.port2]);
const api = new API(webExtension, mediaDrawingWorker, backendPort); await waitForBackendReady(webExtension); if (mediaDrawingWorker !== null) { api.connectToDatabaseWorker(mediaDrawingWorkerToBackendChannel.port1); } setInterval(() => { void api.heartbeat(); }, 20 * 1000);
const {tabId, frameId} = await api.frameInformationGet(); const crossFrameApi = new CrossFrameAPI(api, tabId, frameId); crossFrameApi.prepare(); const application = new Application(api, crossFrameApi); application.prepare(); if (waitForDom) { await waitForDomContentLoaded(); } try { await mainFunction(application); } catch (error) { log.error(error); } finally { application.ready(); } }
// Private
/** * @returns {string} */ _getUrl() { return location.href; }
/** @type {import('extension').ChromeRuntimeOnMessageCallback<import('application').ApiMessageAny>} */ _onMessage({action, params}, _sender, callback) { return invokeApiMapHandler(this._apiMap, action, params, [], callback); }
/** @type {import('application').ApiHandler<'applicationIsReady'>} */ _onMessageIsReady() { return this._isReady; }
/** @type {import('application').ApiHandler<'applicationGetUrl'>} */ _onMessageGetUrl() { return {url: this._getUrl()}; }
/** @type {import('application').ApiHandler<'applicationOptionsUpdated'>} */ _onMessageOptionsUpdated({source}) { if (source !== 'background') { this.trigger('optionsUpdated', {source}); } }
/** @type {import('application').ApiHandler<'applicationDatabaseUpdated'>} */ _onMessageDatabaseUpdated({type, cause}) { this.trigger('databaseUpdated', {type, cause}); }
/** @type {import('application').ApiHandler<'applicationZoomChanged'>} */ _onMessageZoomChanged({oldZoomFactor, newZoomFactor}) { this.trigger('zoomChanged', {oldZoomFactor, newZoomFactor}); }
/** * @param {import('log').Events['logGenericError']} params */ async _onLogGenericError({error, level, context}) { try { await this._api.logGenericErrorBackend(ExtensionError.serialize(error), level, context); } catch (e) { // NOP } }}