tranbox...

This commit is contained in:
Gabe Yuan
2023-10-23 18:02:42 +08:00
parent e89da9120c
commit 4125aba808
13 changed files with 706 additions and 21 deletions

View File

@@ -9,11 +9,13 @@ import {
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
APP_LCNAME,
DEFAULT_TRANBOX_SETTING,
} from "./config";
import { getRulesWithDefault, getFabWithDefault } from "./libs/storage";
import { Translator } from "./libs/translator";
import { sendIframeMsg, sendParentMsg } from "./libs/iframe";
import { matchRule } from "./libs/rules";
import Slection from "./views/Selection";
export async function runTranslator(setting) {
const href = document.location.href;
@@ -50,7 +52,10 @@ export function runIframe(setting) {
export async function showFab(translator) {
const fab = await getFabWithDefault();
if (!fab.isHide) {
if (fab.isHide) {
return;
}
const $action = document.createElement("div");
$action.setAttribute("id", APP_LCNAME);
document.body.parentElement.appendChild($action);
@@ -71,7 +76,33 @@ export async function showFab(translator) {
</CacheProvider>
</React.StrictMode>
);
}
export function showTransbox({ tranboxSetting = DEFAULT_TRANBOX_SETTING }) {
if (!tranboxSetting?.transOpen) {
return;
}
const $tranbox = document.createElement("div");
$tranbox.setAttribute("id", "kiss-transbox");
document.body.parentElement.appendChild($tranbox);
const shadowContainer = $tranbox.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style");
const shadowRootElement = document.createElement("div");
shadowContainer.appendChild(emotionRoot);
shadowContainer.appendChild(shadowRootElement);
const cache = createCache({
key: "kiss-transbox",
prepend: true,
container: emotionRoot,
});
ReactDOM.createRoot(shadowRootElement).render(
<React.StrictMode>
<CacheProvider value={cache}>
<Slection tranboxSetting={tranboxSetting} />
</CacheProvider>
</React.StrictMode>
);
}
export function windowListener(rule) {

View File

@@ -611,4 +611,24 @@ export const I18N = {
zh: `启用`,
en: `Enable`,
},
selection_translate: {
zh: `划词翻译`,
en: `Selection Translate`,
},
toggle_selection_translate: {
zh: `启用划词翻译`,
en: `Use Selection Translate`,
},
trigger_tranbox_shortcut: {
zh: `显示翻译框快捷键`,
en: `Toggle Translate Box Shortcut`,
},
tanbtn_offset_x: {
zh: `翻译按钮偏移X`,
en: `Translate Button Offset (X)`,
},
tanbtn_offset_y: {
zh: `翻译按钮偏移Y`,
en: `Translate Button Offset (Y)`,
},
};

View File

@@ -312,6 +312,18 @@ export const DEFAULT_INPUT_RULE = {
transSign: OPT_INPUT_TRANS_SIGNS[0],
};
// 划词翻译
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyB"];
export const DEFAULT_TRANBOX_SETTING = {
transOpen: true,
translator: OPT_TRANS_MICROSOFT,
fromLang: "auto",
toLang: "zh-CN",
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
btnOffsetX: 10,
btnOffsetY: 10,
};
// 订阅列表
export const DEFAULT_SUBRULES_LIST = [
{
@@ -388,6 +400,7 @@ export const DEFAULT_SETTING = {
mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
};
export const DEFAULT_RULES = [GLOBLA_RULE];

View File

@@ -12,6 +12,7 @@ import {
runIframe,
runTranslator,
showFab,
showTransbox,
windowListener,
showErr,
} from "./common";
@@ -66,6 +67,9 @@ function runtimeListener(translator) {
// 浮球按钮
await showFab(translator);
// 划词翻译
showTransbox(setting);
} catch (err) {
showErr(err);
}

18
src/hooks/Tranbox.js Normal file
View File

@@ -0,0 +1,18 @@
import { useCallback } from "react";
import { DEFAULT_TRANBOX_SETTING } from "../config";
import { useSetting } from "./Setting";
export function useTranbox() {
const { setting, updateSetting } = useSetting();
const tranboxSetting = setting?.tranboxSetting || DEFAULT_TRANBOX_SETTING;
const updateTranbox = useCallback(
async (obj) => {
Object.assign(tranboxSetting, obj);
await updateSetting({ tranboxSetting });
},
[tranboxSetting, updateSetting]
);
return { tranboxSetting, updateTranbox };
}

View File

@@ -8,6 +8,7 @@ import {
runIframe,
runTranslator,
showFab,
showTransbox,
windowListener,
showErr,
} from "./common";
@@ -65,6 +66,9 @@ function runSettingPage() {
// 浮球按钮
await showFab(translator);
// 划词翻译
showTransbox(setting);
// 同步订阅规则
await trySyncAllSubRules(setting);
} catch (err) {

View File

@@ -45,6 +45,12 @@ export default function Navigator(props) {
url: "/input",
icon: <InputIcon />,
},
{
id: "selection_translate",
label: i18n("selection_translate"),
url: "/tranbox",
icon: <InputIcon />,
},
{
id: "apis_setting",
label: i18n("apis_setting"),

View File

@@ -0,0 +1,137 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import { useI18n } from "../../hooks/I18n";
import { OPT_TRANS_ALL, OPT_LANGS_FROM, OPT_LANGS_TO } 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";
export default function Tranbox() {
const i18n = useI18n();
const { tranboxSetting, updateTranbox } = useTranbox();
const handleChange = (e) => {
e.preventDefault();
let { name, value } = e.target;
switch (name) {
case "btnOffsetX" || "btnOffsetY":
value = limitNumber(value, 0, 100);
break;
default:
}
updateTranbox({
[name]: value,
});
};
const handleShortcutInput = useCallback(
(val) => {
updateTranbox({ tranboxShortcut: val });
},
[updateTranbox]
);
const {
transOpen,
translator,
fromLang,
toLang,
tranboxShortcut,
btnOffsetX,
btnOffsetY,
} = tranboxSetting;
return (
<Box>
<Stack spacing={3}>
<FormControlLabel
control={
<Switch
size="small"
name="transOpen"
checked={transOpen}
onChange={() => {
updateTranbox({ transOpen: !transOpen });
}}
/>
}
label={i18n("toggle_selection_translate")}
/>
<TextField
select
size="small"
name="translator"
value={translator}
label={i18n("translate_service")}
onChange={handleChange}
>
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField
select
size="small"
name="fromLang"
value={fromLang}
label={i18n("from_lang")}
onChange={handleChange}
>
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField
select
size="small"
name="toLang"
value={toLang}
label={i18n("to_lang")}
onChange={handleChange}
>
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField
size="small"
label={i18n("tanbtn_offset_x")}
type="number"
name="btnOffsetX"
defaultValue={btnOffsetX}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("tanbtn_offset_y")}
type="number"
name="btnOffsetY"
defaultValue={btnOffsetY}
onChange={handleChange}
/>
<ShortcutInput
value={tranboxShortcut}
onChange={handleShortcutInput}
label={i18n("trigger_tranbox_shortcut")}
/>
</Stack>
</Box>
);
}

View File

@@ -20,6 +20,7 @@ import Alert from "@mui/material/Alert";
import Apis from "./Apis";
import Webfix from "./Webfix";
import InputSetting from "./InputSetting";
import Tranbox from "./Tranbox";
export default function Options() {
const [error, setError] = useState("");
@@ -120,6 +121,7 @@ export default function Options() {
<Route index element={<Setting />} />
<Route path="rules" element={<Rules />} />
<Route path="input" element={<InputSetting />} />
<Route path="tranbox" element={<Tranbox />} />
<Route path="apis" element={<Apis />} />
<Route path="sync" element={<SyncSetting />} />
<Route path="webfix" element={<Webfix />} />

View File

@@ -0,0 +1,248 @@
import { useState } from "react";
import Paper from "@mui/material/Paper";
import Box from "@mui/material/Box";
function Pointer({
direction,
size,
setSize,
position,
setPosition,
children,
minSize,
maxSize,
...props
}) {
const [origin, setOrigin] = useState(null);
function handlePointerDown(e) {
e.target.setPointerCapture(e.pointerId);
setOrigin({
x: position.x,
y: position.y,
w: size.w,
h: size.h,
clientX: e.clientX,
clientY: e.clientY,
});
}
function handlePointerMove(e) {
if (origin) {
const dx = e.clientX - origin.clientX;
const dy = e.clientY - origin.clientY;
let x = position.x;
let y = position.y;
let w = size.w;
let h = size.h;
switch (direction) {
case "Header":
x = origin.x + dx;
y = origin.y + dy;
break;
case "TopLeft":
x = origin.x + dx;
y = origin.y + dy;
w = origin.w - dx;
h = origin.h - dy;
break;
case "Top":
y = origin.y + dy;
h = origin.h - dy;
break;
case "TopRight":
y = origin.y + dy;
w = origin.w + dx;
h = origin.h - dy;
break;
case "Left":
x = origin.x + dx;
w = origin.w - dx;
break;
case "Right":
w = origin.w + dx;
break;
case "BottomLeft":
x = origin.x + dx;
w = origin.w - dx;
h = origin.h + dy;
break;
case "Bottom":
h = origin.h + dy;
break;
case "BottomRight":
w = origin.w + dx;
h = origin.h + dy;
break;
}
if (w < minSize.w) {
w = minSize.w;
x = position.x;
}
if (w > maxSize.w) {
w = maxSize.w;
x = position.x;
}
if (h < minSize.h) {
h = minSize.h;
y = position.y;
}
if (h > maxSize.h) {
h = maxSize.h;
y = position.y;
}
setPosition({ x, y });
setSize({ w, h });
}
}
function handlePointerUp(e) {
setOrigin(null);
}
return (
<div
{...props}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
>
{children}
</div>
);
}
export default function DraggableResizable({
header,
children,
defaultPosition = {
x: 0,
y: 0,
},
defaultSize = {
w: 600,
h: 400,
},
minSize = {
w: 300,
h: 200,
},
maxSize = {
w: 1200,
h: 1200,
},
sx,
}) {
const lineWidth = 4;
const [position, setPosition] = useState(defaultPosition);
const [size, setSize] = useState(defaultSize);
const opts = {
size,
setSize,
position,
setPosition,
minSize,
maxSize,
};
return (
<Box
style={{
position: "fixed",
left: position.x,
top: position.y,
display: "grid",
gridTemplateColumns: `${lineWidth * 2}px auto ${lineWidth * 2}px`,
gridTemplateRows: `${lineWidth * 2}px auto ${lineWidth * 2}px`,
zIndex: 10000,
}}
>
<Pointer
direction="TopLeft"
style={{
transform: `translate(${lineWidth}px, ${lineWidth}px)`,
cursor: "nw-resize",
}}
{...opts}
/>
<Pointer
direction="Top"
style={{
margin: `0 ${lineWidth}px`,
transform: `translate(0px, ${lineWidth}px)`,
cursor: "row-resize",
}}
{...opts}
/>
<Pointer
direction="TopRight"
style={{
transform: `translate(-${lineWidth}px, ${lineWidth}px)`,
cursor: "ne-resize",
}}
{...opts}
/>
<Pointer
direction="Left"
style={{
margin: `${lineWidth}px 0`,
transform: `translate(${lineWidth}px, 0px)`,
cursor: "col-resize",
}}
{...opts}
/>
<Paper elevation={4}>
<Pointer direction="Header" style={{ cursor: "move" }} {...opts}>
{header}
</Pointer>
<div
style={{
width: size.w,
height: size.h,
overflow: "hidden auto",
}}
>
{children}
</div>
</Paper>
<Pointer
direction="Right"
style={{
margin: `${lineWidth}px 0`,
transform: `translate(-${lineWidth}px, 0px)`,
cursor: "col-resize",
}}
{...opts}
/>
<Pointer
direction="BottomLeft"
style={{
transform: `translate(${lineWidth}px, -${lineWidth}px)`,
cursor: "ne-resize",
}}
{...opts}
/>
<Pointer
direction="Bottom"
style={{
margin: `0 ${lineWidth}px`,
transform: `translate(0px, -${lineWidth}px)`,
cursor: "row-resize",
}}
{...opts}
/>
<Pointer
direction="BottomRight"
style={{
transform: `translate(-${lineWidth}px, -${lineWidth}px)`,
cursor: "nw-resize",
}}
{...opts}
/>
</Box>
);
}

View File

@@ -0,0 +1,105 @@
import { SettingProvider } from "../../hooks/Setting";
import ThemeProvider from "../../hooks/Theme";
import DraggableResizable from "./DraggableResizable";
import Header from "../Popup/Header";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider";
import { useI18n } from "../../hooks/I18n";
import { OPT_TRANS_ALL, OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
function TranForm({ tranboxSetting }) {
const i18n = useI18n();
const {
transOpen,
translator,
fromLang,
toLang,
tranboxShortcut,
btnOffsetX,
btnOffsetY,
} = tranboxSetting;
return (
<Stack sx={{ p: 2 }} spacing={2}>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
name="fromLang"
value={fromLang}
label={i18n("from_lang")}
// onChange={handleChange}
>
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
name="toLang"
value={toLang}
label={i18n("to_lang")}
// onChange={handleChange}
>
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
value={translator}
name="translator"
label={i18n("translate_service")}
// onChange={handleChange}
>
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
</Grid>
</Grid>
</Box>
</Stack>
);
}
export default function TranBox({ position, setShowBox, tranboxSetting }) {
return (
<SettingProvider>
<ThemeProvider>
<DraggableResizable
defaultPosition={position}
header={<Header setShowPopup={setShowBox} />}
>
<Divider />
<TranForm tranboxSetting={tranboxSetting} />
</DraggableResizable>
</ThemeProvider>
</SettingProvider>
);
}

View File

@@ -0,0 +1,43 @@
export default function TranBtn({ onClick, position, tranboxSetting }) {
return (
<div
style={{
cursor: "pointer",
position: "fixed",
left: position.x + tranboxSetting.btnOffsetX,
top: position.y + tranboxSetting.btnOffsetY,
}}
onClick={onClick}
onMouseUp={(e) => {
e.stopPropagation();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 32 32"
version="1.1"
>
<path
d="M 0.455 1.257 C 0.173 1.991, 0.068 9.096, 0.221 17.046 L 0.500 31.500 15.365 31.777 C 25.475 31.966, 30.672 31.687, 31.615 30.905 C 32.681 30.020, 33 26.349, 33 14.967 L 33 0.180 16.984 0.050 C 4.455 -0.051, 0.856 0.212, 0.455 1.257 M 0.402 16 C 0.402 24.525, 0.556 28.013, 0.743 23.750 C 0.931 19.488, 0.931 12.513, 0.743 8.250 C 0.556 3.988, 0.402 7.475, 0.402 16 M 3 16 C 3 26.333, 3.121 27, 5 27 C 6.681 27, 7 26.333, 7 22.826 L 7 18.651 9.500 21 C 10.875 22.292, 12 24.170, 12 25.174 C 12 26.333, 12.730 27, 14 27 C 17.333 27, 16.670 21.670, 12.978 18.780 L 9.957 16.415 12.978 13.521 C 14.640 11.928, 16 10.004, 16 9.243 C 16 8.382, 16.848 7.981, 18.250 8.180 C 20.374 8.482, 20.516 9.020, 20.788 17.750 C 21.011 24.955, 21.390 27, 22.500 27 C 23.610 27, 23.989 24.955, 24.212 17.750 C 24.484 9.020, 24.626 8.482, 26.750 8.180 C 27.987 8.005, 29 7.217, 29 6.430 C 29 5.302, 27.205 5, 20.500 5 C 12.937 5, 12 5.201, 12 6.826 C 12 7.830, 10.875 9.708, 9.500 11 L 7 13.349 7 9.174 C 7 5.667, 6.681 5, 5 5 C 3.121 5, 3 5.667, 3 16"
stroke="none"
fill="#209cee"
fillRule="evenodd"
/>
<path
d="M 4 16 C 4 22.667, 4.394 27, 5 27 C 5.550 27, 6 25.200, 6 23 C 6 20.345, 6.446 19, 7.326 19 C 9.199 19, 13 23.286, 13 25.398 C 13 26.371, 13.562 26.979, 14.250 26.750 C 16.713 25.930, 16.145 22.745, 12.946 19.444 L 9.705 16.100 12.852 12.515 C 14.584 10.544, 16 8.558, 16 8.104 C 16 7.649, 17.125 7.492, 18.500 7.755 C 20.957 8.225, 21 8.396, 21 17.700 C 21 24.828, 21.309 27.064, 22.250 26.752 C 23.100 26.471, 23.592 23.401, 23.788 17.169 C 24.049 8.823, 24.251 8, 26.038 8 C 27.140 8, 28 7.323, 28 6.456 C 28 5.179, 26.699 4.953, 20.500 5.151 C 14.356 5.347, 13 5.683, 13 7.008 C 13 8.949, 7.650 14.316, 6.681 13.347 C 6.306 12.973, 6 10.942, 6 8.833 C 6 6.725, 5.550 5, 5 5 C 4.394 5, 4 9.333, 4 16"
stroke="none"
fill="#f5f9fc"
fillRule="evenodd"
/>
<path
d="M 3.378 16 C 3.378 22.325, 3.541 24.912, 3.739 21.750 C 3.937 18.587, 3.937 13.412, 3.739 10.250 C 3.541 7.087, 3.378 9.675, 3.378 16 M 6.175 8.691 C 6.079 10.996, 6.381 13.118, 6.847 13.405 C 8.295 14.300, 13.227 8.362, 12.708 6.349 C 12.289 4.723, 12.218 4.750, 12.116 6.576 C 12.052 7.717, 10.903 9.682, 9.563 10.941 L 7.126 13.230 6.738 8.865 L 6.349 4.500 6.175 8.691 M 15.333 5.667 C 15.700 6.033, 16.300 6.033, 16.667 5.667 C 17.033 5.300, 16.733 5, 16 5 C 15.267 5, 14.967 5.300, 15.333 5.667 M 28.079 6.583 C 28.127 7.748, 28.364 7.985, 28.683 7.188 C 28.972 6.466, 28.936 5.603, 28.604 5.271 C 28.272 4.939, 28.036 5.529, 28.079 6.583 M 16 7.500 C 16 7.775, 16.225 8, 16.500 8 C 16.775 8, 17 7.775, 17 7.500 C 17 7.225, 16.775 7, 16.500 7 C 16.225 7, 16 7.225, 16 7.500 M 20 8.500 C 20 8.775, 20.225 9, 20.500 9 C 20.775 9, 21 8.775, 21 8.500 C 21 8.225, 20.775 8, 20.500 8 C 20.225 8, 20 8.225, 20 8.500 M 24 8.500 C 24 8.775, 24.225 9, 24.500 9 C 24.775 9, 25 8.775, 25 8.500 C 25 8.225, 24.775 8, 24.500 8 C 24.225 8, 24 8.225, 24 8.500 M 12.573 12.906 L 9.645 16.500 13.021 13.282 C 14.878 11.512, 16.196 9.895, 15.949 9.688 C 15.702 9.481, 14.183 10.929, 12.573 12.906 M 13.500 20 C 14.495 21.100, 15.535 22, 15.810 22 C 16.085 22, 15.495 21.100, 14.500 20 C 13.505 18.900, 12.465 18, 12.190 18 C 11.915 18, 12.505 18.900, 13.500 20 M 6.272 23 C 6.272 25.475, 6.467 26.488, 6.706 25.250 C 6.944 24.012, 6.944 21.988, 6.706 20.750 C 6.467 19.512, 6.272 20.525, 6.272 23 M 9.650 21 C 10.833 22.375, 12.018 24.400, 12.284 25.500 C 12.753 27.438, 12.772 27.436, 12.884 25.424 C 12.948 24.283, 11.762 22.258, 10.250 20.924 L 7.500 18.500 9.650 21 M 15 26.500 C 15 26.775, 15.225 27, 15.500 27 C 15.775 27, 16 26.775, 16 26.500 C 16 26.225, 15.775 26, 15.500 26 C 15.225 26, 15 26.225, 15 26.500 M 23 26.500 C 23 26.775, 23.225 27, 23.500 27 C 23.775 27, 24 26.775, 24 26.500 C 24 26.225, 23.775 26, 23.500 26 C 23.225 26, 23 26.225, 23 26.500"
stroke="none"
fill="#259cec"
fillRule="evenodd"
/>
</svg>
</div>
);
}

View File

@@ -0,0 +1,54 @@
import { useState, useEffect } from "react";
import TranBtn from "./Tranbtn";
import TranBox from "./Tranbox";
export default function Slection({ tranboxSetting }) {
const [showBox, setShowBox] = useState(false);
const [showBtn, setShowBtn] = useState(false);
const [text, setText] = useState("");
const [position, setPosition] = useState({ x: 0, y: 0 });
console.log("tranboxSetting", tranboxSetting);
function handleMouseup(e) {
const text = window.getSelection()?.toString()?.trim() || "";
setPosition({ x: e.clientX, y: e.clientY });
setText(text);
setShowBtn(!!text);
}
const handleClick = (e) => {
e.stopPropagation();
setShowBtn(false);
if (!!text) {
setShowBox(true);
}
};
useEffect(() => {
window.addEventListener("mouseup", handleMouseup);
return () => {
window.removeEventListener("mouseup", handleMouseup);
};
}, []);
return (
<>
{showBox && (
<TranBox
position={position}
tranboxSetting={tranboxSetting}
setShowBox={setShowBox}
/>
)}
{showBtn && (
<TranBtn
position={position}
tranboxSetting={tranboxSetting}
onClick={handleClick}
/>
)}
</>
);
}