From 943a9e86f0378f3168abcd9d4662d61d700d6f51 Mon Sep 17 00:00:00 2001 From: Gabe Date: Sun, 21 Sep 2025 19:51:57 +0800 Subject: [PATCH] feat: Restructured core logic to support automatic page scanning and rich text translation --- package.json | 1 + pnpm-lock.yaml | 83 ++ src/apis/trans.js | 2 +- src/common.js | 17 +- src/config/app.js | 4 + src/config/i18n.js | 122 ++- src/config/rules.js | 33 +- src/config/setting.js | 13 +- src/hooks/MouseHover.js | 19 + src/libs/injector.js | 8 +- src/libs/rules.js | 45 +- src/libs/shadowroot.js | 56 + src/libs/style.js | 102 ++ src/libs/svg.js | 42 +- src/libs/translator.js | 1751 ++++++++++++++++++++++--------- src/libs/utils.js | 15 +- src/views/Options/Apis.js | 7 +- src/views/Options/MouseHover.js | 57 + src/views/Options/Navigator.js | 7 + src/views/Options/Rules.js | 298 ++++-- src/views/Options/Setting.js | 4 +- src/views/Options/Tranbox.js | 2 + src/views/Options/index.js | 2 + src/views/Popup/index.js | 110 +- 24 files changed, 2095 insertions(+), 705 deletions(-) create mode 100644 src/hooks/MouseHover.js create mode 100644 src/libs/shadowroot.js create mode 100644 src/libs/style.js create mode 100644 src/views/Options/MouseHover.js diff --git a/package.json b/package.json index 17a3b6a..79072de 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "private": true, "dependencies": { "@emotion/cache": "^11.11.0", + "@emotion/css": "^11.13.5", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.15.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 219aba3..b7fb18a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@emotion/cache': specifier: ^11.11.0 version: 11.11.0 + '@emotion/css': + specifier: ^11.13.5 + version: 11.13.5 '@emotion/react': specifier: ^11.11.1 version: 11.11.1(@types/react@18.2.79)(react@18.2.0) @@ -963,18 +966,33 @@ packages: '@emotion/babel-plugin@11.11.0': resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + '@emotion/cache@11.11.0': resolution: {integrity: sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==} + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/css@11.13.5': + resolution: {integrity: sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==} + '@emotion/hash@0.9.1': resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + '@emotion/is-prop-valid@1.2.1': resolution: {integrity: sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==} '@emotion/memoize@0.8.1': resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + '@emotion/react@11.11.1': resolution: {integrity: sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==} peerDependencies: @@ -987,9 +1005,15 @@ packages: '@emotion/serialize@1.1.2': resolution: {integrity: sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==} + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + '@emotion/sheet@1.2.2': resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + '@emotion/styled@11.11.0': resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} peerDependencies: @@ -1000,6 +1024,9 @@ packages: '@types/react': optional: true + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + '@emotion/unitless@0.8.1': resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} @@ -1011,9 +1038,15 @@ packages: '@emotion/utils@1.2.1': resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==} + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + '@emotion/weak-memoize@0.3.1': resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7012,6 +7045,20 @@ snapshots: source-map: 0.5.7 stylis: 4.2.0 + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.24.3 + '@babel/runtime': 7.24.4 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + '@emotion/cache@11.11.0': dependencies: '@emotion/memoize': 0.8.1 @@ -7020,14 +7067,34 @@ snapshots: '@emotion/weak-memoize': 0.3.1 stylis: 4.2.0 + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/css@11.13.5': + dependencies: + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/hash@0.9.1': {} + '@emotion/hash@0.9.2': {} + '@emotion/is-prop-valid@1.2.1': dependencies: '@emotion/memoize': 0.8.1 '@emotion/memoize@0.8.1': {} + '@emotion/memoize@0.9.0': {} + '@emotion/react@11.11.1(@types/react@18.2.79)(react@18.2.0)': dependencies: '@babel/runtime': 7.22.15 @@ -7050,8 +7117,18 @@ snapshots: '@emotion/utils': 1.2.1 csstype: 3.1.2 + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.1.3 + '@emotion/sheet@1.2.2': {} + '@emotion/sheet@1.4.0': {} + '@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react@18.2.0)': dependencies: '@babel/runtime': 7.22.15 @@ -7065,6 +7142,8 @@ snapshots: optionalDependencies: '@types/react': 18.2.79 + '@emotion/unitless@0.10.0': {} + '@emotion/unitless@0.8.1': {} '@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0)': @@ -7073,8 +7152,12 @@ snapshots: '@emotion/utils@1.2.1': {} + '@emotion/utils@1.4.2': {} + '@emotion/weak-memoize@0.3.1': {} + '@emotion/weak-memoize@0.4.0': {} + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': dependencies: eslint: 8.57.0 diff --git a/src/apis/trans.js b/src/apis/trans.js index 6a3faf2..a60e91f 100644 --- a/src/apis/trans.js +++ b/src/apis/trans.js @@ -812,7 +812,7 @@ export const parseTransRes = ( case OPT_TRANS_TENCENT: return res?.auto_translation?.map((text) => [text, res?.src_lang]); case OPT_TRANS_VOLCENGINE: - return new Map([[0, [res?.translation, res?.detected_language]]]); + return [[res?.translation, res?.detected_language]]; case OPT_TRANS_OPENAI: case OPT_TRANS_OPENAI_2: case OPT_TRANS_OPENAI_3: diff --git a/src/common.js b/src/common.js index 1206115..85dd6bc 100644 --- a/src/common.js +++ b/src/common.js @@ -9,7 +9,7 @@ import { MSG_TRANS_GETRULE, MSG_TRANS_PUTRULE, MSG_OPEN_TRANBOX, - APP_LCNAME, + APP_CONSTS, DEFAULT_TRANBOX_SETTING, } from "./config"; import { getFabWithDefault, getSettingWithDefault } from "./libs/storage"; @@ -106,7 +106,7 @@ function runIframe(translator) { async function showFab(translator) { const fab = await getFabWithDefault(); const $action = document.createElement("div"); - $action.setAttribute("id", APP_LCNAME); + $action.setAttribute("id", APP_CONSTS.fabID); $action.style.fontSize = "0"; $action.style.width = "0"; $action.style.height = "0"; @@ -114,10 +114,11 @@ async function showFab(translator) { const shadowContainer = $action.attachShadow({ mode: "closed" }); const emotionRoot = document.createElement("style"); const shadowRootElement = document.createElement("div"); + shadowRootElement.classList.add(`${APP_CONSTS.fabID}_warpper`); shadowContainer.appendChild(emotionRoot); shadowContainer.appendChild(shadowRootElement); const cache = createCache({ - key: APP_LCNAME, + key: APP_CONSTS.fabID, prepend: true, container: emotionRoot, }); @@ -151,7 +152,7 @@ function showTransbox( } const $tranbox = document.createElement("div"); - $tranbox.setAttribute("id", "kiss-transbox"); + $tranbox.setAttribute("id", APP_CONSTS.boxID); $tranbox.style.fontSize = "0"; $tranbox.style.width = "0"; $tranbox.style.height = "0"; @@ -159,12 +160,14 @@ function showTransbox( const shadowContainer = $tranbox.attachShadow({ mode: "closed" }); const emotionRoot = document.createElement("style"); const shadowRootElement = document.createElement("div"); - shadowRootElement.classList.add(`KT-transbox`); - shadowRootElement.classList.add(`KT-transbox_${darkMode ? "dark" : "light"}`); + 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: "kiss-transbox", + key: APP_CONSTS.boxID, prepend: true, container: emotionRoot, }); diff --git a/src/config/app.js b/src/config/app.js index 3b993dd..234322e 100644 --- a/src/config/app.js +++ b/src/config/app.js @@ -2,6 +2,10 @@ export const APP_NAME = process.env.REACT_APP_NAME.trim() .split(/\s+/) .join("-"); export const APP_LCNAME = APP_NAME.toLowerCase(); +export const APP_CONSTS = { + fabID: `${APP_LCNAME}-fab`, + boxID: `${APP_LCNAME}-box`, +}; export const THEME_LIGHT = "light"; export const THEME_DARK = "dark"; diff --git a/src/config/i18n.js b/src/config/i18n.js index f867dce..62f1e5e 100644 --- a/src/config/i18n.js +++ b/src/config/i18n.js @@ -243,9 +243,9 @@ export const I18N = { zh_TW: `每次請求間隔時間 (0-5000ms)`, }, translate_interval: { - zh: `重新翻译间隔时间 (100-5000ms)`, - en: `Retranslation Interval (100-5000ms)`, - zh_TW: `重新翻譯間隔時間 (100-5000ms)`, + zh: `翻译间隔时间 (10-2000ms)`, + en: `Translation Interval (10-2000ms)`, + zh_TW: `翻譯間隔時間 (10-2000ms)`, }, http_timeout: { zh: `请求超时时间 (5000-60000ms)`, @@ -543,9 +543,9 @@ export const I18N = { zh_TW: `1. 支援星號 (*) 萬用字元。2. 多個 URL 請以換行或英文逗號「,」分隔。`, }, selector_helper: { - zh: `1、遵循CSS选择器语法。2、多个CSS选择器之间用“;”隔开。3、“shadow root”选择器和内部选择器用“>>>”隔开。`, - en: `1. Follow CSS selector syntax. 2. Separate multiple CSS selectors with ";". 3. The "shadow root" selector and the internal selector are separated by ">>>".`, - zh_TW: `1. 遵循 CSS 選擇器語法。2. 多個 CSS 選擇器以「;」分隔。3.「shadow root」與內部選擇器以「>>>」分隔。`, + zh: `1、需要翻译的目标元素。2、开启自动扫描页面后,本设置无效。3、遵循CSS选择器语法。`, + en: `1. The target element to be translated. 2. This setting is invalid when automatic page scanning is enabled. 3. Follow the CSS selector syntax.`, + zh_TW: `1、需要翻譯的目標元素。 2.開啟自動掃描頁面後,本設定無效。 3.遵循CSS選擇器語法。`, }, translate_switch: { zh: `开启翻译`, @@ -573,9 +573,29 @@ export const I18N = { zh_TW: `保留元素選擇器`, }, keep_selector_helper: { - zh: `1、遵循CSS选择器语法。`, - en: `1. Follow CSS selector syntax.`, - zh_TW: `1. 遵循 CSS 選擇器語法。`, + zh: `1、目标元素下面需要原样保留的子节点。2、遵循CSS选择器语法。`, + en: `1. The child nodes under the target element need to remain intact. 2. Follow the CSS selector syntax.`, + zh_TW: `1. 目標元素下的子節點需要保持原樣。 2. 遵循 CSS 選擇器語法。`, + }, + root_selector: { + zh: `根节点选择器`, + en: `Root node selector`, + zh_TW: `根節點選擇器`, + }, + root_selector_helper: { + zh: `1、用于缩小页面翻译范围。2、遵循CSS选择器语法。`, + en: `1. Used to narrow the translation scope of the page. 2. Follow the CSS selector syntax.`, + zh_TW: `1.用於縮小頁面翻譯範圍。 2、遵循CSS選擇器語法。`, + }, + ignore_selector: { + zh: `不翻译节点选择器`, + en: `Ignore node selectors`, + zh_TW: `不翻譯節點選擇器`, + }, + ignore_selector_helper: { + zh: `1、需要忽略的节点。2、遵循CSS选择器语法。`, + en: `1. Nodes to be ignored. 2. Follow CSS selector syntax.`, + zh_TW: `1、需要忽略的節點。 2、遵循CSS選擇器語法。`, }, terms: { zh: `专业术语`, @@ -608,9 +628,9 @@ export const I18N = { zh_TW: `注入 JS`, }, inject_js_helper: { - zh: `1、开启翻译时注入运行,关闭翻译时移除。2、随着页面变化,可能会多次注入运行。`, - en: `1. Inject and run when translation is turned on, and removed when translation is turned off. 2. As the page changes, it may be injected and run multiple times.`, - zh_TW: `1. 開啟翻譯時注入並執行,關閉翻譯時移除。2. 隨頁面變化,可能多次注入與執行。`, + zh: `初始化时注入运行,一个页面仅运行一次。`, + en: `Injected and run at initialization, and only run once per page.`, + zh_TW: `初始化時注入運行,一個頁面僅運行一次。`, }, inject_css: { zh: `注入CSS`, @@ -618,14 +638,9 @@ export const I18N = { zh_TW: `注入 CSS`, }, inject_css_helper: { - zh: `开启翻译时注入,关闭翻译时将移除。`, - en: `Injected when translation is enabled and removed when translation is disabled.`, - zh_TW: `開啟翻譯時注入,關閉翻譯時會移除。`, - }, - root_selector: { - zh: `根选择器`, - en: `Root Selector`, - zh_TW: `根選擇器`, + zh: `初始化时注入运行,一个页面仅运行一次。`, + en: `Injected and run at initialization, and only run once per page.`, + zh_TW: `初始化時注入運行,一個頁面僅運行一次。`, }, fixer_function: { zh: `修复函数`, @@ -1184,9 +1199,9 @@ export const I18N = { zh_TW: `翻譯開始 Hook`, }, translate_start_hook_helper: { - zh: `翻译开始时运行,入参为: 翻译节点,原文文本,返回:待译文本。`, - en: `Run when translation starts, the input parameters are: translation node, original text, and returns: text to be translated.`, - zh_TW: `翻譯開始時執行,入參為:翻譯節點、原文文字,回傳:待譯文本。`, + zh: `翻译前时运行,入参为: 翻译节点列表。`, + en: `Run before translation, input parameters are: translation node list.`, + zh_TW: `翻譯前時運行,入參為: 翻譯節點清單。`, }, translate_end_hook: { zh: `翻译完成钩子函数`, @@ -1194,9 +1209,9 @@ export const I18N = { zh_TW: `翻譯完成 Hook`, }, translate_end_hook_helper: { - zh: `翻译完成时运行,入参为: 翻译节点,译文文本,原文文本,保留元素、术语列表,返回:译文文本。`, - en: `Run when the translation is completed, the input parameters are: translation node, translation text, original text, retained elements, and returns: translation text.`, - zh_TW: `翻譯完成時執行,入參為:翻譯節點、譯文文字、原文文字、保留元素,返回:譯文文本。`, + zh: `翻译完成时运行,入参为: 翻译节点列表。`, + en: `Run when translation is complete, input parameters are: translation node list.`, + zh_TW: `翻譯完成時運行,入參為: 翻譯節點清單。`, }, translate_remove_hook: { zh: `翻译移除钩子函数`, @@ -1258,4 +1273,59 @@ export const I18N = { en: `Number of context sessions(1-20)`, zh_TW: `上下文會話數量(1-20)`, }, + auto_scan_page: { + zh: `自动扫描页面`, + en: `Auto scan page`, + zh_TW: `自動掃描頁面`, + }, + has_rich_text: { + zh: `启用富文本翻译`, + en: `Enable rich text translation`, + zh_TW: `啟用富文本翻譯`, + }, + has_shadowroot: { + zh: `扫描Shadowroot`, + en: `Scan Shadowroot`, + zh_TW: `掃描Shadowroot`, + }, + mousehover_translate: { + zh: `鼠标悬停翻译`, + en: `Mouseover Translation`, + zh_TW: `滑鼠懸停翻譯`, + }, + use_mousehover_translation: { + zh: `启用鼠标悬停翻译`, + en: `Enable mouseover translation`, + zh_TW: `啟用滑鼠懸停翻譯`, + }, + selected_translation_alert: { + zh: `划词翻译的开启和关闭请到“规则设置”里面设置。`, + en: `To turn selected translation on or off, please go to "Rule Settings".`, + zh_TW: `劃詞翻譯的開啟和關閉請到「規則設定」裡面設定。`, + }, + mousehover_key_help: { + zh: `默认为“ControlLeft”`, + en: `Defaults is "ControlLeft"`, + zh_TW: `預設為“ControlLeft”`, + }, + autoscan_alt: { + zh: `自动扫描`, + en: `Auto Scan`, + zh_TW: `自動掃描`, + }, + shadowroot_alt: { + zh: `ShadowRoot`, + en: `ShadowRoot`, + zh_TW: `ShadowRoot`, + }, + richtext_alt: { + zh: `富文本`, + en: `Rich Text`, + zh_TW: `富文本`, + }, + transonly_alt: { + zh: `隐藏原文`, + en: `Hide Original`, + zh_TW: `隱藏原文`, + }, }; diff --git a/src/config/rules.js b/src/config/rules.js index a34bd28..b31ba7e 100644 --- a/src/config/rules.js +++ b/src/config/rules.js @@ -58,7 +58,10 @@ export const OPT_TIMING_ALL = [ OPT_TIMING_ALT, ]; -export const DEFAULT_SELECTOR = `:is(li, p, h1, h2, h3, h4, h5, h6, dd, blockquote, .kiss-p)`; +export const DEFAULT_SELECTOR = + "h1, h2, h3, h4, h5, h6, li, p, dd, blockquote, figcaption, label, legend"; +export const DEFAULT_IGNORE_SELECTOR = + "button, code, footer, form, header, mark, nav, pre"; export const DEFAULT_KEEP_SELECTOR = `code, img, svg, pre`; export const DEFAULT_RULE = { pattern: "", // 匹配网址 @@ -77,17 +80,22 @@ export const DEFAULT_RULE = { injectJs: "", // 注入JS injectCss: "", // 注入CSS transOnly: GLOBAL_KEY, // 是否仅显示译文 - transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译 + // transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译 (暂时作废) transTag: GLOBAL_KEY, // 译文元素标签 transTitle: GLOBAL_KEY, // 是否同时翻译页面标题 transSelected: GLOBAL_KEY, // 是否启用划词翻译 detectRemote: GLOBAL_KEY, // 是否使用远程语言检测 skipLangs: [], // 不翻译的语言 - fixerSelector: "", // 修复函数选择器 - fixerFunc: GLOBAL_KEY, // 修复函数 + // fixerSelector: "", // 修复函数选择器 (暂时作废) + // fixerFunc: GLOBAL_KEY, // 修复函数 (暂时作废) transStartHook: "", // 钩子函数 transEndHook: "", // 钩子函数 - transRemoveHook: "", // 钩子函数 + // transRemoveHook: "", // 钩子函数 (暂时作废) + autoScan: GLOBAL_KEY, // 是否自动识别文本节点 + hasRichText: GLOBAL_KEY, // 是否启用富文本翻译 + hasShadowroot: GLOBAL_KEY, // 是否包含shadowroot + rootsSelector: "", // 翻译范围选择器 + ignoreSelector: "", // 不翻译的选择器 }; // 全局规则 @@ -99,7 +107,7 @@ export const GLOBLA_RULE = { translator: OPT_TRANS_MICROSOFT, // 翻译服务 fromLang: "auto", // 源语言 toLang: "zh-CN", // 目标语言 - textStyle: OPT_STYLE_DASHLINE, // 译文样式 + textStyle: OPT_STYLE_NONE, // 译文样式 transOpen: "false", // 开启翻译 bgColor: "", // 译文颜色 textDiyStyle: "", // 自定义译文样式 @@ -108,17 +116,22 @@ export const GLOBLA_RULE = { injectJs: "", // 注入JS injectCss: "", // 注入CSS transOnly: "false", // 是否仅显示译文 - transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译 + // transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译 (暂时作废) transTag: DEFAULT_TRANS_TAG, // 译文元素标签 transTitle: "false", // 是否同时翻译页面标题 transSelected: "true", // 是否启用划词翻译 detectRemote: "false", // 是否使用远程语言检测 skipLangs: [], // 不翻译的语言 - fixerSelector: "", // 修复函数选择器 - fixerFunc: "-", // 修复函数 + // fixerSelector: "", // 修复函数选择器 (暂时作废) + // fixerFunc: "-", // 修复函数 (暂时作废) transStartHook: "", // 钩子函数 transEndHook: "", // 钩子函数 - transRemoveHook: "", // 钩子函数 + // transRemoveHook: "", // 钩子函数 (暂时作废) + autoScan: "true", // 是否自动识别文本节点 + hasRichText: "true", // 是否启用富文本翻译 + hasShadowroot: "false", // 是否包含shadowroot + rootsSelector: "body", // 翻译范围选择器 + ignoreSelector: DEFAULT_IGNORE_SELECTOR, // 不翻译的选择器 }; export const DEFAULT_RULES = [GLOBLA_RULE]; diff --git a/src/config/setting.js b/src/config/setting.js index e214e32..698e71c 100644 --- a/src/config/setting.js +++ b/src/config/setting.js @@ -18,8 +18,8 @@ export const DEFAULT_SHORTCUTS = { [OPT_SHORTCUT_SETTING]: ["AltLeft", "KeyO"], }; -export const TRANS_MIN_LENGTH = 5; // 最短翻译长度 -export const TRANS_MAX_LENGTH = 10000; // 最长翻译长度 +export const TRANS_MIN_LENGTH = 2; // 最短翻译长度 +export const TRANS_MAX_LENGTH = 100000; // 最长翻译长度 export const TRANS_NEWLINE_LENGTH = 20; // 换行字符数 export const DEFAULT_BLACKLIST = [ "https://fishjar.github.io/kiss-translator/options.html", @@ -108,6 +108,12 @@ export const DEFAULT_SUBRULES_LIST = [ }, ]; +export const DEFAULT__MOUSEHOVER_KEY = ["ControlLeft"]; +export const DEFAULT_MOUSE_HOVER_SETTING = { + useMouseHover: true, // 是否启用鼠标悬停翻译 + mouseHoverKey: DEFAULT__MOUSEHOVER_KEY, // 鼠标悬停翻译组合键 +}; + export const DEFAULT_SETTING = { darkMode: false, // 深色模式 uiLang: "en", // 界面语言 @@ -137,6 +143,7 @@ export const DEFAULT_SETTING = { blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单 csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单 // disableLangs: [], // 不翻译的语言(移至rule,作废) - transInterval: 500, // 翻译间隔时间 + transInterval: 200, // 翻译等待时间 langDetector: OPT_TRANS_MICROSOFT, // 远程语言识别服务 + mouseHoverSetting: DEFAULT_MOUSE_HOVER_SETTING, // 鼠标悬停翻译 }; diff --git a/src/hooks/MouseHover.js b/src/hooks/MouseHover.js new file mode 100644 index 0000000..a2f682b --- /dev/null +++ b/src/hooks/MouseHover.js @@ -0,0 +1,19 @@ +import { useCallback } from "react"; +import { DEFAULT_MOUSE_HOVER_SETTING } from "../config"; +import { useSetting } from "./Setting"; + +export function useMouseHoverSetting() { + const { setting, updateSetting } = useSetting(); + const mouseHoverSetting = + setting?.mouseHoverSetting || DEFAULT_MOUSE_HOVER_SETTING; + + const updateMouseHoverSetting = useCallback( + async (obj) => { + Object.assign(mouseHoverSetting, obj); + await updateSetting({ mouseHoverSetting }); + }, + [mouseHoverSetting, updateSetting] + ); + + return { mouseHoverSetting, updateMouseHoverSetting }; +} diff --git a/src/libs/injector.js b/src/libs/injector.js index ea84ca7..aa5f87c 100644 --- a/src/libs/injector.js +++ b/src/libs/injector.js @@ -1,7 +1,7 @@ // Function to inject inline JavaScript code export const injectInlineJs = (code) => { const el = document.createElement("script"); - el.setAttribute("data-source", "KISS-Calendar injectInlineJs"); + el.setAttribute("data-source", "kiss-inject injectInlineJs"); el.setAttribute("type", "text/javascript"); el.textContent = code; document.body?.appendChild(el); @@ -10,7 +10,7 @@ export const injectInlineJs = (code) => { // Function to inject external JavaScript file export const injectExternalJs = (src) => { const el = document.createElement("script"); - el.setAttribute("data-source", "KISS-Calendar injectExternalJs"); + el.setAttribute("data-source", "kiss-inject injectExternalJs"); el.setAttribute("type", "text/javascript"); el.setAttribute("src", src); document.body?.appendChild(el); @@ -19,7 +19,7 @@ export const injectExternalJs = (src) => { // Function to inject internal CSS code export const injectInternalCss = (styles) => { const el = document.createElement("style"); - el.setAttribute("data-source", "KISS-Calendar injectInternalCss"); + el.setAttribute("data-source", "kiss-inject injectInternalCss"); el.textContent = styles; document.head?.appendChild(el); }; @@ -27,7 +27,7 @@ export const injectInternalCss = (styles) => { // Function to inject external CSS file export const injectExternalCss = (href) => { const el = document.createElement("link"); - el.setAttribute("data-source", "KISS-Calendar injectExternalCss"); + el.setAttribute("data-source", "kiss-inject injectExternalCss"); el.setAttribute("rel", "stylesheet"); el.setAttribute("type", "text/css"); el.setAttribute("href", href); diff --git a/src/libs/rules.js b/src/libs/rules.js index af9be8e..3bac69d 100644 --- a/src/libs/rules.js +++ b/src/libs/rules.js @@ -6,13 +6,13 @@ import { OPT_STYLE_ALL, OPT_LANGS_FROM, OPT_LANGS_TO, - OPT_TIMING_ALL, + // OPT_TIMING_ALL, GLOBLA_RULE, } from "../config"; import { loadOrFetchSubRules } from "./subRules"; import { getRulesWithDefault, setRules } from "./storage"; import { trySyncRules } from "./sync"; -import { FIXER_ALL } from "./webfix"; +// import { FIXER_ALL } from "./webfix"; import { kissLog } from "./log"; /** @@ -68,15 +68,17 @@ export const matchRule = async ( [ "selector", "keepSelector", + "rootsSelector", + "ignoreSelector", "terms", "selectStyle", "parentStyle", "injectJs", "injectCss", - "fixerSelector", + // "fixerSelector", "transStartHook", "transEndHook", - "transRemoveHook", + // "transRemoveHook", ].forEach((key) => { if (!rule[key]?.trim()) { rule[key] = globalRule[key]; @@ -89,12 +91,15 @@ export const matchRule = async ( "toLang", "transOpen", "transOnly", - "transTiming", + // "transTiming", + "autoScan", + "hasRichText", + "hasShadowroot", "transTag", "transTitle", "transSelected", "detectRemote", - "fixerFunc", + // "fixerFunc", ].forEach((key) => { if (rule[key] === undefined || rule[key] === GLOBAL_KEY) { rule[key] = globalRule[key]; @@ -146,6 +151,8 @@ export const checkRules = (rules) => { pattern, selector, keepSelector, + rootsSelector, + ignoreSelector, terms, selectStyle, parentStyle, @@ -159,21 +166,26 @@ export const checkRules = (rules) => { bgColor, textDiyStyle, transOnly, - transTiming, + autoScan, + hasRichText, + hasShadowroot, + // transTiming, transTag, transTitle, transSelected, detectRemote, skipLangs, - fixerSelector, - fixerFunc, + // fixerSelector, + // fixerFunc, transStartHook, transEndHook, - transRemoveHook, + // transRemoveHook, }) => ({ pattern: pattern.trim(), selector: type(selector) === "string" ? selector : "", keepSelector: type(keepSelector) === "string" ? keepSelector : "", + rootsSelector: type(rootsSelector) === "string" ? rootsSelector : "", + ignoreSelector: type(ignoreSelector) === "string" ? ignoreSelector : "", terms: type(terms) === "string" ? terms : "", selectStyle: type(selectStyle) === "string" ? selectStyle : "", parentStyle: type(parentStyle) === "string" ? parentStyle : "", @@ -187,18 +199,21 @@ export const checkRules = (rules) => { textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle), transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen), transOnly: matchValue([GLOBAL_KEY, "true", "false"], transOnly), - transTiming: matchValue([GLOBAL_KEY, ...OPT_TIMING_ALL], transTiming), + autoScan: matchValue([GLOBAL_KEY, "true", "false"], autoScan), + hasRichText: matchValue([GLOBAL_KEY, "true", "false"], hasRichText), + hasShadowroot: matchValue([GLOBAL_KEY, "true", "false"], hasShadowroot), + // 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 : "", + // fixerSelector: type(fixerSelector) === "string" ? fixerSelector : "", transStartHook: type(transStartHook) === "string" ? transStartHook : "", transEndHook: type(transEndHook) === "string" ? transEndHook : "", - transRemoveHook: - type(transRemoveHook) === "string" ? transRemoveHook : "", - fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc), + // transRemoveHook: + // type(transRemoveHook) === "string" ? transRemoveHook : "", + // fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc), }) ); diff --git a/src/libs/shadowroot.js b/src/libs/shadowroot.js new file mode 100644 index 0000000..22461c7 --- /dev/null +++ b/src/libs/shadowroot.js @@ -0,0 +1,56 @@ +import { kissLog } from "./log"; + +/** + * @class ShadowRootMonitor + * @description 通过覆写 Element.prototype.attachShadow 来监控页面上所有新创建的 Shadow DOM + */ +export class ShadowRootMonitor { + /** + * @param {function(ShadowRoot): void} callback - 当一个新的 shadowRoot 被创建时调用的回调函数。 + */ + constructor(callback) { + if (typeof callback !== "function") { + throw new Error("Callback must be a function."); + } + + this.callback = callback; + this.isMonitoring = false; + this.originalAttachShadow = Element.prototype.attachShadow; + } + + /** + * 开始监控 shadowRoot 的创建。 + */ + start() { + if (this.isMonitoring) { + return; + } + const monitorInstance = this; + + Element.prototype.attachShadow = function (...args) { + const shadowRoot = monitorInstance.originalAttachShadow.apply(this, args); + if (shadowRoot) { + try { + monitorInstance.callback(shadowRoot); + } catch (error) { + kissLog(error, "Error in ShadowRootMonitor callback"); + } + } + return shadowRoot; + }; + + this.isMonitoring = true; + } + + /** + * 停止监控,并恢复原始的 attachShadow 方法。 + */ + stop() { + if (!this.isMonitoring) { + return; + } + + Element.prototype.attachShadow = this.originalAttachShadow; + this.isMonitoring = false; + } +} diff --git a/src/libs/style.js b/src/libs/style.js new file mode 100644 index 0000000..c5d9e92 --- /dev/null +++ b/src/libs/style.js @@ -0,0 +1,102 @@ +import { css } from "@emotion/css"; +import { + OPT_STYLE_NONE, + OPT_STYLE_LINE, + OPT_STYLE_DOTLINE, + OPT_STYLE_DASHLINE, + OPT_STYLE_WAVYLINE, + OPT_STYLE_DASHBOX, + OPT_STYLE_FUZZY, + OPT_STYLE_HIGHLIGHT, + OPT_STYLE_BLOCKQUOTE, + OPT_STYLE_DIY, + DEFAULT_COLOR, +} from "../config"; + +const genLineStyle = (style, color) => ` + opacity: 0.6; + -webkit-opacity: 0.6; + text-decoration-line: underline; + text-decoration-style: ${style}; + text-decoration-color: ${color}; + text-decoration-thickness: 2px; + text-underline-offset: 0.3em; + -webkit-text-decoration-line: underline; + -webkit-text-decoration-style: ${style}; + -webkit-text-decoration-color: ${color}; + -webkit-text-decoration-thickness: 2px; + -webkit-text-underline-offset: 0.3em; + &:hover { + opacity: 1; + -webkit-opacity: 1; + } +`; + +const genStyles = ({ textDiyStyle, bgColor = DEFAULT_COLOR }) => ({ + // 无样式 + [OPT_STYLE_NONE]: ``, + // 下划线 + [OPT_STYLE_LINE]: genLineStyle("solid", bgColor), + // 点状线 + [OPT_STYLE_DOTLINE]: genLineStyle("dotted", bgColor), + // 虚线 + [OPT_STYLE_DASHLINE]: genLineStyle("dashed", bgColor), + // 波浪线 + [OPT_STYLE_WAVYLINE]: genLineStyle("wavy", bgColor), + // 虚线框 + [OPT_STYLE_DASHBOX]: ` + color: ${bgColor || DEFAULT_COLOR}; + border: 1px dashed ${bgColor || DEFAULT_COLOR}; + background: transparent; + display: block; + padding: 0.2em 0.5em; + box-sizing: border-box; + `, + // 模糊 + [OPT_STYLE_FUZZY]: ` + filter: blur(0.2em); + -webkit-filter: blur(0.2em); + &:hover { + filter: none; + -webkit-filter: none; + } + `, + // 高亮 + [OPT_STYLE_HIGHLIGHT]: ` + color: #fff; + background-color: ${bgColor || DEFAULT_COLOR}; + `, + // 引用 + [OPT_STYLE_BLOCKQUOTE]: ` + opacity: 0.6; + -webkit-opacity: 0.6; + display: block; + padding: 0 0.75em; + border-left: 0.25em solid ${bgColor || DEFAULT_COLOR}; + &:hover { + opacity: 1; + -webkit-opacity: 1; + } + `, + // 自定义 + [OPT_STYLE_DIY]: textDiyStyle, +}); + +export const genTextClass = ({ textDiyStyle, bgColor = DEFAULT_COLOR }) => { + const styles = genStyles({ textDiyStyle, bgColor }); + const textClass = {}; + let textStyles = ""; + Object.entries(styles).forEach(([k, v]) => { + textClass[k] = css` + ${v} + `; + }); + Object.entries(styles).forEach(([k, v]) => { + textStyles += ` + .${textClass[k]} { + ${v} + } + `; + }); + return [textClass, textStyles]; +}; diff --git a/src/libs/svg.js b/src/libs/svg.js index b9545eb..536facf 100644 --- a/src/libs/svg.js +++ b/src/libs/svg.js @@ -1,34 +1,14 @@ export const loadingSvg = ` - - - - - - - - - - + + + + + + + + + + `; diff --git a/src/libs/translator.js b/src/libs/translator.js index c5265f3..d2c5231 100644 --- a/src/libs/translator.js +++ b/src/libs/translator.js @@ -1,296 +1,1236 @@ -import { createRoot } from "react-dom/client"; import { + APP_NAME, APP_LCNAME, - TRANS_MIN_LENGTH, - TRANS_MAX_LENGTH, - MSG_TRANS_CURRULE, + APP_CONSTS, MSG_INJECT_JS, MSG_INJECT_CSS, - OPT_STYLE_DASHLINE, OPT_STYLE_FUZZY, - SHADOW_KEY, - OPT_TIMING_PAGESCROLL, - OPT_TIMING_PAGEOPEN, - OPT_TIMING_MOUSEOVER, + GLOBLA_RULE, + DEFAULT_SETTING, DEFAULT_TRANS_APIS, + DEFAULT__MOUSEHOVER_KEY, + OPT_STYLE_NONE, } from "../config"; -import Content from "../views/Content"; +import interpreter from "./interpreter"; +import { ShadowRootMonitor } from "./shadowroot"; import { clearFetchPool } from "./pool"; -import { debounce, genEventName, getHtmlText } from "./utils"; -import { runFixer } from "./webfix"; +import { debounce, scheduleIdle, genEventName } from "./utils"; import { apiTranslate } from "../apis"; import { sendBgMsg } from "./msg"; import { isExt } from "./client"; import { injectInlineJs, injectInternalCss } from "./injector"; import { kissLog } from "./log"; -import interpreter from "./interpreter"; import { clearAllBatchQueue } from "./batchQueue"; +import { genTextClass } from "./style"; +import { loadingSvg } from "./svg"; +import { shortcutRegister } from "./shortcut"; +import { tryDetectLang } from "."; /** - * 翻译类 + * @class Translator + * @description 翻译核心逻辑封装 */ export class Translator { - _rule = {}; - _setting = {}; - _rootNodes = new Set(); - _tranNodes = new Map(); - _skipNodeNames = [ - APP_LCNAME, - "style", - "svg", - "img", - "audio", - "video", - "textarea", - "input", - "button", - "select", - "option", - "head", - "script", - "iframe", + static displayCache = new WeakMap(); + static TAGS = { + BREAK_LINE: new Set(["BR", "WBR"]), + BLOCK: new Set([ + "ADDRESS", + "ARTICLE", + "ASIDE", + "BLOCKQUOTE", + "CANVAS", + "DD", + "DIV", + "DL", + "DT", + "FIELDSET", + "FIGCAPTION", + "FIGURE", + "FOOTER", + "FORM", + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "HEADER", + "HR", + "LI", + "MAIN", + "NAV", + "NOSCRIPT", + "OL", + "P", + "PRE", + "SECTION", + "TABLE", + "TFOOT", + "UL", + "VIDEO", + ]), + INLINE: new Set([ + "A", + "ABBR", + "ACRONYM", + "B", + "BDO", + "BIG", + "BR", + "BUTTON", + "CITE", + "CODE", + "DFN", + "DEL", + "FONT", + "EM", + "I", + "IMG", + "INPUT", + "INS", + "KBD", + "LABEL", + "MAP", + "MARK", + "OBJECT", + "OUTPUT", + "Q", + "SAMP", + "SCRIPT", + "SELECT", + "SMALL", + "SPAN", + "STRONG", + "SUB", + "SUP", + "TEXTAREA", + "TIME", + "TT", + "U", + "VAR", + ]), + REPLACE: new Set([ + "ABBR", + "CODE", + "DFN", + "IMG", + "KBD", + "OUTPUT", + "SAMP", + "SUB", + "SUP", + "SVG", + "TIME", + "VAR", + ]), + WARP: new Set([ + "A", + "B", + "BDO", + "BDI", + "BIG", + "CITE", + "DEL", + "EM", + "FONT", + "I", + "INS", + "MARK", + "Q", + "S", + "SMALL", + "SPAN", + "STRONG", + "U", + ]), + }; + + // 译文相关class + static KISS_CLASS = { + warpper: `${APP_LCNAME}-wrapper`, + inner: `${APP_LCNAME}-inner`, + term: `${APP_LCNAME}-term`, + }; + + // 内置跳过翻译文本 + // todo: 验证有效性 + static BUILTIN_SKIP_PATTERNS = [ + // 1. URL (覆盖 http, https, ftp, file 协议) + /^(?:(?:https?|ftp|file):\/\/|www\.)[^\s/$.?#].[^\s]*$/i, + + // 2. 邮箱地址 + /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, + + // 3. 文件路径 (为 Unix 和 Windows 做了简化) + /^(?:[a-zA-Z]:\\|\/|\\)(?:[\w\-. ]+\/|[\w\-. ]+\\)*[\w\-. ]*\.?[\w\-. ]*$/, + + // 4. UUID (通用唯一标识符) + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/, + + // 5. 纯数字字符串 (整数, 浮点数, 包含常见分隔符) + // 同时也处理单位 (如 px, %, em, rem 等) 和货币符号。 + /^[$\u00A2-\u00A5\u20A0-\u20CF]?\s?-?\d{1,3}(?:[.,]\d{3})*(?:[.,]\d+)?\s?(?:px|%|em|rem|pt|vw|vh|deg|s|ms)?$/, + + // 6. 版本号 (例如 v1.2.3, 10.0.1) + /^v?\d+(\.\d+){1,3}$/, + + // 7. ISO 8601 日期/时间格式 + /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/, + + // 8. 模板占位符 (例如 {{var}}, ${var}, __VAR__) + /^({{[^}]+}}|\${[^}]+}|__\w+__|%\w+)$/, + + // 9. CSS 选择器 (简单的 class/ID) 和十六进制颜色值 + /^(?:\.|#)[\w-]+$|^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, + + // 10. 用户名 (例如 @username, @user.name, @user-name) - [已修改] + /^@[\w.-]+$/, + + // 11. HTML 实体 + /^&\w+;$/, + + // 12. 中括号包裹的序号 (例如 [1], [99]) + /^\[\d+\]$/, + + // 13. 简单时间格式 (例如 12:30, 9:45:30) - [新增] + /^\d{1,2}:\d{2}(:\d{2})?$/, ]; - _eventName = genEventName(); - _mouseoverNode = null; - _keepSelector = ""; - _terms = []; - _docTitle = ""; - _docDescription = ""; - // 显示 - _interseObserver = new IntersectionObserver( - (entries, observer) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - observer.unobserve(entry.target); - this._render(entry.target); - } - }); - }, - { - threshold: 0.1, + // 占位符 + static PLACEHOLDER = { + startDelimiter: "{", + endDelimiter: "}", + tagName: "i", + }; + + static DEFAULT_OPTIONS = DEFAULT_SETTING; // 默认配置 + static DEFAULT_RULE = GLOBLA_RULE; // 默认规则 + + static isElementOrFragment(el) { + return el instanceof Element || el instanceof DocumentFragment; + } + + // 判断是否块级元素 + static isBlockNode(el) { + if (!Translator.isElementOrFragment(el)) return false; + + if (Translator.TAGS.INLINE.has(el.nodeName)) return false; + if (Translator.TAGS.BLOCK.has(el.nodeName)) return true; + + if (Translator.displayCache.has(el)) { + return Translator.displayCache.get(el); } - ); - // 变化 - _mutaObserver = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if ( - !this._skipNodeNames.includes(mutation.target.localName) && - mutation.addedNodes.length > 0 - ) { - const nodes = Array.from(mutation.addedNodes).filter((node) => { - if ( - this._skipNodeNames.includes(node.localName) || - node.id === APP_LCNAME - ) { - return false; - } - return true; + const isBlock = !window.getComputedStyle(el).display.startsWith("inline"); + Translator.displayCache.set(el, isBlock); + return isBlock; + } + + // 判断是否直接包含非空文本节点 + static hasTextNode(el) { + if (!Translator.isElementOrFragment(el)) return false; + for (const node of el.childNodes) { + if (node.nodeType === Node.TEXT_NODE && /\S/.test(node.nodeValue)) { + return true; + } + } + return false; + } + + // 特殊字符转义 + static escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } + + // 内置忽略元素 + static BUILTIN_IGNORE_SELECTOR = `abbr, address, area, audio, br, canvas, + data, datalist, dfn, embed, head, iframe, img, input, kbd, noscript, map, + object, option, output, param, picture, progress, + samp, select, script, style, sub, sup, svg, track, time, textarea, template, + var, video, wbr, .notranslate, [contenteditable], [translate='no'], + ${APP_LCNAME}, #${APP_CONSTS.fabID}, #${APP_CONSTS.boxID}, + .${APP_CONSTS.fabID}_warpper, .${APP_CONSTS.boxID}_warpper`; + + #setting; // 设置选项 + #rule; // 规则 + #isInitialized = false; // 初始化状态 + #mouseHoverEnabled = false; // 鼠标悬停翻译 + #enabled = false; // 全局默认状态 + #runId = 0; // 用于中止过期的异步请求 + #termValues = []; // 按顺序存储术语的替换值 + #combinedTermsRegex; // 专业术语正则表达式 + #combinedSkipsRegex; // 跳过文本正则表达式 + #placeholderRegex; // 恢复htnml正则表达式 + #translationTagName = APP_NAME; // 翻译容器的标签名 + #eventName = ""; // 通信事件名称 + #docInfo = {}; // 网页信息 + #textClass = {}; // 译文样式class + #textSheet = ""; // 译文样式字典 + + #observedNodes = new WeakSet(); // 存储所有被识别出的、可翻译的 DOM 节点单元 + #translationNodes = new WeakMap(); // 存储所有插入到页面的译文节点 + #viewNodes = new Set(); // 当前在可视范围内的单元 + #processedNodes = new WeakMap(); // 已处理(已执行翻译DOM操作)的单元 + #rootNodes = new Set(); // 已监控的根节点 + + #removeKeydownHandler; // 快捷键清理函数 + #hoveredNode = null; // 存储当前悬停的可翻译节点 + #boundMouseMoveHandler; // 鼠标事件 + #boundKeyDownHandler; // 键盘事件 + + #io; // IntersectionObserver + #mo; // MutationObserver + #dmm; // DebounceMouseMover + #srm; // ShadowRootMonitor + + #rescanQueue = new Set(); // “脏容器”队列 + #isQueueProcessing = false; // 队列处理状态标志 + + // 忽略元素 + get #ignoreSelector() { + return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`; + } + + constructor(rule = {}, setting = {}) { + this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting }; + this.#rule = { ...Translator.DEFAULT_RULE, ...rule }; + this.#eventName = genEventName(); + this.#docInfo = { + title: document.title, + description: this.#getDocDescription(), + }; + this.#combinedSkipsRegex = new RegExp( + Translator.BUILTIN_SKIP_PATTERNS.map((r) => `(${r.source})`).join("|") + ); + this.#placeholderRegex = this.#createPlaceholderRegex(); + this.#parseTerms(this.#rule.terms); + this.#createTextStyles(); + + this.#boundMouseMoveHandler = this.#handleMouseMove.bind(this); + this.#boundKeyDownHandler = this.#handleKeyDown.bind(this); + + this.#io = this.#createIntersectionObserver(); + this.#mo = this.#createMutationObserver(); + this.#dmm = this.#createDebounceMouseMover(); + this.#srm = this.#createShadowRootMonitor(); + + // 监控shadowroot + if (this.#rule.hasShadowroot === "true") { + this.#srm.start(); + } + + // 鼠标悬停翻译 + if (this.#setting.mouseHoverSetting.useMouseHover) { + this.#enableMouseHover(); + } + + // 是否默认启动 + if (this.#rule.transOpen === "true") { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => this.enable()); + } else { + this.enable(); + } + } + } + + // 初始化 + #init() { + this.#isInitialized = true; + + // 注入JS/CSS + this.#initInjector(); + + // 查找根节点并扫描 + document + .querySelectorAll(this.#rule.rootsSelector || "body") + .forEach((root) => { + this.#startObserveRoot(root); + }); + + // 查找现有的所有shadowroot + if (this.#rule.hasShadowroot === "true") { + try { + this.#findAllShadowRoots().forEach((shadowRoot) => { + this.#startObserveShadowRoot(shadowRoot); }); - if (nodes.length > 0) { - // const rootNode = mutation.target.getRootNode(); - // todo - this._reTranslate(); + } catch (err) { + kissLog(err, "findAllShadowRoots"); + } + } + } + + #createPlaceholderRegex() { + const escapedStart = Translator.escapeRegex( + Translator.PLACEHOLDER.startDelimiter + ); + const escapedEnd = Translator.escapeRegex( + Translator.PLACEHOLDER.endDelimiter + ); + const patternString = `(${escapedStart}\\d+${escapedEnd}|<\\/?\\w+\\d+>)`; + const flags = "g"; + return new RegExp(patternString, flags); + } + + // 创建样式 + #createTextStyles() { + const [textClass, textStyles] = genTextClass({ ...this.#rule }); + const textSheet = new CSSStyleSheet(); + textSheet.replaceSync(textStyles); + this.#textClass = textClass; + this.#textSheet = textSheet; + } + + // 注入样式 + #injectSheet(shadowRoot) { + if (!shadowRoot.adoptedStyleSheets.includes(this.#textSheet)) { + shadowRoot.adoptedStyleSheets = [ + ...shadowRoot.adoptedStyleSheets, + this.#textSheet, + ]; + } + } + + // 解析专业术语字符串 + #parseTerms(termsString) { + this.#termValues = []; + this.#combinedTermsRegex = null; + + if (!termsString || typeof termsString !== "string") return; + + const termPatterns = []; + const lines = termsString.split(/\n|;/); // 按换行或分号分割 + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine) continue; + + let lastCommaIndex = trimmedLine.lastIndexOf(","); + if (lastCommaIndex === -1) { + lastCommaIndex = trimmedLine.length; + } + const key = trimmedLine.substring(0, lastCommaIndex).trim(); + const value = trimmedLine.substring(lastCommaIndex + 1).trim(); + + if (key) { + try { + new RegExp(key); + termPatterns.push(`(${key})`); + this.#termValues.push(value); + } catch (err) { + kissLog(err, `Invalid RegExp for term: "${key}"`); } } - }); - }); + } - _getDocDescription = () => { - const meta = document.querySelector('meta[name="description"]'); - return meta ? meta.getAttribute("content") : ""; - }; - - // 插入 shadowroot - _overrideAttachShadow = () => { - const _this = this; - const _attachShadow = HTMLElement.prototype.attachShadow; - HTMLElement.prototype.attachShadow = function () { - _this._reTranslate(); - return _attachShadow.apply(this, arguments); - }; - }; - - constructor(rule, setting) { - this._overrideAttachShadow(); - - this._setting = setting; - this._rule = rule; - this._docTitle = document.title; - this._docDescription = this._getDocDescription(); - - this._keepSelector = rule.keepSelector || ""; - this._terms = (rule.terms || "") - .split(/\n|;/) - .map((item) => item.split(",").map((item) => item.trim())) - .filter(([term]) => Boolean(term)); - - if (rule.transOpen === "true") { - this._register(); + if (termPatterns.length > 0) { + this.#combinedTermsRegex = new RegExp(termPatterns.join("|"), "g"); } } - get setting() { - return this._setting; + #getDocDescription() { + const meta = document.querySelector('meta[name="description"]'); + const description = meta ? meta.getAttribute("content") : ""; + return description.slice(0, 200); } - get docInfo() { - return { - title: this._docTitle, - description: this._docDescription, - }; - } + // 监控翻译单元的可见性 + #createIntersectionObserver() { + const pending = new Set(); + const flush = debounce(() => { + pending.forEach((node) => this.#performSyncNode(node)); + pending.clear(); + }, this.#setting.transInterval); - get eventName() { - return this._eventName; - } - - get rule() { - // console.log("get rule", this._rule); - return this._rule; - } - - set rule(rule) { - // console.log("set rule", rule); - this._rule = rule; - - // 广播消息 - const eventName = this._eventName; - window.dispatchEvent( - new CustomEvent(eventName, { - detail: { - action: MSG_TRANS_CURRULE, - args: rule, - }, - }) + return new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + this.#viewNodes.add(entry.target); + pending.add(entry.target); + flush(); + } else { + this.#viewNodes.delete(entry.target); + } + }); + }, + { threshold: 0.01 } ); } - updateRule = (obj) => { - this.rule = { ...this.rule, ...obj }; - }; + // 监控页面动态变化 + #createMutationObserver() { + return new MutationObserver((mutations) => { + for (const mutation of mutations) { + if ( + mutation.type === "characterData" && + mutation.oldValue !== mutation.target.nodeValue + ) { + this.#queueForRescan(mutation.target.parentElement); + } else if (mutation.type === "childList") { + if (mutation.nextSibling?.tagName === this.#translationTagName) { + // 恢复原文时插入元素,忽略 + continue; + } - toggle = () => { - if (this.rule.transOpen === "true") { - this.rule = { ...this.rule, transOpen: "false" }; - this._unRegister(); - } else { - this.rule = { ...this.rule, transOpen: "true" }; - this._register(); + let nodes = new Set(); + let hasText = false; + mutation.addedNodes.forEach((node) => { + if (/\S/.test(node.nodeValue)) { + if (node.nodeType === Node.TEXT_NODE) { + hasText = true; + } else if ( + Translator.isElementOrFragment(node) && + node.nodeName !== this.#translationTagName + ) { + nodes.add(node); + } + } + }); + if (hasText) { + this.#queueForRescan(mutation.target); + } else { + nodes.forEach((node) => this.#queueForRescan(node)); + } + } + } + }); + } + + // 节流的鼠标悬停事件 + #createDebounceMouseMover() { + return debounce((targetNode) => { + const startNode = targetNode; + let foundNode = null; + while (targetNode && targetNode !== document.body) { + if (this.#observedNodes.has(targetNode)) { + foundNode = targetNode; + break; + } + targetNode = targetNode.parentElement; + } + this.#hoveredNode = foundNode || startNode; + }, 200); + } + + // 创建shadowroot的回调 + #createShadowRootMonitor() { + return new ShadowRootMonitor((shadowRoot) => { + this.#startObserveShadowRoot(shadowRoot); + }); + } + + // 跟踪鼠标下的可翻译节点 + #handleMouseMove(event) { + let targetNode = event.composedPath()[0]; + this.#dmm(targetNode); + } + + // 快捷键按下时的处理器 + #handleKeyDown() { + if (!this.#isInitialized) { + this.#init(); } - }; + let targetNode = this.#hoveredNode; + if (!targetNode || !this.#observedNodes.has(targetNode)) return; - toggleStyle = () => { - const textStyle = - this.rule.textStyle === OPT_STYLE_FUZZY - ? OPT_STYLE_DASHLINE - : OPT_STYLE_FUZZY; - this.rule = { ...this.rule, textStyle }; - }; + // 切换该节点翻译状态 + if (this.#processedNodes.has(targetNode)) { + this.#cleanupDirectTranslations(targetNode); + } else { + this.#processNode(targetNode); + } + } - translateText = async (text) => { - const { translator, fromLang, toLang } = this._rule; - const apiSetting = - this._setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator]; - const [trText] = await apiTranslate({ + // 找页面所有 ShadowRoot + #findAllShadowRoots(root = document.body, results = new Set()) { + // const start = performance.now(); + try { + const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); + while (walker.nextNode()) { + const node = walker.currentNode; + if (node.shadowRoot) { + results.add(node.shadowRoot); + this.#findAllShadowRoots(node.shadowRoot, results); + } + } + } catch (err) { + kissLog(err, "无法访问某个 shadowRoot"); + } + // const end = performance.now(); + // const duration = end - start; + // console.log(`findAllShadowRoots 耗时:${duration} 毫秒`); + return results; + } + + // 向上查找发生变化的块级元素 + #findChangeContainer(startNode) { + if ( + !Translator.isElementOrFragment(startNode) || + startNode.closest?.(this.#ignoreSelector) + ) { + return null; + } + + let current = startNode; + while (current && current !== document.body) { + if (Translator.isBlockNode(current) || this.#observedNodes.has(current)) { + // 确保找到的容器在我们监控的根节点内 + for (const root of this.#rootNodes) { + if (root.contains(current)) { + return current; + } + } + } + current = current.parentElement; + } + + return null; + } + + // “脏容器”队列 + #queueForRescan(target) { + this.#rescanQueue.add(target); + if (!this.#isQueueProcessing) { + this.#isQueueProcessing = true; + scheduleIdle(() => { + this.#rescanQueue.forEach((t) => this.#rescanContainer(t)); + this.#rescanQueue.clear(); + this.#isQueueProcessing = false; + }, 200); + } + } + + // 处理“脏容器” + #rescanContainer(changedNode) { + const container = this.#findChangeContainer(changedNode); + if (!container) return; + + this.#cleanupAllTranslations(container); + this.#scanNode(container); + } + + // 重新观察 + #reIO(node) { + this.#io.unobserve(node); + this.#io.observe(node); + } + + // 重新观察可视范围内全部节点 + #reIOViewNodes() { + this.#viewNodes.forEach((n) => this.#reIO(n)); + } + + // 监控shadowroot + #startObserveShadowRoot(shadowRoot) { + if (shadowRoot.host.matches(`#${APP_CONSTS.fabID}, #${APP_CONSTS.boxID}`)) { + return; + } + this.#startObserveRoot(shadowRoot); + this.#injectSheet(shadowRoot); + } + + // 监控根节点 + #startObserveRoot(root) { + if (this.#rootNodes.has(root)) return; + this.#rootNodes.add(root); + this.#mo.observe(root, { + childList: true, + subtree: true, + characterData: true, + characterDataOldValue: true, + }); + this.#scanNode(root); + } + + // 开始/重新监控节点 + #startObserveNode(node) { + if (this.#observedNodes.has(node)) { + // 已监控,但未处理状态,且在可视范围 + if (!this.#processedNodes.has(node) && this.#viewNodes.has(node)) { + this.#reIO(node); + } + } else { + this.#observedNodes.add(node); + this.#io.observe(node); + } + } + + // 非自动识别文本模式下,快速查询目标节点 + #queryNode(rootNode) { + // root 也可能是目标节点 + if (rootNode.matches?.(this.#rule.selector)) { + this.#startObserveNode(rootNode); + } + + rootNode.querySelectorAll(this.#rule.selector).forEach((node) => { + if (!node.closest?.(this.#ignoreSelector)) { + this.#startObserveNode(node); + } + }); + } + + // 寻找需要被监控的文本节点 + #scanNode(rootNode) { + if ( + !Translator.isElementOrFragment(rootNode) || + rootNode.matches?.(this.#ignoreSelector) + ) { + return; + } + + if (this.#rule.autoScan === "false") { + this.#queryNode(rootNode); + return; + } + + const hasText = Translator.hasTextNode(rootNode); + if (hasText) { + this.#startObserveNode(rootNode); + } + + for (const child of rootNode.children) { + if (!hasText || Translator.isBlockNode(child)) { + this.#scanNode(child); + } + } + } + + // 处理一个待翻译的节点 + async #processNode(node) { + if ( + !Translator.isElementOrFragment(node) || + this.#processedNodes.has(node) + ) { + return; + } + + this.#processedNodes.set(node, { ...this.#rule }); + + // 提前检测文本 + if (this.#isInvalidText(node.textContent)) { + return; + } + + // 提前进行语言检测 + const { detectRemote, toLang, skipLangs = [] } = this.#rule; + const { langDetector } = this.#setting; + const deLang = await tryDetectLang( + node.textContent, + detectRemote, + langDetector + ); + // console.log("deLang", deLang, toLang); + if ( + deLang && + (toLang.slice(0, 2) === deLang.slice(0, 2) || skipLangs.includes(deLang)) + ) { + return; + } + + let nodeGroup = []; + [...node.childNodes].forEach((child) => { + const shouldBreak = this.#shouldBreak(child); + const shouldGroup = + child.nodeType === Node.ELEMENT_NODE || + child.nodeType === Node.TEXT_NODE; + if (!shouldBreak && shouldGroup) { + nodeGroup.push(child); + } else if (shouldBreak && nodeGroup.length) { + this.#translateNodeGroup(nodeGroup, node); + nodeGroup = []; + } + }); + + if (nodeGroup.length) { + this.#translateNodeGroup(nodeGroup, node); + } + } + + // 判断是否需要换行 + #shouldBreak(node) { + if (!Translator.isElementOrFragment(node)) return false; + if (node.matches(this.#rule.keepSelector)) return false; + + if ( + Translator.TAGS.BREAK_LINE.has(node.nodeName) || + node.nodeName === this.#translationTagName + ) { + return true; + } + + if (this.#rule.autoScan && Translator.isBlockNode(node)) { + return true; + } + + if ( + !this.#rule.autoScan && + (node.matches(this.#rule.selector) || + node.querySelector(this.#rule.selector)) + ) { + return true; + } + + return false; + } + + // 过滤文本 + #isInvalidText(text) { + if (typeof text !== "string") { + return true; + } + + const trimmedText = text.trim(); + + // 文本长度 + if ( + trimmedText.length < this.#setting.minLength || + trimmedText.length > this.#setting.maxLength + ) { + return true; + } + + // 单个非字母数字字符。 + if (trimmedText.length === 1 && !trimmedText.match(/[a-zA-Z]/)) { + return true; + } + + // 只是一个数字 + if (!isNaN(parseFloat(trimmedText)) && isFinite(trimmedText)) { + return true; + } + + // 正则匹配 + if (this.#combinedSkipsRegex.test(trimmedText)) { + return true; + } + + return false; + } + + // 翻译内联节点 + async #translateNodeGroup(nodes, hostNode) { + const { + transTag, + textStyle, + transStartHook, + transEndHook, + transOnly, + selectStyle, + parentStyle, + // detectRemote, + // toLang, + // skipLangs = [], + } = this.#rule; + const { + newlineLength, + // langDetector, + } = this.#setting; + const parentNode = hostNode.parentElement; + + // 翻译开始钩子函数 + if (transStartHook?.trim()) { + try { + interpreter.run(`exports.transStartHook = ${transStartHook}`); + interpreter.exports.transStartHook({ + hostNode, + parentNode, + nodes, + }); + } catch (err) { + kissLog(err, "transStartHook"); + } + } + + const [processedString, placeholderMap] = + this.#serializeForTranslation(nodes); + // console.log("processedString", processedString); + if (this.#isInvalidText(processedString)) return; + + const wrapper = document.createElement(this.#translationTagName); + wrapper.className = Translator.KISS_CLASS.warpper; + + if (processedString.length > newlineLength) { + const br = document.createElement("br"); + br.hidden = transOnly === "true"; + wrapper.appendChild(br); + } + + const inner = document.createElement(transTag); + inner.className = `${Translator.KISS_CLASS.inner} ${this.#textClass[textStyle]}`; + inner.innerHTML = loadingSvg; + wrapper.appendChild(inner); + nodes[nodes.length - 1].after(wrapper); + + this.#translationNodes.set(wrapper, nodes); + const currentRunId = this.#runId; + + try { + // const deLang = await tryDetectLang( + // processedString, + // detectRemote, + // langDetector + // ); + // if (deLang && (toLang.includes(deLang) || skipLangs.includes(deLang))) { + // wrapper.remove(); + // return; + // } + + const [translatedText, isSameLang] = + await this.#translateFetch(processedString); + // console.log("translatedText", translatedText); + if (isSameLang || this.#runId !== currentRunId) { + wrapper.remove(); + return; + } + + inner.innerHTML = this.#restoreFromTranslation( + translatedText, + placeholderMap + ); + if (transOnly === "true") { + this.#removeNodes(nodes); + } + + // 附加样式 + if (selectStyle && hostNode.style) { + hostNode.style.cssText += selectStyle; + } + if (parentStyle && parentNode && parentNode.style) { + parentNode.style.cssText += parentStyle; + } + + // 翻译完成钩子函数 + if (transEndHook?.trim()) { + try { + interpreter.run(`exports.transEndHook = ${transEndHook}`); + interpreter.exports.transEndHook({ + hostNode, + parentNode, + nodes, + wrapperNode: wrapper, + innerNode: inner, + }); + } catch (err) { + kissLog(err, "transEndHook"); + } + } + } catch (err) { + // inner.textContent = `[失败]...`; + // todo: 失败重试按钮 + wrapper.remove(); + kissLog(err, "translateNodeGroup"); + } + } + + // 处理节点转为翻译字符串 + #serializeForTranslation(nodes) { + let replaceCounter = 0; // {{n}} + let wrapCounter = 0; // + const placeholderMap = new Map(); + + const pushReplace = (html) => { + replaceCounter++; + const placeholder = `${Translator.PLACEHOLDER.startDelimiter}${replaceCounter}${Translator.PLACEHOLDER.endDelimiter}`; + placeholderMap.set(placeholder, html); + return placeholder; + }; + + const traverse = (node) => { + if ( + node.nodeType !== Node.ELEMENT_NODE && + node.nodeType !== Node.TEXT_NODE + ) { + return ""; + } + + // 文本节点 + if ( + this.#rule.hasRichText === "false" || + node.nodeType === Node.TEXT_NODE + ) { + let text = node.textContent; + + // 专业术语替换 + if (this.#combinedTermsRegex) { + this.#combinedTermsRegex.lastIndex = 0; + text = text.replace(this.#combinedTermsRegex, (...args) => { + const groups = args.slice(1, -2); + const matchedIndex = groups.findIndex( + (group) => group !== undefined + ); + const fullMatch = args[0]; + const termValue = this.#termValues[matchedIndex]; + + return pushReplace( + `${termValue || fullMatch}` + ); + }); + } + + return text; + } + + // 元素节点 + if (node.nodeType === Node.ELEMENT_NODE) { + if ( + Translator.TAGS.REPLACE.has(node.tagName) || + node.matches(this.#rule.keepSelector) + ) { + if (node.tagName === "IMG" || node.tagName === "SVG") { + node.style.width = `${node.offsetWidth}px`; + node.style.height = `${node.offsetHeight}px`; + } + return pushReplace(node.outerHTML); + } + + let innerContent = ""; + node.childNodes.forEach((child) => { + innerContent += traverse(child); + }); + + if (Translator.TAGS.WARP.has(node.tagName)) { + if (this.#isInvalidText(innerContent)) { + return pushReplace(node.outerHTML); + } + + wrapCounter++; + const startPlaceholder = `<${Translator.PLACEHOLDER.tagName}${wrapCounter}>`; + const endPlaceholder = ``; + placeholderMap.set(startPlaceholder, buildOpeningTag(node)); + placeholderMap.set(endPlaceholder, ``); + return `${startPlaceholder}${innerContent}${endPlaceholder}`; + } + + return innerContent; + } + + return ""; + }; + + function buildOpeningTag(node) { + const escapeAttr = (str) => str.replace(/"/g, """); + let tag = `<${node.tagName.toLowerCase()}`; + for (const attr of node.attributes) { + tag += ` ${attr.name}="${escapeAttr(attr.value)}"`; + } + tag += ">"; + return tag; + } + + const processedString = nodes.map(traverse).join("").trim(); + + return [processedString, placeholderMap]; + } + + // 组装恢复html字符串 + #restoreFromTranslation(translatedText, placeholderMap) { + if (!placeholderMap.size) { + return translatedText; + } + + if (!translatedText) return ""; + + return translatedText.replace( + this.#placeholderRegex, + (match) => placeholderMap.get(match) || match + ); + } + + // 发起翻译请求 + #translateFetch(text) { + const { translator, fromLang, toLang } = this.#rule; + // const apiSetting = this.#setting.transApis[translator]; + const apiSetting = { + ...DEFAULT_TRANS_APIS[translator], + ...(this.#setting.transApis[translator] || {}), + }; + + return apiTranslate({ text, translator, fromLang, toLang, apiSetting, + docInfo: this.#docInfo, }); - return trText; - }; + } - _querySelectorAll = (selector, node) => { - try { - return Array.from(node.querySelectorAll(selector)); - } catch (err) { - kissLog(selector, "querySelectorAll err"); + // 查找指定节点下所有译文节点 + #findTranslationWrappers(parentNode) { + return parentNode.querySelectorAll(`:scope > ${APP_LCNAME}`); + } + + // 清理所有插入的译文dom + #cleanupAllNodes() { + this.#rootNodes.forEach((root) => this.#cleanupAllTranslations(root)); + } + + // 清理节点下面所有译文dom + #cleanupAllTranslations(root) { + root + .querySelectorAll(APP_LCNAME) + .forEach((el) => this.#removeTranslationElement(el)); + } + + // 清理子节点译文dom + #cleanupDirectTranslations(node) { + this.#findTranslationWrappers(node).forEach((el) => { + this.#removeTranslationElement(el); + }); + } + + // 清理译文 + #removeTranslationElement(el) { + this.#processedNodes.delete(el.parentElement); + + // 如果是仅显示译文模式,先恢复原文 + if (this.#rule.transOnly === "true") { + this.#restoreOriginal(el); } - return []; - }; - _queryFilter = (selector, rootNode) => { - return this._querySelectorAll(selector, rootNode).filter( - (node) => this._queryFilter(selector, node).length === 0 - ); - }; + this.#translationNodes.delete(el); + el.remove(); + } - _queryShadowNodes = (selector, rootNode) => { - this._rootNodes.add(rootNode); - this._queryFilter(selector, rootNode).forEach((item) => { - if (!this._tranNodes.has(item)) { - this._tranNodes.set(item, ""); + // 恢复原文 + #restoreOriginal(el) { + const nodes = this.#translationNodes.get(el); + if (nodes) { + const frag = document.createDocumentFragment(); + nodes.forEach((n) => frag.appendChild(n)); + const parent = el.parentElement; + parent?.insertBefore(frag, el); + } + } + + // 移除多个节点 + #removeNodes(nodes) { + const frag = document.createDocumentFragment(); + nodes.forEach((n) => frag.appendChild(n)); + } + + // 切换译文和双语显示 + #toggleTranslationOnly(node, transOnly) { + this.#findTranslationWrappers(node).forEach((el) => { + const br = el.querySelector(":scope > br"); + if (transOnly === "true") { + // 双语变为仅译文 + if (br) br.hidden = true; + const nodes = this.#translationNodes.get(el) || []; + this.#removeNodes(nodes); + } else { + // 仅译文变为双语 + this.#restoreOriginal(el); + if (br) br.hidden = false; } }); + } - Array.from(rootNode.querySelectorAll("*")) - .map((item) => item.shadowRoot) - .filter(Boolean) - .forEach((item) => { - this._queryShadowNodes(selector, item); - }); - }; + // 更新样式 + #updateStyle(node, oldStyle, newStyle) { + this.#findTranslationWrappers(node).forEach((el) => { + const inner = el.querySelector( + `:scope > .${Translator.KISS_CLASS.inner}` + ); + inner.classList.remove(this.#textClass[oldStyle]); + inner.classList.add(this.#textClass[newStyle]); + }); + } - _queryNodes = (rootNode = document) => { - // const childRoots = Array.from(rootNode.querySelectorAll("*")) - // .map((item) => item.shadowRoot) - // .filter(Boolean); - // const childNodes = childRoots.map((item) => this._queryNodes(item)); - // const nodes = Array.from(rootNode.querySelectorAll(this.rule.selector)); - // return nodes.concat(childNodes).flat(); + // 刷新节点翻译 + #refreshNode(node) { + this.#cleanupDirectTranslations(node); + this.#processNode(node); + } - this._rootNodes.add(rootNode); - this._rule.selector - .split(";") - .map((item) => item.trim()) - .filter(Boolean) - .forEach((selector) => { - if (selector.includes(SHADOW_KEY)) { - const [outSelector, inSelector] = selector - .split(SHADOW_KEY) - .map((item) => item.trim()); - if (outSelector && inSelector) { - const outNodes = this._querySelectorAll(outSelector, rootNode); - outNodes.forEach((outNode) => { - if (outNode.shadowRoot) { - // this._rootNodes.add(outNode.shadowRoot); - // this._queryFilter(inSelector, outNode.shadowRoot).forEach( - // (item) => { - // if (!this._tranNodes.has(item)) { - // this._tranNodes.set(item, ""); - // } - // } - // ); - this._queryShadowNodes(inSelector, outNode.shadowRoot); - } - }); - } - } else { - this._queryFilter(selector, rootNode).forEach((item) => { - if (!this._tranNodes.has(item)) { - this._tranNodes.set(item, ""); - } - }); - } - }); - }; - - _register = () => { - const { fromLang, toLang, injectJs, injectCss, fixerSelector, fixerFunc } = - this._rule; - if (fromLang === toLang) { + // 使指定节点的状态与当前的全局同步 + #performSyncNode(node) { + const appliedRule = this.#processedNodes.get(node); + if (!appliedRule) { + this.#enabled && this.#processNode(node); return; } - // webfix - if (fixerSelector && fixerFunc !== "-") { - runFixer(fixerSelector, fixerFunc); + const { translator, fromLang, toLang, hasRichText, textStyle, transOnly } = + this.#rule; + + const needsRefresh = + appliedRule.translator !== translator || + appliedRule.fromLang !== fromLang || + appliedRule.toLang !== toLang || + appliedRule.hasRichText !== hasRichText; + + // 需要重新翻译 + if (needsRefresh) { + Object.assign(appliedRule, { + translator, + fromLang, + toLang, + hasRichText, + textStyle, + transOnly, + }); + this.#refreshNode(node); // 会自动应用新样式 + return; } - // 注入用户JS/CSS + // 样式规则过时 + if (appliedRule.textStyle !== textStyle) { + const oldStyle = appliedRule.textStyle; + appliedRule.textStyle = textStyle; + this.#updateStyle(node, oldStyle, textStyle); + } + + // 切换原文显示 + if (appliedRule.transOnly !== transOnly) { + appliedRule.transOnly = transOnly; + this.#toggleTranslationOnly(node, transOnly); + } + } + + // 停止监听,重置参数 + #resetOptions() { + this.#io.disconnect(); + this.#mo.disconnect(); + this.#viewNodes.clear(); + this.#rootNodes.clear(); + this.#observedNodes = new WeakSet(); + this.#translationNodes = new WeakMap(); + this.#processedNodes = new WeakMap(); + } + + // 开启鼠标悬停翻译 + #enableMouseHover() { + if (this.#mouseHoverEnabled) return; + this.#mouseHoverEnabled = true; + this.#setting.mouseHoverSetting.useMouseHover = true; + + document.addEventListener("mousemove", this.#boundMouseMoveHandler); + let { mouseHoverKey } = this.#setting.mouseHoverSetting; + if (mouseHoverKey.length === 0) { + mouseHoverKey = DEFAULT__MOUSEHOVER_KEY; + } + this.#removeKeydownHandler = shortcutRegister( + mouseHoverKey, + this.#boundKeyDownHandler + ); + } + + // 禁用鼠标悬停翻译 + #disableMouseHover() { + if (!this.#mouseHoverEnabled) return; + this.#mouseHoverEnabled = false; + this.#setting.mouseHoverSetting.useMouseHover = false; + + document.removeEventListener("mousemove", this.#boundMouseMoveHandler); + this.#removeKeydownHandler?.(); + } + + // 注入JS/CSS + #initInjector() { + const { injectJs, injectCss } = this.#rule; if (isExt) { injectJs && sendBgMsg(MSG_INJECT_JS, injectJs); injectCss && sendBgMsg(MSG_INJECT_CSS, injectCss); @@ -298,276 +1238,145 @@ export class Translator { injectJs && injectInlineJs(injectJs); injectCss && injectInternalCss(injectCss); } + } - // 搜索节点 - this._queryNodes(); + // 移除JS/CSS + #removeInjector() { + document + .querySelectorAll(`[data-source^="kiss-inject"]`) + ?.forEach((el) => el.remove()); + } - this._rootNodes.forEach((node) => { - // 监听节点变化; - this._mutaObserver.observe(node, { - childList: true, - subtree: true, - // characterData: true, - }); - }); + // 切换鼠标悬停翻译 + toggleMouseHover() { + this.#mouseHoverEnabled + ? this.#disableMouseHover() + : this.#enableMouseHover(); + } - if ( - !this._rule.transTiming || - this._rule.transTiming === OPT_TIMING_PAGESCROLL - ) { - // 监听节点显示 - this._tranNodes.forEach((_, node) => { - this._interseObserver.observe(node); - }); - } else if (this._rule.transTiming === OPT_TIMING_PAGEOPEN) { - // 全文直接翻译 - this._tranNodes.forEach((_, node) => { - this._render(node); - }); + // 开启翻译 + enable() { + if (this.#enabled) return; + this.#enabled = true; + this.#rule.transOpen = "true"; + this.#runId++; + + if (this.#isInitialized) { + this.#reIOViewNodes(); } else { - // 监听鼠标悬停 - window.addEventListener("keydown", this._handleKeydown); - this._tranNodes.forEach((_, node) => { - node.addEventListener("mouseenter", this._handleMouseover); - node.addEventListener("mouseleave", this._handleMouseout); - }); + this.#init(); } // 翻译页面标题 - if (this._rule.transTitle === "true" && !this._docTitle) { + if (this.#rule.transTitle === "true") { const title = document.title; - this._docTitle = title; - this.translateText(title).then((trText) => { - document.title = `${trText} | ${title}`; - }); + this.#docInfo.title = title; + this.#translateFetch(title) + .then(([trText]) => { + document.title = trText || title; + }) + .catch((err) => { + kissLog(err, "tanslate title"); + }); } - }; + } - _handleMouseover = (e) => { - // console.log("mouseenter", e); - if (!this._tranNodes.has(e.target)) { - return; - } + // 关闭翻译 + disable() { + if (!this.#enabled) return; + this.#enabled = false; + this.#rule.transOpen = "false"; + this.#runId++; - const key = this._rule.transTiming.slice(3); - if (this._rule.transTiming === OPT_TIMING_MOUSEOVER || e[key]) { - e.target.removeEventListener("mouseenter", this._handleMouseover); - e.target.removeEventListener("mouseleave", this._handleMouseout); - this._render(e.target); - } else { - this._mouseoverNode = e.target; - } - }; - - _handleMouseout = (e) => { - // console.log("mouseleave", e); - if (!this._tranNodes.has(e.target)) { - return; - } - - this._mouseoverNode = null; - }; - - _handleKeydown = (e) => { - // console.log("keydown", e); - const key = this._rule.transTiming.slice(3); - if (e[key] && this._mouseoverNode) { - this._mouseoverNode.removeEventListener( - "mouseenter", - this._handleMouseover - ); - this._mouseoverNode.removeEventListener( - "mouseleave", - this._handleMouseout - ); - - const node = this._mouseoverNode; - this._render(node); - this._mouseoverNode = null; - } - }; - - _unRegister = () => { - // 恢复页面标题 - if (this._docTitle) { - document.title = this._docTitle; - this._docTitle = ""; - } - - // 解除节点变化监听 - this._mutaObserver.disconnect(); - - // 解除节点显示监听 - // this._interseObserver.disconnect(); - - // 移除键盘监听 - window.removeEventListener("keydown", this._handleKeydown); - - const { transRemoveHook } = this._rule; - this._tranNodes.forEach((innerHTML, node) => { - if ( - !this._rule.transTiming || - this._rule.transTiming === OPT_TIMING_PAGESCROLL - ) { - // 解除节点显示监听 - this._interseObserver.unobserve(node); - } else if (this._rule.transTiming !== OPT_TIMING_PAGEOPEN) { - // 移除鼠标悬停监听 - // node.style.pointerEvents = "none"; - node.removeEventListener("mouseenter", this._handleMouseover); - node.removeEventListener("mouseleave", this._handleMouseout); - } - - // 移除/恢复元素 - if (innerHTML) { - if (this._rule.transOnly === "true") { - node.innerHTML = innerHTML; - } else { - node.querySelector(APP_LCNAME)?.remove(); - } - // 钩子函数 - if (transRemoveHook?.trim()) { - interpreter.run(`exports.transRemoveHook = ${transRemoveHook}`); - interpreter.exports.transRemoveHook(node); - } - } - }); - - // 移除用户JS/CSS - this._removeInjector(); - - // 清空节点集合 - this._rootNodes.clear(); - this._tranNodes.clear(); - - // 清空任务池 + this.#cleanupAllNodes(); clearFetchPool(); clearAllBatchQueue(); - }; - _removeInjector = () => { - document - .querySelectorAll(`[data-source^="KISS-Calendar"]`) - ?.forEach((el) => el.remove()); - }; - - _reTranslate = debounce(() => { - if (this._rule.transOpen === "true") { - window.removeEventListener("keydown", this._handleKeydown); - this._mutaObserver.disconnect(); - this._interseObserver.disconnect(); - this._removeInjector(); - this._register(); + // 恢复页面标题 + if (this.#docInfo.title) { + document.title = this.#docInfo.title; } - }, this._setting.transInterval); + } - _invalidLength = (q) => - !q || - q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) || - q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH); + // 重新扫描页面 + rescan() { + if (!this.#isInitialized) return; + this.#runId++; - _render = (el) => { - // 检查元素是否有效 - if (!el || typeof el.innerText === "undefined") { - return; - } + this.#cleanupAllNodes(); + this.#resetOptions(); + clearFetchPool(); + clearAllBatchQueue(); - let traEl = el.querySelector(APP_LCNAME); + // 重新初始化 + this.#init(); + } - // 已翻译 - if (traEl) { - if (this._rule.transOnly === "true") { - return; - } + // 切换是否翻译 + toggle() { + this.#enabled ? this.disable() : this.enable(); + } - const preText = getHtmlText(this._tranNodes.get(el)); - const curText = getHtmlText(el.innerHTML, APP_LCNAME); - if (preText === curText) { - return; - } + // 快速切换模糊样式 + toggleStyle() { + const textStyle = + this.#rule.textStyle === OPT_STYLE_FUZZY + ? OPT_STYLE_NONE + : OPT_STYLE_FUZZY; + this.updateRule({ textStyle }); + } - traEl.remove(); - } + // 停止运行 + stop() { + this.disable(); + this.#resetOptions(); + this.#srm.stop(); + this.#disableMouseHover(); + this.#removeInjector(); + this.#isInitialized = false; + } - // 缓存已翻译元素 - this._tranNodes.set(el, el.innerHTML); - - let q = el.innerText.trim(); - const keeps = []; - - // 保留元素 - const keepSelector = this._keepSelector.trim(); - if (keepSelector) { - let text = ""; - el.childNodes.forEach((child) => { - if (child.nodeType === 1 && child.matches(keepSelector)) { - if (child.nodeName === "IMG") { - child.style.cssText += `width: ${child.width}px;`; - child.style.cssText += `height: ${child.height}px;`; - } - text += `[${keeps.length}]`; - keeps.push(child.outerHTML); - } else if (child.nodeType === 1 || child.nodeType === 3) { - text += child.textContent; - } - }); - - if (keeps.length > 0) { - // textContent会保留些无用的换行符,严重影响翻译质量 - if (q.includes("\n")) { - q = text; + // 更新规则 + updateRule(newRule) { + let hasChanged = false; + let needsRescan = false; + for (const key in newRule) { + if ( + Object.prototype.hasOwnProperty.call(this.#rule, key) && + this.#rule[key] !== newRule[key] + ) { + this.#rule[key] = newRule[key]; + if (key === "autoScan" || key === "hasShadowroot") { + needsRescan = true; } else { - q = text.replaceAll("\n", " "); + hasChanged = true; } } } - // 太长或太短 - if (this._invalidLength(q.replace(/\[(\d+)\]/g, "").trim())) { + if (needsRescan) { + this.rescan(); return; } - // 专业术语 - if (this._terms.length > 0) { - for (const term of this._terms) { - const re = new RegExp(term[0], "g"); - q = q.replace(re, (t) => { - const text = `[${keeps.length}]`; - keeps.push(`${term[1] || t}`); - return text; - }); - } + if (hasChanged) { + this.#reIOViewNodes(); } + } - // 翻译开始钩子函数 - const { transStartHook } = this._rule; - if (transStartHook?.trim()) { - interpreter.run(`exports.transStartHook = ${transStartHook}`); - q = interpreter.exports.transStartHook(el, q); - } + get setting() { + return { ...this.#setting }; + } - // 终止翻译 - if (!q) { - return; - } + get rule() { + return { ...this.#rule }; + } - // 插入译文节点 - traEl = document.createElement(APP_LCNAME); - traEl.style.visibility = "visible"; - // if (this._rule.transOnly === "true") { - // el.innerHTML = ""; - // } - el.appendChild(traEl); + get docInfo() { + return { ...this.#docInfo }; + } - // 渲染译文节点 - const root = createRoot(traEl); - root.render(); - - // 附加样式 - const { selectStyle, parentStyle } = this._rule; - el.style.cssText += selectStyle; - if (el.parentElement) { - el.parentElement.style.cssText += parentStyle; - } - }; + get eventName() { + return this.#eventName; + } } diff --git a/src/libs/utils.js b/src/libs/utils.js index 4cdfde4..eeed98d 100644 --- a/src/libs/utils.js +++ b/src/libs/utils.js @@ -177,7 +177,7 @@ export const sha256 = async (text, salt) => { * 生成随机事件名称 * @returns */ -export const genEventName = () => btoa(Math.random()).slice(3, 11); +export const genEventName = () => `kiss-${btoa(Math.random()).slice(3, 11)}`; /** * 判断两个 Set 是否相同 @@ -302,3 +302,16 @@ export const extractJson = (raw) => { const match = s.match(/\{[\s\S]*\}/); return match ? match[0] : "{}"; }; + +/** + * 空闲执行 + * @param {*} cb + * @param {*} timeout + * @returns + */ +export const scheduleIdle = (cb, timeout = 200) => { + if (window.requestIdleCallback) { + return requestIdleCallback(cb, { timeout }); + } + return setTimeout(cb, timeout); +}; diff --git a/src/views/Options/Apis.js b/src/views/Options/Apis.js index 860b0fe..bfc291b 100644 --- a/src/views/Options/Apis.js +++ b/src/views/Options/Apis.js @@ -560,7 +560,12 @@ function ApiAccordion({ translator }) { return ( }> - + {api.apiName ? `${translator} (${api.apiName})` : translator} diff --git a/src/views/Options/MouseHover.js b/src/views/Options/MouseHover.js new file mode 100644 index 0000000..4dc3714 --- /dev/null +++ b/src/views/Options/MouseHover.js @@ -0,0 +1,57 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import { useI18n } from "../../hooks/I18n"; +import ShortcutInput from "./ShortcutInput"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Switch from "@mui/material/Switch"; +import { useMouseHoverSetting } from "../../hooks/MouseHover"; +import { useCallback } from "react"; +import Grid from "@mui/material/Grid"; + +export default function MouseHoverSetting() { + const i18n = useI18n(); + const { mouseHoverSetting, updateMouseHoverSetting } = useMouseHoverSetting(); + + const handleShortcutInput = useCallback( + (val) => { + updateMouseHoverSetting({ mouseHoverKey: val }); + }, + [updateMouseHoverSetting] + ); + + const { useMouseHover = true, mouseHoverKey = ["ControlLeft"] } = + mouseHoverSetting; + + return ( + + + { + updateMouseHoverSetting({ useMouseHover: !useMouseHover }); + }} + /> + } + label={i18n("use_mousehover_translation")} + /> + + + + + + + + + + + ); +} diff --git a/src/views/Options/Navigator.js b/src/views/Options/Navigator.js index efa9336..bcdf36a 100644 --- a/src/views/Options/Navigator.js +++ b/src/views/Options/Navigator.js @@ -14,6 +14,7 @@ import ApiIcon from "@mui/icons-material/Api"; import InputIcon from "@mui/icons-material/Input"; import SelectAllIcon from "@mui/icons-material/SelectAll"; import EventNoteIcon from "@mui/icons-material/EventNote"; +import MouseIcon from '@mui/icons-material/Mouse'; function LinkItem({ label, url, icon }) { const match = useMatch(url); @@ -52,6 +53,12 @@ export default function Navigator(props) { url: "/tranbox", icon: , }, + { + id: "mousehover_translate", + label: i18n("mousehover_translate"), + url: "/mousehover", + icon: , + }, { id: "apis_setting", label: i18n("apis_setting"), diff --git a/src/views/Options/Rules.js b/src/views/Options/Rules.js index 558e58d..bdac01f 100644 --- a/src/views/Options/Rules.js +++ b/src/views/Options/Rules.js @@ -16,9 +16,7 @@ import { OPT_STYLE_USE_COLOR, URL_KISS_RULES_NEW_ISSUE, OPT_SYNCTYPE_WORKER, - OPT_TIMING_PAGESCROLL, DEFAULT_TRANS_TAG, - OPT_TIMING_ALL, } from "../../config"; import { useState, useEffect, useMemo } from "react"; import { useI18n } from "../../hooks/I18n"; @@ -55,7 +53,6 @@ import HelpButton from "./HelpButton"; import { useSyncCaches } from "../../hooks/Sync"; import DownloadButton from "./DownloadButton"; import UploadButton from "./UploadButton"; -import { FIXER_ALL } from "../../libs/webfix"; import AddIcon from "@mui/icons-material/Add"; import EditIcon from "@mui/icons-material/Edit"; import CancelIcon from "@mui/icons-material/Cancel"; @@ -78,6 +75,8 @@ function RuleFields({ rule, rules, setShow, setKeyword }) { pattern, selector, keepSelector = "", + rootsSelector = "", + ignoreSelector = "", terms = "", selectStyle = "", parentStyle = "", @@ -91,17 +90,20 @@ function RuleFields({ rule, rules, setShow, setKeyword }) { bgColor, textDiyStyle, transOnly = "false", - transTiming = OPT_TIMING_PAGESCROLL, + autoScan = "true", + hasRichText = "true", + hasShadowroot = "false", + // transTiming = OPT_TIMING_PAGESCROLL, transTag = DEFAULT_TRANS_TAG, transTitle = "false", transSelected = "true", detectRemote = "false", skipLangs = [], - fixerSelector = "", - fixerFunc = "-", + // fixerSelector = "", + // fixerFunc = "-", transStartHook = "", transEndHook = "", - transRemoveHook = "", + // transRemoveHook = "", } = formValues; const hasSamePattern = (str) => { @@ -236,7 +238,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) { helperText={errors.selector || i18n("selector_helper")} name="selector" value={selector} - disabled={disabled} + disabled={autoScan === "true" || disabled} onChange={handleChange} onFocus={handleFocus} multiline @@ -251,6 +253,26 @@ function RuleFields({ rule, rules, setShow, setKeyword }) { onChange={handleChange} multiline /> + + @@ -270,6 +292,126 @@ function RuleFields({ rule, rules, setShow, setKeyword }) { {i18n("default_disabled")} + + + {GlobalItem} + {i18n("disable")} + {i18n("enable")} + + + + + {GlobalItem} + {i18n("disable")} + {i18n("enable")} + + + + + {GlobalItem} + {i18n("disable")} + {i18n("enable")} + + + + + {GlobalItem} + {i18n("disable")} + {i18n("enable")} + + + + + {GlobalItem} + {i18n("disable")} + {i18n("enable")} + + + + + + + + {/* + + {GlobalItem} + {OPT_TIMING_ALL.map((item) => ( + + {i18n(item)} + + ))} + + */} + + + {GlobalItem} + {i18n("disable")} + {i18n("enable")} + + - - - {GlobalItem} - {i18n("disable")} - {i18n("enable")} - - - - - {GlobalItem} - {OPT_TIMING_ALL.map((item) => ( - - {i18n(item)} - - ))} - - {``} - - - {GlobalItem} - {i18n("disable")} - {i18n("enable")} - - - - - {GlobalItem} - {i18n("disable")} - {i18n("enable")} - - - ))} - + */} - - - - + + + + {/* */} + + {i18n("selected_translation_alert")} } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/views/Popup/index.js b/src/views/Popup/index.js index a360c05..5979e27 100644 --- a/src/views/Popup/index.js +++ b/src/views/Popup/index.js @@ -5,6 +5,7 @@ import MenuItem from "@mui/material/MenuItem"; import FormControlLabel from "@mui/material/FormControlLabel"; import Switch from "@mui/material/Switch"; import Button from "@mui/material/Button"; +import Grid from "@mui/material/Grid"; import { sendBgMsg, sendTabMsg, getCurTab } from "../../libs/msg"; import { browser } from "../../libs/browser"; import { isExt } from "../../libs/client"; @@ -30,6 +31,8 @@ import { saveRule } from "../../libs/rules"; import { tryClearCaches } from "../../libs"; import { kissLog } from "../../libs/log"; +// 插件popup没有参数 +// 网页弹框有 export default function Popup({ setShowPopup, translator: tran }) { const i18n = useI18n(); const [rule, setRule] = useState(tran?.rule); @@ -173,10 +176,20 @@ export default function Popup({ setShowPopup, translator: tran }) { ); } - const { transOpen, translator, fromLang, toLang, textStyle } = rule; + const { + transOpen, + translator, + fromLang, + toLang, + textStyle, + autoScan, + transOnly, + hasRichText, + hasShadowroot, + } = rule; return ( - + {!tran && ( <>
@@ -184,26 +197,79 @@ export default function Popup({ setShowPopup, translator: tran }) { )} - - - } - label={ - commands["toggleTranslate"] - ? `${i18n("translate_alt")}(${commands["toggleTranslate"]})` - : i18n("translate_alt") - } - /> - + + + + } + label={ + commands["toggleTranslate"] + ? `${i18n("translate_alt")}(${commands["toggleTranslate"]})` + : i18n("translate_alt") + } + /> + + + + } + label={i18n("autoscan_alt")} + /> + + + + } + label={i18n("shadowroot_alt")} + /> + + + + } + label={i18n("transonly_alt")} + /> + + + + } + label={i18n("richtext_alt")} + /> + +