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) => ( + + {item} + + ))} + + + + {OPT_LANGS_FROM.map(([lang, name]) => ( + + {name} + + ))} + + + + {OPT_LANGS_TO.map(([lang, name]) => ( + + {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]) => ( + + {name} + + ))} + + + + + {OPT_LANGS_TO.map(([lang, name]) => ( + + {name} + + ))} + + + + + {OPT_TRANS_ALL.map((item) => ( + + {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 && ( + + )} + + ); +}