refactor: add TranslatorManager
This commit is contained in:
14
src/libs/fabManager.js
Normal file
14
src/libs/fabManager.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import ShadowDomManager from "./shadowDomManager";
|
||||
import { APP_CONSTS } from "../config";
|
||||
import ContentFab from "../views/Action/ContentFab";
|
||||
|
||||
export class FabManager extends ShadowDomManager {
|
||||
constructor({ translator, popupManager, fabConfig }) {
|
||||
super({
|
||||
id: APP_CONSTS.fabID,
|
||||
className: "notranslate",
|
||||
reactComponent: ContentFab,
|
||||
props: { translator, popupManager, fabConfig },
|
||||
});
|
||||
}
|
||||
}
|
||||
14
src/libs/popupManager.js
Normal file
14
src/libs/popupManager.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import ShadowDomManager from "./shadowDomManager";
|
||||
import { APP_CONSTS } from "../config";
|
||||
import Action from "../views/Action";
|
||||
|
||||
export class PopupManager extends ShadowDomManager {
|
||||
constructor({ translator }) {
|
||||
super({
|
||||
id: APP_CONSTS.popupID,
|
||||
className: "notranslate",
|
||||
reactComponent: Action,
|
||||
props: { translator },
|
||||
});
|
||||
}
|
||||
}
|
||||
128
src/libs/shadowDomManager.js
Normal file
128
src/libs/shadowDomManager.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import createCache from "@emotion/cache";
|
||||
import { logger } from "./log";
|
||||
|
||||
export default class ShadowDomManager {
|
||||
#hostElement = null;
|
||||
#reactRoot = null;
|
||||
#isVisible = false;
|
||||
#isProcessing = false;
|
||||
|
||||
_id;
|
||||
_className;
|
||||
_ReactComponent;
|
||||
_props;
|
||||
|
||||
constructor({ id, className = "", reactComponent, props = {} }) {
|
||||
if (!id || !reactComponent) {
|
||||
throw new Error("ID and a React Component must be provided.");
|
||||
}
|
||||
this._id = id;
|
||||
this._className = className;
|
||||
this._ReactComponent = reactComponent;
|
||||
this._props = props;
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
return this.#isVisible;
|
||||
}
|
||||
|
||||
show(props) {
|
||||
if (this.#isVisible || this.#isProcessing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#hostElement) {
|
||||
this.#isProcessing = true;
|
||||
try {
|
||||
this.#mount(props || this._props);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to mount component with id "${this._id}":`, error);
|
||||
this.#isProcessing = false;
|
||||
return;
|
||||
} finally {
|
||||
this.#isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.#hostElement.style.display = "";
|
||||
this.#isVisible = true;
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.#isVisible || !this.#hostElement) {
|
||||
return;
|
||||
}
|
||||
this.#hostElement.style.display = "none";
|
||||
this.#isVisible = false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.#hostElement) {
|
||||
return;
|
||||
}
|
||||
this.#isProcessing = true;
|
||||
|
||||
if (this.#reactRoot) {
|
||||
this.#reactRoot.unmount();
|
||||
}
|
||||
|
||||
this.#hostElement.remove();
|
||||
|
||||
this.#hostElement = null;
|
||||
this.#reactRoot = null;
|
||||
this.#isVisible = false;
|
||||
this.#isProcessing = false;
|
||||
logger.info(`Component with id "${this._id}" has been destroyed.`);
|
||||
}
|
||||
|
||||
toggle(props) {
|
||||
if (this.#isVisible) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show(props || this._props);
|
||||
}
|
||||
}
|
||||
|
||||
#mount(props) {
|
||||
const host = document.createElement("div");
|
||||
host.id = this._id;
|
||||
if (this._className) {
|
||||
host.className = this._className;
|
||||
}
|
||||
host.style.display = "none";
|
||||
document.body.parentElement.appendChild(host);
|
||||
this.#hostElement = host;
|
||||
|
||||
const shadowContainer = host.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
const appRoot = document.createElement("div");
|
||||
appRoot.className = `${this._id}_wrapper`;
|
||||
|
||||
shadowContainer.appendChild(emotionRoot);
|
||||
shadowContainer.appendChild(appRoot);
|
||||
|
||||
const cache = createCache({
|
||||
key: this._id,
|
||||
prepend: true,
|
||||
container: emotionRoot,
|
||||
});
|
||||
|
||||
const enhancedProps = {
|
||||
...props,
|
||||
onClose: this.hide.bind(this),
|
||||
};
|
||||
|
||||
const ComponentToRender = this._ReactComponent;
|
||||
this.#reactRoot = ReactDOM.createRoot(appRoot);
|
||||
this.#reactRoot.render(
|
||||
<React.StrictMode>
|
||||
<CacheProvider value={cache}>
|
||||
<ComponentToRender {...enhancedProps} />
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { kissLog } from "./log";
|
||||
* @class ShadowRootMonitor
|
||||
* @description 通过覆写 Element.prototype.attachShadow 来监控页面上所有新创建的 Shadow DOM
|
||||
*/
|
||||
export class ShadowRootMonitor {
|
||||
export default class ShadowRootMonitor {
|
||||
/**
|
||||
* @param {function(ShadowRoot): void} callback - 当一个新的 shadowRoot 被创建时调用的回调函数。
|
||||
*/
|
||||
@@ -10,14 +10,6 @@ import {
|
||||
// DEFAULT_MOUSEHOVER_KEY,
|
||||
OPT_STYLE_NONE,
|
||||
DEFAULT_API_SETTING,
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
MSG_OPEN_TRANBOX,
|
||||
MSG_TRANSBOX_TOGGLE,
|
||||
MSG_MOUSEHOVER_TOGGLE,
|
||||
MSG_TRANSINPUT_TOGGLE,
|
||||
OPT_HIGHLIGHT_WORDS_BEFORETRANS,
|
||||
OPT_HIGHLIGHT_WORDS_AFTERTRANS,
|
||||
OPT_SPLIT_PARAGRAPH_PUNCTUATION,
|
||||
@@ -25,7 +17,7 @@ import {
|
||||
OPT_SPLIT_PARAGRAPH_TEXTLENGTH,
|
||||
} from "../config";
|
||||
import interpreter from "./interpreter";
|
||||
import { ShadowRootMonitor } from "./shadowroot";
|
||||
import ShadowRootMonitor from "./shadowRootMonitor";
|
||||
import { clearFetchPool } from "./pool";
|
||||
import { debounce, scheduleIdle, genEventName, truncateWords } from "./utils";
|
||||
import { apiTranslate } from "../apis";
|
||||
@@ -38,10 +30,6 @@ import { genTextClass } from "./style";
|
||||
import { createLoadingSVG } from "./svg";
|
||||
import { shortcutRegister } from "./shortcut";
|
||||
import { tryDetectLang } from "./detect";
|
||||
import { browser } from "./browser";
|
||||
import { isIframe, sendIframeMsg } from "./iframe";
|
||||
import { TransboxManager } from "./tranbox";
|
||||
import { InputTranslator } from "./inputTranslate";
|
||||
import { trustedTypesHelper } from "./trustedTypes";
|
||||
|
||||
/**
|
||||
@@ -288,10 +276,6 @@ export class Translator {
|
||||
#apisMap = new Map(); // 用于接口快速查找
|
||||
#favWords = []; // 收藏词汇
|
||||
|
||||
#isUserscript = false;
|
||||
#transboxManager = null; // 划词翻译
|
||||
#inputTranslator = null; // 输入框翻译
|
||||
|
||||
#observedNodes = new WeakSet(); // 存储所有被识别出的、可翻译的 DOM 节点单元
|
||||
#translationNodes = new WeakMap(); // 存储所有插入到页面的译文节点
|
||||
#viewNodes = new Set(); // 当前在可视范围内的单元
|
||||
@@ -339,12 +323,7 @@ export class Translator {
|
||||
};
|
||||
}
|
||||
|
||||
constructor({
|
||||
rule = {},
|
||||
setting = {},
|
||||
favWords = [],
|
||||
isUserscript = false,
|
||||
}) {
|
||||
constructor({ rule = {}, setting = {}, favWords = [] }) {
|
||||
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
|
||||
this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
|
||||
this.#favWords = favWords;
|
||||
@@ -352,7 +331,6 @@ export class Translator {
|
||||
this.#setting.transApis.map((api) => [api.apiSlug, api])
|
||||
);
|
||||
|
||||
this.#isUserscript = isUserscript;
|
||||
this.#eventName = genEventName();
|
||||
this.#docInfo = {
|
||||
title: document.title,
|
||||
@@ -384,19 +362,6 @@ export class Translator {
|
||||
this.#enableMouseHover();
|
||||
}
|
||||
|
||||
if (!isIframe) {
|
||||
// 监听后端事件
|
||||
if (!isUserscript) {
|
||||
this.#runtimeListener();
|
||||
}
|
||||
|
||||
// 划词翻译
|
||||
this.#transboxManager = new TransboxManager(this.setting);
|
||||
|
||||
// 输入框翻译
|
||||
this.#inputTranslator = new InputTranslator(this.setting);
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => this.#run());
|
||||
} else {
|
||||
@@ -439,43 +404,6 @@ export class Translator {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听后端事件
|
||||
#runtimeListener() {
|
||||
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
this.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
this.toggleStyle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
case MSG_TRANS_GETRULE:
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
this.updateRule(args);
|
||||
sendIframeMsg(MSG_TRANS_PUTRULE, args);
|
||||
break;
|
||||
case MSG_OPEN_TRANBOX:
|
||||
window.dispatchEvent(new CustomEvent(MSG_OPEN_TRANBOX));
|
||||
break;
|
||||
case MSG_TRANSBOX_TOGGLE:
|
||||
this.toggleTransbox();
|
||||
break;
|
||||
case MSG_MOUSEHOVER_TOGGLE:
|
||||
this.toggleMouseHover();
|
||||
break;
|
||||
case MSG_TRANSINPUT_TOGGLE:
|
||||
this.toggleInputTranslate();
|
||||
break;
|
||||
default:
|
||||
return { error: `message action is unavailable: ${action}` };
|
||||
}
|
||||
return { rule: this.rule, setting: this.setting };
|
||||
});
|
||||
}
|
||||
|
||||
#createPlaceholderRegex() {
|
||||
const escapedStart = Translator.escapeRegex(
|
||||
this.#placeholder.startDelimiter
|
||||
@@ -1716,19 +1644,6 @@ export class Translator {
|
||||
this.updateRule({ textStyle });
|
||||
}
|
||||
|
||||
// 切换划词翻译
|
||||
toggleTransbox() {
|
||||
this.#setting.tranboxSetting.transOpen =
|
||||
!this.#setting.tranboxSetting.transOpen;
|
||||
this.#transboxManager?.toggle();
|
||||
}
|
||||
|
||||
// 切换输入框翻译
|
||||
toggleInputTranslate() {
|
||||
this.#setting.inputRule.transOpen = !this.#setting.inputRule.transOpen;
|
||||
this.#inputTranslator?.toggle();
|
||||
}
|
||||
|
||||
// 停止运行
|
||||
stop() {
|
||||
this.disable();
|
||||
|
||||
258
src/libs/translatorManager.js
Normal file
258
src/libs/translatorManager.js
Normal file
@@ -0,0 +1,258 @@
|
||||
import { browser } from "./browser";
|
||||
import { Translator } from "./translator";
|
||||
import { InputTranslator } from "./inputTranslate";
|
||||
import { TransboxManager } from "./tranbox";
|
||||
import { shortcutRegister } from "./shortcut";
|
||||
import { sendIframeMsg } from "./iframe";
|
||||
import { newI18n } from "../config";
|
||||
import { touchTapListener } from "./touch";
|
||||
import { debounce } from "./utils";
|
||||
import { PopupManager } from "./popupManager";
|
||||
import { FabManager } from "./fabManager";
|
||||
import {
|
||||
OPT_SHORTCUT_TRANSLATE,
|
||||
OPT_SHORTCUT_STYLE,
|
||||
OPT_SHORTCUT_POPUP,
|
||||
OPT_SHORTCUT_SETTING,
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
MSG_OPEN_TRANBOX,
|
||||
MSG_TRANSBOX_TOGGLE,
|
||||
MSG_MOUSEHOVER_TOGGLE,
|
||||
MSG_TRANSINPUT_TOGGLE,
|
||||
} from "../config";
|
||||
import { logger } from "./log";
|
||||
|
||||
export default class TranslatorManager {
|
||||
#clearShortcuts = [];
|
||||
#menuCommandIds = [];
|
||||
#clearTouchListener = null;
|
||||
#isActive = false;
|
||||
#isUserscript;
|
||||
#isIframe;
|
||||
|
||||
#windowMessageHandler = null;
|
||||
#browserMessageHandler = null;
|
||||
|
||||
_translator;
|
||||
_transboxManager;
|
||||
_inputTranslator;
|
||||
_popupManager;
|
||||
_fabManager;
|
||||
|
||||
constructor({ setting, rule, fabConfig, favWords, isIframe, isUserscript }) {
|
||||
this.#isIframe = isIframe;
|
||||
this.#isUserscript = isUserscript;
|
||||
|
||||
this._translator = new Translator({
|
||||
rule,
|
||||
setting,
|
||||
favWords,
|
||||
isUserscript,
|
||||
isIframe,
|
||||
});
|
||||
|
||||
if (!isIframe) {
|
||||
this._transboxManager = new TransboxManager(setting);
|
||||
this._inputTranslator = new InputTranslator(setting);
|
||||
this._popupManager = new PopupManager({ translator: this._translator });
|
||||
|
||||
if (fabConfig && !fabConfig.isHide) {
|
||||
this._fabManager = new FabManager({
|
||||
translator: this._translator,
|
||||
popupManager: this._popupManager,
|
||||
fabConfig,
|
||||
});
|
||||
this._fabManager.show();
|
||||
}
|
||||
}
|
||||
|
||||
this.#windowMessageHandler = this.#handleWindowMessage.bind(this);
|
||||
this.#browserMessageHandler = this.#handleBrowserMessage.bind(this);
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.#isActive) {
|
||||
logger.info("TranslatorManager is already started.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.#setupMessageListeners();
|
||||
this.#setupTouchOperations();
|
||||
|
||||
if (!this.#isIframe && this.#isUserscript) {
|
||||
this.#registerShortcuts();
|
||||
this.#registerMenus();
|
||||
}
|
||||
|
||||
this.#isActive = true;
|
||||
logger.info("TranslatorManager started.");
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.#isActive) {
|
||||
logger.info("TranslatorManager is not running.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 移除消息监听器
|
||||
if (this.#isUserscript) {
|
||||
window.removeEventListener("message", this.#windowMessageHandler);
|
||||
} else if (
|
||||
browser.runtime.onMessage.hasListener(this.#browserMessageHandler)
|
||||
) {
|
||||
browser.runtime.onMessage.removeListener(this.#browserMessageHandler);
|
||||
}
|
||||
|
||||
// 已注册的快捷键
|
||||
this.#clearShortcuts.forEach((clear) => clear());
|
||||
this.#clearShortcuts = [];
|
||||
|
||||
// 触屏
|
||||
if (this.#clearTouchListener) {
|
||||
this.#clearTouchListener();
|
||||
this.#clearTouchListener = null;
|
||||
}
|
||||
|
||||
// 油猴菜单
|
||||
if (globalThis.GM && this.#menuCommandIds.length > 0) {
|
||||
this.#menuCommandIds.forEach((id) =>
|
||||
globalThis.GM.unregisterMenuCommand(id)
|
||||
);
|
||||
this.#menuCommandIds = [];
|
||||
}
|
||||
|
||||
// 子模块
|
||||
this._popupManager?.hide();
|
||||
this._fabManager?.hide();
|
||||
this._transboxManager?.disable();
|
||||
this._inputTranslator?.disable();
|
||||
this._translator.stop();
|
||||
|
||||
this.#isActive = false;
|
||||
logger.info("TranslatorManager stopped.");
|
||||
}
|
||||
|
||||
#setupMessageListeners() {
|
||||
if (this.#isUserscript) {
|
||||
window.addEventListener("message", this.#windowMessageHandler);
|
||||
} else {
|
||||
browser.runtime.onMessage.addListener(this.#browserMessageHandler);
|
||||
}
|
||||
}
|
||||
|
||||
#setupTouchOperations() {
|
||||
if (this.#isIframe) return;
|
||||
|
||||
const { touchTranslate = 2 } = this._translator.setting;
|
||||
if (touchTranslate === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleTap = debounce(() => {
|
||||
this.#processActions({ action: MSG_TRANS_TOGGLE });
|
||||
}, 300);
|
||||
|
||||
this.#clearTouchListener = touchTapListener(handleTap, touchTranslate);
|
||||
}
|
||||
|
||||
#handleWindowMessage(event) {
|
||||
this.#processActions(event.data);
|
||||
}
|
||||
|
||||
#handleBrowserMessage(message, sender, sendResponse) {
|
||||
const result = this.#processActions(message);
|
||||
const response = result || {
|
||||
rule: this._translator.rule,
|
||||
setting: this._translator.setting,
|
||||
};
|
||||
sendResponse(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
#registerShortcuts() {
|
||||
const { shortcuts } = this._translator.setting;
|
||||
this.#clearShortcuts = [
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () =>
|
||||
this.#processActions({ action: MSG_TRANS_TOGGLE })
|
||||
),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () =>
|
||||
this.#processActions({ action: MSG_TRANS_TOGGLE_STYLE })
|
||||
),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () =>
|
||||
this._popupManager.toggle()
|
||||
),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () =>
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank")
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
#registerMenus() {
|
||||
if (!globalThis.GM) return;
|
||||
const { contextMenuType, uiLang } = this._translator.setting;
|
||||
if (contextMenuType === 0) return;
|
||||
|
||||
const i18n = newI18n(uiLang || "zh");
|
||||
const GM = globalThis.GM;
|
||||
this.#menuCommandIds = [
|
||||
GM.registerMenuCommand(
|
||||
i18n("translate_switch"),
|
||||
() => this.#processActions({ action: MSG_TRANS_TOGGLE }),
|
||||
"Q"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
i18n("toggle_style"),
|
||||
() => this.#processActions({ action: MSG_TRANS_TOGGLE_STYLE }),
|
||||
"C"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
i18n("open_menu"),
|
||||
() => this._popupManager.toggle(),
|
||||
"K"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
i18n("open_setting"),
|
||||
() => window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank"),
|
||||
"O"
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
#processActions({ action, args } = {}) {
|
||||
if (this.#isUserscript) {
|
||||
sendIframeMsg(action);
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
this._translator.toggle();
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
this._translator.toggleStyle();
|
||||
break;
|
||||
case MSG_TRANS_GETRULE:
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
this._translator.updateRule(args);
|
||||
break;
|
||||
case MSG_OPEN_TRANBOX:
|
||||
this._transboxManager?.enable();
|
||||
break;
|
||||
case MSG_TRANSBOX_TOGGLE:
|
||||
this._transboxManager?.toggle();
|
||||
break;
|
||||
case MSG_MOUSEHOVER_TOGGLE:
|
||||
this._translator.toggleMouseHover();
|
||||
break;
|
||||
case MSG_TRANSINPUT_TOGGLE:
|
||||
this._inputTranslator?.toggle();
|
||||
break;
|
||||
default:
|
||||
logger.info(`Message action is unavailable: ${action}`);
|
||||
return { error: `Message action is unavailable: ${action}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { logger } from "./log";
|
||||
|
||||
export const trustedTypesHelper = (() => {
|
||||
const POLICY_NAME = "kiss-translator-policy";
|
||||
let policy = null;
|
||||
@@ -13,7 +15,7 @@ export const trustedTypesHelper = (() => {
|
||||
if (err.message.includes("already exists")) {
|
||||
policy = globalThis.trustedTypes.policies.get(POLICY_NAME);
|
||||
} else {
|
||||
console.error("cont create Trusted Types", err);
|
||||
logger.info("cont create Trusted Types", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user