Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
315164f142 | ||
|
|
429cab5223 | ||
|
|
deecbc874b | ||
|
|
504f4cafa0 | ||
|
|
6d327d17af | ||
|
|
74290eb52b | ||
|
|
60b9653fd3 | ||
|
|
53e32d3031 | ||
|
|
ed279cf8a1 | ||
|
|
296b898bda | ||
|
|
2325155b1e | ||
|
|
b6ff4aae6a | ||
|
|
f04002fdb8 | ||
|
|
b8cb254f56 | ||
|
|
3983477904 | ||
|
|
f011f5ae38 | ||
|
|
18ecab18df | ||
|
|
793c481221 | ||
|
|
4fee3688ea |
2
.env
2
.env
@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
|
|||||||
|
|
||||||
REACT_APP_NAME=KISS Translator
|
REACT_APP_NAME=KISS Translator
|
||||||
REACT_APP_NAME_CN=简约翻译
|
REACT_APP_NAME_CN=简约翻译
|
||||||
REACT_APP_VERSION=2.0.2
|
REACT_APP_VERSION=2.0.3
|
||||||
|
|
||||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ async ({ res }) => {
|
|||||||
v2.0.2 版后内置`parseAIRes`函数,Response Hook 可以简化为:
|
v2.0.2 版后内置`parseAIRes`函数,Response Hook 可以简化为:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
async ({ res, parseAIRes, }) => {
|
async ({ res, parseAIRes }) => {
|
||||||
const translations = parseAIRes(res?.choices?.[0]?.message?.content);
|
const translations = parseAIRes(res?.choices?.[0]?.message?.content);
|
||||||
return { translations };
|
return { translations };
|
||||||
};
|
};
|
||||||
@@ -186,7 +186,7 @@ async (args) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: `Translate the following source text from to ${args.to}. Output translation directly without any additional text.\n\nSource Text: ${args.texts[0]}\n\nTranslated Text:`,
|
content: `Translate the following source text to ${args.to}. Output translation directly without any additional text.\n\nSource Text: ${args.texts[0]}\n\nTranslated Text:`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
@@ -204,3 +204,53 @@ async ({ res }) => {
|
|||||||
return { translations: [[res?.choices?.[0]?.message?.content || ""]] };
|
return { translations: [[res?.choices?.[0]?.message?.content || ""]] };
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 语言代码表及说明
|
||||||
|
|
||||||
|
Hook参数里面的语言含义说明:
|
||||||
|
|
||||||
|
- `toLang`, `fromLang` 是本插件支持的标准语言代码
|
||||||
|
- `to`, `from` 是转换后的适用于特定接口的语言代码
|
||||||
|
|
||||||
|
如果你的自定义接口与下面的标准语言代码不匹配,需要自行映射转换。
|
||||||
|
|
||||||
|
```
|
||||||
|
["en", "English - English"],
|
||||||
|
["zh-CN", "Simplified Chinese - 简体中文"],
|
||||||
|
["zh-TW", "Traditional Chinese - 繁體中文"],
|
||||||
|
["ar", "Arabic - العربية"],
|
||||||
|
["bg", "Bulgarian - Български"],
|
||||||
|
["ca", "Catalan - Català"],
|
||||||
|
["hr", "Croatian - Hrvatski"],
|
||||||
|
["cs", "Czech - Čeština"],
|
||||||
|
["da", "Danish - Dansk"],
|
||||||
|
["nl", "Dutch - Nederlands"],
|
||||||
|
["fi", "Finnish - Suomi"],
|
||||||
|
["fr", "French - Français"],
|
||||||
|
["de", "German - Deutsch"],
|
||||||
|
["el", "Greek - Ελληνικά"],
|
||||||
|
["hi", "Hindi - हिन्दी"],
|
||||||
|
["hu", "Hungarian - Magyar"],
|
||||||
|
["id", "Indonesian - Indonesia"],
|
||||||
|
["it", "Italian - Italiano"],
|
||||||
|
["ja", "Japanese - 日本語"],
|
||||||
|
["ko", "Korean - 한국어"],
|
||||||
|
["ms", "Malay - Melayu"],
|
||||||
|
["mt", "Maltese - Malti"],
|
||||||
|
["nb", "Norwegian - Norsk Bokmål"],
|
||||||
|
["pl", "Polish - Polski"],
|
||||||
|
["pt", "Portuguese - Português"],
|
||||||
|
["ro", "Romanian - Română"],
|
||||||
|
["ru", "Russian - Русский"],
|
||||||
|
["sk", "Slovak - Slovenčina"],
|
||||||
|
["sl", "Slovenian - Slovenščina"],
|
||||||
|
["es", "Spanish - Español"],
|
||||||
|
["sv", "Swedish - Svenska"],
|
||||||
|
["ta", "Tamil - தமிழ்"],
|
||||||
|
["te", "Telugu - తెలుగు"],
|
||||||
|
["th", "Thai - ไทย"],
|
||||||
|
["tr", "Turkish - Türkçe"],
|
||||||
|
["uk", "Ukrainian - Українська"],
|
||||||
|
["vi", "Vietnamese - Tiếng Việt"],
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kiss-translator",
|
"name": "kiss-translator",
|
||||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
|
|||||||
@@ -919,7 +919,7 @@ export const handleTranslate = async (
|
|||||||
httpTimeout,
|
httpTimeout,
|
||||||
});
|
});
|
||||||
if (!response) {
|
if (!response) {
|
||||||
throw new Error("tranlate got empty response");
|
throw new Error("translate got empty response");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await parseTransRes(response, {
|
const result = await parseTransRes(response, {
|
||||||
@@ -934,7 +934,7 @@ export const handleTranslate = async (
|
|||||||
...apiSetting,
|
...apiSetting,
|
||||||
});
|
});
|
||||||
if (!result?.length) {
|
if (!result?.length) {
|
||||||
throw new Error("tranlate got an unexpected result");
|
throw new Error("translate got an unexpected result");
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
138
src/common.js
138
src/common.js
@@ -1,19 +1,11 @@
|
|||||||
import React from "react";
|
import { OPT_HIGHLIGHT_WORDS_DISABLE } from "./config";
|
||||||
import ReactDOM from "react-dom/client";
|
|
||||||
import Action from "./views/Action";
|
|
||||||
import createCache from "@emotion/cache";
|
|
||||||
import { CacheProvider } from "@emotion/react";
|
|
||||||
import {
|
import {
|
||||||
MSG_TRANS_TOGGLE,
|
getFabWithDefault,
|
||||||
MSG_TRANS_TOGGLE_STYLE,
|
getSettingWithDefault,
|
||||||
MSG_TRANS_PUTRULE,
|
getWordsWithDefault,
|
||||||
APP_CONSTS,
|
} from "./libs/storage";
|
||||||
} from "./config";
|
import { isIframe } from "./libs/iframe";
|
||||||
import { getFabWithDefault, getSettingWithDefault } from "./libs/storage";
|
import { genEventName } from "./libs/utils";
|
||||||
import { Translator } from "./libs/translator";
|
|
||||||
import { isIframe, sendIframeMsg } from "./libs/iframe";
|
|
||||||
import { touchTapListener } from "./libs/touch";
|
|
||||||
import { debounce, genEventName } from "./libs/utils";
|
|
||||||
import { handlePing, injectScript } from "./libs/gm";
|
import { handlePing, injectScript } from "./libs/gm";
|
||||||
import { matchRule } from "./libs/rules";
|
import { matchRule } from "./libs/rules";
|
||||||
import { trySyncAllSubRules } from "./libs/subRules";
|
import { trySyncAllSubRules } from "./libs/subRules";
|
||||||
@@ -21,6 +13,7 @@ import { isInBlacklist } from "./libs/blacklist";
|
|||||||
import { runSubtitle } from "./subtitle/subtitle";
|
import { runSubtitle } from "./subtitle/subtitle";
|
||||||
import { logger } from "./libs/log";
|
import { logger } from "./libs/log";
|
||||||
import { injectInlineJs } from "./libs/injector";
|
import { injectInlineJs } from "./libs/injector";
|
||||||
|
import TranslatorManager from "./libs/translatorManager";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 油猴脚本设置页面
|
* 油猴脚本设置页面
|
||||||
@@ -43,62 +36,6 @@ function runSettingPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* iframe 页面执行
|
|
||||||
* @param {*} translator
|
|
||||||
*/
|
|
||||||
function runIframe(translator) {
|
|
||||||
window.addEventListener("message", (e) => {
|
|
||||||
const { action, args } = e.data || {};
|
|
||||||
switch (action) {
|
|
||||||
case MSG_TRANS_TOGGLE:
|
|
||||||
translator?.toggle();
|
|
||||||
break;
|
|
||||||
case MSG_TRANS_TOGGLE_STYLE:
|
|
||||||
translator?.toggleStyle();
|
|
||||||
break;
|
|
||||||
case MSG_TRANS_PUTRULE:
|
|
||||||
translator.updateRule(args || {});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 悬浮按钮
|
|
||||||
* @param {*} translator
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async function showFab(translator) {
|
|
||||||
const fab = await getFabWithDefault();
|
|
||||||
const $action = document.createElement("div");
|
|
||||||
$action.id = APP_CONSTS.fabID;
|
|
||||||
$action.className = "notranslate";
|
|
||||||
$action.style.fontSize = "0";
|
|
||||||
$action.style.width = "0";
|
|
||||||
$action.style.height = "0";
|
|
||||||
document.body.parentElement.appendChild($action);
|
|
||||||
const shadowContainer = $action.attachShadow({ mode: "closed" });
|
|
||||||
const emotionRoot = document.createElement("style");
|
|
||||||
const shadowRootElement = document.createElement("div");
|
|
||||||
shadowRootElement.className = `${APP_CONSTS.fabID}_warpper notranslate`;
|
|
||||||
shadowContainer.appendChild(emotionRoot);
|
|
||||||
shadowContainer.appendChild(shadowRootElement);
|
|
||||||
const cache = createCache({
|
|
||||||
key: APP_CONSTS.fabID,
|
|
||||||
prepend: true,
|
|
||||||
container: emotionRoot,
|
|
||||||
});
|
|
||||||
ReactDOM.createRoot(shadowRootElement).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<CacheProvider value={cache}>
|
|
||||||
<Action translator={translator} fab={fab} />
|
|
||||||
</CacheProvider>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示错误信息到页面顶部
|
* 显示错误信息到页面顶部
|
||||||
* @param {*} message
|
* @param {*} message
|
||||||
@@ -161,22 +98,19 @@ function showErr(message) {
|
|||||||
setTimeout(removeBanner, 10000);
|
setTimeout(removeBanner, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async function getFavWords(rule) {
|
||||||
* 监听触屏操作
|
if (
|
||||||
* @param {*} translator
|
rule.highlightWords &&
|
||||||
* @returns
|
rule.highlightWords !== OPT_HIGHLIGHT_WORDS_DISABLE
|
||||||
*/
|
) {
|
||||||
function touchOperation(translator) {
|
try {
|
||||||
const { touchTranslate = 2 } = translator.setting;
|
return Object.keys(await getWordsWithDefault());
|
||||||
if (touchTranslate === 0) {
|
} catch (err) {
|
||||||
return;
|
logger.info("get fav words", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTap = debounce(() => {
|
return [];
|
||||||
translator.toggle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
|
||||||
});
|
|
||||||
touchTapListener(handleTap, touchTranslate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -209,34 +143,28 @@ export async function run(isUserscript = false) {
|
|||||||
|
|
||||||
// 翻译网页
|
// 翻译网页
|
||||||
const rule = await matchRule(href, setting);
|
const rule = await matchRule(href, setting);
|
||||||
const translator = new Translator(rule, setting, isUserscript);
|
const favWords = await getFavWords(rule);
|
||||||
|
const fabConfig = await getFabWithDefault();
|
||||||
|
const translatorManager = new TranslatorManager({
|
||||||
|
setting,
|
||||||
|
rule,
|
||||||
|
fabConfig,
|
||||||
|
favWords,
|
||||||
|
isIframe,
|
||||||
|
isUserscript,
|
||||||
|
});
|
||||||
|
translatorManager.start();
|
||||||
|
|
||||||
// 适配iframe
|
|
||||||
if (isIframe) {
|
if (isIframe) {
|
||||||
runIframe(translator);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 字幕翻译
|
// 字幕翻译
|
||||||
runSubtitle({ href, setting, rule, isUserscript });
|
runSubtitle({ href, setting, rule, isUserscript });
|
||||||
|
|
||||||
// 监听消息
|
if (isUserscript) {
|
||||||
// !isUserscript && runtimeListener(translator);
|
trySyncAllSubRules(setting);
|
||||||
|
}
|
||||||
// 输入框翻译
|
|
||||||
// inputTranslate(setting);
|
|
||||||
|
|
||||||
// 划词翻译
|
|
||||||
// showTransbox(setting, rule);
|
|
||||||
|
|
||||||
// 浮球按钮
|
|
||||||
await showFab(translator);
|
|
||||||
|
|
||||||
// 触屏操作
|
|
||||||
touchOperation(translator);
|
|
||||||
|
|
||||||
// 同步订阅规则
|
|
||||||
isUserscript && (await trySyncAllSubRules(setting));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[KISS-Translator]", err);
|
console.error("[KISS-Translator]", err);
|
||||||
showErr(err.message);
|
showErr(err.message);
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ export const APP_NAME = process.env.REACT_APP_NAME.trim()
|
|||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.join("-");
|
.join("-");
|
||||||
export const APP_LCNAME = APP_NAME.toLowerCase();
|
export const APP_LCNAME = APP_NAME.toLowerCase();
|
||||||
|
export const APP_UPNAME = APP_NAME.toUpperCase();
|
||||||
export const APP_CONSTS = {
|
export const APP_CONSTS = {
|
||||||
fabID: `${APP_LCNAME}-fab`,
|
fabID: `${APP_LCNAME}-fab`,
|
||||||
boxID: `${APP_LCNAME}-box`,
|
boxID: `${APP_LCNAME}-box`,
|
||||||
|
popupID: `${APP_LCNAME}-popup`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const APP_VERSION = process.env.REACT_APP_VERSION.split(".");
|
export const APP_VERSION = process.env.REACT_APP_VERSION.split(".");
|
||||||
|
|||||||
@@ -156,13 +156,13 @@ async (args, { url, body, headers, userMsg, method } = {}) => {
|
|||||||
|
|
||||||
const responsetHookHelperZH = `1、第一个参数包含如下字段:'res', ...
|
const responsetHookHelperZH = `1、第一个参数包含如下字段:'res', ...
|
||||||
2、返回值必须是包含以下字段的对象: 'translations'
|
2、返回值必须是包含以下字段的对象: 'translations'
|
||||||
('translations' 应为一个二维数组:[[译文, 源语言]])
|
('translations' 应为一个二维数组:[[译文, 原文语言]])
|
||||||
3、如返回空值,则hook函数不会产生任何效果。
|
3、如返回空值,则hook函数不会产生任何效果。
|
||||||
|
|
||||||
// 示例
|
// 示例
|
||||||
async ({ res, ...args }) => {
|
async ({ res, ...args }) => {
|
||||||
const translations = [["你好", "zh"]];
|
const translations = [["你好", "en"]];
|
||||||
const modelMsg = "";
|
const modelMsg = {}; // 用于AI上下文
|
||||||
return { translations, modelMsg };
|
return { translations, modelMsg };
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
@@ -173,8 +173,8 @@ const responsetHookHelperEN = `1. The first parameter contains the following fie
|
|||||||
|
|
||||||
// Example
|
// Example
|
||||||
async ({ res, ...args }) => {
|
async ({ res, ...args }) => {
|
||||||
const translations = [["你好", "zh"]];
|
const translations = [["你好", "en"]];
|
||||||
const modelMsg = "";
|
const modelMsg = {}; // For AI context
|
||||||
return { translations, modelMsg };
|
return { translations, modelMsg };
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
@@ -531,9 +531,9 @@ export const I18N = {
|
|||||||
zh_TW: `2.大部分AI介面都與OpenAI相容,因此選擇新增OpenAI類型即可。`,
|
zh_TW: `2.大部分AI介面都與OpenAI相容,因此選擇新增OpenAI類型即可。`,
|
||||||
},
|
},
|
||||||
about_api_3: {
|
about_api_3: {
|
||||||
zh: `2、暂未列出的接口,理论上都可以通过自定义接口 (Custom) 的形式支持。`,
|
zh: `3、暂未列出的接口,理论上都可以通过自定义接口 (Custom) 的形式支持。`,
|
||||||
en: `2. Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`,
|
en: `3. Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`,
|
||||||
zh_TW: `2、暫未列出的介面,理論上都可透過自訂介面 (Custom) 的形式支援。`,
|
zh_TW: `3、暫未列出的介面,理論上都可透過自訂介面 (Custom) 的形式支援。`,
|
||||||
},
|
},
|
||||||
about_api_proxy: {
|
about_api_proxy: {
|
||||||
zh: `查看自建一个翻译接口代理`,
|
zh: `查看自建一个翻译接口代理`,
|
||||||
@@ -719,6 +719,11 @@ export const I18N = {
|
|||||||
en: `Terms Style`,
|
en: `Terms Style`,
|
||||||
zh_TW: `專業術語樣式`,
|
zh_TW: `專業術語樣式`,
|
||||||
},
|
},
|
||||||
|
highlight_style: {
|
||||||
|
zh: `词汇高亮样式`,
|
||||||
|
en: `Fav Words highlight style`,
|
||||||
|
zh_TW: `詞彙高亮樣式`,
|
||||||
|
},
|
||||||
selector_style_helper: {
|
selector_style_helper: {
|
||||||
zh: `开启翻译时注入。`,
|
zh: `开启翻译时注入。`,
|
||||||
en: `It is injected when translation is turned on.`,
|
en: `It is injected when translation is turned on.`,
|
||||||
@@ -1179,6 +1184,21 @@ export const I18N = {
|
|||||||
en: `Four finger tap`,
|
en: `Four finger tap`,
|
||||||
zh_TW: `四指輕觸`,
|
zh_TW: `四指輕觸`,
|
||||||
},
|
},
|
||||||
|
touch_tap_5: {
|
||||||
|
zh: `单指双击`,
|
||||||
|
en: `Double-click`,
|
||||||
|
zh_TW: `單指雙擊`,
|
||||||
|
},
|
||||||
|
touch_tap_6: {
|
||||||
|
zh: `单指三击`,
|
||||||
|
en: `Triple-click`,
|
||||||
|
zh_TW: `單指三擊`,
|
||||||
|
},
|
||||||
|
touch_tap_7: {
|
||||||
|
zh: `双指双击`,
|
||||||
|
en: `Two-finger double-click`,
|
||||||
|
zh_TW: `雙指雙擊`,
|
||||||
|
},
|
||||||
translate_blacklist: {
|
translate_blacklist: {
|
||||||
zh: `禁用翻译名单`,
|
zh: `禁用翻译名单`,
|
||||||
en: `Translate Blacklist`,
|
en: `Translate Blacklist`,
|
||||||
@@ -1669,6 +1689,52 @@ export const I18N = {
|
|||||||
en: `Click to view [Custom Interface Example]`,
|
en: `Click to view [Custom Interface Example]`,
|
||||||
zh_TW: `點選查看【自訂介面範例】`,
|
zh_TW: `點選查看【自訂介面範例】`,
|
||||||
},
|
},
|
||||||
|
split_paragraph: {
|
||||||
|
zh: `切分长段落`,
|
||||||
|
en: `Split long paragraph`,
|
||||||
|
zh_TW: `切分長段落`,
|
||||||
|
},
|
||||||
|
split_length: {
|
||||||
|
zh: `切分长度 (0-10000)`,
|
||||||
|
en: `Segmentation length(0-10000)`,
|
||||||
|
zh_TW: `切分長度(0-10000)`,
|
||||||
|
},
|
||||||
|
highlight_words: {
|
||||||
|
zh: `高亮收藏词汇`,
|
||||||
|
en: `Highlight favorite words`,
|
||||||
|
zh_TW: `高亮收藏詞彙`,
|
||||||
|
},
|
||||||
|
|
||||||
|
split_disable: {
|
||||||
|
zh: `禁用`,
|
||||||
|
en: `Disable`,
|
||||||
|
zh_TW: `停用`,
|
||||||
|
},
|
||||||
|
split_textlength: {
|
||||||
|
zh: `按照长度切分`,
|
||||||
|
en: `Split by length`,
|
||||||
|
zh_TW: `依長度切分`,
|
||||||
|
},
|
||||||
|
split_punctuation: {
|
||||||
|
zh: `按照句子切分`,
|
||||||
|
en: `Split by sentence`,
|
||||||
|
zh_TW: `按照句子切分`,
|
||||||
|
},
|
||||||
|
highlight_disable: {
|
||||||
|
zh: `禁用`,
|
||||||
|
en: `Disable`,
|
||||||
|
zh_TW: `停用`,
|
||||||
|
},
|
||||||
|
highlight_beforetrans: {
|
||||||
|
zh: `翻译前高亮`,
|
||||||
|
en: `Highlight before translation`,
|
||||||
|
zh_TW: `翻譯前高亮`,
|
||||||
|
},
|
||||||
|
highlight_aftertrans: {
|
||||||
|
zh: `翻译后高亮`,
|
||||||
|
en: `Highlight after translation`,
|
||||||
|
zh_TW: `翻譯後高亮`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const i18n = (lang) => (key) => I18N[key]?.[lang] || "";
|
export const newI18n = (lang) => (key) => I18N[key]?.[lang] || "";
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const MSG_TRANS_GETRULE = "trans_getrule";
|
|||||||
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
||||||
export const MSG_TRANS_CURRULE = "trans_currule";
|
export const MSG_TRANS_CURRULE = "trans_currule";
|
||||||
export const MSG_TRANSBOX_TOGGLE = "transbox_toggle";
|
export const MSG_TRANSBOX_TOGGLE = "transbox_toggle";
|
||||||
|
export const MSG_POPUP_TOGGLE = "popup_toggle";
|
||||||
export const MSG_MOUSEHOVER_TOGGLE = "mousehover_toggle";
|
export const MSG_MOUSEHOVER_TOGGLE = "mousehover_toggle";
|
||||||
export const MSG_TRANSINPUT_TOGGLE = "transinput_toggle";
|
export const MSG_TRANSINPUT_TOGGLE = "transinput_toggle";
|
||||||
export const MSG_CONTEXT_MENUS = "context_menus";
|
export const MSG_CONTEXT_MENUS = "context_menus";
|
||||||
@@ -27,6 +28,8 @@ export const MSG_BUILTINAI_TRANSLATE = "builtinai_translte";
|
|||||||
export const MSG_SET_LOGLEVEL = "set_loglevel";
|
export const MSG_SET_LOGLEVEL = "set_loglevel";
|
||||||
export const MSG_CLEAR_CACHES = "clear_caches";
|
export const MSG_CLEAR_CACHES = "clear_caches";
|
||||||
|
|
||||||
|
export const EVENT_KISS = "event_kiss_translate";
|
||||||
|
|
||||||
export const MSG_XHR_DATA_YOUTUBE = "KISS_XHR_DATA_YOUTUBE";
|
export const MSG_XHR_DATA_YOUTUBE = "KISS_XHR_DATA_YOUTUBE";
|
||||||
// export const MSG_GLOBAL_VAR_FETCH = "KISS_GLOBAL_VAR_FETCH";
|
// export const MSG_GLOBAL_VAR_FETCH = "KISS_GLOBAL_VAR_FETCH";
|
||||||
// export const MSG_GLOBAL_VAR_BACK = "KISS_GLOBAL_VAR_BACK";
|
// export const MSG_GLOBAL_VAR_BACK = "KISS_GLOBAL_VAR_BACK";
|
||||||
|
|||||||
@@ -63,6 +63,24 @@ export const OPT_TIMING_ALL = [
|
|||||||
OPT_TIMING_ALT,
|
OPT_TIMING_ALT,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const OPT_SPLIT_PARAGRAPH_DISABLE = "split_disable";
|
||||||
|
export const OPT_SPLIT_PARAGRAPH_TEXTLENGTH = "split_textlength";
|
||||||
|
export const OPT_SPLIT_PARAGRAPH_PUNCTUATION = "split_punctuation";
|
||||||
|
export const OPT_SPLIT_PARAGRAPH_ALL = [
|
||||||
|
OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||||
|
OPT_SPLIT_PARAGRAPH_PUNCTUATION,
|
||||||
|
OPT_SPLIT_PARAGRAPH_TEXTLENGTH,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const OPT_HIGHLIGHT_WORDS_DISABLE = "highlight_disable";
|
||||||
|
export const OPT_HIGHLIGHT_WORDS_BEFORETRANS = "highlight_beforetrans";
|
||||||
|
export const OPT_HIGHLIGHT_WORDS_AFTERTRANS = "highlight_aftertrans";
|
||||||
|
export const OPT_HIGHLIGHT_WORDS_ALL = [
|
||||||
|
OPT_HIGHLIGHT_WORDS_DISABLE,
|
||||||
|
OPT_HIGHLIGHT_WORDS_BEFORETRANS,
|
||||||
|
OPT_HIGHLIGHT_WORDS_AFTERTRANS,
|
||||||
|
];
|
||||||
|
|
||||||
export const DEFAULT_DIY_STYLE = `color: #333;
|
export const DEFAULT_DIY_STYLE = `color: #333;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
45deg,
|
45deg,
|
||||||
@@ -94,6 +112,7 @@ export const DEFAULT_RULE = {
|
|||||||
bgColor: "", // 译文颜色
|
bgColor: "", // 译文颜色
|
||||||
textDiyStyle: "", // 自定义译文样式
|
textDiyStyle: "", // 自定义译文样式
|
||||||
termsStyle: "", // 专业术语样式
|
termsStyle: "", // 专业术语样式
|
||||||
|
highlightStyle: "", // 高亮词汇样式
|
||||||
selectStyle: "", // 选择器节点样式
|
selectStyle: "", // 选择器节点样式
|
||||||
parentStyle: "", // 选择器父节点样式
|
parentStyle: "", // 选择器父节点样式
|
||||||
grandStyle: "", // 选择器父节点样式
|
grandStyle: "", // 选择器父节点样式
|
||||||
@@ -116,6 +135,9 @@ export const DEFAULT_RULE = {
|
|||||||
hasShadowroot: GLOBAL_KEY, // 是否包含shadowroot
|
hasShadowroot: GLOBAL_KEY, // 是否包含shadowroot
|
||||||
rootsSelector: "", // 翻译范围选择器
|
rootsSelector: "", // 翻译范围选择器
|
||||||
ignoreSelector: "", // 不翻译的选择器
|
ignoreSelector: "", // 不翻译的选择器
|
||||||
|
splitParagraph: GLOBAL_KEY, // 切分段落
|
||||||
|
splitLength: 0, // 切分段落长度
|
||||||
|
highlightWords: GLOBAL_KEY, // 高亮词汇
|
||||||
};
|
};
|
||||||
|
|
||||||
// 全局规则
|
// 全局规则
|
||||||
@@ -133,6 +155,7 @@ export const GLOBLA_RULE = {
|
|||||||
bgColor: "", // 译文颜色
|
bgColor: "", // 译文颜色
|
||||||
textDiyStyle: DEFAULT_DIY_STYLE, // 自定义译文样式
|
textDiyStyle: DEFAULT_DIY_STYLE, // 自定义译文样式
|
||||||
termsStyle: "font-weight: bold;", // 专业术语样式
|
termsStyle: "font-weight: bold;", // 专业术语样式
|
||||||
|
highlightStyle: "color: red;", // 高亮词汇样式
|
||||||
selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
|
selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
|
||||||
parentStyle: DEFAULT_SELECT_STYLE, // 选择器父节点样式
|
parentStyle: DEFAULT_SELECT_STYLE, // 选择器父节点样式
|
||||||
grandStyle: DEFAULT_SELECT_STYLE, // 选择器祖节点样式
|
grandStyle: DEFAULT_SELECT_STYLE, // 选择器祖节点样式
|
||||||
@@ -155,6 +178,9 @@ export const GLOBLA_RULE = {
|
|||||||
hasShadowroot: "false", // 是否包含shadowroot
|
hasShadowroot: "false", // 是否包含shadowroot
|
||||||
rootsSelector: "body", // 翻译范围选择器
|
rootsSelector: "body", // 翻译范围选择器
|
||||||
ignoreSelector: DEFAULT_IGNORE_SELECTOR, // 不翻译的选择器
|
ignoreSelector: DEFAULT_IGNORE_SELECTOR, // 不翻译的选择器
|
||||||
|
splitParagraph: OPT_SPLIT_PARAGRAPH_DISABLE, // 切分段落
|
||||||
|
splitLength: 100, // 切分段落长度
|
||||||
|
highlightWords: OPT_HIGHLIGHT_WORDS_DISABLE, // 高亮词汇
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||||
@@ -171,15 +197,14 @@ export const DEFAULT_OW_RULE = {
|
|||||||
|
|
||||||
// todo: 校验几个内置规则
|
// todo: 校验几个内置规则
|
||||||
const RULES_MAP = {
|
const RULES_MAP = {
|
||||||
"www.google.com/search": {
|
// "www.google.com/search": {
|
||||||
rootsSelector: `#rcnt`,
|
// rootsSelector: `#rcnt`,
|
||||||
},
|
// },
|
||||||
"en.wikipedia.org": {
|
"en.wikipedia.org": {
|
||||||
ignoreSelector: `.button, code, footer, form, mark, pre, .mwe-math-element, .mw-editsection`,
|
ignoreSelector: `.button, code, footer, form, mark, pre, .mwe-math-element, .mw-editsection`,
|
||||||
},
|
},
|
||||||
"news.ycombinator.com": {
|
"news.ycombinator.com": {
|
||||||
selector: `p, .titleline, .commtext`,
|
selector: `p, .titleline, .commtext, .hn-item-title, .hn-comment-text, .hn-story-title`,
|
||||||
rootsSelector: `#bigbox`,
|
|
||||||
keepSelector: `code, img, svg, pre, .sitebit`,
|
keepSelector: `code, img, svg, pre, .sitebit`,
|
||||||
ignoreSelector: `button, code, footer, form, header, mark, nav, pre, .reply`,
|
ignoreSelector: `button, code, footer, form, header, mark, nav, pre, .reply`,
|
||||||
autoScan: `false`,
|
autoScan: `false`,
|
||||||
@@ -189,21 +214,21 @@ const RULES_MAP = {
|
|||||||
keepSelector: `img, svg, span:has(a), div:has(a)`,
|
keepSelector: `img, svg, span:has(a), div:has(a)`,
|
||||||
autoScan: `false`,
|
autoScan: `false`,
|
||||||
},
|
},
|
||||||
"www.youtube.com": {
|
|
||||||
rootsSelector: `ytd-page-manager`,
|
|
||||||
ignoreSelector: `aside, button, footer, form, header, pre, mark, nav, #player, #container, .caption-window, .ytp-settings-menu`,
|
|
||||||
},
|
|
||||||
"www.youtube.com/live_chat": {
|
"www.youtube.com/live_chat": {
|
||||||
rootsSelector: `div#items`,
|
rootsSelector: `div#items`,
|
||||||
selector: `span.yt-live-chat-text-message-renderer`,
|
selector: `span.yt-live-chat-text-message-renderer`,
|
||||||
autoScan: `false`,
|
autoScan: `false`,
|
||||||
},
|
},
|
||||||
|
"www.youtube.com": {
|
||||||
|
rootsSelector: `ytd-page-manager`,
|
||||||
|
ignoreSelector: `aside, button, footer, form, header, pre, mark, nav, #player, #container, .caption-window, .ytp-settings-menu`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BUILTIN_RULES = Object.entries(RULES_MAP)
|
export const BUILTIN_RULES = Object.entries(RULES_MAP).map(
|
||||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
([pattern, rule]) => ({
|
||||||
.map(([pattern, rule]) => ({
|
|
||||||
// ...DEFAULT_RULE,
|
// ...DEFAULT_RULE,
|
||||||
...rule,
|
...rule,
|
||||||
pattern,
|
pattern,
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export const DEFAULT_SETTING = {
|
|||||||
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
||||||
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
||||||
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
|
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
|
||||||
touchTranslate: 2, // 触屏翻译
|
touchTranslate: 2, // 触屏翻译 {5:单指双击,6:单指三击,7:双指双击}
|
||||||
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
|
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
|
||||||
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
|
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
|
||||||
orilist: DEFAULT_ORILIST.join(",\n"), // 禁用CSP名单
|
orilist: DEFAULT_ORILIST.join(",\n"), // 禁用CSP名单
|
||||||
|
|||||||
29
src/hooks/WindowSize.js
Normal file
29
src/hooks/WindowSize.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useDebouncedCallback } from "./DebouncedCallback";
|
||||||
|
|
||||||
|
function useWindowSize() {
|
||||||
|
const [windowSize, setWindowSize] = useState({
|
||||||
|
w: window.innerWidth,
|
||||||
|
h: window.innerHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
const debounceWindowResize = useDebouncedCallback(() => {
|
||||||
|
setWindowSize({
|
||||||
|
w: window.innerWidth,
|
||||||
|
h: window.innerHeight,
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debounceWindowResize();
|
||||||
|
|
||||||
|
window.addEventListener("resize", debounceWindowResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", debounceWindowResize);
|
||||||
|
};
|
||||||
|
}, [debounceWindowResize]);
|
||||||
|
|
||||||
|
return windowSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useWindowSize;
|
||||||
18
src/libs/fabManager.js
Normal file
18
src/libs/fabManager.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import ShadowDomManager from "./shadowDomManager";
|
||||||
|
import { APP_CONSTS } from "../config";
|
||||||
|
import ContentFab from "../views/Action/ContentFab";
|
||||||
|
|
||||||
|
export class FabManager extends ShadowDomManager {
|
||||||
|
constructor({ translator, processActions, fabConfig }) {
|
||||||
|
super({
|
||||||
|
id: APP_CONSTS.fabID,
|
||||||
|
className: "notranslate",
|
||||||
|
reactComponent: ContentFab,
|
||||||
|
props: { translator, processActions, fabConfig },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fabConfig?.isHide) {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/libs/popupManager.js
Normal file
26
src/libs/popupManager.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import ShadowDomManager from "./shadowDomManager";
|
||||||
|
import { APP_CONSTS, EVENT_KISS, MSG_POPUP_TOGGLE } from "../config";
|
||||||
|
import Action from "../views/Action";
|
||||||
|
|
||||||
|
export class PopupManager extends ShadowDomManager {
|
||||||
|
constructor({ translator, processActions }) {
|
||||||
|
super({
|
||||||
|
id: APP_CONSTS.popupID,
|
||||||
|
className: "notranslate",
|
||||||
|
reactComponent: Action,
|
||||||
|
props: { translator, processActions },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(props) {
|
||||||
|
if (this.isVisible) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(EVENT_KISS, {
|
||||||
|
detail: { action: MSG_POPUP_TOGGLE },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.show(props || this._props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
// OPT_TIMING_ALL,
|
// OPT_TIMING_ALL,
|
||||||
DEFAULT_RULE,
|
DEFAULT_RULE,
|
||||||
GLOBLA_RULE,
|
GLOBLA_RULE,
|
||||||
|
OPT_SPLIT_PARAGRAPH_ALL,
|
||||||
|
OPT_HIGHLIGHT_WORDS_ALL,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { loadOrFetchSubRules } from "./subRules";
|
import { loadOrFetchSubRules } from "./subRules";
|
||||||
import { getRulesWithDefault, setRules } from "./storage";
|
import { getRulesWithDefault, setRules } from "./storage";
|
||||||
@@ -53,6 +55,7 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
|
|||||||
"terms",
|
"terms",
|
||||||
"aiTerms",
|
"aiTerms",
|
||||||
"termsStyle",
|
"termsStyle",
|
||||||
|
"highlightStyle",
|
||||||
"selectStyle",
|
"selectStyle",
|
||||||
"parentStyle",
|
"parentStyle",
|
||||||
"grandStyle",
|
"grandStyle",
|
||||||
@@ -82,12 +85,20 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
|
|||||||
"transTitle",
|
"transTitle",
|
||||||
// "detectRemote",
|
// "detectRemote",
|
||||||
// "fixerFunc",
|
// "fixerFunc",
|
||||||
|
"splitParagraph",
|
||||||
|
"highlightWords",
|
||||||
].forEach((key) => {
|
].forEach((key) => {
|
||||||
if (!rule[key] || rule[key] === GLOBAL_KEY) {
|
if (!rule[key] || rule[key] === GLOBAL_KEY) {
|
||||||
rule[key] = globalRule[key];
|
rule[key] = globalRule[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
["splitLength"].forEach((key) => {
|
||||||
|
if (!rule[key]) {
|
||||||
|
rule[key] = globalRule[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// if (!rule.skipLangs || rule.skipLangs.length === 0) {
|
// if (!rule.skipLangs || rule.skipLangs.length === 0) {
|
||||||
// rule.skipLangs = globalRule.skipLangs;
|
// rule.skipLangs = globalRule.skipLangs;
|
||||||
// }
|
// }
|
||||||
@@ -138,6 +149,7 @@ export const checkRules = (rules) => {
|
|||||||
terms,
|
terms,
|
||||||
aiTerms,
|
aiTerms,
|
||||||
termsStyle,
|
termsStyle,
|
||||||
|
highlightStyle,
|
||||||
selectStyle,
|
selectStyle,
|
||||||
parentStyle,
|
parentStyle,
|
||||||
grandStyle,
|
grandStyle,
|
||||||
@@ -164,6 +176,9 @@ export const checkRules = (rules) => {
|
|||||||
transStartHook,
|
transStartHook,
|
||||||
transEndHook,
|
transEndHook,
|
||||||
// transRemoveHook,
|
// transRemoveHook,
|
||||||
|
splitParagraph,
|
||||||
|
splitLength,
|
||||||
|
highlightWords,
|
||||||
}) => ({
|
}) => ({
|
||||||
pattern: pattern.trim(),
|
pattern: pattern.trim(),
|
||||||
selector: type(selector) === "string" ? selector : "",
|
selector: type(selector) === "string" ? selector : "",
|
||||||
@@ -173,6 +188,7 @@ export const checkRules = (rules) => {
|
|||||||
terms: type(terms) === "string" ? terms : "",
|
terms: type(terms) === "string" ? terms : "",
|
||||||
aiTerms: type(aiTerms) === "string" ? aiTerms : "",
|
aiTerms: type(aiTerms) === "string" ? aiTerms : "",
|
||||||
termsStyle: type(termsStyle) === "string" ? termsStyle : "",
|
termsStyle: type(termsStyle) === "string" ? termsStyle : "",
|
||||||
|
highlightStyle: type(highlightStyle) === "string" ? highlightStyle : "",
|
||||||
selectStyle: type(selectStyle) === "string" ? selectStyle : "",
|
selectStyle: type(selectStyle) === "string" ? selectStyle : "",
|
||||||
parentStyle: type(parentStyle) === "string" ? parentStyle : "",
|
parentStyle: type(parentStyle) === "string" ? parentStyle : "",
|
||||||
grandStyle: type(grandStyle) === "string" ? grandStyle : "",
|
grandStyle: type(grandStyle) === "string" ? grandStyle : "",
|
||||||
@@ -203,6 +219,15 @@ export const checkRules = (rules) => {
|
|||||||
// transRemoveHook:
|
// transRemoveHook:
|
||||||
// type(transRemoveHook) === "string" ? transRemoveHook : "",
|
// type(transRemoveHook) === "string" ? transRemoveHook : "",
|
||||||
// fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc),
|
// fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc),
|
||||||
|
splitParagraph: matchValue(
|
||||||
|
[GLOBAL_KEY, ...OPT_SPLIT_PARAGRAPH_ALL],
|
||||||
|
splitParagraph
|
||||||
|
),
|
||||||
|
splitLength: Number.isInteger(splitLength) ? splitLength : 0,
|
||||||
|
highlightWords: matchValue(
|
||||||
|
[GLOBAL_KEY, ...OPT_HIGHLIGHT_WORDS_ALL],
|
||||||
|
highlightWords
|
||||||
|
),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
128
src/libs/shadowDomManager.js
Normal file
128
src/libs/shadowDomManager.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { CacheProvider } from "@emotion/react";
|
||||||
|
import createCache from "@emotion/cache";
|
||||||
|
import { logger } from "./log";
|
||||||
|
|
||||||
|
export default class ShadowDomManager {
|
||||||
|
#hostElement = null;
|
||||||
|
#reactRoot = null;
|
||||||
|
#isVisible = false;
|
||||||
|
#isProcessing = false;
|
||||||
|
|
||||||
|
_id;
|
||||||
|
_className;
|
||||||
|
_ReactComponent;
|
||||||
|
_props;
|
||||||
|
|
||||||
|
constructor({ id, className = "", reactComponent, props = {} }) {
|
||||||
|
if (!id || !reactComponent) {
|
||||||
|
throw new Error("ID and a React Component must be provided.");
|
||||||
|
}
|
||||||
|
this._id = id;
|
||||||
|
this._className = className;
|
||||||
|
this._ReactComponent = reactComponent;
|
||||||
|
this._props = props;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isVisible() {
|
||||||
|
return this.#isVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
show(props) {
|
||||||
|
if (this.#isVisible || this.#isProcessing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#hostElement) {
|
||||||
|
this.#isProcessing = true;
|
||||||
|
try {
|
||||||
|
this.#mount(props || this._props);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to mount component with id "${this._id}":`, error);
|
||||||
|
this.#isProcessing = false;
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
this.#isProcessing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#hostElement.style.display = "";
|
||||||
|
this.#isVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
if (!this.#isVisible || !this.#hostElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#hostElement.style.display = "none";
|
||||||
|
this.#isVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (!this.#hostElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#isProcessing = true;
|
||||||
|
|
||||||
|
if (this.#reactRoot) {
|
||||||
|
this.#reactRoot.unmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#hostElement.remove();
|
||||||
|
|
||||||
|
this.#hostElement = null;
|
||||||
|
this.#reactRoot = null;
|
||||||
|
this.#isVisible = false;
|
||||||
|
this.#isProcessing = false;
|
||||||
|
logger.info(`Component with id "${this._id}" has been destroyed.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(props) {
|
||||||
|
if (this.#isVisible) {
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
this.show(props || this._props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mount(props) {
|
||||||
|
const host = document.createElement("div");
|
||||||
|
host.id = this._id;
|
||||||
|
if (this._className) {
|
||||||
|
host.className = this._className;
|
||||||
|
}
|
||||||
|
host.style.display = "none";
|
||||||
|
document.body.parentElement.appendChild(host);
|
||||||
|
this.#hostElement = host;
|
||||||
|
|
||||||
|
const shadowContainer = host.attachShadow({ mode: "closed" });
|
||||||
|
const emotionRoot = document.createElement("style");
|
||||||
|
const appRoot = document.createElement("div");
|
||||||
|
appRoot.className = `${this._id}_wrapper`;
|
||||||
|
|
||||||
|
shadowContainer.appendChild(emotionRoot);
|
||||||
|
shadowContainer.appendChild(appRoot);
|
||||||
|
|
||||||
|
const cache = createCache({
|
||||||
|
key: this._id,
|
||||||
|
prepend: true,
|
||||||
|
container: emotionRoot,
|
||||||
|
});
|
||||||
|
|
||||||
|
const enhancedProps = {
|
||||||
|
...props,
|
||||||
|
onClose: this.hide.bind(this),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ComponentToRender = this._ReactComponent;
|
||||||
|
this.#reactRoot = ReactDOM.createRoot(appRoot);
|
||||||
|
this.#reactRoot.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<CacheProvider value={cache}>
|
||||||
|
<ComponentToRender {...enhancedProps} />
|
||||||
|
</CacheProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { kissLog } from "./log";
|
|||||||
* @class ShadowRootMonitor
|
* @class ShadowRootMonitor
|
||||||
* @description 通过覆写 Element.prototype.attachShadow 来监控页面上所有新创建的 Shadow DOM
|
* @description 通过覆写 Element.prototype.attachShadow 来监控页面上所有新创建的 Shadow DOM
|
||||||
*/
|
*/
|
||||||
export class ShadowRootMonitor {
|
export default class ShadowRootMonitor {
|
||||||
/**
|
/**
|
||||||
* @param {function(ShadowRoot): void} callback - 当一个新的 shadowRoot 被创建时调用的回调函数。
|
* @param {function(ShadowRoot): void} callback - 当一个新的 shadowRoot 被创建时调用的回调函数。
|
||||||
*/
|
*/
|
||||||
@@ -63,7 +63,7 @@ export const shortcutRegister = (targetKeys = [], fn, target = document) => {
|
|||||||
const targetKeySet = new Set(targetKeys);
|
const targetKeySet = new Set(targetKeys);
|
||||||
const onKeyDown = (pressedKeys, event) => {
|
const onKeyDown = (pressedKeys, event) => {
|
||||||
if (isSameSet(targetKeySet, pressedKeys)) {
|
if (isSameSet(targetKeySet, pressedKeys)) {
|
||||||
event.preventDefault();
|
// event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
fn();
|
fn();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ export function createLoadingSVG() {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function createLogoSVG({
|
export function createLogoSVG({
|
||||||
width = "100%",
|
width = "24",
|
||||||
height = "100%",
|
height = "24",
|
||||||
viewBox = "-20 -20 70 70",
|
viewBox = "-5 -5 40 40",
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const svg = createSVGElement("svg", {
|
const svg = createSVGElement("svg", {
|
||||||
@@ -80,30 +80,26 @@ export function createLogoSVG({
|
|||||||
version: "1.1",
|
version: "1.1",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const primaryColor = "#209CEE";
|
||||||
|
const secondaryColor = "#E9F5FD";
|
||||||
|
|
||||||
|
const path1Fill = isSelected ? primaryColor : secondaryColor;
|
||||||
|
const path2Fill = isSelected ? secondaryColor : primaryColor;
|
||||||
|
|
||||||
const path1 = createSVGElement("path", {
|
const path1 = createSVGElement("path", {
|
||||||
d: "M0 0 C10.56 0 21.12 0 32 0 C32 10.56 32 21.12 32 32 C21.44 32 10.88 32 0 32 C0 21.44 0 10.88 0 0 Z ",
|
d: "M0 0 C10.56 0 21.12 0 32 0 C32 10.56 32 21.12 32 32 C21.44 32 10.88 32 0 32 C0 21.44 0 10.88 0 0 Z ",
|
||||||
fill: "#209CEE",
|
fill: path1Fill,
|
||||||
transform: "translate(0,0)",
|
transform: "translate(0,0)",
|
||||||
});
|
});
|
||||||
|
|
||||||
const path2 = createSVGElement("path", {
|
const path2 = createSVGElement("path", {
|
||||||
d: "M0 0 C0.66 0 1.32 0 2 0 C2 2.97 2 5.94 2 9 C2.969375 8.2575 3.93875 7.515 4.9375 6.75 C5.48277344 6.33234375 6.02804688 5.9146875 6.58984375 5.484375 C8.39053593 3.83283924 8.39053593 3.83283924 9 0 C13.95 0 18.9 0 24 0 C24 0.99 24 1.98 24 3 C22.68 3 21.36 3 20 3 C20 9.27 20 15.54 20 22 C19.01 22 18.02 22 17 22 C17 15.73 17 9.46 17 3 C15.35 3 13.7 3 12 3 C11.731875 3.598125 11.46375 4.19625 11.1875 4.8125 C10.01506533 6.97224808 8.80630718 8.35790256 7 10 C8.01790655 12.27071461 8.77442829 13.80784632 10.6875 15.4375 C11.120625 15.953125 11.55375 16.46875 12 17 C11.6875 19.6875 11.6875 19.6875 11 22 C10.34 22 9.68 22 9 22 C8.773125 21.236875 8.54625 20.47375 8.3125 19.6875 C6.73268318 16.45263699 5.16717283 15.58358642 2 14 C2 16.64 2 19.28 2 22 C1.34 22 0.68 22 0 22 C0 14.74 0 7.48 0 0 Z ",
|
d: "M0 0 C0.66 0 1.32 0 2 0 C2 2.97 2 5.94 2 9 C2.969375 8.2575 3.93875 7.515 4.9375 6.75 C5.48277344 6.33234375 6.02804688 5.9146875 6.58984375 5.484375 C8.39053593 3.83283924 8.39053593 3.83283924 9 0 C13.95 0 18.9 0 24 0 C24 0.99 24 1.98 24 3 C22.68 3 21.36 3 20 3 C20 9.27 20 15.54 20 22 C19.01 22 18.02 22 17 22 C17 15.73 17 9.46 17 3 C15.35 3 13.7 3 12 3 C11.731875 3.598125 11.46375 4.19625 11.1875 4.8125 C10.01506533 6.97224808 8.80630718 8.35790256 7 10 C8.01790655 12.27071461 8.77442829 13.80784632 10.6875 15.4375 C11.120625 15.953125 11.55375 16.46875 12 17 C11.6875 19.6875 11.6875 19.6875 11 22 C10.34 22 9.68 22 9 22 C8.773125 21.236875 8.54625 20.47375 8.3125 19.6875 C6.73268318 16.45263699 5.16717283 15.58358642 2 14 C2 16.64 2 19.28 2 22 C1.34 22 0.68 22 0 22 C0 14.74 0 7.48 0 0 Z ",
|
||||||
fill: "#E9F5FD",
|
fill: path2Fill,
|
||||||
transform: "translate(4,5)",
|
transform: "translate(4,5)",
|
||||||
});
|
});
|
||||||
|
|
||||||
svg.appendChild(path1);
|
svg.appendChild(path1);
|
||||||
svg.appendChild(path2);
|
svg.appendChild(path2);
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
const redLine = createSVGElement("path", {
|
|
||||||
d: "M0 36 L32 36",
|
|
||||||
stroke: "red",
|
|
||||||
"stroke-width": "3",
|
|
||||||
"stroke-linecap": "round",
|
|
||||||
});
|
|
||||||
svg.appendChild(redLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
return svg;
|
return svg;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,47 @@
|
|||||||
export function touchTapListener(fn, touchsLength) {
|
export function touchTapListener(fn, options = {}) {
|
||||||
|
const config = {
|
||||||
|
taps: 2,
|
||||||
|
fingers: 1,
|
||||||
|
delay: 300,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
let maxTouches = 0;
|
||||||
|
let tapCount = 0;
|
||||||
|
let tapTimer = null;
|
||||||
|
|
||||||
|
const handleTouchStart = (e) => {
|
||||||
|
maxTouches = Math.max(maxTouches, e.touches.length);
|
||||||
|
};
|
||||||
|
|
||||||
const handleTouchend = (e) => {
|
const handleTouchend = (e) => {
|
||||||
if (e.touches.length === touchsLength) {
|
if (e.touches.length === 0) {
|
||||||
fn();
|
if (maxTouches === config.fingers) {
|
||||||
|
tapCount++;
|
||||||
|
clearTimeout(tapTimer);
|
||||||
|
|
||||||
|
if (tapCount === config.taps) {
|
||||||
|
fn(e);
|
||||||
|
tapCount = 0;
|
||||||
|
} else {
|
||||||
|
tapTimer = setTimeout(() => {
|
||||||
|
tapCount = 0;
|
||||||
|
}, config.delay);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tapCount = 0;
|
||||||
|
clearTimeout(tapTimer);
|
||||||
|
}
|
||||||
|
maxTouches = 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("touchstart", handleTouchend);
|
document.addEventListener("touchstart", handleTouchStart, { passive: true });
|
||||||
|
document.addEventListener("touchend", handleTouchend, { passive: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("touchstart", handleTouchend);
|
clearTimeout(tapTimer);
|
||||||
|
document.removeEventListener("touchstart", handleTouchStart);
|
||||||
|
document.removeEventListener("touchend", handleTouchend);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
APP_NAME,
|
APP_UPNAME,
|
||||||
APP_LCNAME,
|
APP_LCNAME,
|
||||||
APP_CONSTS,
|
APP_CONSTS,
|
||||||
MSG_INJECT_JS,
|
MSG_INJECT_JS,
|
||||||
@@ -10,17 +10,14 @@ import {
|
|||||||
// DEFAULT_MOUSEHOVER_KEY,
|
// DEFAULT_MOUSEHOVER_KEY,
|
||||||
OPT_STYLE_NONE,
|
OPT_STYLE_NONE,
|
||||||
DEFAULT_API_SETTING,
|
DEFAULT_API_SETTING,
|
||||||
MSG_TRANS_TOGGLE,
|
OPT_HIGHLIGHT_WORDS_BEFORETRANS,
|
||||||
MSG_TRANS_TOGGLE_STYLE,
|
OPT_HIGHLIGHT_WORDS_AFTERTRANS,
|
||||||
MSG_TRANS_GETRULE,
|
OPT_SPLIT_PARAGRAPH_PUNCTUATION,
|
||||||
MSG_TRANS_PUTRULE,
|
OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||||
MSG_OPEN_TRANBOX,
|
OPT_SPLIT_PARAGRAPH_TEXTLENGTH,
|
||||||
MSG_TRANSBOX_TOGGLE,
|
|
||||||
MSG_MOUSEHOVER_TOGGLE,
|
|
||||||
MSG_TRANSINPUT_TOGGLE,
|
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import interpreter from "./interpreter";
|
import interpreter from "./interpreter";
|
||||||
import { ShadowRootMonitor } from "./shadowroot";
|
import ShadowRootMonitor from "./shadowRootMonitor";
|
||||||
import { clearFetchPool } from "./pool";
|
import { clearFetchPool } from "./pool";
|
||||||
import { debounce, scheduleIdle, genEventName, truncateWords } from "./utils";
|
import { debounce, scheduleIdle, genEventName, truncateWords } from "./utils";
|
||||||
import { apiTranslate } from "../apis";
|
import { apiTranslate } from "../apis";
|
||||||
@@ -33,10 +30,6 @@ import { genTextClass } from "./style";
|
|||||||
import { createLoadingSVG } from "./svg";
|
import { createLoadingSVG } from "./svg";
|
||||||
import { shortcutRegister } from "./shortcut";
|
import { shortcutRegister } from "./shortcut";
|
||||||
import { tryDetectLang } from "./detect";
|
import { tryDetectLang } from "./detect";
|
||||||
import { browser } from "./browser";
|
|
||||||
import { isIframe, sendIframeMsg } from "./iframe";
|
|
||||||
import { TransboxManager } from "./tranbox";
|
|
||||||
import { InputTranslator } from "./inputTranslate";
|
|
||||||
import { trustedTypesHelper } from "./trustedTypes";
|
import { trustedTypesHelper } from "./trustedTypes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,6 +157,8 @@ export class Translator {
|
|||||||
warpper: `${APP_LCNAME}-wrapper notranslate`,
|
warpper: `${APP_LCNAME}-wrapper notranslate`,
|
||||||
inner: `${APP_LCNAME}-inner`,
|
inner: `${APP_LCNAME}-inner`,
|
||||||
term: `${APP_LCNAME}-term`,
|
term: `${APP_LCNAME}-term`,
|
||||||
|
br: `${APP_LCNAME}-br`,
|
||||||
|
highlight: `${APP_LCNAME}-highlight`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 内置跳过翻译文本
|
// 内置跳过翻译文本
|
||||||
@@ -272,22 +267,21 @@ export class Translator {
|
|||||||
#combinedTermsRegex; // 专业术语正则表达式
|
#combinedTermsRegex; // 专业术语正则表达式
|
||||||
#combinedSkipsRegex; // 跳过文本正则表达式
|
#combinedSkipsRegex; // 跳过文本正则表达式
|
||||||
#placeholderRegex; // 恢复htnml正则表达式
|
#placeholderRegex; // 恢复htnml正则表达式
|
||||||
#translationTagName = APP_NAME; // 翻译容器的标签名
|
#translationTagName = APP_UPNAME; // 翻译容器的标签名
|
||||||
#eventName = ""; // 通信事件名称
|
#eventName = ""; // 通信事件名称
|
||||||
#docInfo = {}; // 网页信息
|
#docInfo = {}; // 网页信息
|
||||||
#glossary = {}; // AI词典
|
#glossary = {}; // AI词典
|
||||||
#textClass = {}; // 译文样式class
|
#textClass = {}; // 译文样式class
|
||||||
#textSheet = ""; // 译文样式字典
|
#textSheet = ""; // 译文样式字典
|
||||||
|
#apisMap = new Map(); // 用于接口快速查找
|
||||||
#isUserscript = false;
|
#favWords = []; // 收藏词汇
|
||||||
#transboxManager = null; // 划词翻译
|
|
||||||
#inputTranslator = null; // 输入框翻译
|
|
||||||
|
|
||||||
#observedNodes = new WeakSet(); // 存储所有被识别出的、可翻译的 DOM 节点单元
|
#observedNodes = new WeakSet(); // 存储所有被识别出的、可翻译的 DOM 节点单元
|
||||||
#translationNodes = new WeakMap(); // 存储所有插入到页面的译文节点
|
#translationNodes = new WeakMap(); // 存储所有插入到页面的译文节点
|
||||||
#viewNodes = new Set(); // 当前在可视范围内的单元
|
#viewNodes = new Set(); // 当前在可视范围内的单元
|
||||||
#processedNodes = new WeakMap(); // 已处理(已执行翻译DOM操作)的单元
|
#processedNodes = new WeakMap(); // 已处理(已执行翻译DOM操作)的单元
|
||||||
#rootNodes = new Set(); // 已监控的根节点
|
#rootNodes = new Set(); // 已监控的根节点
|
||||||
|
#skipMoNodes = new WeakSet(); // 忽略变化的节点
|
||||||
|
|
||||||
#removeKeydownHandler; // 快捷键清理函数
|
#removeKeydownHandler; // 快捷键清理函数
|
||||||
#hoveredNode = null; // 存储当前悬停的可翻译节点
|
#hoveredNode = null; // 存储当前悬停的可翻译节点
|
||||||
@@ -310,11 +304,12 @@ export class Translator {
|
|||||||
// 接口参数
|
// 接口参数
|
||||||
// todo: 不用频繁查找计算
|
// todo: 不用频繁查找计算
|
||||||
get #apiSetting() {
|
get #apiSetting() {
|
||||||
return (
|
// return (
|
||||||
this.#setting.transApis.find(
|
// this.#setting.transApis.find(
|
||||||
(api) => api.apiSlug === this.#rule.apiSlug
|
// (api) => api.apiSlug === this.#rule.apiSlug
|
||||||
) || DEFAULT_API_SETTING
|
// ) || DEFAULT_API_SETTING
|
||||||
);
|
// );
|
||||||
|
return this.#apisMap.get(this.#rule.apiSlug) || DEFAULT_API_SETTING;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 占位符
|
// 占位符
|
||||||
@@ -328,11 +323,14 @@ export class Translator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(rule = {}, setting = {}, isUserscript = false) {
|
constructor({ rule = {}, setting = {}, favWords = [] }) {
|
||||||
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
|
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
|
||||||
this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
|
this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
|
||||||
|
this.#favWords = favWords;
|
||||||
|
this.#apisMap = new Map(
|
||||||
|
this.#setting.transApis.map((api) => [api.apiSlug, api])
|
||||||
|
);
|
||||||
|
|
||||||
this.#isUserscript = isUserscript;
|
|
||||||
this.#eventName = genEventName();
|
this.#eventName = genEventName();
|
||||||
this.#docInfo = {
|
this.#docInfo = {
|
||||||
title: document.title,
|
title: document.title,
|
||||||
@@ -364,19 +362,6 @@ export class Translator {
|
|||||||
this.#enableMouseHover();
|
this.#enableMouseHover();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isIframe) {
|
|
||||||
// 监听后端事件
|
|
||||||
if (!isUserscript) {
|
|
||||||
this.#runtimeListener();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 划词翻译
|
|
||||||
this.#transboxManager = new TransboxManager(this.setting);
|
|
||||||
|
|
||||||
// 输入框翻译
|
|
||||||
this.#inputTranslator = new InputTranslator(this.setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", () => this.#run());
|
document.addEventListener("DOMContentLoaded", () => this.#run());
|
||||||
} else {
|
} else {
|
||||||
@@ -419,43 +404,6 @@ export class Translator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听后端事件
|
|
||||||
#runtimeListener() {
|
|
||||||
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
|
||||||
switch (action) {
|
|
||||||
case MSG_TRANS_TOGGLE:
|
|
||||||
this.toggle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
|
||||||
break;
|
|
||||||
case MSG_TRANS_TOGGLE_STYLE:
|
|
||||||
this.toggleStyle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
|
||||||
break;
|
|
||||||
case MSG_TRANS_GETRULE:
|
|
||||||
break;
|
|
||||||
case MSG_TRANS_PUTRULE:
|
|
||||||
this.updateRule(args);
|
|
||||||
sendIframeMsg(MSG_TRANS_PUTRULE, args);
|
|
||||||
break;
|
|
||||||
case MSG_OPEN_TRANBOX:
|
|
||||||
window.dispatchEvent(new CustomEvent(MSG_OPEN_TRANBOX));
|
|
||||||
break;
|
|
||||||
case MSG_TRANSBOX_TOGGLE:
|
|
||||||
this.toggleTransbox();
|
|
||||||
break;
|
|
||||||
case MSG_MOUSEHOVER_TOGGLE:
|
|
||||||
this.toggleMouseHover();
|
|
||||||
break;
|
|
||||||
case MSG_TRANSINPUT_TOGGLE:
|
|
||||||
this.toggleInputTranslate();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return { error: `message action is unavailable: ${action}` };
|
|
||||||
}
|
|
||||||
return { rule: this.rule, setting: this.setting };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#createPlaceholderRegex() {
|
#createPlaceholderRegex() {
|
||||||
const escapedStart = Translator.escapeRegex(
|
const escapedStart = Translator.escapeRegex(
|
||||||
this.#placeholder.startDelimiter
|
this.#placeholder.startDelimiter
|
||||||
@@ -580,6 +528,8 @@ export class Translator {
|
|||||||
#createMutationObserver() {
|
#createMutationObserver() {
|
||||||
return new MutationObserver((mutations) => {
|
return new MutationObserver((mutations) => {
|
||||||
for (const mutation of mutations) {
|
for (const mutation of mutations) {
|
||||||
|
if (this.#skipMoNodes.has(mutation.target)) return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
mutation.type === "characterData" &&
|
mutation.type === "characterData" &&
|
||||||
mutation.oldValue !== mutation.target.nodeValue
|
mutation.oldValue !== mutation.target.nodeValue
|
||||||
@@ -594,6 +544,8 @@ export class Translator {
|
|||||||
let nodes = new Set();
|
let nodes = new Set();
|
||||||
let hasText = false;
|
let hasText = false;
|
||||||
mutation.addedNodes.forEach((node) => {
|
mutation.addedNodes.forEach((node) => {
|
||||||
|
if (this.#skipMoNodes.has(node)) return;
|
||||||
|
|
||||||
if (/\S/.test(node.nodeValue)) {
|
if (/\S/.test(node.nodeValue)) {
|
||||||
if (node.nodeType === Node.TEXT_NODE) {
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
hasText = true;
|
hasText = true;
|
||||||
@@ -775,6 +727,10 @@ export class Translator {
|
|||||||
|
|
||||||
// 开始/重新监控节点
|
// 开始/重新监控节点
|
||||||
#startObserveNode(node) {
|
#startObserveNode(node) {
|
||||||
|
if (this.#rule.highlightWords === OPT_HIGHLIGHT_WORDS_BEFORETRANS) {
|
||||||
|
this.#highlightWordsDeeply(node);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.#observedNodes.has(node) &&
|
!this.#observedNodes.has(node) &&
|
||||||
this.#enabled &&
|
this.#enabled &&
|
||||||
@@ -856,7 +812,12 @@ export class Translator {
|
|||||||
|
|
||||||
// 提前进行语言检测
|
// 提前进行语言检测
|
||||||
let deLang = "";
|
let deLang = "";
|
||||||
const { fromLang = "auto", toLang } = this.#rule;
|
const {
|
||||||
|
fromLang = "auto",
|
||||||
|
toLang,
|
||||||
|
splitParagraph = OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||||
|
splitLength = 100,
|
||||||
|
} = this.#rule;
|
||||||
const { langDetector, skipLangs = [] } = this.#setting;
|
const { langDetector, skipLangs = [] } = this.#setting;
|
||||||
if (fromLang === "auto") {
|
if (fromLang === "auto") {
|
||||||
deLang = await tryDetectLang(node.textContent, langDetector);
|
deLang = await tryDetectLang(node.textContent, langDetector);
|
||||||
@@ -871,6 +832,11 @@ export class Translator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切分长段落
|
||||||
|
if (splitParagraph !== OPT_SPLIT_PARAGRAPH_DISABLE) {
|
||||||
|
this.#splitTextNodesBySentence(node, splitParagraph, splitLength);
|
||||||
|
}
|
||||||
|
|
||||||
let nodeGroup = [];
|
let nodeGroup = [];
|
||||||
[...node.childNodes].forEach((child) => {
|
[...node.childNodes].forEach((child) => {
|
||||||
const shouldBreak = this.#shouldBreak(child);
|
const shouldBreak = this.#shouldBreak(child);
|
||||||
@@ -890,6 +856,171 @@ export class Translator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 高亮词汇
|
||||||
|
#highlightTextNode(textNode, wordRegex) {
|
||||||
|
if (textNode.parentNode?.nodeName.toLowerCase() === "b") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wordRegex.test(textNode.textContent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wordRegex.lastIndex = 0;
|
||||||
|
const fragments = textNode.textContent.split(wordRegex);
|
||||||
|
const newNodes = [];
|
||||||
|
|
||||||
|
fragments.forEach((fragment, i) => {
|
||||||
|
if (!fragment) return;
|
||||||
|
|
||||||
|
if (i % 2 === 1) {
|
||||||
|
// 奇数索引是匹配到的关键词
|
||||||
|
const bTag = document.createElement("b");
|
||||||
|
bTag.className = Translator.KISS_CLASS.highlight;
|
||||||
|
bTag.style.cssText = this.#rule.highlightStyle || "";
|
||||||
|
bTag.textContent = fragment;
|
||||||
|
this.#skipMoNodes.add(bTag);
|
||||||
|
newNodes.push(bTag);
|
||||||
|
} else {
|
||||||
|
// 偶数索引是普通文本
|
||||||
|
const newTextNode = document.createTextNode(fragment);
|
||||||
|
this.#skipMoNodes.add(newTextNode);
|
||||||
|
newNodes.push(newTextNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newNodes.length > 0) {
|
||||||
|
textNode.replaceWith(...newNodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 高亮词汇
|
||||||
|
#highlightWordsDeeply(parentNode) {
|
||||||
|
if (!parentNode || this.#favWords.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const escapedWords = this.#favWords.map(escapeRegex);
|
||||||
|
const wordRegex = new RegExp(`\\b(${escapedWords.join("|")})\\b`, "gi");
|
||||||
|
|
||||||
|
if (parentNode.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const walker = document.createTreeWalker(
|
||||||
|
parentNode,
|
||||||
|
NodeFilter.SHOW_TEXT,
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodesToProcess = [];
|
||||||
|
let node;
|
||||||
|
while ((node = walker.nextNode())) {
|
||||||
|
nodesToProcess.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
nodesToProcess.forEach((textNode) => {
|
||||||
|
this.#highlightTextNode(textNode, wordRegex);
|
||||||
|
});
|
||||||
|
} else if (parentNode.nodeType === Node.TEXT_NODE) {
|
||||||
|
this.#highlightTextNode(parentNode, wordRegex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切分文本段落
|
||||||
|
#splitTextNodesBySentence(parentNode, splitParagraph, splitLength) {
|
||||||
|
const sentenceEndRegexForSplit = /[。!?]+|[.?!]+(?=\s+|$)/g;
|
||||||
|
|
||||||
|
[...parentNode.childNodes].forEach((node) => {
|
||||||
|
if (node.nodeType !== Node.TEXT_NODE || node.textContent.trim() === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = node.textContent;
|
||||||
|
const parts = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = sentenceEndRegexForSplit.exec(text)) !== null) {
|
||||||
|
let realEndIndex = match.index + match[0].length;
|
||||||
|
while (realEndIndex < text.length && /\s/.test(text[realEndIndex])) {
|
||||||
|
realEndIndex++;
|
||||||
|
}
|
||||||
|
parts.push(text.substring(lastIndex, realEndIndex));
|
||||||
|
lastIndex = realEndIndex;
|
||||||
|
sentenceEndRegexForSplit.lastIndex = realEndIndex;
|
||||||
|
}
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
parts.push(text.substring(lastIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
const validParts = parts.filter((part) => part.trim().length > 0);
|
||||||
|
if (validParts.length <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNodes = validParts.map((part) => {
|
||||||
|
const newNode = document.createTextNode(part);
|
||||||
|
this.#skipMoNodes.add(newNode);
|
||||||
|
return newNode;
|
||||||
|
});
|
||||||
|
|
||||||
|
node.replaceWith(...newNodes);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sentenceEndRegexForTest = /(?:[。!??!]+|(?<!\d)\.)\s*$/;
|
||||||
|
let textLength = 0;
|
||||||
|
|
||||||
|
[...parentNode.childNodes].forEach((node) => {
|
||||||
|
textLength += node.textContent.length;
|
||||||
|
|
||||||
|
const isSentenceEnd = sentenceEndRegexForTest.test(node.textContent);
|
||||||
|
if (!isSentenceEnd || node.nextSibling?.nodeName === "BR") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
splitParagraph === OPT_SPLIT_PARAGRAPH_PUNCTUATION ||
|
||||||
|
(splitParagraph === OPT_SPLIT_PARAGRAPH_TEXTLENGTH &&
|
||||||
|
textLength >= splitLength)
|
||||||
|
) {
|
||||||
|
textLength = 0;
|
||||||
|
|
||||||
|
const br = document.createElement("br");
|
||||||
|
br.className = Translator.KISS_CLASS.br;
|
||||||
|
this.#skipMoNodes.add(br);
|
||||||
|
|
||||||
|
node.after(br);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除高亮
|
||||||
|
#removeHighlights(parentNode) {
|
||||||
|
if (!parentNode) return;
|
||||||
|
|
||||||
|
const highlightedElements = parentNode.querySelectorAll(
|
||||||
|
`.${Translator.KISS_CLASS.highlight}`
|
||||||
|
);
|
||||||
|
|
||||||
|
highlightedElements.forEach((element) => {
|
||||||
|
const textNode = document.createTextNode(element.textContent);
|
||||||
|
element.replaceWith(textNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
parentNode.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除br
|
||||||
|
#removeBrTags(parentNode) {
|
||||||
|
if (!parentNode) return;
|
||||||
|
|
||||||
|
parentNode
|
||||||
|
.querySelectorAll(`.${Translator.KISS_CLASS.br}`)
|
||||||
|
.forEach((br) => br.remove());
|
||||||
|
|
||||||
|
parentNode.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
// 判断是否需要换行
|
// 判断是否需要换行
|
||||||
#shouldBreak(node) {
|
#shouldBreak(node) {
|
||||||
if (!Translator.isElementOrFragment(node)) return false;
|
if (!Translator.isElementOrFragment(node)) return false;
|
||||||
@@ -966,6 +1097,7 @@ export class Translator {
|
|||||||
// detectRemote,
|
// detectRemote,
|
||||||
// toLang,
|
// toLang,
|
||||||
// skipLangs = [],
|
// skipLangs = [],
|
||||||
|
highlightWords,
|
||||||
} = this.#rule;
|
} = this.#rule;
|
||||||
const {
|
const {
|
||||||
newlineLength,
|
newlineLength,
|
||||||
@@ -1057,6 +1189,11 @@ export class Translator {
|
|||||||
parentNode.parentElement.style.cssText += grandStyle;
|
parentNode.parentElement.style.cssText += grandStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 高亮词汇
|
||||||
|
if (highlightWords === OPT_HIGHLIGHT_WORDS_AFTERTRANS) {
|
||||||
|
nodes.forEach((node) => this.#highlightWordsDeeply(node));
|
||||||
|
}
|
||||||
|
|
||||||
// 翻译完成钩子函数
|
// 翻译完成钩子函数
|
||||||
if (transEndHook?.trim()) {
|
if (transEndHook?.trim()) {
|
||||||
try {
|
try {
|
||||||
@@ -1232,7 +1369,8 @@ export class Translator {
|
|||||||
|
|
||||||
// 清理译文
|
// 清理译文
|
||||||
#removeTranslationElement(el) {
|
#removeTranslationElement(el) {
|
||||||
this.#processedNodes.delete(el.parentElement);
|
const parentElement = el.parentElement;
|
||||||
|
this.#processedNodes.delete(parentElement);
|
||||||
|
|
||||||
// 如果是仅显示译文模式,先恢复原文
|
// 如果是仅显示译文模式,先恢复原文
|
||||||
const { nodes, isHide } = this.#translationNodes.get(el) || {};
|
const { nodes, isHide } = this.#translationNodes.get(el) || {};
|
||||||
@@ -1242,6 +1380,12 @@ export class Translator {
|
|||||||
|
|
||||||
this.#translationNodes.delete(el);
|
this.#translationNodes.delete(el);
|
||||||
el.remove();
|
el.remove();
|
||||||
|
|
||||||
|
// todo: 可能不应深度清除
|
||||||
|
if (this.#rule.highlightWords === OPT_HIGHLIGHT_WORDS_AFTERTRANS) {
|
||||||
|
this.#removeHighlights(parentElement);
|
||||||
|
}
|
||||||
|
this.#removeBrTags(parentElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复原文
|
// 恢复原文
|
||||||
@@ -1504,13 +1648,11 @@ export class Translator {
|
|||||||
toggleTransbox() {
|
toggleTransbox() {
|
||||||
this.#setting.tranboxSetting.transOpen =
|
this.#setting.tranboxSetting.transOpen =
|
||||||
!this.#setting.tranboxSetting.transOpen;
|
!this.#setting.tranboxSetting.transOpen;
|
||||||
this.#transboxManager?.toggle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换输入框翻译
|
// 切换输入框翻译
|
||||||
toggleInputTranslate() {
|
toggleInputTranslate() {
|
||||||
this.#setting.inputRule.transOpen = !this.#setting.inputRule.transOpen;
|
this.#setting.inputRule.transOpen = !this.#setting.inputRule.transOpen;
|
||||||
this.#inputTranslator?.toggle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止运行
|
// 停止运行
|
||||||
|
|||||||
294
src/libs/translatorManager.js
Normal file
294
src/libs/translatorManager.js
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import { browser } from "./browser";
|
||||||
|
import { Translator } from "./translator";
|
||||||
|
import { InputTranslator } from "./inputTranslate";
|
||||||
|
import { TransboxManager } from "./tranbox";
|
||||||
|
import { shortcutRegister } from "./shortcut";
|
||||||
|
import { sendIframeMsg } from "./iframe";
|
||||||
|
import { EVENT_KISS, newI18n } from "../config";
|
||||||
|
import { touchTapListener } from "./touch";
|
||||||
|
import { PopupManager } from "./popupManager";
|
||||||
|
import { FabManager } from "./fabManager";
|
||||||
|
import {
|
||||||
|
OPT_SHORTCUT_TRANSLATE,
|
||||||
|
OPT_SHORTCUT_STYLE,
|
||||||
|
OPT_SHORTCUT_POPUP,
|
||||||
|
OPT_SHORTCUT_SETTING,
|
||||||
|
MSG_TRANS_TOGGLE,
|
||||||
|
MSG_TRANS_TOGGLE_STYLE,
|
||||||
|
MSG_TRANS_GETRULE,
|
||||||
|
MSG_TRANS_PUTRULE,
|
||||||
|
MSG_OPEN_TRANBOX,
|
||||||
|
MSG_TRANSBOX_TOGGLE,
|
||||||
|
MSG_POPUP_TOGGLE,
|
||||||
|
MSG_MOUSEHOVER_TOGGLE,
|
||||||
|
MSG_TRANSINPUT_TOGGLE,
|
||||||
|
} from "../config";
|
||||||
|
import { logger } from "./log";
|
||||||
|
|
||||||
|
export default class TranslatorManager {
|
||||||
|
#clearShortcuts = [];
|
||||||
|
#menuCommandIds = [];
|
||||||
|
#clearTouchListener = null;
|
||||||
|
#isActive = false;
|
||||||
|
#isUserscript;
|
||||||
|
#isIframe;
|
||||||
|
|
||||||
|
#windowMessageHandler = null;
|
||||||
|
#browserMessageHandler = null;
|
||||||
|
|
||||||
|
_translator;
|
||||||
|
_transboxManager;
|
||||||
|
_inputTranslator;
|
||||||
|
_popupManager;
|
||||||
|
_fabManager;
|
||||||
|
|
||||||
|
constructor({ setting, rule, fabConfig, favWords, isIframe, isUserscript }) {
|
||||||
|
this.#isIframe = isIframe;
|
||||||
|
this.#isUserscript = isUserscript;
|
||||||
|
|
||||||
|
this._translator = new Translator({
|
||||||
|
rule,
|
||||||
|
setting,
|
||||||
|
favWords,
|
||||||
|
isUserscript,
|
||||||
|
isIframe,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isIframe) {
|
||||||
|
this._transboxManager = new TransboxManager(setting);
|
||||||
|
this._inputTranslator = new InputTranslator(setting);
|
||||||
|
this._popupManager = new PopupManager({
|
||||||
|
translator: this._translator,
|
||||||
|
processActions: this.#processActions.bind(this),
|
||||||
|
});
|
||||||
|
this._fabManager = new FabManager({
|
||||||
|
translator: this._translator,
|
||||||
|
processActions: this.#processActions.bind(this),
|
||||||
|
fabConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#windowMessageHandler = this.#handleWindowMessage.bind(this);
|
||||||
|
this.#browserMessageHandler = this.#handleBrowserMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.#isActive) {
|
||||||
|
logger.info("TranslatorManager is already started.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#setupMessageListeners();
|
||||||
|
this.#setupTouchOperations();
|
||||||
|
|
||||||
|
if (!this.#isIframe && this.#isUserscript) {
|
||||||
|
this.#registerShortcuts();
|
||||||
|
this.#registerMenus();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#isActive = true;
|
||||||
|
logger.info("TranslatorManager started.");
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (!this.#isActive) {
|
||||||
|
logger.info("TranslatorManager is not running.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除消息监听器
|
||||||
|
if (this.#isUserscript) {
|
||||||
|
window.removeEventListener("message", this.#windowMessageHandler);
|
||||||
|
} else if (
|
||||||
|
browser.runtime.onMessage.hasListener(this.#browserMessageHandler)
|
||||||
|
) {
|
||||||
|
browser.runtime.onMessage.removeListener(this.#browserMessageHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已注册的快捷键
|
||||||
|
this.#clearShortcuts.forEach((clear) => clear());
|
||||||
|
this.#clearShortcuts = [];
|
||||||
|
|
||||||
|
// 触屏
|
||||||
|
if (this.#clearTouchListener) {
|
||||||
|
this.#clearTouchListener();
|
||||||
|
this.#clearTouchListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 油猴菜单
|
||||||
|
if (globalThis.GM && this.#menuCommandIds.length > 0) {
|
||||||
|
this.#menuCommandIds.forEach((id) =>
|
||||||
|
globalThis.GM.unregisterMenuCommand(id)
|
||||||
|
);
|
||||||
|
this.#menuCommandIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子模块
|
||||||
|
this._popupManager?.destroy();
|
||||||
|
this._fabManager?.destroy();
|
||||||
|
this._transboxManager?.disable();
|
||||||
|
this._inputTranslator?.disable();
|
||||||
|
this._translator.stop();
|
||||||
|
|
||||||
|
this.#isActive = false;
|
||||||
|
logger.info("TranslatorManager stopped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#setupMessageListeners() {
|
||||||
|
if (this.#isUserscript) {
|
||||||
|
window.addEventListener("message", this.#windowMessageHandler);
|
||||||
|
} else {
|
||||||
|
browser.runtime.onMessage.addListener(this.#browserMessageHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#setupTouchOperations() {
|
||||||
|
if (this.#isIframe) return;
|
||||||
|
|
||||||
|
const { touchTranslate = 2 } = this._translator.setting;
|
||||||
|
if (touchTranslate === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTap = () => {
|
||||||
|
this.#processActions({ action: MSG_TRANS_TOGGLE });
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (touchTranslate) {
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
this.#clearTouchListener = touchTapListener(handleTap, {
|
||||||
|
taps: 1,
|
||||||
|
fingers: touchTranslate,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
this.#clearTouchListener = touchTapListener(handleTap, {
|
||||||
|
taps: 2,
|
||||||
|
fingers: 1,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
this.#clearTouchListener = touchTapListener(handleTap, {
|
||||||
|
taps: 3,
|
||||||
|
fingers: 1,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
this.#clearTouchListener = touchTapListener(handleTap, {
|
||||||
|
taps: 2,
|
||||||
|
fingers: 2,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleWindowMessage(event) {
|
||||||
|
this.#processActions(event.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleBrowserMessage(message, sender, sendResponse) {
|
||||||
|
const result = this.#processActions(message);
|
||||||
|
const response = result || {
|
||||||
|
rule: this._translator.rule,
|
||||||
|
setting: this._translator.setting,
|
||||||
|
};
|
||||||
|
sendResponse(response);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#registerShortcuts() {
|
||||||
|
const { shortcuts } = this._translator.setting;
|
||||||
|
this.#clearShortcuts = [
|
||||||
|
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () =>
|
||||||
|
this.#processActions({ action: MSG_TRANS_TOGGLE })
|
||||||
|
),
|
||||||
|
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () =>
|
||||||
|
this.#processActions({ action: MSG_TRANS_TOGGLE_STYLE })
|
||||||
|
),
|
||||||
|
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () =>
|
||||||
|
this.#processActions({ action: MSG_POPUP_TOGGLE })
|
||||||
|
),
|
||||||
|
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () =>
|
||||||
|
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank")
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#registerMenus() {
|
||||||
|
if (!globalThis.GM) return;
|
||||||
|
const { contextMenuType, uiLang } = this._translator.setting;
|
||||||
|
if (contextMenuType === 0) return;
|
||||||
|
|
||||||
|
const i18n = newI18n(uiLang || "zh");
|
||||||
|
const GM = globalThis.GM;
|
||||||
|
this.#menuCommandIds = [
|
||||||
|
GM.registerMenuCommand(
|
||||||
|
i18n("translate_switch"),
|
||||||
|
() => this.#processActions({ action: MSG_TRANS_TOGGLE }),
|
||||||
|
"Q"
|
||||||
|
),
|
||||||
|
GM.registerMenuCommand(
|
||||||
|
i18n("toggle_style"),
|
||||||
|
() => this.#processActions({ action: MSG_TRANS_TOGGLE_STYLE }),
|
||||||
|
"C"
|
||||||
|
),
|
||||||
|
GM.registerMenuCommand(
|
||||||
|
i18n("open_menu"),
|
||||||
|
() => this.#processActions({ action: MSG_POPUP_TOGGLE }),
|
||||||
|
"K"
|
||||||
|
),
|
||||||
|
GM.registerMenuCommand(
|
||||||
|
i18n("open_setting"),
|
||||||
|
() => window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank"),
|
||||||
|
"O"
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#processActions({ action, args } = {}) {
|
||||||
|
if (this.#isUserscript) {
|
||||||
|
sendIframeMsg(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case MSG_TRANS_TOGGLE:
|
||||||
|
this._translator.toggle();
|
||||||
|
break;
|
||||||
|
case MSG_TRANS_TOGGLE_STYLE:
|
||||||
|
this._translator.toggleStyle();
|
||||||
|
break;
|
||||||
|
case MSG_TRANS_GETRULE:
|
||||||
|
break;
|
||||||
|
case MSG_TRANS_PUTRULE:
|
||||||
|
this._translator.updateRule(args);
|
||||||
|
break;
|
||||||
|
case MSG_OPEN_TRANBOX:
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(EVENT_KISS, {
|
||||||
|
detail: { action: MSG_OPEN_TRANBOX },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case MSG_POPUP_TOGGLE:
|
||||||
|
this._popupManager?.toggle();
|
||||||
|
break;
|
||||||
|
case MSG_TRANSBOX_TOGGLE:
|
||||||
|
this._transboxManager?.toggle();
|
||||||
|
this._translator.toggleTransbox();
|
||||||
|
break;
|
||||||
|
case MSG_MOUSEHOVER_TOGGLE:
|
||||||
|
this._translator.toggleMouseHover();
|
||||||
|
break;
|
||||||
|
case MSG_TRANSINPUT_TOGGLE:
|
||||||
|
this._inputTranslator?.toggle();
|
||||||
|
this._translator.toggleInputTranslate();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.info(`Message action is unavailable: ${action}`);
|
||||||
|
return { error: `Message action is unavailable: ${action}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { logger } from "./log";
|
||||||
|
|
||||||
export const trustedTypesHelper = (() => {
|
export const trustedTypesHelper = (() => {
|
||||||
const POLICY_NAME = "kiss-translator-policy";
|
const POLICY_NAME = "kiss-translator-policy";
|
||||||
let policy = null;
|
let policy = null;
|
||||||
@@ -13,7 +15,7 @@ export const trustedTypesHelper = (() => {
|
|||||||
if (err.message.includes("already exists")) {
|
if (err.message.includes("already exists")) {
|
||||||
policy = globalThis.trustedTypes.policies.get(POLICY_NAME);
|
policy = globalThis.trustedTypes.policies.get(POLICY_NAME);
|
||||||
} else {
|
} else {
|
||||||
console.error("cont create Trusted Types", err);
|
logger.info("cont create Trusted Types", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import { sleep } from "../libs/utils.js";
|
import { sleep } from "../libs/utils.js";
|
||||||
import { createLogoSVG } from "../libs/svg.js";
|
import { createLogoSVG } from "../libs/svg.js";
|
||||||
import { randomBetween } from "../libs/utils.js";
|
import { randomBetween } from "../libs/utils.js";
|
||||||
import { i18n } from "../config";
|
import { newI18n } from "../config";
|
||||||
|
|
||||||
const VIDEO_SELECT = "#container video";
|
const VIDEO_SELECT = "#container video";
|
||||||
const CONTORLS_SELECT = ".ytp-right-controls";
|
const CONTORLS_SELECT = ".ytp-right-controls";
|
||||||
@@ -33,7 +33,7 @@ class YouTubeCaptionProvider {
|
|||||||
|
|
||||||
constructor(setting = {}) {
|
constructor(setting = {}) {
|
||||||
this.#setting = setting;
|
this.#setting = setting;
|
||||||
this.#i18n = i18n(setting.uiLang || "zh");
|
this.#i18n = newI18n(setting.uiLang || "zh");
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
@@ -199,7 +199,7 @@ class YouTubeCaptionProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.#toggleButton = toggleButton;
|
this.#toggleButton = toggleButton;
|
||||||
this.#ytControls?.before(kissControls);
|
this.#ytControls?.prepend(kissControls);
|
||||||
}
|
}
|
||||||
|
|
||||||
#isSameLang(lang1, lang2) {
|
#isSameLang(lang1, lang2) {
|
||||||
|
|||||||
73
src/views/Action/ContentFab.js
Normal file
73
src/views/Action/ContentFab.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import Fab from "@mui/material/Fab";
|
||||||
|
import TranslateIcon from "@mui/icons-material/Translate";
|
||||||
|
import ThemeProvider from "../../hooks/Theme";
|
||||||
|
import Draggable from "./Draggable";
|
||||||
|
import { useState, useMemo, useCallback } from "react";
|
||||||
|
import { SettingProvider } from "../../hooks/Setting";
|
||||||
|
import { MSG_TRANS_TOGGLE, MSG_POPUP_TOGGLE } from "../../config";
|
||||||
|
import { sendIframeMsg } from "../../libs/iframe";
|
||||||
|
import useWindowSize from "../../hooks/WindowSize";
|
||||||
|
|
||||||
|
export default function ContentFab({
|
||||||
|
translator,
|
||||||
|
fabConfig: { x: fabX, y: fabY, fabClickAction = 0 } = {},
|
||||||
|
processActions,
|
||||||
|
}) {
|
||||||
|
const fabWidth = 40;
|
||||||
|
const windowSize = useWindowSize();
|
||||||
|
const [moved, setMoved] = useState(false);
|
||||||
|
|
||||||
|
const handleStart = useCallback(() => {
|
||||||
|
setMoved(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMove = useCallback(() => {
|
||||||
|
setMoved(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (!moved) {
|
||||||
|
if (fabClickAction === 1) {
|
||||||
|
translator.toggle();
|
||||||
|
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||||
|
} else {
|
||||||
|
processActions({ action: MSG_POPUP_TOGGLE });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [moved, translator, fabClickAction, processActions]);
|
||||||
|
|
||||||
|
const fabProps = useMemo(
|
||||||
|
() => ({
|
||||||
|
windowSize,
|
||||||
|
width: fabWidth,
|
||||||
|
height: fabWidth,
|
||||||
|
left: fabX ?? -fabWidth,
|
||||||
|
top: fabY ?? windowSize.h / 2,
|
||||||
|
}),
|
||||||
|
[windowSize, fabWidth, fabX, fabY]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingProvider>
|
||||||
|
<ThemeProvider>
|
||||||
|
<Draggable
|
||||||
|
key="fab"
|
||||||
|
snapEdge
|
||||||
|
{...fabProps}
|
||||||
|
onStart={handleStart}
|
||||||
|
onMove={handleMove}
|
||||||
|
handler={
|
||||||
|
<Fab size="small" color="primary" onClick={handleClick}>
|
||||||
|
<TranslateIcon
|
||||||
|
sx={{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Fab>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
|
</SettingProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -50,7 +50,7 @@ export default function Draggable({
|
|||||||
height,
|
height,
|
||||||
left,
|
left,
|
||||||
top,
|
top,
|
||||||
show,
|
show = true,
|
||||||
snapEdge,
|
snapEdge,
|
||||||
onStart,
|
onStart,
|
||||||
onMove,
|
onMove,
|
||||||
|
|||||||
@@ -1,168 +1,62 @@
|
|||||||
import Fab from "@mui/material/Fab";
|
|
||||||
import TranslateIcon from "@mui/icons-material/Translate";
|
|
||||||
import ThemeProvider from "../../hooks/Theme";
|
import ThemeProvider from "../../hooks/Theme";
|
||||||
import Draggable from "./Draggable";
|
import Draggable from "./Draggable";
|
||||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
import { useEffect, useMemo, useCallback, useState } from "react";
|
||||||
import { SettingProvider } from "../../hooks/Setting";
|
import { SettingProvider } from "../../hooks/Setting";
|
||||||
import Popup from "../Popup";
|
|
||||||
import { debounce } from "../../libs/utils";
|
|
||||||
import { isGm } from "../../libs/client";
|
|
||||||
import Header from "../Popup/Header";
|
import Header from "../Popup/Header";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import {
|
import useWindowSize from "../../hooks/WindowSize";
|
||||||
DEFAULT_SHORTCUTS,
|
import { EVENT_KISS, MSG_OPEN_OPTIONS, MSG_POPUP_TOGGLE } from "../../config";
|
||||||
OPT_SHORTCUT_TRANSLATE,
|
import PopupCont from "../Popup/PopupCont";
|
||||||
OPT_SHORTCUT_STYLE,
|
import { isExt } from "../../libs/client";
|
||||||
OPT_SHORTCUT_POPUP,
|
import { sendBgMsg } from "../../libs/msg";
|
||||||
OPT_SHORTCUT_SETTING,
|
|
||||||
MSG_TRANS_TOGGLE,
|
|
||||||
MSG_TRANS_TOGGLE_STYLE,
|
|
||||||
} from "../../config";
|
|
||||||
import { shortcutRegister } from "../../libs/shortcut";
|
|
||||||
import { sendIframeMsg } from "../../libs/iframe";
|
|
||||||
import { kissLog } from "../../libs/log";
|
|
||||||
import { getI18n } from "../../hooks/I18n";
|
|
||||||
|
|
||||||
export default function Action({ translator, fab }) {
|
export default function Action({ translator, processActions }) {
|
||||||
const fabWidth = 40;
|
const [showPopup, setShowPopup] = useState(true);
|
||||||
const [showPopup, setShowPopup] = useState(false);
|
const [rule, setRule] = useState(translator.rule);
|
||||||
const [windowSize, setWindowSize] = useState({
|
const [setting, setSetting] = useState(translator.setting);
|
||||||
w: window.innerWidth,
|
const windowSize = useWindowSize();
|
||||||
h: window.innerHeight,
|
|
||||||
});
|
|
||||||
const [moved, setMoved] = useState(false);
|
|
||||||
|
|
||||||
const { fabClickAction = 0 } = fab || {};
|
const handleOpenSetting = useCallback(() => {
|
||||||
|
if (isExt) {
|
||||||
const handleWindowResize = useMemo(
|
sendBgMsg(MSG_OPEN_OPTIONS);
|
||||||
() =>
|
} else {
|
||||||
debounce(() => {
|
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||||
setWindowSize({
|
}
|
||||||
w: window.innerWidth,
|
|
||||||
h: window.innerHeight,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleWindowClick = (e) => {
|
|
||||||
setShowPopup(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStart = useCallback(() => {
|
|
||||||
setMoved(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleMove = useCallback(() => {
|
|
||||||
setMoved(true);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isGm) {
|
const handleWindowClick = () => {
|
||||||
return;
|
setShowPopup(false);
|
||||||
}
|
|
||||||
|
|
||||||
// 注册快捷键
|
|
||||||
const shortcuts = translator.setting.shortcuts || DEFAULT_SHORTCUTS;
|
|
||||||
const clearShortcuts = [
|
|
||||||
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () => {
|
|
||||||
translator.toggle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
|
||||||
setShowPopup(false);
|
|
||||||
}),
|
|
||||||
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () => {
|
|
||||||
translator.toggleStyle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
|
||||||
setShowPopup(false);
|
|
||||||
}),
|
|
||||||
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () => {
|
|
||||||
setShowPopup((pre) => !pre);
|
|
||||||
}),
|
|
||||||
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () => {
|
|
||||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearShortcuts.forEach((fn) => {
|
|
||||||
fn();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}, [translator]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isGm) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册菜单
|
|
||||||
try {
|
|
||||||
const menuCommandIds = [];
|
|
||||||
const { contextMenuType, uiLang } = translator.setting;
|
|
||||||
contextMenuType !== 0 &&
|
|
||||||
menuCommandIds.push(
|
|
||||||
GM.registerMenuCommand(
|
|
||||||
getI18n(uiLang, "translate_switch"),
|
|
||||||
(event) => {
|
|
||||||
translator.toggle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
|
||||||
setShowPopup(false);
|
|
||||||
},
|
|
||||||
"Q"
|
|
||||||
),
|
|
||||||
GM.registerMenuCommand(
|
|
||||||
getI18n(uiLang, "toggle_style"),
|
|
||||||
(event) => {
|
|
||||||
translator.toggleStyle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
|
||||||
setShowPopup(false);
|
|
||||||
},
|
|
||||||
"C"
|
|
||||||
),
|
|
||||||
GM.registerMenuCommand(
|
|
||||||
getI18n(uiLang, "open_menu"),
|
|
||||||
(event) => {
|
|
||||||
setShowPopup((pre) => !pre);
|
|
||||||
},
|
|
||||||
"K"
|
|
||||||
),
|
|
||||||
GM.registerMenuCommand(
|
|
||||||
getI18n(uiLang, "open_setting"),
|
|
||||||
(event) => {
|
|
||||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
|
||||||
},
|
|
||||||
"O"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
menuCommandIds.forEach((id) => {
|
|
||||||
GM.unregisterMenuCommand(id);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
kissLog("registerMenuCommand", err);
|
|
||||||
}
|
|
||||||
}, [translator]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener("resize", handleWindowResize);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("resize", handleWindowResize);
|
|
||||||
};
|
|
||||||
}, [handleWindowResize]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener("click", handleWindowClick);
|
window.addEventListener("click", handleWindowClick);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("click", handleWindowClick);
|
window.removeEventListener("click", handleWindowClick);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleStatusUpdate = (event) => {
|
||||||
|
if (event.detail?.action === MSG_POPUP_TOGGLE) {
|
||||||
|
setShowPopup((pre) => !pre);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener(EVENT_KISS, handleStatusUpdate);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener(EVENT_KISS, handleStatusUpdate);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showPopup) {
|
||||||
|
setRule(translator.rule);
|
||||||
|
setSetting(translator.setting);
|
||||||
|
}
|
||||||
|
}, [showPopup, translator]);
|
||||||
|
|
||||||
const popProps = useMemo(() => {
|
const popProps = useMemo(() => {
|
||||||
const width = Math.min(windowSize.w, 300);
|
const width = Math.min(windowSize.w, 360);
|
||||||
const height = Math.min(windowSize.h, 442);
|
const height = Math.min(windowSize.h, 442);
|
||||||
const left = (windowSize.w - width) / 2;
|
const left = (windowSize.w - width) / 2;
|
||||||
const top = (windowSize.h - height) / 2;
|
const top = (windowSize.h - height) / 2;
|
||||||
@@ -175,67 +69,38 @@ export default function Action({ translator, fab }) {
|
|||||||
};
|
};
|
||||||
}, [windowSize]);
|
}, [windowSize]);
|
||||||
|
|
||||||
const fabProps = {
|
|
||||||
windowSize,
|
|
||||||
width: fabWidth,
|
|
||||||
height: fabWidth,
|
|
||||||
left: fab.x ?? -fabWidth,
|
|
||||||
top: fab.y ?? windowSize.h / 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingProvider>
|
<SettingProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Draggable
|
{showPopup && (
|
||||||
key="pop"
|
<Draggable
|
||||||
{...popProps}
|
key="pop"
|
||||||
show={showPopup}
|
{...popProps}
|
||||||
onStart={handleStart}
|
usePaper
|
||||||
onMove={handleMove}
|
handler={
|
||||||
usePaper
|
<Box style={{ cursor: "move" }}>
|
||||||
handler={
|
<Header
|
||||||
<Box style={{ cursor: "move" }}>
|
onClose={() => {
|
||||||
<Header setShowPopup={setShowPopup} />
|
|
||||||
<Divider />
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{showPopup && (
|
|
||||||
<Popup setShowPopup={setShowPopup} translator={translator} />
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
<Draggable
|
|
||||||
key="fab"
|
|
||||||
snapEdge
|
|
||||||
{...fabProps}
|
|
||||||
show={fab.isHide ? false : !showPopup}
|
|
||||||
onStart={handleStart}
|
|
||||||
onMove={handleMove}
|
|
||||||
handler={
|
|
||||||
<Fab
|
|
||||||
size="small"
|
|
||||||
color="primary"
|
|
||||||
onClick={(e) => {
|
|
||||||
if (!moved) {
|
|
||||||
if (fabClickAction === 1) {
|
|
||||||
translator.toggle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
|
||||||
setShowPopup(false);
|
setShowPopup(false);
|
||||||
} else {
|
}}
|
||||||
setShowPopup((pre) => !pre);
|
/>
|
||||||
}
|
<Divider />
|
||||||
}
|
</Box>
|
||||||
}}
|
}
|
||||||
>
|
>
|
||||||
<TranslateIcon
|
<Box width={360}>
|
||||||
sx={{
|
<PopupCont
|
||||||
width: 24,
|
rule={rule}
|
||||||
height: 24,
|
setting={setting}
|
||||||
}}
|
setRule={setRule}
|
||||||
|
setSetting={setSetting}
|
||||||
|
handleOpenSetting={handleOpenSetting}
|
||||||
|
processActions={processActions}
|
||||||
|
isContent={true}
|
||||||
/>
|
/>
|
||||||
</Fab>
|
</Box>
|
||||||
}
|
</Draggable>
|
||||||
/>
|
)}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</SettingProvider>
|
</SettingProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ import {
|
|||||||
URL_KISS_RULES_NEW_ISSUE,
|
URL_KISS_RULES_NEW_ISSUE,
|
||||||
OPT_SYNCTYPE_WORKER,
|
OPT_SYNCTYPE_WORKER,
|
||||||
DEFAULT_TRANS_TAG,
|
DEFAULT_TRANS_TAG,
|
||||||
|
OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||||
|
OPT_HIGHLIGHT_WORDS_DISABLE,
|
||||||
|
OPT_SPLIT_PARAGRAPH_ALL,
|
||||||
|
OPT_HIGHLIGHT_WORDS_ALL,
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
@@ -59,6 +63,7 @@ import AddIcon from "@mui/icons-material/Add";
|
|||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
import CancelIcon from "@mui/icons-material/Cancel";
|
import CancelIcon from "@mui/icons-material/Cancel";
|
||||||
import SaveIcon from "@mui/icons-material/Save";
|
import SaveIcon from "@mui/icons-material/Save";
|
||||||
|
import ValidationInput from "../../hooks/ValidationInput";
|
||||||
import { kissLog } from "../../libs/log";
|
import { kissLog } from "../../libs/log";
|
||||||
import { useApiList } from "../../hooks/Api";
|
import { useApiList } from "../../hooks/Api";
|
||||||
import ShowMoreButton from "./ShowMoreButton";
|
import ShowMoreButton from "./ShowMoreButton";
|
||||||
@@ -98,6 +103,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
terms = "",
|
terms = "",
|
||||||
aiTerms = "",
|
aiTerms = "",
|
||||||
termsStyle = "",
|
termsStyle = "",
|
||||||
|
highlightStyle = "color: red;",
|
||||||
selectStyle = "",
|
selectStyle = "",
|
||||||
parentStyle = "",
|
parentStyle = "",
|
||||||
grandStyle = "",
|
grandStyle = "",
|
||||||
@@ -124,6 +130,9 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
transStartHook = "",
|
transStartHook = "",
|
||||||
transEndHook = "",
|
transEndHook = "",
|
||||||
// transRemoveHook = "",
|
// transRemoveHook = "",
|
||||||
|
splitParagraph = OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||||
|
splitLength = 0,
|
||||||
|
highlightWords = OPT_HIGHLIGHT_WORDS_DISABLE,
|
||||||
} = formValues;
|
} = formValues;
|
||||||
|
|
||||||
const isModified = useMemo(() => {
|
const isModified = useMemo(() => {
|
||||||
@@ -423,6 +432,58 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
</TextField>
|
</TextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
name="splitParagraph"
|
||||||
|
value={splitParagraph}
|
||||||
|
label={i18n("split_paragraph")}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{GlobalItem}
|
||||||
|
{OPT_SPLIT_PARAGRAPH_ALL.map((item) => (
|
||||||
|
<MenuItem key={item} value={item}>
|
||||||
|
{i18n(item)}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||||
|
<ValidationInput
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
label={i18n("split_length")}
|
||||||
|
type="number"
|
||||||
|
name="splitLength"
|
||||||
|
value={splitLength}
|
||||||
|
onChange={handleChange}
|
||||||
|
min={0}
|
||||||
|
max={1000}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
name="highlightWords"
|
||||||
|
value={highlightWords}
|
||||||
|
label={i18n("highlight_words")}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{GlobalItem}
|
||||||
|
{OPT_HIGHLIGHT_WORDS_ALL.map((item) => (
|
||||||
|
<MenuItem key={item} value={item}>
|
||||||
|
{i18n(item)}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
@@ -558,6 +619,16 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
maxRows={10}
|
maxRows={10}
|
||||||
multiline
|
multiline
|
||||||
/>
|
/>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("highlight_style")}
|
||||||
|
name="highlightStyle"
|
||||||
|
value={highlightStyle}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
maxRows={10}
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("selector_style")}
|
label={i18n("selector_style")}
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ export default function Settings() {
|
|||||||
label={i18n("touch_translate_shortcut")}
|
label={i18n("touch_translate_shortcut")}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{[0, 2, 3, 4].map((item) => (
|
{[0, 2, 3, 4, 5, 6, 7].map((item) => (
|
||||||
<MenuItem key={item} value={item}>
|
<MenuItem key={item} value={item}>
|
||||||
{i18n(`touch_tap_${item}`)}
|
{i18n(`touch_tap_${item}`)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -379,18 +379,19 @@ export default function Settings() {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("translate_blacklist")}
|
||||||
|
helperText={i18n("pattern_helper")}
|
||||||
|
name="blacklist"
|
||||||
|
value={blacklist}
|
||||||
|
onChange={handleChange}
|
||||||
|
maxRows={10}
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
|
|
||||||
{isExt ? (
|
{isExt ? (
|
||||||
<>
|
<>
|
||||||
<TextField
|
|
||||||
size="small"
|
|
||||||
label={i18n("disabled_orilist")}
|
|
||||||
helperText={i18n("pattern_helper")}
|
|
||||||
name="orilist"
|
|
||||||
value={orilist}
|
|
||||||
onChange={handleChange}
|
|
||||||
multiline
|
|
||||||
/>
|
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -409,6 +410,15 @@ export default function Settings() {
|
|||||||
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
|
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("disabled_orilist")}
|
||||||
|
helperText={i18n("pattern_helper")}
|
||||||
|
name="orilist"
|
||||||
|
value={orilist}
|
||||||
|
onChange={handleChange}
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("disabled_csplist")}
|
label={i18n("disabled_csplist")}
|
||||||
@@ -453,17 +463,6 @@ export default function Settings() {
|
|||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TextField
|
|
||||||
size="small"
|
|
||||||
label={i18n("translate_blacklist")}
|
|
||||||
helperText={i18n("pattern_helper")}
|
|
||||||
name="blacklist"
|
|
||||||
value={blacklist}
|
|
||||||
onChange={handleChange}
|
|
||||||
maxRows={10}
|
|
||||||
multiline
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Stack from "@mui/material/Stack";
|
|||||||
import DarkModeButton from "../Options/DarkModeButton";
|
import DarkModeButton from "../Options/DarkModeButton";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
export default function Header({ setShowPopup }) {
|
export default function Header({ onClose }) {
|
||||||
const handleHomepage = () => {
|
const handleHomepage = () => {
|
||||||
window.open(process.env.REACT_APP_HOMEPAGE, "_blank");
|
window.open(process.env.REACT_APP_HOMEPAGE, "_blank");
|
||||||
};
|
};
|
||||||
@@ -33,10 +33,10 @@ export default function Header({ setShowPopup }) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{setShowPopup ? (
|
{onClose ? (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowPopup(false);
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
|
|||||||
422
src/views/Popup/PopupCont.js
Normal file
422
src/views/Popup/PopupCont.js
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
import { useState, useEffect, useMemo } from "react";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
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 { isExt } from "../../libs/client";
|
||||||
|
import { useI18n } from "../../hooks/I18n";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import {
|
||||||
|
MSG_TRANS_TOGGLE,
|
||||||
|
MSG_TRANS_PUTRULE,
|
||||||
|
MSG_SAVE_RULE,
|
||||||
|
MSG_COMMAND_SHORTCUTS,
|
||||||
|
MSG_TRANSBOX_TOGGLE,
|
||||||
|
MSG_MOUSEHOVER_TOGGLE,
|
||||||
|
MSG_TRANSINPUT_TOGGLE,
|
||||||
|
OPT_LANGS_FROM,
|
||||||
|
OPT_LANGS_TO,
|
||||||
|
OPT_STYLE_ALL,
|
||||||
|
} from "../../config";
|
||||||
|
import { saveRule } from "../../libs/rules";
|
||||||
|
import { tryClearCaches } from "../../libs/cache";
|
||||||
|
import { kissLog } from "../../libs/log";
|
||||||
|
import { parseUrlPattern } from "../../libs/utils";
|
||||||
|
|
||||||
|
export default function PopupCont({
|
||||||
|
rule,
|
||||||
|
setting,
|
||||||
|
setRule,
|
||||||
|
setSetting,
|
||||||
|
handleOpenSetting,
|
||||||
|
processActions,
|
||||||
|
isContent = false,
|
||||||
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
|
const [commands, setCommands] = useState({});
|
||||||
|
|
||||||
|
const handleTransToggle = async (e) => {
|
||||||
|
try {
|
||||||
|
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
|
||||||
|
|
||||||
|
if (!processActions) {
|
||||||
|
await sendTabMsg(MSG_TRANS_TOGGLE);
|
||||||
|
} else {
|
||||||
|
processActions({ action: MSG_TRANS_TOGGLE });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("toggle trans", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTransboxToggle = async (e) => {
|
||||||
|
try {
|
||||||
|
setSetting((pre) => ({
|
||||||
|
...pre,
|
||||||
|
tranboxSetting: { ...pre.tranboxSetting, transOpen: e.target.checked },
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!processActions) {
|
||||||
|
await sendTabMsg(MSG_TRANSBOX_TOGGLE);
|
||||||
|
} else {
|
||||||
|
processActions({ action: 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 (!processActions) {
|
||||||
|
await sendTabMsg(MSG_MOUSEHOVER_TOGGLE);
|
||||||
|
} else {
|
||||||
|
processActions({ action: 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 (!processActions) {
|
||||||
|
await sendTabMsg(MSG_TRANSINPUT_TOGGLE);
|
||||||
|
} else {
|
||||||
|
processActions({ action: MSG_TRANSINPUT_TOGGLE });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("toggle inputtrans", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = async (e) => {
|
||||||
|
try {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setRule((pre) => ({ ...pre, [name]: value }));
|
||||||
|
|
||||||
|
if (!processActions) {
|
||||||
|
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||||
|
} else {
|
||||||
|
processActions({ action: MSG_TRANS_PUTRULE, args: { [name]: value } });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("update rule", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearCache = () => {
|
||||||
|
tryClearCaches();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveRule = async () => {
|
||||||
|
try {
|
||||||
|
let href = "";
|
||||||
|
if (!isContent) {
|
||||||
|
const tab = await getCurTab();
|
||||||
|
href = tab.url;
|
||||||
|
} else {
|
||||||
|
href = window.location?.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!href || typeof href !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = parseUrlPattern(href);
|
||||||
|
const curRule = { ...rule, pattern };
|
||||||
|
if (isExt && isContent) {
|
||||||
|
sendBgMsg(MSG_SAVE_RULE, curRule);
|
||||||
|
} else {
|
||||||
|
saveRule(curRule);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("save rule", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const commands = {};
|
||||||
|
if (isExt) {
|
||||||
|
const res = await sendBgMsg(MSG_COMMAND_SHORTCUTS);
|
||||||
|
res.forEach(({ name, shortcut }) => {
|
||||||
|
commands[name] = shortcut;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const shortcuts = setting.shortcuts;
|
||||||
|
if (shortcuts) {
|
||||||
|
Object.entries(shortcuts).forEach(([key, val]) => {
|
||||||
|
commands[key] = val.join("+");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCommands(commands);
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("query cmds", err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [setting.shortcuts]);
|
||||||
|
|
||||||
|
const optApis = useMemo(
|
||||||
|
() =>
|
||||||
|
setting.transApis
|
||||||
|
.filter((api) => !api.isDisabled)
|
||||||
|
.map((api) => ({
|
||||||
|
key: api.apiSlug,
|
||||||
|
name: api.apiName || api.apiSlug,
|
||||||
|
})),
|
||||||
|
[setting.transApis]
|
||||||
|
);
|
||||||
|
|
||||||
|
const tranboxEnabled = setting.tranboxSetting.transOpen;
|
||||||
|
const mouseHoverEnabled = setting.mouseHoverSetting.useMouseHover;
|
||||||
|
const inputTransEnabled = setting.inputRule.transOpen;
|
||||||
|
|
||||||
|
const {
|
||||||
|
transOpen,
|
||||||
|
apiSlug,
|
||||||
|
fromLang,
|
||||||
|
toLang,
|
||||||
|
textStyle,
|
||||||
|
autoScan,
|
||||||
|
transOnly,
|
||||||
|
hasRichText,
|
||||||
|
hasShadowroot,
|
||||||
|
} = rule;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack sx={{ p: 2 }} spacing={2}>
|
||||||
|
<Grid container columns={12} spacing={1}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={transOpen === "true"}
|
||||||
|
onChange={handleTransToggle}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
commands["toggleTranslate"]
|
||||||
|
? `${i18n("translate_alt")}(${commands["toggleTranslate"]})`
|
||||||
|
: i18n("translate_alt")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
name="autoScan"
|
||||||
|
value={autoScan === "true" ? "false" : "true"}
|
||||||
|
checked={autoScan === "true"}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n("autoscan_alt")}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
name="hasShadowroot"
|
||||||
|
value={hasShadowroot === "true" ? "false" : "true"}
|
||||||
|
checked={hasShadowroot === "true"}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n("shadowroot_alt")}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
name="hasRichText"
|
||||||
|
value={hasRichText === "true" ? "false" : "true"}
|
||||||
|
checked={hasRichText === "true"}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n("richtext_alt")}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
name="transOnly"
|
||||||
|
value={transOnly === "true" ? "false" : "true"}
|
||||||
|
checked={transOnly === "true"}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n("transonly_alt")}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
name="tranboxEnabled"
|
||||||
|
value={!tranboxEnabled}
|
||||||
|
checked={tranboxEnabled}
|
||||||
|
onChange={handleTransboxToggle}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n("selection_translate")}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
name="mouseHoverEnabled"
|
||||||
|
value={!mouseHoverEnabled}
|
||||||
|
checked={mouseHoverEnabled}
|
||||||
|
onChange={handleMousehoverToggle}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n("mousehover_translate")}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
name="inputTransEnabled"
|
||||||
|
value={!inputTransEnabled}
|
||||||
|
checked={inputTransEnabled}
|
||||||
|
onChange={handleInputTransToggle}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n("input_translate")}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||||
|
size="small"
|
||||||
|
value={apiSlug}
|
||||||
|
name="apiSlug"
|
||||||
|
label={i18n("translate_service")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{optApis.map(({ key, name }) => (
|
||||||
|
<MenuItem key={key} value={key}>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||||
|
size="small"
|
||||||
|
value={fromLang}
|
||||||
|
name="fromLang"
|
||||||
|
label={i18n("from_lang")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||||
|
<MenuItem key={lang} value={lang}>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||||
|
size="small"
|
||||||
|
value={toLang}
|
||||||
|
name="toLang"
|
||||||
|
label={i18n("to_lang")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||||
|
<MenuItem key={lang} value={lang}>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||||
|
size="small"
|
||||||
|
value={textStyle}
|
||||||
|
name="textStyle"
|
||||||
|
label={
|
||||||
|
commands["toggleStyle"]
|
||||||
|
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
|
||||||
|
: i18n("text_style_alt")
|
||||||
|
}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{OPT_STYLE_ALL.map((item) => (
|
||||||
|
<MenuItem key={item} value={item}>
|
||||||
|
{i18n(item)}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
{/* {OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
name="bgColor"
|
||||||
|
value={bgColor}
|
||||||
|
label={i18n("bg_color")}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={2}
|
||||||
|
>
|
||||||
|
<Button variant="text" onClick={handleSaveRule}>
|
||||||
|
{i18n("save_rule")}
|
||||||
|
</Button>
|
||||||
|
<Button variant="text" onClick={handleClearCache}>
|
||||||
|
{i18n("clear_cache")}
|
||||||
|
</Button>
|
||||||
|
<Button variant="text" onClick={handleOpenSetting}>
|
||||||
|
{i18n("setting")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,182 +1,26 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
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 Button from "@mui/material/Button";
|
||||||
import Grid from "@mui/material/Grid";
|
import { sendTabMsg } from "../../libs/msg";
|
||||||
import { sendBgMsg, sendTabMsg, getCurTab } from "../../libs/msg";
|
|
||||||
import { browser } from "../../libs/browser";
|
import { browser } from "../../libs/browser";
|
||||||
import { isExt } from "../../libs/client";
|
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import TextField from "@mui/material/TextField";
|
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Header from "./Header";
|
import Header from "./Header";
|
||||||
import {
|
import { MSG_TRANS_GETRULE } from "../../config";
|
||||||
MSG_TRANS_TOGGLE,
|
|
||||||
MSG_TRANS_GETRULE,
|
|
||||||
MSG_TRANS_PUTRULE,
|
|
||||||
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,
|
|
||||||
} from "../../config";
|
|
||||||
import { sendIframeMsg } from "../../libs/iframe";
|
|
||||||
import { saveRule } from "../../libs/rules";
|
|
||||||
import { tryClearCaches } from "../../libs/cache";
|
|
||||||
import { kissLog } from "../../libs/log";
|
import { kissLog } from "../../libs/log";
|
||||||
import { parseUrlPattern } from "../../libs/utils";
|
import PopupCont from "./PopupCont";
|
||||||
|
|
||||||
// 插件popup没有参数
|
export default function Popup() {
|
||||||
// 网页弹框有
|
|
||||||
export default function Popup({ setShowPopup, translator }) {
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const [rule, setRule] = useState(translator?.rule);
|
const [rule, setRule] = useState(null);
|
||||||
const [setting, setSetting] = useState(translator?.setting);
|
const [setting, setSetting] = useState(null);
|
||||||
const [commands, setCommands] = useState({});
|
|
||||||
|
|
||||||
const handleOpenSetting = () => {
|
const handleOpenSetting = useCallback(() => {
|
||||||
if (!translator) {
|
browser?.runtime.openOptionsPage();
|
||||||
browser?.runtime.openOptionsPage();
|
}, []);
|
||||||
} else if (isExt) {
|
|
||||||
sendBgMsg(MSG_OPEN_OPTIONS);
|
|
||||||
} else {
|
|
||||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
|
||||||
}
|
|
||||||
setShowPopup && setShowPopup(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTransToggle = async (e) => {
|
|
||||||
try {
|
|
||||||
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
|
|
||||||
|
|
||||||
if (!translator) {
|
|
||||||
await sendTabMsg(MSG_TRANS_TOGGLE);
|
|
||||||
} else {
|
|
||||||
translator.toggle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
kissLog("toggle trans", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
setRule((pre) => ({ ...pre, [name]: value }));
|
|
||||||
|
|
||||||
if (!translator) {
|
|
||||||
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
|
||||||
} else {
|
|
||||||
translator.updateRule({ [name]: value });
|
|
||||||
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
kissLog("update rule", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearCache = () => {
|
|
||||||
tryClearCaches();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveRule = async () => {
|
|
||||||
try {
|
|
||||||
let href = "";
|
|
||||||
if (!translator) {
|
|
||||||
const tab = await getCurTab();
|
|
||||||
href = tab.url;
|
|
||||||
} else {
|
|
||||||
href = window.location?.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!href || typeof href !== "string") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pattern = parseUrlPattern(href);
|
|
||||||
const curRule = { ...rule, pattern };
|
|
||||||
if (isExt && translator) {
|
|
||||||
sendBgMsg(MSG_SAVE_RULE, curRule);
|
|
||||||
} else {
|
|
||||||
saveRule(curRule);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
kissLog("save rule", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (translator) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await sendTabMsg(MSG_TRANS_GETRULE);
|
const res = await sendTabMsg(MSG_TRANS_GETRULE);
|
||||||
@@ -188,297 +32,27 @@ export default function Popup({ setShowPopup, translator }) {
|
|||||||
kissLog("query rule", err);
|
kissLog("query rule", err);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [translator]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
return (
|
||||||
(async () => {
|
<Box width={360}>
|
||||||
try {
|
<Header />
|
||||||
const commands = {};
|
<Divider />
|
||||||
if (isExt) {
|
{rule && setting ? (
|
||||||
const res = await sendBgMsg(MSG_COMMAND_SHORTCUTS);
|
<PopupCont
|
||||||
res.forEach(({ name, shortcut }) => {
|
rule={rule}
|
||||||
commands[name] = shortcut;
|
setting={setting}
|
||||||
});
|
setRule={setRule}
|
||||||
} else {
|
setSetting={setSetting}
|
||||||
const shortcuts = translator.setting.shortcuts;
|
handleOpenSetting={handleOpenSetting}
|
||||||
if (shortcuts) {
|
/>
|
||||||
Object.entries(shortcuts).forEach(([key, val]) => {
|
) : (
|
||||||
commands[key] = val.join("+");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setCommands(commands);
|
|
||||||
} catch (err) {
|
|
||||||
kissLog("query cmds", err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [translator]);
|
|
||||||
|
|
||||||
const optApis = useMemo(
|
|
||||||
() =>
|
|
||||||
setting?.transApis
|
|
||||||
.filter((api) => !api.isDisabled)
|
|
||||||
.map((api) => ({
|
|
||||||
key: api.apiSlug,
|
|
||||||
name: api.apiName || api.apiSlug,
|
|
||||||
})),
|
|
||||||
[setting]
|
|
||||||
);
|
|
||||||
|
|
||||||
const tranboxEnabled = setting?.tranboxSetting.transOpen;
|
|
||||||
const mouseHoverEnabled = setting?.mouseHoverSetting.useMouseHover;
|
|
||||||
const inputTransEnabled = setting?.inputRule.transOpen;
|
|
||||||
|
|
||||||
if (!rule) {
|
|
||||||
return (
|
|
||||||
<Box minWidth={300}>
|
|
||||||
{!translator && (
|
|
||||||
<>
|
|
||||||
<Header />
|
|
||||||
<Divider />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Stack sx={{ p: 2 }} spacing={3}>
|
<Stack sx={{ p: 2 }} spacing={3}>
|
||||||
<Button variant="text" onClick={handleOpenSetting}>
|
<Button variant="text" onClick={handleOpenSetting}>
|
||||||
{i18n("setting")}
|
{i18n("setting")}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
transOpen,
|
|
||||||
apiSlug,
|
|
||||||
fromLang,
|
|
||||||
toLang,
|
|
||||||
textStyle,
|
|
||||||
autoScan,
|
|
||||||
transOnly,
|
|
||||||
hasRichText,
|
|
||||||
hasShadowroot,
|
|
||||||
} = rule;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box width={360}>
|
|
||||||
{!translator && (
|
|
||||||
<>
|
|
||||||
<Header />
|
|
||||||
<Divider />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<Stack sx={{ p: 2 }} spacing={2}>
|
|
||||||
<Grid container columns={12} spacing={1}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
checked={transOpen === "true"}
|
|
||||||
onChange={handleTransToggle}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={
|
|
||||||
commands["toggleTranslate"]
|
|
||||||
? `${i18n("translate_alt")}(${commands["toggleTranslate"]})`
|
|
||||||
: i18n("translate_alt")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
name="autoScan"
|
|
||||||
value={autoScan === "true" ? "false" : "true"}
|
|
||||||
checked={autoScan === "true"}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={i18n("autoscan_alt")}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
name="hasShadowroot"
|
|
||||||
value={hasShadowroot === "true" ? "false" : "true"}
|
|
||||||
checked={hasShadowroot === "true"}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={i18n("shadowroot_alt")}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
name="hasRichText"
|
|
||||||
value={hasRichText === "true" ? "false" : "true"}
|
|
||||||
checked={hasRichText === "true"}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={i18n("richtext_alt")}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
name="transOnly"
|
|
||||||
value={transOnly === "true" ? "false" : "true"}
|
|
||||||
checked={transOnly === "true"}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={i18n("transonly_alt")}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
name="tranboxEnabled"
|
|
||||||
value={!tranboxEnabled}
|
|
||||||
checked={tranboxEnabled}
|
|
||||||
onChange={handleTransboxToggle}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={i18n("selection_translate")}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
name="mouseHoverEnabled"
|
|
||||||
value={!mouseHoverEnabled}
|
|
||||||
checked={mouseHoverEnabled}
|
|
||||||
onChange={handleMousehoverToggle}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={i18n("mousehover_translate")}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
name="inputTransEnabled"
|
|
||||||
value={!inputTransEnabled}
|
|
||||||
checked={inputTransEnabled}
|
|
||||||
onChange={handleInputTransToggle}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={i18n("input_translate")}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
select
|
|
||||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
|
||||||
size="small"
|
|
||||||
value={apiSlug}
|
|
||||||
name="apiSlug"
|
|
||||||
label={i18n("translate_service")}
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
{optApis.map(({ key, name }) => (
|
|
||||||
<MenuItem key={key} value={key}>
|
|
||||||
{name}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
select
|
|
||||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
|
||||||
size="small"
|
|
||||||
value={fromLang}
|
|
||||||
name="fromLang"
|
|
||||||
label={i18n("from_lang")}
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
|
||||||
<MenuItem key={lang} value={lang}>
|
|
||||||
{name}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
select
|
|
||||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
|
||||||
size="small"
|
|
||||||
value={toLang}
|
|
||||||
name="toLang"
|
|
||||||
label={i18n("to_lang")}
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
|
||||||
<MenuItem key={lang} value={lang}>
|
|
||||||
{name}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
select
|
|
||||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
|
||||||
size="small"
|
|
||||||
value={textStyle}
|
|
||||||
name="textStyle"
|
|
||||||
label={
|
|
||||||
commands["toggleStyle"]
|
|
||||||
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
|
|
||||||
: i18n("text_style_alt")
|
|
||||||
}
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
{OPT_STYLE_ALL.map((item) => (
|
|
||||||
<MenuItem key={item} value={item}>
|
|
||||||
{i18n(item)}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
{/* {OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
|
||||||
<TextField
|
|
||||||
size="small"
|
|
||||||
name="bgColor"
|
|
||||||
value={bgColor}
|
|
||||||
label={i18n("bg_color")}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
justifyContent="space-between"
|
|
||||||
alignItems="center"
|
|
||||||
spacing={2}
|
|
||||||
>
|
|
||||||
<Button variant="text" onClick={handleSaveRule}>
|
|
||||||
{i18n("save_rule")}
|
|
||||||
</Button>
|
|
||||||
<Button variant="text" onClick={handleClearCache}>
|
|
||||||
{i18n("clear_cache")}
|
|
||||||
</Button>
|
|
||||||
<Button variant="text" onClick={handleOpenSetting}>
|
|
||||||
{i18n("setting")}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { isMobile } from "../../libs/mobile";
|
|||||||
import TranForm from "./TranForm.js";
|
import TranForm from "./TranForm.js";
|
||||||
|
|
||||||
function Header({
|
function Header({
|
||||||
setShowPopup,
|
setShowBox,
|
||||||
simpleStyle,
|
simpleStyle,
|
||||||
setSimpleStyle,
|
setSimpleStyle,
|
||||||
hideClickAway,
|
hideClickAway,
|
||||||
@@ -98,7 +98,7 @@ function Header({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowPopup(false);
|
setShowBox(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CloseIcon fontSize="small" />
|
<CloseIcon fontSize="small" />
|
||||||
@@ -111,6 +111,7 @@ function Header({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TranBox({
|
export default function TranBox({
|
||||||
|
showBox,
|
||||||
text,
|
text,
|
||||||
setText,
|
setText,
|
||||||
setShowBox,
|
setShowBox,
|
||||||
@@ -134,43 +135,45 @@ export default function TranBox({
|
|||||||
return (
|
return (
|
||||||
<SettingProvider>
|
<SettingProvider>
|
||||||
<ThemeProvider styles={extStyles}>
|
<ThemeProvider styles={extStyles}>
|
||||||
<DraggableResizable
|
{showBox && (
|
||||||
position={boxPosition}
|
<DraggableResizable
|
||||||
size={boxSize}
|
position={boxPosition}
|
||||||
setSize={setBoxSize}
|
size={boxSize}
|
||||||
setPosition={setBoxPosition}
|
setSize={setBoxSize}
|
||||||
header={
|
setPosition={setBoxPosition}
|
||||||
<Header
|
header={
|
||||||
setShowPopup={setShowBox}
|
<Header
|
||||||
simpleStyle={simpleStyle}
|
setShowBox={setShowBox}
|
||||||
setSimpleStyle={setSimpleStyle}
|
simpleStyle={simpleStyle}
|
||||||
hideClickAway={hideClickAway}
|
setSimpleStyle={setSimpleStyle}
|
||||||
setHideClickAway={setHideClickAway}
|
hideClickAway={hideClickAway}
|
||||||
followSelection={followSelection}
|
setHideClickAway={setHideClickAway}
|
||||||
setFollowSelection={setFollowSelection}
|
followSelection={followSelection}
|
||||||
mouseHover={mouseHover}
|
setFollowSelection={setFollowSelection}
|
||||||
/>
|
mouseHover={mouseHover}
|
||||||
}
|
/>
|
||||||
onClick={(e) => e.stopPropagation()}
|
}
|
||||||
onMouseEnter={() => setMouseHover(true)}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onMouseLeave={() => setMouseHover(false)}
|
onMouseEnter={() => setMouseHover(true)}
|
||||||
>
|
onMouseLeave={() => setMouseHover(false)}
|
||||||
<Box sx={{ p: simpleStyle ? 1 : 2 }}>
|
>
|
||||||
<TranForm
|
<Box sx={{ p: simpleStyle ? 1 : 2 }}>
|
||||||
text={text}
|
<TranForm
|
||||||
setText={setText}
|
text={text}
|
||||||
apiSlugs={apiSlugs}
|
setText={setText}
|
||||||
fromLang={fromLang}
|
apiSlugs={apiSlugs}
|
||||||
toLang={toLang}
|
fromLang={fromLang}
|
||||||
toLang2={toLang2}
|
toLang={toLang}
|
||||||
transApis={transApis}
|
toLang2={toLang2}
|
||||||
simpleStyle={simpleStyle}
|
transApis={transApis}
|
||||||
langDetector={langDetector}
|
simpleStyle={simpleStyle}
|
||||||
enDict={enDict}
|
langDetector={langDetector}
|
||||||
enSug={enSug}
|
enDict={enDict}
|
||||||
/>
|
enSug={enSug}
|
||||||
</Box>
|
/>
|
||||||
</DraggableResizable>
|
</Box>
|
||||||
|
</DraggableResizable>
|
||||||
|
)}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</SettingProvider>
|
</SettingProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
OPT_TRANBOX_TRIGGER_CLICK,
|
OPT_TRANBOX_TRIGGER_CLICK,
|
||||||
OPT_TRANBOX_TRIGGER_HOVER,
|
OPT_TRANBOX_TRIGGER_HOVER,
|
||||||
OPT_TRANBOX_TRIGGER_SELECT,
|
OPT_TRANBOX_TRIGGER_SELECT,
|
||||||
|
EVENT_KISS,
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { isMobile } from "../../libs/mobile";
|
import { isMobile } from "../../libs/mobile";
|
||||||
import { kissLog } from "../../libs/log";
|
import { kissLog } from "../../libs/log";
|
||||||
@@ -167,12 +168,26 @@ export default function Slection({
|
|||||||
};
|
};
|
||||||
}, [tranboxShortcut, handleTranbox]);
|
}, [tranboxShortcut, handleTranbox]);
|
||||||
|
|
||||||
|
const handleToggle = useCallback(() => {
|
||||||
|
if (showBox) {
|
||||||
|
setShowBox(false);
|
||||||
|
} else {
|
||||||
|
handleTranbox();
|
||||||
|
}
|
||||||
|
}, [showBox, handleTranbox]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener(MSG_OPEN_TRANBOX, handleTranbox);
|
const handleStatusUpdate = (event) => {
|
||||||
return () => {
|
if (event.detail?.action === MSG_OPEN_TRANBOX) {
|
||||||
window.removeEventListener(MSG_OPEN_TRANBOX, handleTranbox);
|
handleToggle();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [handleTranbox]);
|
|
||||||
|
document.addEventListener(EVENT_KISS, handleStatusUpdate);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener(EVENT_KISS, handleStatusUpdate);
|
||||||
|
};
|
||||||
|
}, [handleToggle]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isGm) {
|
if (!isGm) {
|
||||||
@@ -217,8 +232,9 @@ export default function Slection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showBox && (
|
{
|
||||||
<TranBox
|
<TranBox
|
||||||
|
showBox={showBox}
|
||||||
text={text}
|
text={text}
|
||||||
setText={setText}
|
setText={setText}
|
||||||
boxSize={boxSize}
|
boxSize={boxSize}
|
||||||
@@ -237,7 +253,7 @@ export default function Slection({
|
|||||||
// extStyles={extStyles}
|
// extStyles={extStyles}
|
||||||
langDetector={langDetector}
|
langDetector={langDetector}
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
|
|
||||||
{showBtn && (
|
{showBtn && (
|
||||||
<TranBtn
|
<TranBtn
|
||||||
|
|||||||
Reference in New Issue
Block a user