refactor: add TranslatorManager

This commit is contained in:
Gabe
2025-10-21 02:07:33 +08:00
parent ed279cf8a1
commit 53e32d3031
17 changed files with 566 additions and 405 deletions

View File

@@ -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(
<React.StrictMode>
<CacheProvider value={cache}>
<Action translator={translator} fab={fab} />
</CacheProvider>
</React.StrictMode>
);
}
/**
* 显示错误信息到页面顶部
* @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);

View File

@@ -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(".");

View File

@@ -1722,4 +1722,4 @@ export const I18N = {
},
};
export const i18n = (lang) => (key) => I18N[key]?.[lang] || "";
export const newI18n = (lang) => (key) => I18N[key]?.[lang] || "";

29
src/hooks/WindowSize.js Normal file
View File

@@ -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;

14
src/libs/fabManager.js Normal file
View 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
View 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 },
});
}
}

View 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>
);
}
}

View File

@@ -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 被创建时调用的回调函数
*/

View File

@@ -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();

View 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}` };
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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() {

View File

@@ -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 (
<SettingProvider>
<ThemeProvider>
<Draggable
key="fab"
snapEdge
{...fabProps}
onStart={handleStart}
onMove={handleMove}
handler={
<Fab size="small" color="primary" onClick={handleClick}>
<TranslateIcon
sx={{
width: 24,
height: 24,
}}
/>
</Fab>
}
/>
</ThemeProvider>
</SettingProvider>
);
}

View File

@@ -50,7 +50,7 @@ export default function Draggable({
height,
left,
top,
show,
show = true,
snapEdge,
onStart,
onMove,

View File

@@ -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 (
<SettingProvider>
<ThemeProvider>
<Draggable
key="pop"
{...popProps}
show={showPopup}
onStart={handleStart}
onMove={handleMove}
usePaper
handler={
<Box style={{ cursor: "move" }}>
<Header setShowPopup={setShowPopup} />
<Header onClose={onClose} />
<Divider />
</Box>
}
>
{showPopup && (
<Popup setShowPopup={setShowPopup} translator={translator} />
)}
<Popup translator={translator} />
</Draggable>
<Draggable
key="fab"
snapEdge
{...fabProps}
show={fab.isHide ? false : !showPopup}
onStart={handleStart}
onMove={handleMove}
handler={
<Fab
size="small"
color="primary"
onClick={(e) => {
if (!moved) {
if (fabClickAction === 1) {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
setShowPopup(false);
} else {
setShowPopup((pre) => !pre);
}
}
}}
>
<TranslateIcon
sx={{
width: 24,
height: 24,
}}
/>
</Fab>
}
/>
</ThemeProvider>
</SettingProvider>
);

View File

@@ -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 }) {
</Typography>
</Stack>
{setShowPopup ? (
{onClose ? (
<IconButton
onClick={() => {
setShowPopup(false);
onClose();
}}
>
<CloseIcon />

View File

@@ -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) => {