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("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")}
+ />
+