From 7412b3a5c81585e3fe251f833c02ec352d48a4b3 Mon Sep 17 00:00:00 2001 From: Gabe Date: Wed, 1 Oct 2025 01:47:15 +0800 Subject: [PATCH] feat: Add more shortcut keys to popup --- src/common.js | 98 +---------- src/config/i18n.js | 4 +- src/config/msg.js | 3 + src/config/rules.js | 4 +- src/config/setting.js | 2 +- src/libs/inputTranslate.js | 313 ++++++++++++++++++++--------------- src/libs/rules.js | 3 - src/libs/tranbox.js | 95 +++++++++++ src/libs/translator.js | 96 ++++++++++- src/views/Options/Rules.js | 17 -- src/views/Options/Tranbox.js | 18 +- src/views/Popup/index.js | 121 +++++++++++++- 12 files changed, 505 insertions(+), 269 deletions(-) create mode 100644 src/libs/tranbox.js diff --git a/src/common.js b/src/common.js index 85dd6bc..18c8c66 100644 --- a/src/common.js +++ b/src/common.js @@ -6,24 +6,18 @@ import { CacheProvider } from "@emotion/react"; import { MSG_TRANS_TOGGLE, MSG_TRANS_TOGGLE_STYLE, - MSG_TRANS_GETRULE, MSG_TRANS_PUTRULE, - MSG_OPEN_TRANBOX, APP_CONSTS, - DEFAULT_TRANBOX_SETTING, } from "./config"; import { getFabWithDefault, getSettingWithDefault } from "./libs/storage"; import { Translator } from "./libs/translator"; import { isIframe, sendIframeMsg } from "./libs/iframe"; -import Slection from "./views/Selection"; import { touchTapListener } from "./libs/touch"; import { debounce, genEventName } from "./libs/utils"; import { handlePing, injectScript } from "./libs/gm"; -import { browser } from "./libs/browser"; import { matchRule } from "./libs/rules"; import { trySyncAllSubRules } from "./libs/subRules"; import { isInBlacklist } from "./libs/blacklist"; -import inputTranslate from "./libs/inputTranslate"; /** * 油猴脚本设置页面 @@ -45,37 +39,6 @@ function runSettingPage() { } } -/** - * 插件监听后端事件 - * @param {*} translator - */ -function runtimeListener(translator) { - browser?.runtime.onMessage.addListener(async ({ action, args }) => { - switch (action) { - case MSG_TRANS_TOGGLE: - translator.toggle(); - sendIframeMsg(MSG_TRANS_TOGGLE); - break; - case MSG_TRANS_TOGGLE_STYLE: - translator.toggleStyle(); - sendIframeMsg(MSG_TRANS_TOGGLE_STYLE); - break; - case MSG_TRANS_GETRULE: - break; - case MSG_TRANS_PUTRULE: - translator.updateRule(args); - sendIframeMsg(MSG_TRANS_PUTRULE, args); - break; - case MSG_OPEN_TRANBOX: - window.dispatchEvent(new CustomEvent(MSG_OPEN_TRANBOX)); - break; - default: - return { error: `message action is unavailable: ${action}` }; - } - return { rule: translator.rule, setting: translator.setting }; - }); -} - /** * iframe 页面执行 * @param {*} translator @@ -131,61 +94,6 @@ async function showFab(translator) { ); } -/** - * 划词翻译 - * @param {*} param0 - * @returns - */ -function showTransbox( - { - contextMenuType, - tranboxSetting = DEFAULT_TRANBOX_SETTING, - transApis, - darkMode, - uiLang, - langDetector, - }, - { transSelected } -) { - if (transSelected === "false") { - return; - } - - const $tranbox = document.createElement("div"); - $tranbox.setAttribute("id", APP_CONSTS.boxID); - $tranbox.style.fontSize = "0"; - $tranbox.style.width = "0"; - $tranbox.style.height = "0"; - document.body.parentElement.appendChild($tranbox); - const shadowContainer = $tranbox.attachShadow({ mode: "closed" }); - const emotionRoot = document.createElement("style"); - const shadowRootElement = document.createElement("div"); - shadowRootElement.classList.add(`${APP_CONSTS.boxID}_warpper`); - shadowRootElement.classList.add( - `${APP_CONSTS.boxID}_${darkMode ? "dark" : "light"}` - ); - shadowContainer.appendChild(emotionRoot); - shadowContainer.appendChild(shadowRootElement); - const cache = createCache({ - key: APP_CONSTS.boxID, - prepend: true, - container: emotionRoot, - }); - ReactDOM.createRoot(shadowRootElement).render( - - - - - - ); -} - /** * 显示错误信息到页面顶部 * @param {*} message @@ -251,13 +159,13 @@ export async function run(isUserscript = false) { } // 监听消息 - !isUserscript && runtimeListener(translator); + // !isUserscript && runtimeListener(translator); // 输入框翻译 - inputTranslate(setting); + // inputTranslate(setting); // 划词翻译 - showTransbox(setting, rule); + // showTransbox(setting, rule); // 浮球按钮 await showFab(translator); diff --git a/src/config/i18n.js b/src/config/i18n.js index d4ab356..cecc5b9 100644 --- a/src/config/i18n.js +++ b/src/config/i18n.js @@ -1399,9 +1399,9 @@ export const I18N = { zh_TW: `ShadowRoot`, }, richtext_alt: { - zh: `富文本`, + zh: `保留富文本`, en: `Rich Text`, - zh_TW: `富文本`, + zh_TW: `保留富文本`, }, transonly_alt: { zh: `隐藏原文`, diff --git a/src/config/msg.js b/src/config/msg.js index 6903924..50d6d83 100644 --- a/src/config/msg.js +++ b/src/config/msg.js @@ -14,6 +14,9 @@ export const MSG_OPEN_TRANBOX = "open_tranbox"; export const MSG_TRANS_GETRULE = "trans_getrule"; export const MSG_TRANS_PUTRULE = "trans_putrule"; export const MSG_TRANS_CURRULE = "trans_currule"; +export const MSG_TRANSBOX_TOGGLE = "transbox_toggle"; +export const MSG_MOUSEHOVER_TOGGLE = "mousehover_toggle"; +export const MSG_TRANSINPUT_TOGGLE = "transinput_toggle"; export const MSG_CONTEXT_MENUS = "context_menus"; export const MSG_COMMAND_SHORTCUTS = "command_shortcuts"; export const MSG_INJECT_JS = "inject_js"; diff --git a/src/config/rules.js b/src/config/rules.js index 6ba2964..82cae71 100644 --- a/src/config/rules.js +++ b/src/config/rules.js @@ -95,7 +95,7 @@ export const DEFAULT_RULE = { // transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译 (暂时作废) transTag: GLOBAL_KEY, // 译文元素标签 transTitle: GLOBAL_KEY, // 是否同时翻译页面标题 - transSelected: GLOBAL_KEY, // 是否启用划词翻译 + // transSelected: GLOBAL_KEY, // 是否启用划词翻译 (移回setting) // detectRemote: GLOBAL_KEY, // 是否使用远程语言检测 (移回setting) // skipLangs: [], // 不翻译的语言 (移回setting) // fixerSelector: "", // 修复函数选择器 (暂时作废) @@ -131,7 +131,7 @@ export const GLOBLA_RULE = { // transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译 (暂时作废) transTag: DEFAULT_TRANS_TAG, // 译文元素标签 transTitle: "false", // 是否同时翻译页面标题 - transSelected: "true", // 是否启用划词翻译 + // transSelected: "true", // 是否启用划词翻译 (移回setting) // detectRemote: "true", // 是否使用远程语言检测 (移回setting) // skipLangs: [], // 不翻译的语言 (移回setting) // fixerSelector: "", // 修复函数选择器 (暂时作废) diff --git a/src/config/setting.js b/src/config/setting.js index 3585b12..f86ebcf 100644 --- a/src/config/setting.js +++ b/src/config/setting.js @@ -73,7 +73,7 @@ export const OPT_TRANBOX_TRIGGER_ALL = [ ]; export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"]; export const DEFAULT_TRANBOX_SETTING = { - // transOpen: true, // 是否启用划词翻译(作废,移至rule) + transOpen: true, // 是否启用划词翻译 apiSlug: OPT_TRANS_MICROSOFT, fromLang: "auto", toLang: "zh-CN", diff --git a/src/libs/inputTranslate.js b/src/libs/inputTranslate.js index 54a6172..4ab7282 100644 --- a/src/libs/inputTranslate.js +++ b/src/libs/inputTranslate.js @@ -4,7 +4,7 @@ import { OPT_LANGS_LIST, DEFAULT_API_SETTING, } from "../config"; -import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils"; +import { genEventName, removeEndchar, matchInputStr } from "./utils"; import { stepShortcutRegister } from "./shortcut"; import { apiTranslate } from "../apis"; import { loadingSvg } from "./svg"; @@ -18,34 +18,20 @@ function isEditAbleNode(node) { return node.hasAttribute("contenteditable"); } -function selectContent(node) { +function replaceContentEditableText(node, newText) { node.focus(); + const selection = window.getSelection(); + if (!selection) return; + const range = document.createRange(); range.selectNodeContents(node); - - const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); -} -function pasteContentEvent(node, text) { - node.focus(); - const data = new DataTransfer(); - data.setData("text/plain", text); + range.deleteContents(); + const textNode = document.createTextNode(newText); + range.insertNode(textNode); - const event = new ClipboardEvent("paste", { clipboardData: data }); - document.dispatchEvent(event); - data.clearData(); -} - -function pasteContentCommand(node, text) { - node.focus(); - document.execCommand("insertText", false, text); -} - -function collapseToEnd(node) { - node.focus(); - const selection = window.getSelection(); selection.collapseToEnd(); } @@ -57,144 +43,205 @@ function getNodeText(node) { } function addLoading(node, loadingId) { + const rect = node.getBoundingClientRect(); const div = document.createElement("div"); div.id = loadingId; div.innerHTML = loadingSvg; div.style.cssText = ` - width: ${node.offsetWidth}px; - height: ${node.offsetHeight}px; - line-height: ${node.offsetHeight}px; - position: absolute; - text-align: center; - left: ${node.offsetLeft}px; - top: ${node.offsetTop}px; - z-index: 2147483647; + position: fixed; + left: ${rect.left}px; + top: ${rect.top}px; + width: ${rect.width}px; + height: ${rect.height}px; + line-height: ${rect.height}px; + text-align: center; + z-index: 2147483647; + pointer-events: none; /* 允许点击穿透 */ `; - node.offsetParent?.appendChild(div); + document.body.appendChild(div); } -function removeLoading(node, loadingId) { - const div = node.offsetParent.querySelector(`#${loadingId}`); - if (div) { - div.remove(); - } +function removeLoading(loadingId) { + const div = document.getElementById(loadingId); + if (div) div.remove(); } /** * 输入框翻译 */ -export default function inputTranslate({ - inputRule: { - transOpen, - triggerShortcut, - apiSlug, - fromLang, - toLang, - triggerCount, - triggerTime, - transSign, - } = DEFAULT_INPUT_RULE, - transApis, -}) { - if (!transOpen) { - return; +export class InputTranslator { + #config; + #unregisterShortcut = null; + #isEnabled = false; + #triggerShortcut; // 用于缓存快捷键 + + constructor({ inputRule = DEFAULT_INPUT_RULE, transApis = [] } = {}) { + this.#config = { inputRule, transApis }; + + const { triggerShortcut: initialTriggerShortcut } = this.#config.inputRule; + if (initialTriggerShortcut && initialTriggerShortcut.length > 0) { + this.#triggerShortcut = initialTriggerShortcut; + } else { + this.#triggerShortcut = DEFAULT_INPUT_SHORTCUT; + } + + if (this.#config.inputRule.transOpen) { + this.enable(); + } } - const apiSetting = - transApis.find((api) => api.apiSlug === apiSlug) || DEFAULT_API_SETTING; - if (triggerShortcut.length === 0) { - triggerShortcut = DEFAULT_INPUT_SHORTCUT; - triggerCount = 1; + /** + * 启用输入翻译功能 + */ + enable() { + if (this.#isEnabled || !this.#config.inputRule.transOpen) { + return; + } + + const { triggerCount, triggerTime } = this.#config.inputRule; + this.#unregisterShortcut = stepShortcutRegister( + this.#triggerShortcut, + this.#handleTranslate.bind(this), + triggerCount, + triggerTime + ); + + this.#isEnabled = true; + kissLog("Input Translator enabled."); } - stepShortcutRegister( - triggerShortcut, - async () => { - let node = document.activeElement; + /** + * 禁用输入翻译功能 + */ + disable() { + if (!this.#isEnabled) { + return; + } + if (this.#unregisterShortcut) { + this.#unregisterShortcut(); + this.#unregisterShortcut = null; + } + this.#isEnabled = false; + kissLog("Input Translator disabled."); + } - if (!node) { - return; - } + /** + * 切换启用/禁用状态 + */ + toggle() { + if (this.#isEnabled) { + this.disable(); + } else { + this.enable(); + } + } - while (node.shadowRoot) { - node = node.shadowRoot.activeElement; - } + /** + * 翻译核心逻辑 + * @private + */ + async #handleTranslate() { + let node = document.activeElement; + if (!node) return; - if (!isInputNode(node) && !isEditAbleNode(node)) { - return; - } + while (node.shadowRoot && node.shadowRoot.activeElement) { + node = node.shadowRoot.activeElement; + } - let initText = getNodeText(node); - if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) { - // todo: remove multiple char - initText = removeEndchar(initText, triggerShortcut[0], triggerCount); - } - if (!initText.trim()) { - return; - } + if (!isInputNode(node) && !isEditAbleNode(node)) { + return; + } - let text = initText; - if (transSign) { - const res = matchInputStr(text, transSign); - if (res) { - let lang = res[1]; - if (lang === "zh" || lang === "cn") { - lang = "zh-CN"; - } else if (lang === "tw" || lang === "hk") { - lang = "zh-TW"; - } - if (lang && OPT_LANGS_LIST.includes(lang)) { - toLang = lang; - } - text = res[2]; + const { apiSlug, transSign, triggerCount } = this.#config.inputRule; + let { fromLang, toLang } = this.#config.inputRule; + + let initText = getNodeText(node); + + if ( + this.#triggerShortcut.length === 1 && + this.#triggerShortcut[0].length === 1 + ) { + initText = removeEndchar( + initText, + this.#triggerShortcut[0], + triggerCount + ); + } + + if (!initText.trim()) return; + + let text = initText; + if (transSign) { + const res = matchInputStr(text, transSign); + if (res) { + let lang = res[1]; + if (lang === "zh" || lang === "cn") lang = "zh-CN"; + else if (lang === "tw" || lang === "hk") lang = "zh-TW"; + + if (lang && OPT_LANGS_LIST.includes(lang)) { + toLang = lang; } + text = res[2]; } + } - // console.log("input -->", text); + const apiSetting = + this.#config.transApis.find((api) => api.apiSlug === apiSlug) || + DEFAULT_API_SETTING; + const loadingId = "kiss-loading-" + genEventName(); - const loadingId = "kiss-" + genEventName(); - try { - addLoading(node, loadingId); + try { + addLoading(node, loadingId); - const [trText, isSame] = await apiTranslate({ - apiSlug, - text, - fromLang, - toLang, - apiSetting, - }); - if (!trText || isSame) { - return; - } + const [trText, isSame] = await apiTranslate({ + text, + fromLang, + toLang, + apiSlug, + apiSetting, + }); - if (isInputNode(node)) { - node.value = trText; - node.dispatchEvent( - new Event("input", { bubbles: true, cancelable: true }) - ); - return; - } + if (!trText || isSame) return; - selectContent(node); - await sleep(200); - - pasteContentEvent(node, trText); - await sleep(200); - - // todo: use includes? - if (getNodeText(node).startsWith(initText)) { - pasteContentCommand(node, trText); - await sleep(100); - } else { - collapseToEnd(node); - } - } catch (err) { - kissLog("translate input", err); - } finally { - removeLoading(node, loadingId); + if (isInputNode(node)) { + node.value = trText; + node.dispatchEvent( + new Event("input", { bubbles: true, cancelable: true }) + ); + } else { + replaceContentEditableText(node, trText); } - }, - triggerCount, - triggerTime - ); + } catch (err) { + kissLog("Translate input error:", err); + } finally { + removeLoading(loadingId); + } + } + + /** + * 更新配置 + */ + updateConfig({ inputRule, transApis }) { + const wasEnabled = this.#isEnabled; + if (wasEnabled) { + this.disable(); + } + + if (inputRule) { + this.#config.inputRule = inputRule; + } + if (transApis) { + this.#config.transApis = transApis; + } + + const { triggerShortcut: initialTriggerShortcut } = this.#config.inputRule; + this.#triggerShortcut = + initialTriggerShortcut && initialTriggerShortcut.length > 0 + ? initialTriggerShortcut + : DEFAULT_INPUT_SHORTCUT; + + if (wasEnabled) { + this.enable(); + } + } } diff --git a/src/libs/rules.js b/src/libs/rules.js index 5c52cd3..a334564 100644 --- a/src/libs/rules.js +++ b/src/libs/rules.js @@ -78,7 +78,6 @@ export const matchRule = async (href, { injectRules, subrulesList }) => { "hasShadowroot", "transTag", "transTitle", - "transSelected", // "detectRemote", // "fixerFunc", ].forEach((key) => { @@ -153,7 +152,6 @@ export const checkRules = (rules) => { // transTiming, transTag, transTitle, - transSelected, // detectRemote, // skipLangs, // fixerSelector, @@ -186,7 +184,6 @@ export const checkRules = (rules) => { // transTiming: matchValue([GLOBAL_KEY, ...OPT_TIMING_ALL], transTiming), transTag: matchValue([GLOBAL_KEY, "span", "font"], transTag), transTitle: matchValue([GLOBAL_KEY, "true", "false"], transTitle), - transSelected: matchValue([GLOBAL_KEY, "true", "false"], transSelected), // detectRemote: matchValue([GLOBAL_KEY, "true", "false"], detectRemote), // skipLangs: type(skipLangs) === "array" ? skipLangs : [], // fixerSelector: type(fixerSelector) === "string" ? fixerSelector : "", diff --git a/src/libs/tranbox.js b/src/libs/tranbox.js new file mode 100644 index 0000000..c39973a --- /dev/null +++ b/src/libs/tranbox.js @@ -0,0 +1,95 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import createCache from "@emotion/cache"; +import { CacheProvider } from "@emotion/react"; +import Slection from "../views/Selection"; +import { DEFAULT_TRANBOX_SETTING, APP_CONSTS } from "../config"; + +export class TransboxManager { + #container = null; + #reactRoot = null; + #shadowContainer = null; + #props = {}; + + constructor(initialProps = {}) { + this.#props = initialProps; + + const { tranboxSetting = DEFAULT_TRANBOX_SETTING } = this.#props; + if (tranboxSetting?.transOpen) { + this.enable(); + } + } + + isEnabled() { + return ( + !!this.#container && document.body.parentElement.contains(this.#container) + ); + } + + enable() { + if (!this.isEnabled()) { + this.#container = document.createElement("div"); + this.#container.setAttribute("id", APP_CONSTS.boxID); + this.#container.style.cssText = + "font-size: 0; width: 0; height: 0; border: 0; padding: 0; margin: 0;"; + document.body.parentElement.appendChild(this.#container); + + this.#shadowContainer = this.#container.attachShadow({ mode: "closed" }); + const emotionRoot = document.createElement("style"); + const shadowRootElement = document.createElement("div"); + shadowRootElement.classList.add(`${APP_CONSTS.boxID}_warpper`); + this.#shadowContainer.appendChild(emotionRoot); + this.#shadowContainer.appendChild(shadowRootElement); + const cache = createCache({ + key: APP_CONSTS.boxID, + prepend: true, + container: emotionRoot, + }); + + this.#reactRoot = ReactDOM.createRoot(shadowRootElement); + this.CacheProvider = ({ children }) => ( + {children} + ); + } + + const AppProvider = this.CacheProvider; + this.#reactRoot.render( + + + + + + ); + } + + disable() { + if (!this.isEnabled() || !this.#reactRoot) { + return; + } + this.#reactRoot.unmount(); + this.#container.remove(); + this.#container = null; + this.#reactRoot = null; + this.#shadowContainer = null; + this.CacheProvider = null; + } + + toggle() { + if (this.isEnabled()) { + this.disable(); + } else { + this.enable(); + } + } + + update(newProps) { + this.#props = { ...this.#props, ...newProps }; + if (this.isEnabled()) { + if (!this.#props.tranboxSetting?.transOpen) { + this.disable(); + } else { + this.enable(); + } + } + } +} diff --git a/src/libs/translator.js b/src/libs/translator.js index 2340cc4..19fd7a5 100644 --- a/src/libs/translator.js +++ b/src/libs/translator.js @@ -10,6 +10,14 @@ 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, } from "../config"; import interpreter from "./interpreter"; import { ShadowRootMonitor } from "./shadowroot"; @@ -25,6 +33,10 @@ import { genTextClass } from "./style"; import { loadingSvg } 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"; /** * @class Translator @@ -269,6 +281,10 @@ export class Translator { #textClass = {}; // 译文样式class #textSheet = ""; // 译文样式字典 + #isUserscript = false; + #transboxManager = null; // 划词翻译 + #inputTranslator = null; // 输入框翻译 + #observedNodes = new WeakSet(); // 存储所有被识别出的、可翻译的 DOM 节点单元 #translationNodes = new WeakMap(); // 存储所有插入到页面的译文节点 #viewNodes = new Set(); // 当前在可视范围内的单元 @@ -293,9 +309,10 @@ export class Translator { return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`; } - constructor(rule = {}, setting = {}) { + constructor(rule = {}, setting = {}, isUserscript) { this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting }; this.#rule = { ...Translator.DEFAULT_RULE, ...rule }; + this.#isUserscript = isUserscript; this.#eventName = genEventName(); this.#docInfo = { title: document.title, @@ -326,6 +343,19 @@ 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 { @@ -368,6 +398,43 @@ 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( Translator.PLACEHOLDER.startDelimiter @@ -671,14 +738,16 @@ export class Translator { // 开始/重新监控节点 #startObserveNode(node) { - if (this.#observedNodes.has(node)) { - // 已监控,但未处理状态,且在可视范围 - if (!this.#processedNodes.has(node) && this.#viewNodes.has(node)) { - this.#reIO(node); - } - } else { + // 未监控 + if (!this.#observedNodes.has(node)) { this.#observedNodes.add(node); this.#io.observe(node); + return; + } + + // 已监控,但未处理状态,且在可视范围 + if (!this.#processedNodes.has(node) && this.#viewNodes.has(node)) { + this.#reIO(node); } } @@ -1369,6 +1438,19 @@ 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/views/Options/Rules.js b/src/views/Options/Rules.js index 6e5a40c..cbd817e 100644 --- a/src/views/Options/Rules.js +++ b/src/views/Options/Rules.js @@ -112,7 +112,6 @@ function RuleFields({ rule, rules, setShow, setKeyword }) { // transTiming = OPT_TIMING_PAGESCROLL, transTag = DEFAULT_TRANS_TAG, transTitle = "false", - transSelected = "true", // detectRemote = "true", // skipLangs = [], // fixerSelector = "", @@ -337,22 +336,6 @@ function RuleFields({ rule, rules, setShow, setKeyword }) { - - - {GlobalItem} - {i18n("disable")} - {i18n("enable")} - - - {i18n("selected_translation_alert")} + { + updateTranbox({ transOpen: !transOpen }); + }} + /> + } + label={i18n("toggle_selection_translate")} + /> diff --git a/src/views/Popup/index.js b/src/views/Popup/index.js index 4bc4994..9576a5c 100644 --- a/src/views/Popup/index.js +++ b/src/views/Popup/index.js @@ -20,6 +20,9 @@ import { MSG_OPEN_OPTIONS, MSG_SAVE_RULE, MSG_COMMAND_SHORTCUTS, + MSG_TRANSBOX_TOGGLE, + MSG_MOUSEHOVER_TOGGLE, + MSG_TRANSINPUT_TOGGLE, OPT_LANGS_FROM, OPT_LANGS_TO, OPT_STYLE_ALL, @@ -35,9 +38,7 @@ import { parseUrlPattern } from "../../libs/utils"; export default function Popup({ setShowPopup, translator }) { const i18n = useI18n(); const [rule, setRule] = useState(translator?.rule); - const [transApis, setTransApis] = useState( - translator?.setting?.transApis || [] - ); + const [setting, setSetting] = useState(translator?.setting); const [commands, setCommands] = useState({}); const handleOpenSetting = () => { @@ -66,6 +67,66 @@ export default function Popup({ setShowPopup, translator }) { } }; + const handleTransboxToggle = async (e) => { + try { + setSetting((pre) => ({ + ...pre, + tranboxSetting: { ...pre.tranboxSetting, transOpen: e.target.checked }, + })); + + if (!translator) { + await sendTabMsg(MSG_TRANSBOX_TOGGLE); + } else { + translator.toggleTransbox(); + sendIframeMsg(MSG_TRANSBOX_TOGGLE); + } + } catch (err) { + kissLog("toggle transbox", err); + } + }; + + const handleMousehoverToggle = async (e) => { + try { + setSetting((pre) => ({ + ...pre, + mouseHoverSetting: { + ...pre.mouseHoverSetting, + useMouseHover: e.target.checked, + }, + })); + + if (!translator) { + await sendTabMsg(MSG_MOUSEHOVER_TOGGLE); + } else { + translator.toggleMouseHover(); + sendIframeMsg(MSG_MOUSEHOVER_TOGGLE); + } + } catch (err) { + kissLog("toggle mousehover", err); + } + }; + + const handleInputTransToggle = async (e) => { + try { + setSetting((pre) => ({ + ...pre, + inputRule: { + ...pre.inputRule, + transOpen: e.target.checked, + }, + })); + + if (!translator) { + await sendTabMsg(MSG_TRANSINPUT_TOGGLE); + } else { + translator.toggleInputTranslate(); + sendIframeMsg(MSG_TRANSINPUT_TOGGLE); + } + } catch (err) { + kissLog("toggle inputtrans", err); + } + }; + const handleChange = async (e) => { try { const { name, value } = e.target; @@ -121,7 +182,7 @@ export default function Popup({ setShowPopup, translator }) { const res = await sendTabMsg(MSG_TRANS_GETRULE); if (!res.error) { setRule(res.rule); - setTransApis(res.setting.transApis); + setSetting(res.setting); } } catch (err) { kissLog("query rule", err); @@ -155,15 +216,19 @@ export default function Popup({ setShowPopup, translator }) { const optApis = useMemo( () => - transApis + setting?.transApis .filter((api) => !api.isDisabled) .map((api) => ({ key: api.apiSlug, name: api.apiName || api.apiSlug, })), - [transApis] + [setting] ); + const tranboxEnabled = setting?.tranboxSetting.transOpen; + const mouseHoverEnabled = setting?.mouseHoverSetting.useMouseHover; + const inputTransEnabled = setting?.inputRule.transOpen; + if (!rule) { return ( @@ -195,7 +260,7 @@ export default function Popup({ setShowPopup, translator }) { } = rule; return ( - + {!translator && ( <>
@@ -275,6 +340,48 @@ export default function Popup({ setShowPopup, translator }) { label={i18n("richtext_alt")} /> + + + } + label={i18n("selection_translate")} + /> + + + + } + label={i18n("mousehover_translate")} + /> + + + + } + label={i18n("input_translate")} + /> +