diff --git a/src/common.js b/src/common.js
index a35cada..f9d0cbc 100644
--- a/src/common.js
+++ b/src/common.js
@@ -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,28 +52,57 @@ export function runIframe(setting) {
export async function showFab(translator) {
const fab = await getFabWithDefault();
- if (!fab.isHide) {
- const $action = document.createElement("div");
- $action.setAttribute("id", APP_LCNAME);
- document.body.parentElement.appendChild($action);
- const shadowContainer = $action.attachShadow({ mode: "closed" });
- const emotionRoot = document.createElement("style");
- const shadowRootElement = document.createElement("div");
- shadowContainer.appendChild(emotionRoot);
- shadowContainer.appendChild(shadowRootElement);
- const cache = createCache({
- key: APP_LCNAME,
- prepend: true,
- container: emotionRoot,
- });
- ReactDOM.createRoot(shadowRootElement).render(
-
-
-
-
-
- );
+ if (fab.isHide) {
+ return;
}
+
+ const $action = document.createElement("div");
+ $action.setAttribute("id", APP_LCNAME);
+ document.body.parentElement.appendChild($action);
+ const shadowContainer = $action.attachShadow({ mode: "closed" });
+ const emotionRoot = document.createElement("style");
+ const shadowRootElement = document.createElement("div");
+ shadowContainer.appendChild(emotionRoot);
+ shadowContainer.appendChild(shadowRootElement);
+ const cache = createCache({
+ key: APP_LCNAME,
+ prepend: true,
+ container: emotionRoot,
+ });
+ ReactDOM.createRoot(shadowRootElement).render(
+
+
+
+
+
+ );
+}
+
+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(
+
+
+
+
+
+ );
}
export function windowListener(rule) {
diff --git a/src/config/i18n.js b/src/config/i18n.js
index 5118be2..d88f079 100644
--- a/src/config/i18n.js
+++ b/src/config/i18n.js
@@ -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)`,
+ },
};
diff --git a/src/config/index.js b/src/config/index.js
index b275da5..e28325a 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -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];
diff --git a/src/content.js b/src/content.js
index 50518ab..415a793 100644
--- a/src/content.js
+++ b/src/content.js
@@ -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);
}
diff --git a/src/hooks/Tranbox.js b/src/hooks/Tranbox.js
new file mode 100644
index 0000000..119dbd8
--- /dev/null
+++ b/src/hooks/Tranbox.js
@@ -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 };
+}
diff --git a/src/userscript.js b/src/userscript.js
index e3fe7ee..5b32726 100644
--- a/src/userscript.js
+++ b/src/userscript.js
@@ -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) {
diff --git a/src/views/Options/Navigator.js b/src/views/Options/Navigator.js
index 06d4125..f0ab47c 100644
--- a/src/views/Options/Navigator.js
+++ b/src/views/Options/Navigator.js
@@ -45,6 +45,12 @@ export default function Navigator(props) {
url: "/input",
icon: ,
},
+ {
+ id: "selection_translate",
+ label: i18n("selection_translate"),
+ url: "/tranbox",
+ icon: ,
+ },
{
id: "apis_setting",
label: i18n("apis_setting"),
diff --git a/src/views/Options/Tranbox.js b/src/views/Options/Tranbox.js
new file mode 100644
index 0000000..b60ef30
--- /dev/null
+++ b/src/views/Options/Tranbox.js
@@ -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 (
+
+
+ {
+ updateTranbox({ transOpen: !transOpen });
+ }}
+ />
+ }
+ label={i18n("toggle_selection_translate")}
+ />
+
+
+ {OPT_TRANS_ALL.map((item) => (
+
+ ))}
+
+
+
+ {OPT_LANGS_FROM.map(([lang, name]) => (
+
+ ))}
+
+
+
+ {OPT_LANGS_TO.map(([lang, name]) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/views/Options/index.js b/src/views/Options/index.js
index f46f55e..8085d77 100644
--- a/src/views/Options/index.js
+++ b/src/views/Options/index.js
@@ -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() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/src/views/Selection/DraggableResizable.js b/src/views/Selection/DraggableResizable.js
new file mode 100644
index 0000000..34264b7
--- /dev/null
+++ b/src/views/Selection/DraggableResizable.js
@@ -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 (
+
+ {children}
+
+ );
+}
+
+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 (
+
+
+
+
+
+
+
+ {header}
+
+
+ {children}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/views/Selection/Tranbox.js b/src/views/Selection/Tranbox.js
new file mode 100644
index 0000000..a8dba2e
--- /dev/null
+++ b/src/views/Selection/Tranbox.js
@@ -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 (
+
+
+
+
+
+ {OPT_LANGS_FROM.map(([lang, name]) => (
+
+ ))}
+
+
+
+
+ {OPT_LANGS_TO.map(([lang, name]) => (
+
+ ))}
+
+
+
+
+ {OPT_TRANS_ALL.map((item) => (
+
+ ))}
+
+
+
+
+
+ );
+}
+
+export default function TranBox({ position, setShowBox, tranboxSetting }) {
+ return (
+
+
+ }
+ >
+
+
+
+
+
+ );
+}
diff --git a/src/views/Selection/Tranbtn.js b/src/views/Selection/Tranbtn.js
new file mode 100644
index 0000000..e28ffa8
--- /dev/null
+++ b/src/views/Selection/Tranbtn.js
@@ -0,0 +1,43 @@
+export default function TranBtn({ onClick, position, tranboxSetting }) {
+ return (
+ {
+ e.stopPropagation();
+ }}
+ >
+
+
+ );
+}
diff --git a/src/views/Selection/index.js b/src/views/Selection/index.js
new file mode 100644
index 0000000..99835e5
--- /dev/null
+++ b/src/views/Selection/index.js
@@ -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 && (
+
+ )}
+
+ {showBtn && (
+
+ )}
+ >
+ );
+}