yomichan/ext/js/yomichan.js

204 lines
5.6 KiB
JavaScript
Raw Normal View History

/*
* Copyright (C) 2020-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
* API
* CrossFrameAPI
*/
// Set up chrome alias if it's not available (Edge Legacy)
if ((() => {
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);
})()) {
chrome = browser;
}
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._isBackground = null;
this._api = null;
this._crossFrame = null;
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)}]
]);
}
// Public
get isBackground() {
return this._isBackground;
}
get isExtensionUnloaded() {
return this._isExtensionUnloaded;
}
get api() {
return this._api;
}
get crossFrame() {
return this._crossFrame;
}
async prepare(isBackground=false) {
this._isBackground = isBackground;
chrome.runtime.onMessage.addListener(this._onMessage.bind(this));
if (!isBackground) {
this._api = new API(this);
this._crossFrame = new CrossFrameAPI();
this._crossFrame.prepare();
this.sendMessage({action: 'requestBackendReadySignal'});
await this._isBackendReadyPromise;
log.on('log', this._onForwardLog.bind(this));
}
}
ready() {
this._isReady = true;
this.sendMessage({action: 'yomichanReady'});
}
isExtensionUrl(url) {
try {
return url.startsWith(chrome.runtime.getURL('/'));
} catch (e) {
return false;
}
}
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;
}
}
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 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();