mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
initial commit
This commit is contained in:
293
vendor/yomitan/js/application.js
vendored
Normal file
293
vendor/yomitan/js/application.js
vendored
Normal file
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user