feat: Add more shortcut keys to popup

This commit is contained in:
Gabe
2025-10-01 01:47:15 +08:00
parent b60b770ed6
commit 7412b3a5c8
12 changed files with 505 additions and 269 deletions

View File

@@ -6,24 +6,18 @@ import { CacheProvider } from "@emotion/react";
import {
MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
MSG_OPEN_TRANBOX,
APP_CONSTS,
DEFAULT_TRANBOX_SETTING,
} from "./config";
import { getFabWithDefault, getSettingWithDefault } from "./libs/storage";
import { Translator } from "./libs/translator";
import { isIframe, sendIframeMsg } from "./libs/iframe";
import Slection from "./views/Selection";
import { touchTapListener } from "./libs/touch";
import { debounce, genEventName } from "./libs/utils";
import { handlePing, injectScript } from "./libs/gm";
import { browser } from "./libs/browser";
import { matchRule } from "./libs/rules";
import { trySyncAllSubRules } from "./libs/subRules";
import { isInBlacklist } from "./libs/blacklist";
import inputTranslate from "./libs/inputTranslate";
/**
* 油猴脚本设置页面
@@ -45,37 +39,6 @@ function runSettingPage() {
}
}
/**
* 插件监听后端事件
* @param {*} translator
*/
function runtimeListener(translator) {
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) {
case MSG_TRANS_TOGGLE:
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
break;
case MSG_TRANS_TOGGLE_STYLE:
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
break;
case MSG_TRANS_GETRULE:
break;
case MSG_TRANS_PUTRULE:
translator.updateRule(args);
sendIframeMsg(MSG_TRANS_PUTRULE, args);
break;
case MSG_OPEN_TRANBOX:
window.dispatchEvent(new CustomEvent(MSG_OPEN_TRANBOX));
break;
default:
return { error: `message action is unavailable: ${action}` };
}
return { rule: translator.rule, setting: translator.setting };
});
}
/**
* iframe 页面执行
* @param {*} translator
@@ -131,61 +94,6 @@ async function showFab(translator) {
);
}
/**
* 划词翻译
* @param {*} param0
* @returns
*/
function showTransbox(
{
contextMenuType,
tranboxSetting = DEFAULT_TRANBOX_SETTING,
transApis,
darkMode,
uiLang,
langDetector,
},
{ transSelected }
) {
if (transSelected === "false") {
return;
}
const $tranbox = document.createElement("div");
$tranbox.setAttribute("id", APP_CONSTS.boxID);
$tranbox.style.fontSize = "0";
$tranbox.style.width = "0";
$tranbox.style.height = "0";
document.body.parentElement.appendChild($tranbox);
const shadowContainer = $tranbox.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style");
const shadowRootElement = document.createElement("div");
shadowRootElement.classList.add(`${APP_CONSTS.boxID}_warpper`);
shadowRootElement.classList.add(
`${APP_CONSTS.boxID}_${darkMode ? "dark" : "light"}`
);
shadowContainer.appendChild(emotionRoot);
shadowContainer.appendChild(shadowRootElement);
const cache = createCache({
key: APP_CONSTS.boxID,
prepend: true,
container: emotionRoot,
});
ReactDOM.createRoot(shadowRootElement).render(
<React.StrictMode>
<CacheProvider value={cache}>
<Slection
contextMenuType={contextMenuType}
tranboxSetting={tranboxSetting}
transApis={transApis}
uiLang={uiLang}
langDetector={langDetector}
/>
</CacheProvider>
</React.StrictMode>
);
}
/**
* 显示错误信息到页面顶部
* @param {*} message
@@ -251,13 +159,13 @@ export async function run(isUserscript = false) {
}
// 监听消息
!isUserscript && runtimeListener(translator);
// !isUserscript && runtimeListener(translator);
// 输入框翻译
inputTranslate(setting);
// inputTranslate(setting);
// 划词翻译
showTransbox(setting, rule);
// showTransbox(setting, rule);
// 浮球按钮
await showFab(translator);

View File

@@ -1399,9 +1399,9 @@ export const I18N = {
zh_TW: `ShadowRoot`,
},
richtext_alt: {
zh: `富文本`,
zh: `保留富文本`,
en: `Rich Text`,
zh_TW: `富文本`,
zh_TW: `保留富文本`,
},
transonly_alt: {
zh: `隐藏原文`,

View File

@@ -14,6 +14,9 @@ export const MSG_OPEN_TRANBOX = "open_tranbox";
export const MSG_TRANS_GETRULE = "trans_getrule";
export const MSG_TRANS_PUTRULE = "trans_putrule";
export const MSG_TRANS_CURRULE = "trans_currule";
export const MSG_TRANSBOX_TOGGLE = "transbox_toggle";
export const MSG_MOUSEHOVER_TOGGLE = "mousehover_toggle";
export const MSG_TRANSINPUT_TOGGLE = "transinput_toggle";
export const MSG_CONTEXT_MENUS = "context_menus";
export const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
export const MSG_INJECT_JS = "inject_js";

View File

@@ -95,7 +95,7 @@ export const DEFAULT_RULE = {
// transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译 (暂时作废)
transTag: GLOBAL_KEY, // 译文元素标签
transTitle: GLOBAL_KEY, // 是否同时翻译页面标题
transSelected: GLOBAL_KEY, // 是否启用划词翻译
// transSelected: GLOBAL_KEY, // 是否启用划词翻译 (移回setting)
// detectRemote: GLOBAL_KEY, // 是否使用远程语言检测 (移回setting)
// skipLangs: [], // 不翻译的语言 (移回setting)
// fixerSelector: "", // 修复函数选择器 (暂时作废)
@@ -131,7 +131,7 @@ export const GLOBLA_RULE = {
// transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译 (暂时作废)
transTag: DEFAULT_TRANS_TAG, // 译文元素标签
transTitle: "false", // 是否同时翻译页面标题
transSelected: "true", // 是否启用划词翻译
// transSelected: "true", // 是否启用划词翻译 (移回setting)
// detectRemote: "true", // 是否使用远程语言检测 (移回setting)
// skipLangs: [], // 不翻译的语言 (移回setting)
// fixerSelector: "", // 修复函数选择器 (暂时作废)

View File

@@ -73,7 +73,7 @@ export const OPT_TRANBOX_TRIGGER_ALL = [
];
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
export const DEFAULT_TRANBOX_SETTING = {
// transOpen: true, // 是否启用划词翻译作废移至rule
transOpen: true, // 是否启用划词翻译
apiSlug: OPT_TRANS_MICROSOFT,
fromLang: "auto",
toLang: "zh-CN",

View File

@@ -4,7 +4,7 @@ import {
OPT_LANGS_LIST,
DEFAULT_API_SETTING,
} from "../config";
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
import { genEventName, removeEndchar, matchInputStr } from "./utils";
import { stepShortcutRegister } from "./shortcut";
import { apiTranslate } from "../apis";
import { loadingSvg } from "./svg";
@@ -18,34 +18,20 @@ function isEditAbleNode(node) {
return node.hasAttribute("contenteditable");
}
function selectContent(node) {
function replaceContentEditableText(node, newText) {
node.focus();
const selection = window.getSelection();
if (!selection) return;
const range = document.createRange();
range.selectNodeContents(node);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
function pasteContentEvent(node, text) {
node.focus();
const data = new DataTransfer();
data.setData("text/plain", text);
range.deleteContents();
const textNode = document.createTextNode(newText);
range.insertNode(textNode);
const event = new ClipboardEvent("paste", { clipboardData: data });
document.dispatchEvent(event);
data.clearData();
}
function pasteContentCommand(node, text) {
node.focus();
document.execCommand("insertText", false, text);
}
function collapseToEnd(node) {
node.focus();
const selection = window.getSelection();
selection.collapseToEnd();
}
@@ -57,66 +43,108 @@ function getNodeText(node) {
}
function addLoading(node, loadingId) {
const rect = node.getBoundingClientRect();
const div = document.createElement("div");
div.id = loadingId;
div.innerHTML = loadingSvg;
div.style.cssText = `
width: ${node.offsetWidth}px;
height: ${node.offsetHeight}px;
line-height: ${node.offsetHeight}px;
position: absolute;
position: fixed;
left: ${rect.left}px;
top: ${rect.top}px;
width: ${rect.width}px;
height: ${rect.height}px;
line-height: ${rect.height}px;
text-align: center;
left: ${node.offsetLeft}px;
top: ${node.offsetTop}px;
z-index: 2147483647;
pointer-events: none; /* 允许点击穿透 */
`;
node.offsetParent?.appendChild(div);
document.body.appendChild(div);
}
function removeLoading(node, loadingId) {
const div = node.offsetParent.querySelector(`#${loadingId}`);
if (div) {
div.remove();
}
function removeLoading(loadingId) {
const div = document.getElementById(loadingId);
if (div) div.remove();
}
/**
* 输入框翻译
*/
export default function inputTranslate({
inputRule: {
transOpen,
triggerShortcut,
apiSlug,
fromLang,
toLang,
export class InputTranslator {
#config;
#unregisterShortcut = null;
#isEnabled = false;
#triggerShortcut; // 用于缓存快捷键
constructor({ inputRule = DEFAULT_INPUT_RULE, transApis = [] } = {}) {
this.#config = { inputRule, transApis };
const { triggerShortcut: initialTriggerShortcut } = this.#config.inputRule;
if (initialTriggerShortcut && initialTriggerShortcut.length > 0) {
this.#triggerShortcut = initialTriggerShortcut;
} else {
this.#triggerShortcut = DEFAULT_INPUT_SHORTCUT;
}
if (this.#config.inputRule.transOpen) {
this.enable();
}
}
/**
* 启用输入翻译功能
*/
enable() {
if (this.#isEnabled || !this.#config.inputRule.transOpen) {
return;
}
const { triggerCount, triggerTime } = this.#config.inputRule;
this.#unregisterShortcut = stepShortcutRegister(
this.#triggerShortcut,
this.#handleTranslate.bind(this),
triggerCount,
triggerTime,
transSign,
} = DEFAULT_INPUT_RULE,
transApis,
}) {
if (!transOpen) {
triggerTime
);
this.#isEnabled = true;
kissLog("Input Translator enabled.");
}
/**
* 禁用输入翻译功能
*/
disable() {
if (!this.#isEnabled) {
return;
}
const apiSetting =
transApis.find((api) => api.apiSlug === apiSlug) || DEFAULT_API_SETTING;
if (triggerShortcut.length === 0) {
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
triggerCount = 1;
if (this.#unregisterShortcut) {
this.#unregisterShortcut();
this.#unregisterShortcut = null;
}
this.#isEnabled = false;
kissLog("Input Translator disabled.");
}
stepShortcutRegister(
triggerShortcut,
async () => {
/**
* 切换启用/禁用状态
*/
toggle() {
if (this.#isEnabled) {
this.disable();
} else {
this.enable();
}
}
/**
* 翻译核心逻辑
* @private
*/
async #handleTranslate() {
let node = document.activeElement;
if (!node) return;
if (!node) {
return;
}
while (node.shadowRoot) {
while (node.shadowRoot && node.shadowRoot.activeElement) {
node = node.shadowRoot.activeElement;
}
@@ -124,25 +152,32 @@ export default function inputTranslate({
return;
}
const { apiSlug, transSign, triggerCount } = this.#config.inputRule;
let { fromLang, toLang } = this.#config.inputRule;
let initText = getNodeText(node);
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
// todo: remove multiple char
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
}
if (!initText.trim()) {
return;
if (
this.#triggerShortcut.length === 1 &&
this.#triggerShortcut[0].length === 1
) {
initText = removeEndchar(
initText,
this.#triggerShortcut[0],
triggerCount
);
}
if (!initText.trim()) return;
let text = initText;
if (transSign) {
const res = matchInputStr(text, transSign);
if (res) {
let lang = res[1];
if (lang === "zh" || lang === "cn") {
lang = "zh-CN";
} else if (lang === "tw" || lang === "hk") {
lang = "zh-TW";
}
if (lang === "zh" || lang === "cn") lang = "zh-CN";
else if (lang === "tw" || lang === "hk") lang = "zh-TW";
if (lang && OPT_LANGS_LIST.includes(lang)) {
toLang = lang;
}
@@ -150,51 +185,63 @@ export default function inputTranslate({
}
}
// console.log("input -->", text);
const apiSetting =
this.#config.transApis.find((api) => api.apiSlug === apiSlug) ||
DEFAULT_API_SETTING;
const loadingId = "kiss-loading-" + genEventName();
const loadingId = "kiss-" + genEventName();
try {
addLoading(node, loadingId);
const [trText, isSame] = await apiTranslate({
apiSlug,
text,
fromLang,
toLang,
apiSlug,
apiSetting,
});
if (!trText || isSame) {
return;
}
if (!trText || isSame) return;
if (isInputNode(node)) {
node.value = trText;
node.dispatchEvent(
new Event("input", { bubbles: true, cancelable: true })
);
return;
}
selectContent(node);
await sleep(200);
pasteContentEvent(node, trText);
await sleep(200);
// todo: use includes?
if (getNodeText(node).startsWith(initText)) {
pasteContentCommand(node, trText);
await sleep(100);
} else {
collapseToEnd(node);
replaceContentEditableText(node, trText);
}
} catch (err) {
kissLog("translate input", err);
kissLog("Translate input error:", err);
} finally {
removeLoading(node, loadingId);
removeLoading(loadingId);
}
}
/**
* 更新配置
*/
updateConfig({ inputRule, transApis }) {
const wasEnabled = this.#isEnabled;
if (wasEnabled) {
this.disable();
}
if (inputRule) {
this.#config.inputRule = inputRule;
}
if (transApis) {
this.#config.transApis = transApis;
}
const { triggerShortcut: initialTriggerShortcut } = this.#config.inputRule;
this.#triggerShortcut =
initialTriggerShortcut && initialTriggerShortcut.length > 0
? initialTriggerShortcut
: DEFAULT_INPUT_SHORTCUT;
if (wasEnabled) {
this.enable();
}
}
},
triggerCount,
triggerTime
);
}

View File

@@ -78,7 +78,6 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
"hasShadowroot",
"transTag",
"transTitle",
"transSelected",
// "detectRemote",
// "fixerFunc",
].forEach((key) => {
@@ -153,7 +152,6 @@ export const checkRules = (rules) => {
// transTiming,
transTag,
transTitle,
transSelected,
// detectRemote,
// skipLangs,
// fixerSelector,
@@ -186,7 +184,6 @@ export const checkRules = (rules) => {
// transTiming: matchValue([GLOBAL_KEY, ...OPT_TIMING_ALL], transTiming),
transTag: matchValue([GLOBAL_KEY, "span", "font"], transTag),
transTitle: matchValue([GLOBAL_KEY, "true", "false"], transTitle),
transSelected: matchValue([GLOBAL_KEY, "true", "false"], transSelected),
// detectRemote: matchValue([GLOBAL_KEY, "true", "false"], detectRemote),
// skipLangs: type(skipLangs) === "array" ? skipLangs : [],
// fixerSelector: type(fixerSelector) === "string" ? fixerSelector : "",

95
src/libs/tranbox.js Normal file
View File

@@ -0,0 +1,95 @@
import React from "react";
import ReactDOM from "react-dom/client";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import Slection from "../views/Selection";
import { DEFAULT_TRANBOX_SETTING, APP_CONSTS } from "../config";
export class TransboxManager {
#container = null;
#reactRoot = null;
#shadowContainer = null;
#props = {};
constructor(initialProps = {}) {
this.#props = initialProps;
const { tranboxSetting = DEFAULT_TRANBOX_SETTING } = this.#props;
if (tranboxSetting?.transOpen) {
this.enable();
}
}
isEnabled() {
return (
!!this.#container && document.body.parentElement.contains(this.#container)
);
}
enable() {
if (!this.isEnabled()) {
this.#container = document.createElement("div");
this.#container.setAttribute("id", APP_CONSTS.boxID);
this.#container.style.cssText =
"font-size: 0; width: 0; height: 0; border: 0; padding: 0; margin: 0;";
document.body.parentElement.appendChild(this.#container);
this.#shadowContainer = this.#container.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style");
const shadowRootElement = document.createElement("div");
shadowRootElement.classList.add(`${APP_CONSTS.boxID}_warpper`);
this.#shadowContainer.appendChild(emotionRoot);
this.#shadowContainer.appendChild(shadowRootElement);
const cache = createCache({
key: APP_CONSTS.boxID,
prepend: true,
container: emotionRoot,
});
this.#reactRoot = ReactDOM.createRoot(shadowRootElement);
this.CacheProvider = ({ children }) => (
<CacheProvider value={cache}>{children}</CacheProvider>
);
}
const AppProvider = this.CacheProvider;
this.#reactRoot.render(
<React.StrictMode>
<AppProvider>
<Slection {...this.#props} />
</AppProvider>
</React.StrictMode>
);
}
disable() {
if (!this.isEnabled() || !this.#reactRoot) {
return;
}
this.#reactRoot.unmount();
this.#container.remove();
this.#container = null;
this.#reactRoot = null;
this.#shadowContainer = null;
this.CacheProvider = null;
}
toggle() {
if (this.isEnabled()) {
this.disable();
} else {
this.enable();
}
}
update(newProps) {
this.#props = { ...this.#props, ...newProps };
if (this.isEnabled()) {
if (!this.#props.tranboxSetting?.transOpen) {
this.disable();
} else {
this.enable();
}
}
}
}

View File

@@ -10,6 +10,14 @@ import {
// DEFAULT_MOUSEHOVER_KEY,
OPT_STYLE_NONE,
DEFAULT_API_SETTING,
MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
MSG_OPEN_TRANBOX,
MSG_TRANSBOX_TOGGLE,
MSG_MOUSEHOVER_TOGGLE,
MSG_TRANSINPUT_TOGGLE,
} from "../config";
import interpreter from "./interpreter";
import { ShadowRootMonitor } from "./shadowroot";
@@ -25,6 +33,10 @@ import { genTextClass } from "./style";
import { loadingSvg } from "./svg";
import { shortcutRegister } from "./shortcut";
import { tryDetectLang } from "./detect";
import { browser } from "./browser";
import { isIframe, sendIframeMsg } from "./iframe";
import { TransboxManager } from "./tranbox";
import { InputTranslator } from "./inputTranslate";
/**
* @class Translator
@@ -269,6 +281,10 @@ export class Translator {
#textClass = {}; // 译文样式class
#textSheet = ""; // 译文样式字典
#isUserscript = false;
#transboxManager = null; // 划词翻译
#inputTranslator = null; // 输入框翻译
#observedNodes = new WeakSet(); // 存储所有被识别出的、可翻译的 DOM 节点单元
#translationNodes = new WeakMap(); // 存储所有插入到页面的译文节点
#viewNodes = new Set(); // 当前在可视范围内的单元
@@ -293,9 +309,10 @@ export class Translator {
return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
}
constructor(rule = {}, setting = {}) {
constructor(rule = {}, setting = {}, isUserscript) {
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
this.#isUserscript = isUserscript;
this.#eventName = genEventName();
this.#docInfo = {
title: document.title,
@@ -326,6 +343,19 @@ export class Translator {
this.#enableMouseHover();
}
if (!isIframe) {
// 监听后端事件
if (!isUserscript) {
this.#runtimeListener();
}
// 划词翻译
this.#transboxManager = new TransboxManager(this.setting);
// 输入框翻译
this.#inputTranslator = new InputTranslator(this.setting);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => this.#run());
} else {
@@ -368,6 +398,43 @@ export class Translator {
}
}
// 监听后端事件
#runtimeListener() {
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) {
case MSG_TRANS_TOGGLE:
this.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
break;
case MSG_TRANS_TOGGLE_STYLE:
this.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
break;
case MSG_TRANS_GETRULE:
break;
case MSG_TRANS_PUTRULE:
this.updateRule(args);
sendIframeMsg(MSG_TRANS_PUTRULE, args);
break;
case MSG_OPEN_TRANBOX:
window.dispatchEvent(new CustomEvent(MSG_OPEN_TRANBOX));
break;
case MSG_TRANSBOX_TOGGLE:
this.toggleTransbox();
break;
case MSG_MOUSEHOVER_TOGGLE:
this.toggleMouseHover();
break;
case MSG_TRANSINPUT_TOGGLE:
this.toggleInputTranslate();
break;
default:
return { error: `message action is unavailable: ${action}` };
}
return { rule: this.rule, setting: this.setting };
});
}
#createPlaceholderRegex() {
const escapedStart = Translator.escapeRegex(
Translator.PLACEHOLDER.startDelimiter
@@ -671,15 +738,17 @@ export class Translator {
// 开始/重新监控节点
#startObserveNode(node) {
if (this.#observedNodes.has(node)) {
// 未监控
if (!this.#observedNodes.has(node)) {
this.#observedNodes.add(node);
this.#io.observe(node);
return;
}
// 已监控,但未处理状态,且在可视范围
if (!this.#processedNodes.has(node) && this.#viewNodes.has(node)) {
this.#reIO(node);
}
} else {
this.#observedNodes.add(node);
this.#io.observe(node);
}
}
// 非自动识别文本模式下,快速查询目标节点
@@ -1369,6 +1438,19 @@ export class Translator {
this.updateRule({ textStyle });
}
// 切换划词翻译
toggleTransbox() {
this.#setting.tranboxSetting.transOpen =
!this.#setting.tranboxSetting.transOpen;
this.#transboxManager?.toggle();
}
// 切换输入框翻译
toggleInputTranslate() {
this.#setting.inputRule.transOpen = !this.#setting.inputRule.transOpen;
this.#inputTranslator?.toggle();
}
// 停止运行
stop() {
this.disable();

View File

@@ -112,7 +112,6 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
// transTiming = OPT_TIMING_PAGESCROLL,
transTag = DEFAULT_TRANS_TAG,
transTitle = "false",
transSelected = "true",
// detectRemote = "true",
// skipLangs = [],
// fixerSelector = "",
@@ -337,22 +336,6 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="transSelected"
value={transSelected}
label={i18n("translate_selected")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select

View File

@@ -12,12 +12,13 @@ import {
OPT_DICT_BAIDU,
} from "../../config";
import ShortcutInput from "./ShortcutInput";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import { useCallback } from "react";
import { limitNumber } from "../../libs/utils";
import { useTranbox } from "../../hooks/Tranbox";
import { isExt } from "../../libs/client";
import { useApiList } from "../../hooks/Api";
import Alert from "@mui/material/Alert";
export default function Tranbox() {
const i18n = useI18n();
@@ -49,6 +50,7 @@ export default function Tranbox() {
);
const {
transOpen,
apiSlug,
fromLang,
toLang,
@@ -70,7 +72,19 @@ export default function Tranbox() {
return (
<Box>
<Stack spacing={3}>
<Alert severity="info">{i18n("selected_translation_alert")}</Alert>
<FormControlLabel
control={
<Switch
size="small"
name="transOpen"
checked={transOpen}
onChange={() => {
updateTranbox({ transOpen: !transOpen });
}}
/>
}
label={i18n("toggle_selection_translate")}
/>
<Box>
<Grid container spacing={2} columns={12}>

View File

@@ -20,6 +20,9 @@ import {
MSG_OPEN_OPTIONS,
MSG_SAVE_RULE,
MSG_COMMAND_SHORTCUTS,
MSG_TRANSBOX_TOGGLE,
MSG_MOUSEHOVER_TOGGLE,
MSG_TRANSINPUT_TOGGLE,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_STYLE_ALL,
@@ -35,9 +38,7 @@ import { parseUrlPattern } from "../../libs/utils";
export default function Popup({ setShowPopup, translator }) {
const i18n = useI18n();
const [rule, setRule] = useState(translator?.rule);
const [transApis, setTransApis] = useState(
translator?.setting?.transApis || []
);
const [setting, setSetting] = useState(translator?.setting);
const [commands, setCommands] = useState({});
const handleOpenSetting = () => {
@@ -66,6 +67,66 @@ export default function Popup({ setShowPopup, translator }) {
}
};
const handleTransboxToggle = async (e) => {
try {
setSetting((pre) => ({
...pre,
tranboxSetting: { ...pre.tranboxSetting, transOpen: e.target.checked },
}));
if (!translator) {
await sendTabMsg(MSG_TRANSBOX_TOGGLE);
} else {
translator.toggleTransbox();
sendIframeMsg(MSG_TRANSBOX_TOGGLE);
}
} catch (err) {
kissLog("toggle transbox", err);
}
};
const handleMousehoverToggle = async (e) => {
try {
setSetting((pre) => ({
...pre,
mouseHoverSetting: {
...pre.mouseHoverSetting,
useMouseHover: e.target.checked,
},
}));
if (!translator) {
await sendTabMsg(MSG_MOUSEHOVER_TOGGLE);
} else {
translator.toggleMouseHover();
sendIframeMsg(MSG_MOUSEHOVER_TOGGLE);
}
} catch (err) {
kissLog("toggle mousehover", err);
}
};
const handleInputTransToggle = async (e) => {
try {
setSetting((pre) => ({
...pre,
inputRule: {
...pre.inputRule,
transOpen: e.target.checked,
},
}));
if (!translator) {
await sendTabMsg(MSG_TRANSINPUT_TOGGLE);
} else {
translator.toggleInputTranslate();
sendIframeMsg(MSG_TRANSINPUT_TOGGLE);
}
} catch (err) {
kissLog("toggle inputtrans", err);
}
};
const handleChange = async (e) => {
try {
const { name, value } = e.target;
@@ -121,7 +182,7 @@ export default function Popup({ setShowPopup, translator }) {
const res = await sendTabMsg(MSG_TRANS_GETRULE);
if (!res.error) {
setRule(res.rule);
setTransApis(res.setting.transApis);
setSetting(res.setting);
}
} catch (err) {
kissLog("query rule", err);
@@ -155,15 +216,19 @@ export default function Popup({ setShowPopup, translator }) {
const optApis = useMemo(
() =>
transApis
setting?.transApis
.filter((api) => !api.isDisabled)
.map((api) => ({
key: api.apiSlug,
name: api.apiName || api.apiSlug,
})),
[transApis]
[setting]
);
const tranboxEnabled = setting?.tranboxSetting.transOpen;
const mouseHoverEnabled = setting?.mouseHoverSetting.useMouseHover;
const inputTransEnabled = setting?.inputRule.transOpen;
if (!rule) {
return (
<Box minWidth={300}>
@@ -195,7 +260,7 @@ export default function Popup({ setShowPopup, translator }) {
} = rule;
return (
<Box width={320}>
<Box width={360}>
{!translator && (
<>
<Header />
@@ -275,6 +340,48 @@ export default function Popup({ setShowPopup, translator }) {
label={i18n("richtext_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