feat: more touch operations

This commit is contained in:
Gabe
2025-10-22 01:50:49 +08:00
parent 53e32d3031
commit 60b9653fd3
15 changed files with 730 additions and 550 deletions

View File

@@ -1184,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`,

View File

@@ -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";

View File

@@ -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名单

View File

@@ -3,12 +3,16 @@ import { APP_CONSTS } from "../config";
import ContentFab from "../views/Action/ContentFab"; import ContentFab from "../views/Action/ContentFab";
export class FabManager extends ShadowDomManager { export class FabManager extends ShadowDomManager {
constructor({ translator, popupManager, fabConfig }) { constructor({ translator, processActions, fabConfig }) {
super({ super({
id: APP_CONSTS.fabID, id: APP_CONSTS.fabID,
className: "notranslate", className: "notranslate",
reactComponent: ContentFab, reactComponent: ContentFab,
props: { translator, popupManager, fabConfig }, props: { translator, processActions, fabConfig },
}); });
if (!fabConfig?.isHide) {
this.show();
}
} }
} }

View File

@@ -1,14 +1,26 @@
import ShadowDomManager from "./shadowDomManager"; import ShadowDomManager from "./shadowDomManager";
import { APP_CONSTS } from "../config"; import { APP_CONSTS, EVENT_KISS, MSG_POPUP_TOGGLE } from "../config";
import Action from "../views/Action"; import Action from "../views/Action";
export class PopupManager extends ShadowDomManager { export class PopupManager extends ShadowDomManager {
constructor({ translator }) { constructor({ translator, processActions }) {
super({ super({
id: APP_CONSTS.popupID, id: APP_CONSTS.popupID,
className: "notranslate", className: "notranslate",
reactComponent: Action, reactComponent: Action,
props: { translator }, 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);
}
}
} }

View File

@@ -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);
}; };
} }

View File

@@ -1644,6 +1644,17 @@ export class Translator {
this.updateRule({ textStyle }); this.updateRule({ textStyle });
} }
// 切换划词翻译
toggleTransbox() {
this.#setting.tranboxSetting.transOpen =
!this.#setting.tranboxSetting.transOpen;
}
// 切换输入框翻译
toggleInputTranslate() {
this.#setting.inputRule.transOpen = !this.#setting.inputRule.transOpen;
}
// 停止运行 // 停止运行
stop() { stop() {
this.disable(); this.disable();

View File

@@ -4,9 +4,8 @@ import { InputTranslator } from "./inputTranslate";
import { TransboxManager } from "./tranbox"; import { TransboxManager } from "./tranbox";
import { shortcutRegister } from "./shortcut"; import { shortcutRegister } from "./shortcut";
import { sendIframeMsg } from "./iframe"; import { sendIframeMsg } from "./iframe";
import { newI18n } from "../config"; import { EVENT_KISS, newI18n } from "../config";
import { touchTapListener } from "./touch"; import { touchTapListener } from "./touch";
import { debounce } from "./utils";
import { PopupManager } from "./popupManager"; import { PopupManager } from "./popupManager";
import { FabManager } from "./fabManager"; import { FabManager } from "./fabManager";
import { import {
@@ -20,6 +19,7 @@ import {
MSG_TRANS_PUTRULE, MSG_TRANS_PUTRULE,
MSG_OPEN_TRANBOX, MSG_OPEN_TRANBOX,
MSG_TRANSBOX_TOGGLE, MSG_TRANSBOX_TOGGLE,
MSG_POPUP_TOGGLE,
MSG_MOUSEHOVER_TOGGLE, MSG_MOUSEHOVER_TOGGLE,
MSG_TRANSINPUT_TOGGLE, MSG_TRANSINPUT_TOGGLE,
} from "../config"; } from "../config";
@@ -57,16 +57,15 @@ export default class TranslatorManager {
if (!isIframe) { if (!isIframe) {
this._transboxManager = new TransboxManager(setting); this._transboxManager = new TransboxManager(setting);
this._inputTranslator = new InputTranslator(setting); this._inputTranslator = new InputTranslator(setting);
this._popupManager = new PopupManager({ translator: this._translator }); this._popupManager = new PopupManager({
translator: this._translator,
if (fabConfig && !fabConfig.isHide) { processActions: this.#processActions.bind(this),
this._fabManager = new FabManager({ });
translator: this._translator, this._fabManager = new FabManager({
popupManager: this._popupManager, translator: this._translator,
fabConfig, processActions: this.#processActions.bind(this),
}); fabConfig,
this._fabManager.show(); });
}
} }
this.#windowMessageHandler = this.#handleWindowMessage.bind(this); this.#windowMessageHandler = this.#handleWindowMessage.bind(this);
@@ -125,8 +124,8 @@ export default class TranslatorManager {
} }
// 子模块 // 子模块
this._popupManager?.hide(); this._popupManager?.destroy();
this._fabManager?.hide(); this._fabManager?.destroy();
this._transboxManager?.disable(); this._transboxManager?.disable();
this._inputTranslator?.disable(); this._inputTranslator?.disable();
this._translator.stop(); this._translator.stop();
@@ -151,11 +150,39 @@ export default class TranslatorManager {
return; return;
} }
const handleTap = debounce(() => { const handleTap = () => {
this.#processActions({ action: MSG_TRANS_TOGGLE }); this.#processActions({ action: MSG_TRANS_TOGGLE });
}, 300); };
this.#clearTouchListener = touchTapListener(handleTap, touchTranslate); 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) { #handleWindowMessage(event) {
@@ -182,7 +209,7 @@ export default class TranslatorManager {
this.#processActions({ action: MSG_TRANS_TOGGLE_STYLE }) this.#processActions({ action: MSG_TRANS_TOGGLE_STYLE })
), ),
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () => shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () =>
this._popupManager.toggle() this.#processActions({ action: MSG_POPUP_TOGGLE })
), ),
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () => shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () =>
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank") window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank")
@@ -210,7 +237,7 @@ export default class TranslatorManager {
), ),
GM.registerMenuCommand( GM.registerMenuCommand(
i18n("open_menu"), i18n("open_menu"),
() => this._popupManager.toggle(), () => this.#processActions({ action: MSG_POPUP_TOGGLE }),
"K" "K"
), ),
GM.registerMenuCommand( GM.registerMenuCommand(
@@ -239,16 +266,25 @@ export default class TranslatorManager {
this._translator.updateRule(args); this._translator.updateRule(args);
break; break;
case MSG_OPEN_TRANBOX: case MSG_OPEN_TRANBOX:
this._transboxManager?.enable(); document.dispatchEvent(
new CustomEvent(EVENT_KISS, {
detail: { action: MSG_OPEN_TRANBOX },
})
);
break;
case MSG_POPUP_TOGGLE:
this._popupManager?.toggle();
break; break;
case MSG_TRANSBOX_TOGGLE: case MSG_TRANSBOX_TOGGLE:
this._transboxManager?.toggle(); this._transboxManager?.toggle();
this._translator.toggleTransbox();
break; break;
case MSG_MOUSEHOVER_TOGGLE: case MSG_MOUSEHOVER_TOGGLE:
this._translator.toggleMouseHover(); this._translator.toggleMouseHover();
break; break;
case MSG_TRANSINPUT_TOGGLE: case MSG_TRANSINPUT_TOGGLE:
this._inputTranslator?.toggle(); this._inputTranslator?.toggle();
this._translator.toggleInputTranslate();
break; break;
default: default:
logger.info(`Message action is unavailable: ${action}`); logger.info(`Message action is unavailable: ${action}`);

View File

@@ -4,14 +4,14 @@ import ThemeProvider from "../../hooks/Theme";
import Draggable from "./Draggable"; import Draggable from "./Draggable";
import { useState, useMemo, useCallback } from "react"; import { useState, useMemo, useCallback } from "react";
import { SettingProvider } from "../../hooks/Setting"; import { SettingProvider } from "../../hooks/Setting";
import { MSG_TRANS_TOGGLE } from "../../config"; import { MSG_TRANS_TOGGLE, MSG_POPUP_TOGGLE } from "../../config";
import { sendIframeMsg } from "../../libs/iframe"; import { sendIframeMsg } from "../../libs/iframe";
import useWindowSize from "../../hooks/WindowSize"; import useWindowSize from "../../hooks/WindowSize";
export default function ContentFab({ export default function ContentFab({
translator, translator,
fabConfig: { x: fabX, y: fabY, fabClickAction = 0 } = {}, fabConfig: { x: fabX, y: fabY, fabClickAction = 0 } = {},
popupManager, processActions,
}) { }) {
const fabWidth = 40; const fabWidth = 40;
const windowSize = useWindowSize(); const windowSize = useWindowSize();
@@ -31,10 +31,10 @@ export default function ContentFab({
translator.toggle(); translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE); sendIframeMsg(MSG_TRANS_TOGGLE);
} else { } else {
popupManager.toggle(); processActions({ action: MSG_POPUP_TOGGLE });
} }
} }
}, [moved, translator, popupManager, fabClickAction]); }, [moved, translator, fabClickAction, processActions]);
const fabProps = useMemo( const fabProps = useMemo(
() => ({ () => ({

View File

@@ -1,27 +1,59 @@
import ThemeProvider from "../../hooks/Theme"; import ThemeProvider from "../../hooks/Theme";
import Draggable from "./Draggable"; import Draggable from "./Draggable";
import { useEffect, 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 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 useWindowSize from "../../hooks/WindowSize"; import useWindowSize from "../../hooks/WindowSize";
import { EVENT_KISS, MSG_OPEN_OPTIONS, MSG_POPUP_TOGGLE } from "../../config";
import PopupCont from "../Popup/PopupCont";
import { isExt } from "../../libs/client";
import { sendBgMsg } from "../../libs/msg";
export default function Action({ translator, onClose }) { export default function Action({ translator, processActions }) {
const [showPopup, setShowPopup] = useState(true);
const [rule, setRule] = useState(translator.rule);
const [setting, setSetting] = useState(translator.setting);
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const handleWindowClick = useCallback(() => { const handleOpenSetting = useCallback(() => {
onClose(); if (isExt) {
}, [onClose]); sendBgMsg(MSG_OPEN_OPTIONS);
} else {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
}
}, []);
useEffect(() => { useEffect(() => {
const handleWindowClick = () => {
setShowPopup(false);
};
window.addEventListener("click", handleWindowClick); window.addEventListener("click", handleWindowClick);
return () => { return () => {
window.removeEventListener("click", handleWindowClick); window.removeEventListener("click", handleWindowClick);
}; };
}, [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, 360); const width = Math.min(windowSize.w, 360);
@@ -40,19 +72,35 @@ export default function Action({ translator, onClose }) {
return ( return (
<SettingProvider> <SettingProvider>
<ThemeProvider> <ThemeProvider>
<Draggable {showPopup && (
key="pop" <Draggable
{...popProps} key="pop"
usePaper {...popProps}
handler={ usePaper
<Box style={{ cursor: "move" }}> handler={
<Header onClose={onClose} /> <Box style={{ cursor: "move" }}>
<Divider /> <Header
onClose={() => {
setShowPopup(false);
}}
/>
<Divider />
</Box>
}
>
<Box width={360}>
<PopupCont
rule={rule}
setting={setting}
setRule={setRule}
setSetting={setSetting}
handleOpenSetting={handleOpenSetting}
processActions={processActions}
isContent={true}
/>
</Box> </Box>
} </Draggable>
> )}
<Popup translator={translator} />
</Draggable>
</ThemeProvider> </ThemeProvider>
</SettingProvider> </SettingProvider>
); );

View File

@@ -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>

View 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>
);
}

View File

@@ -1,181 +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({ 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");
}
};
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);
@@ -187,297 +32,27 @@ export default function Popup({ 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={360}>
{!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>
); );
} }

View File

@@ -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>
); );

View File

@@ -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