diff --git a/src/common.js b/src/common.js
index 9dbb711..275e178 100644
--- a/src/common.js
+++ b/src/common.js
@@ -1,24 +1,11 @@
-import React from "react";
-import ReactDOM from "react-dom/client";
-import Action from "./views/Action";
-import createCache from "@emotion/cache";
-import { CacheProvider } from "@emotion/react";
-import {
- MSG_TRANS_TOGGLE,
- MSG_TRANS_TOGGLE_STYLE,
- MSG_TRANS_PUTRULE,
- APP_CONSTS,
- OPT_HIGHLIGHT_WORDS_DISABLE,
-} from "./config";
+import { OPT_HIGHLIGHT_WORDS_DISABLE } from "./config";
import {
getFabWithDefault,
getSettingWithDefault,
getWordsWithDefault,
} from "./libs/storage";
-import { Translator } from "./libs/translator";
-import { isIframe, sendIframeMsg } from "./libs/iframe";
-import { touchTapListener } from "./libs/touch";
-import { debounce, genEventName } from "./libs/utils";
+import { isIframe } from "./libs/iframe";
+import { genEventName } from "./libs/utils";
import { handlePing, injectScript } from "./libs/gm";
import { matchRule } from "./libs/rules";
import { trySyncAllSubRules } from "./libs/subRules";
@@ -26,6 +13,7 @@ import { isInBlacklist } from "./libs/blacklist";
import { runSubtitle } from "./subtitle/subtitle";
import { logger } from "./libs/log";
import { injectInlineJs } from "./libs/injector";
+import TranslatorManager from "./libs/translatorManager";
/**
* 油猴脚本设置页面
@@ -48,62 +36,6 @@ function runSettingPage() {
}
}
-/**
- * iframe 页面执行
- * @param {*} translator
- */
-function runIframe(translator) {
- window.addEventListener("message", (e) => {
- const { action, args } = e.data || {};
- switch (action) {
- case MSG_TRANS_TOGGLE:
- translator?.toggle();
- break;
- case MSG_TRANS_TOGGLE_STYLE:
- translator?.toggleStyle();
- break;
- case MSG_TRANS_PUTRULE:
- translator.updateRule(args || {});
- break;
- default:
- }
- });
-}
-
-/**
- * 悬浮按钮
- * @param {*} translator
- * @returns
- */
-async function showFab(translator) {
- const fab = await getFabWithDefault();
- const $action = document.createElement("div");
- $action.id = APP_CONSTS.fabID;
- $action.className = "notranslate";
- $action.style.fontSize = "0";
- $action.style.width = "0";
- $action.style.height = "0";
- document.body.parentElement.appendChild($action);
- const shadowContainer = $action.attachShadow({ mode: "closed" });
- const emotionRoot = document.createElement("style");
- const shadowRootElement = document.createElement("div");
- shadowRootElement.className = `${APP_CONSTS.fabID}_warpper notranslate`;
- shadowContainer.appendChild(emotionRoot);
- shadowContainer.appendChild(shadowRootElement);
- const cache = createCache({
- key: APP_CONSTS.fabID,
- prepend: true,
- container: emotionRoot,
- });
- ReactDOM.createRoot(shadowRootElement).render(
-
-
-
-
-
- );
-}
-
/**
* 显示错误信息到页面顶部
* @param {*} message
@@ -166,22 +98,19 @@ function showErr(message) {
setTimeout(removeBanner, 10000);
}
-/**
- * 监听触屏操作
- * @param {*} translator
- * @returns
- */
-function touchOperation(translator) {
- const { touchTranslate = 2 } = translator.setting;
- if (touchTranslate === 0) {
- return;
+async function getFavWords(rule) {
+ if (
+ rule.highlightWords &&
+ rule.highlightWords !== OPT_HIGHLIGHT_WORDS_DISABLE
+ ) {
+ try {
+ return Object.keys(await getWordsWithDefault());
+ } catch (err) {
+ logger.info("get fav words", err);
+ }
}
- const handleTap = debounce(() => {
- translator.toggle();
- sendIframeMsg(MSG_TRANS_TOGGLE);
- });
- touchTapListener(handleTap, touchTranslate);
+ return [];
}
/**
@@ -214,46 +143,28 @@ export async function run(isUserscript = false) {
// 翻译网页
const rule = await matchRule(href, setting);
- let favWords = [];
- if (
- rule.highlightWords &&
- rule.highlightWords !== OPT_HIGHLIGHT_WORDS_DISABLE
- ) {
- favWords = Object.keys(await getWordsWithDefault());
- }
- const translator = new Translator({
- rule,
+ const favWords = await getFavWords(rule);
+ const fabConfig = await getFabWithDefault();
+ const translatorManager = new TranslatorManager({
setting,
+ rule,
+ fabConfig,
favWords,
+ isIframe,
isUserscript,
});
+ translatorManager.start();
- // 适配iframe
if (isIframe) {
- runIframe(translator);
return;
}
// 字幕翻译
runSubtitle({ href, setting, rule, isUserscript });
- // 监听消息
- // !isUserscript && runtimeListener(translator);
-
- // 输入框翻译
- // inputTranslate(setting);
-
- // 划词翻译
- // showTransbox(setting, rule);
-
- // 浮球按钮
- await showFab(translator);
-
- // 触屏操作
- touchOperation(translator);
-
- // 同步订阅规则
- isUserscript && (await trySyncAllSubRules(setting));
+ if (isUserscript) {
+ trySyncAllSubRules(setting);
+ }
} catch (err) {
console.error("[KISS-Translator]", err);
showErr(err.message);
diff --git a/src/config/app.js b/src/config/app.js
index 2c8d4ee..5737ca4 100644
--- a/src/config/app.js
+++ b/src/config/app.js
@@ -5,6 +5,7 @@ export const APP_LCNAME = APP_NAME.toLowerCase();
export const APP_CONSTS = {
fabID: `${APP_LCNAME}-fab`,
boxID: `${APP_LCNAME}-box`,
+ popupID: `${APP_LCNAME}-popup`,
};
export const APP_VERSION = process.env.REACT_APP_VERSION.split(".");
diff --git a/src/config/i18n.js b/src/config/i18n.js
index f866f64..33fd548 100644
--- a/src/config/i18n.js
+++ b/src/config/i18n.js
@@ -1722,4 +1722,4 @@ export const I18N = {
},
};
-export const i18n = (lang) => (key) => I18N[key]?.[lang] || "";
+export const newI18n = (lang) => (key) => I18N[key]?.[lang] || "";
diff --git a/src/hooks/WindowSize.js b/src/hooks/WindowSize.js
new file mode 100644
index 0000000..9c020a7
--- /dev/null
+++ b/src/hooks/WindowSize.js
@@ -0,0 +1,29 @@
+import { useState, useEffect } from "react";
+import { useDebouncedCallback } from "./DebouncedCallback";
+
+function useWindowSize() {
+ const [windowSize, setWindowSize] = useState({
+ w: window.innerWidth,
+ h: window.innerHeight,
+ });
+
+ const debounceWindowResize = useDebouncedCallback(() => {
+ setWindowSize({
+ w: window.innerWidth,
+ h: window.innerHeight,
+ });
+ }, 200);
+
+ useEffect(() => {
+ debounceWindowResize();
+
+ window.addEventListener("resize", debounceWindowResize);
+ return () => {
+ window.removeEventListener("resize", debounceWindowResize);
+ };
+ }, [debounceWindowResize]);
+
+ return windowSize;
+}
+
+export default useWindowSize;
diff --git a/src/libs/fabManager.js b/src/libs/fabManager.js
new file mode 100644
index 0000000..d2e7b45
--- /dev/null
+++ b/src/libs/fabManager.js
@@ -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 },
+ });
+ }
+}
diff --git a/src/libs/popupManager.js b/src/libs/popupManager.js
new file mode 100644
index 0000000..d9a9906
--- /dev/null
+++ b/src/libs/popupManager.js
@@ -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 },
+ });
+ }
+}
diff --git a/src/libs/shadowDomManager.js b/src/libs/shadowDomManager.js
new file mode 100644
index 0000000..74d3222
--- /dev/null
+++ b/src/libs/shadowDomManager.js
@@ -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(
+
+
+
+
+
+ );
+ }
+}
diff --git a/src/libs/shadowroot.js b/src/libs/shadowRootMonitor.js
similarity index 97%
rename from src/libs/shadowroot.js
rename to src/libs/shadowRootMonitor.js
index 82548bb..bed604b 100644
--- a/src/libs/shadowroot.js
+++ b/src/libs/shadowRootMonitor.js
@@ -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 被创建时调用的回调函数。
*/
diff --git a/src/libs/translator.js b/src/libs/translator.js
index dc599dc..ad3e0da 100644
--- a/src/libs/translator.js
+++ b/src/libs/translator.js
@@ -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();
diff --git a/src/libs/translatorManager.js b/src/libs/translatorManager.js
new file mode 100644
index 0000000..8d8266c
--- /dev/null
+++ b/src/libs/translatorManager.js
@@ -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}` };
+ }
+ }
+}
diff --git a/src/libs/trustedTypes.js b/src/libs/trustedTypes.js
index 7e97b7e..49590a8 100644
--- a/src/libs/trustedTypes.js
+++ b/src/libs/trustedTypes.js
@@ -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);
}
}
}
diff --git a/src/subtitle/YouTubeCaptionProvider.js b/src/subtitle/YouTubeCaptionProvider.js
index 52f8ca8..085f6e2 100644
--- a/src/subtitle/YouTubeCaptionProvider.js
+++ b/src/subtitle/YouTubeCaptionProvider.js
@@ -10,7 +10,7 @@ import {
import { sleep } from "../libs/utils.js";
import { createLogoSVG } from "../libs/svg.js";
import { randomBetween } from "../libs/utils.js";
-import { i18n } from "../config";
+import { newI18n } from "../config";
const VIDEO_SELECT = "#container video";
const CONTORLS_SELECT = ".ytp-right-controls";
@@ -33,7 +33,7 @@ class YouTubeCaptionProvider {
constructor(setting = {}) {
this.#setting = setting;
- this.#i18n = i18n(setting.uiLang || "zh");
+ this.#i18n = newI18n(setting.uiLang || "zh");
}
initialize() {
diff --git a/src/views/Action/ContentFab.js b/src/views/Action/ContentFab.js
new file mode 100644
index 0000000..4e66ca6
--- /dev/null
+++ b/src/views/Action/ContentFab.js
@@ -0,0 +1,73 @@
+import Fab from "@mui/material/Fab";
+import TranslateIcon from "@mui/icons-material/Translate";
+import ThemeProvider from "../../hooks/Theme";
+import Draggable from "./Draggable";
+import { useState, useMemo, useCallback } from "react";
+import { SettingProvider } from "../../hooks/Setting";
+import { MSG_TRANS_TOGGLE } from "../../config";
+import { sendIframeMsg } from "../../libs/iframe";
+import useWindowSize from "../../hooks/WindowSize";
+
+export default function ContentFab({
+ translator,
+ fabConfig: { x: fabX, y: fabY, fabClickAction = 0 } = {},
+ popupManager,
+}) {
+ const fabWidth = 40;
+ const windowSize = useWindowSize();
+ const [moved, setMoved] = useState(false);
+
+ const handleStart = useCallback(() => {
+ setMoved(false);
+ }, []);
+
+ const handleMove = useCallback(() => {
+ setMoved(true);
+ }, []);
+
+ const handleClick = useCallback(() => {
+ if (!moved) {
+ if (fabClickAction === 1) {
+ translator.toggle();
+ sendIframeMsg(MSG_TRANS_TOGGLE);
+ } else {
+ popupManager.toggle();
+ }
+ }
+ }, [moved, translator, popupManager, fabClickAction]);
+
+ const fabProps = useMemo(
+ () => ({
+ windowSize,
+ width: fabWidth,
+ height: fabWidth,
+ left: fabX ?? -fabWidth,
+ top: fabY ?? windowSize.h / 2,
+ }),
+ [windowSize, fabWidth, fabX, fabY]
+ );
+
+ return (
+
+
+
+
+
+ }
+ />
+
+
+ );
+}
diff --git a/src/views/Action/Draggable.js b/src/views/Action/Draggable.js
index 2e3a575..2e17244 100644
--- a/src/views/Action/Draggable.js
+++ b/src/views/Action/Draggable.js
@@ -50,7 +50,7 @@ export default function Draggable({
height,
left,
top,
- show,
+ show = true,
snapEdge,
onStart,
onMove,
diff --git a/src/views/Action/index.js b/src/views/Action/index.js
index 1830b30..ac99dbd 100644
--- a/src/views/Action/index.js
+++ b/src/views/Action/index.js
@@ -1,157 +1,19 @@
-import Fab from "@mui/material/Fab";
-import TranslateIcon from "@mui/icons-material/Translate";
import ThemeProvider from "../../hooks/Theme";
import Draggable from "./Draggable";
-import { useEffect, useState, useMemo, useCallback } from "react";
+import { useEffect, useMemo, useCallback } from "react";
import { SettingProvider } from "../../hooks/Setting";
import Popup from "../Popup";
-import { debounce } from "../../libs/utils";
-import { isGm } from "../../libs/client";
import Header from "../Popup/Header";
import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider";
-import {
- DEFAULT_SHORTCUTS,
- OPT_SHORTCUT_TRANSLATE,
- OPT_SHORTCUT_STYLE,
- OPT_SHORTCUT_POPUP,
- OPT_SHORTCUT_SETTING,
- MSG_TRANS_TOGGLE,
- MSG_TRANS_TOGGLE_STYLE,
-} from "../../config";
-import { shortcutRegister } from "../../libs/shortcut";
-import { sendIframeMsg } from "../../libs/iframe";
-import { kissLog } from "../../libs/log";
-import { getI18n } from "../../hooks/I18n";
+import useWindowSize from "../../hooks/WindowSize";
-export default function Action({ translator, fab }) {
- const fabWidth = 40;
- const [showPopup, setShowPopup] = useState(false);
- const [windowSize, setWindowSize] = useState({
- w: window.innerWidth,
- h: window.innerHeight,
- });
- const [moved, setMoved] = useState(false);
+export default function Action({ translator, onClose }) {
+ const windowSize = useWindowSize();
- const { fabClickAction = 0 } = fab || {};
-
- const handleWindowResize = useMemo(
- () =>
- debounce(() => {
- setWindowSize({
- w: window.innerWidth,
- h: window.innerHeight,
- });
- }),
- []
- );
-
- const handleWindowClick = (e) => {
- setShowPopup(false);
- };
-
- const handleStart = useCallback(() => {
- setMoved(false);
- }, []);
-
- const handleMove = useCallback(() => {
- setMoved(true);
- }, []);
-
- useEffect(() => {
- if (!isGm) {
- return;
- }
-
- // 注册快捷键
- const shortcuts = translator.setting.shortcuts || DEFAULT_SHORTCUTS;
- const clearShortcuts = [
- shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () => {
- translator.toggle();
- sendIframeMsg(MSG_TRANS_TOGGLE);
- setShowPopup(false);
- }),
- shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () => {
- translator.toggleStyle();
- sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
- setShowPopup(false);
- }),
- shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () => {
- setShowPopup((pre) => !pre);
- }),
- shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () => {
- window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
- }),
- ];
-
- return () => {
- clearShortcuts.forEach((fn) => {
- fn();
- });
- };
- }, [translator]);
-
- useEffect(() => {
- if (!isGm) {
- return;
- }
-
- // 注册菜单
- try {
- const menuCommandIds = [];
- const { contextMenuType, uiLang } = translator.setting;
- contextMenuType !== 0 &&
- menuCommandIds.push(
- GM.registerMenuCommand(
- getI18n(uiLang, "translate_switch"),
- (event) => {
- translator.toggle();
- sendIframeMsg(MSG_TRANS_TOGGLE);
- setShowPopup(false);
- },
- "Q"
- ),
- GM.registerMenuCommand(
- getI18n(uiLang, "toggle_style"),
- (event) => {
- translator.toggleStyle();
- sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
- setShowPopup(false);
- },
- "C"
- ),
- GM.registerMenuCommand(
- getI18n(uiLang, "open_menu"),
- (event) => {
- setShowPopup((pre) => !pre);
- },
- "K"
- ),
- GM.registerMenuCommand(
- getI18n(uiLang, "open_setting"),
- (event) => {
- window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
- },
- "O"
- )
- );
-
- return () => {
- menuCommandIds.forEach((id) => {
- GM.unregisterMenuCommand(id);
- });
- };
- } catch (err) {
- kissLog("registerMenuCommand", err);
- }
- }, [translator]);
-
- useEffect(() => {
- window.addEventListener("resize", handleWindowResize);
- return () => {
- window.removeEventListener("resize", handleWindowResize);
- };
- }, [handleWindowResize]);
+ const handleWindowClick = useCallback(() => {
+ onClose();
+ }, [onClose]);
useEffect(() => {
window.addEventListener("click", handleWindowClick);
@@ -159,7 +21,7 @@ export default function Action({ translator, fab }) {
return () => {
window.removeEventListener("click", handleWindowClick);
};
- }, []);
+ }, [handleWindowClick]);
const popProps = useMemo(() => {
const width = Math.min(windowSize.w, 360);
@@ -175,67 +37,22 @@ export default function Action({ translator, fab }) {
};
}, [windowSize]);
- const fabProps = {
- windowSize,
- width: fabWidth,
- height: fabWidth,
- left: fab.x ?? -fabWidth,
- top: fab.y ?? windowSize.h / 2,
- };
-
return (
-
+
}
>
- {showPopup && (
-
- )}
+
- {
- if (!moved) {
- if (fabClickAction === 1) {
- translator.toggle();
- sendIframeMsg(MSG_TRANS_TOGGLE);
- setShowPopup(false);
- } else {
- setShowPopup((pre) => !pre);
- }
- }
- }}
- >
-
-
- }
- />
);
diff --git a/src/views/Popup/Header.js b/src/views/Popup/Header.js
index 3d090a7..8805901 100644
--- a/src/views/Popup/Header.js
+++ b/src/views/Popup/Header.js
@@ -5,7 +5,7 @@ import Stack from "@mui/material/Stack";
import DarkModeButton from "../Options/DarkModeButton";
import Typography from "@mui/material/Typography";
-export default function Header({ setShowPopup }) {
+export default function Header({ onClose }) {
const handleHomepage = () => {
window.open(process.env.REACT_APP_HOMEPAGE, "_blank");
};
@@ -33,10 +33,10 @@ export default function Header({ setShowPopup }) {
- {setShowPopup ? (
+ {onClose ? (
{
- setShowPopup(false);
+ onClose();
}}
>
diff --git a/src/views/Popup/index.js b/src/views/Popup/index.js
index e401162..9bc2447 100644
--- a/src/views/Popup/index.js
+++ b/src/views/Popup/index.js
@@ -35,7 +35,7 @@ import { parseUrlPattern } from "../../libs/utils";
// 插件popup没有参数
// 网页弹框有
-export default function Popup({ setShowPopup, translator }) {
+export default function Popup({ translator }) {
const i18n = useI18n();
const [rule, setRule] = useState(translator?.rule);
const [setting, setSetting] = useState(translator?.setting);
@@ -49,7 +49,6 @@ export default function Popup({ setShowPopup, translator }) {
} else {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
}
- setShowPopup && setShowPopup(false);
};
const handleTransToggle = async (e) => {