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