feat: add playground

This commit is contained in:
Gabe
2025-10-02 21:59:31 +08:00
parent 389f0b6f82
commit 65e8fabe7d
14 changed files with 468 additions and 282 deletions

View File

@@ -63,6 +63,9 @@ export const OPT_LANGDETECTOR_ALL = [
OPT_TRANS_TENCENT, OPT_TRANS_TENCENT,
]; ];
export const OPT_DICT_ALL = [OPT_TRANS_BAIDU];
export const OPT_DICT_MAP = new Set(OPT_DICT_ALL);
// 翻译引擎特殊集合 // 翻译引擎特殊集合
export const API_SPE_TYPES = { export const API_SPE_TYPES = {
// 内置翻译 // 内置翻译

View File

@@ -1513,4 +1513,14 @@ export const I18N = {
en: `Placeholder tags`, en: `Placeholder tags`,
zh_TW: `佔位標`, zh_TW: `佔位標`,
}, },
detected_lang: {
zh: `语言检测`,
en: `Language detection`,
zh_TW: `語言偵測`,
},
detected_result: {
zh: `检测结果`,
en: `Detect result`,
zh_TW: `檢測結果`,
},
}; };

View File

@@ -88,7 +88,7 @@ export const DEFAULT_TRANBOX_SETTING = {
simpleStyle: false, // 是否简洁界面 simpleStyle: false, // 是否简洁界面
followSelection: false, // 翻译框是否跟随选中文本 followSelection: false, // 翻译框是否跟随选中文本
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式 triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
extStyles: "", // 附加样式 // extStyles: "", // 附加样式
enDict: OPT_DICT_BAIDU, // 英文词典 enDict: OPT_DICT_BAIDU, // 英文词典
}; };

View File

@@ -14,7 +14,7 @@ import ApiIcon from "@mui/icons-material/Api";
import InputIcon from "@mui/icons-material/Input"; import InputIcon from "@mui/icons-material/Input";
import SelectAllIcon from "@mui/icons-material/SelectAll"; import SelectAllIcon from "@mui/icons-material/SelectAll";
import EventNoteIcon from "@mui/icons-material/EventNote"; import EventNoteIcon from "@mui/icons-material/EventNote";
import MouseIcon from '@mui/icons-material/Mouse'; import MouseIcon from "@mui/icons-material/Mouse";
function LinkItem({ label, url, icon }) { function LinkItem({ label, url, icon }) {
const match = useMatch(url); const match = useMatch(url);
@@ -77,6 +77,12 @@ export default function Navigator(props) {
url: "/words", url: "/words",
icon: <EventNoteIcon />, icon: <EventNoteIcon />,
}, },
{
id: "playground",
label: "Playground",
url: "/playground",
icon: <EventNoteIcon />,
},
{ id: "about", label: i18n("about"), url: "/about", icon: <InfoIcon /> }, { id: "about", label: i18n("about"), url: "/about", icon: <InfoIcon /> },
]; ];
return ( return (

View File

@@ -0,0 +1,28 @@
import { useState } from "react";
import TranForm from "../Selection/TranForm";
import { DEFAULT_SETTING, DEFAULT_TRANBOX_SETTING } from "../../config";
import { useSetting } from "../../hooks/Setting";
export default function Playgound() {
const [text, setText] = useState("");
const { setting } = useSetting();
const { transApis, langDetector, tranboxSetting } =
setting || DEFAULT_SETTING;
const { apiSlugs, fromLang, toLang, toLang2, enDict } =
tranboxSetting || DEFAULT_TRANBOX_SETTING;
return (
<TranForm
text={text}
setText={setText}
apiSlugs={apiSlugs}
fromLang={fromLang}
toLang={toLang}
toLang2={toLang2}
transApis={transApis}
simpleStyle={false}
langDetector={langDetector}
enDict={enDict}
isPlaygound={true}
/>
);
}

View File

@@ -10,6 +10,7 @@ import {
OPT_TRANBOX_TRIGGER_CLICK, OPT_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_ALL, OPT_TRANBOX_TRIGGER_ALL,
OPT_DICT_BAIDU, OPT_DICT_BAIDU,
OPT_DICT_ALL,
} from "../../config"; } from "../../config";
import ShortcutInput from "./ShortcutInput"; import ShortcutInput from "./ShortcutInput";
import FormControlLabel from "@mui/material/FormControlLabel"; import FormControlLabel from "@mui/material/FormControlLabel";
@@ -65,7 +66,7 @@ export default function Tranbox() {
simpleStyle = false, simpleStyle = false,
followSelection = false, followSelection = false,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK, triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
extStyles = "", // extStyles = "",
enDict = OPT_DICT_BAIDU, enDict = OPT_DICT_BAIDU,
} = tranboxSetting; } = tranboxSetting;
@@ -153,7 +154,8 @@ export default function Tranbox() {
helperText={i18n("to_lang2_helper")} helperText={i18n("to_lang2_helper")}
onChange={handleChange} onChange={handleChange}
> >
{[["none", "None"], ...OPT_LANGS_TO].map(([lang, name]) => ( <MenuItem value={"-"}>{i18n("disable")}</MenuItem>
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}> <MenuItem key={lang} value={lang}>
{name} {name}
</MenuItem> </MenuItem>
@@ -172,7 +174,11 @@ export default function Tranbox() {
onChange={handleChange} onChange={handleChange}
> >
<MenuItem value={"-"}>{i18n("disable")}</MenuItem> <MenuItem value={"-"}>{i18n("disable")}</MenuItem>
<MenuItem value={OPT_DICT_BAIDU}>{OPT_DICT_BAIDU}</MenuItem> {OPT_DICT_ALL.map((item) => (
<MenuItem value={item} key={item}>
{item}
</MenuItem>
))}
</TextField> </TextField>
</Grid> </Grid>
<Grid item xs={12} sm={12} md={6} lg={3}> <Grid item xs={12} sm={12} md={6} lg={3}>
@@ -305,7 +311,7 @@ export default function Tranbox() {
</Grid> </Grid>
</Box> </Box>
<TextField {/* <TextField
size="small" size="small"
label={i18n("extend_styles")} label={i18n("extend_styles")}
name="extStyles" name="extStyles"
@@ -313,7 +319,7 @@ export default function Tranbox() {
onChange={handleChange} onChange={handleChange}
maxRows={10} maxRows={10}
multiline multiline
/> /> */}
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -21,6 +21,7 @@ import Apis from "./Apis";
import InputSetting from "./InputSetting"; import InputSetting from "./InputSetting";
import Tranbox from "./Tranbox"; import Tranbox from "./Tranbox";
import FavWords from "./FavWords"; import FavWords from "./FavWords";
import Playgound from "./Playground";
import MouseHoverSetting from "./MouseHover"; import MouseHoverSetting from "./MouseHover";
import Loading from "../../hooks/Loading"; import Loading from "../../hooks/Loading";
@@ -111,6 +112,7 @@ export default function Options() {
<Route path="apis" element={<Apis />} /> <Route path="apis" element={<Apis />} />
<Route path="sync" element={<SyncSetting />} /> <Route path="sync" element={<SyncSetting />} />
<Route path="words" element={<FavWords />} /> <Route path="words" element={<FavWords />} />
<Route path="playground" element={<Playgound />} />
<Route path="about" element={<About />} /> <Route path="about" element={<About />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -312,20 +312,6 @@ export default function Popup({ setShowPopup, translator }) {
label={i18n("shadowroot_alt")} label={i18n("shadowroot_alt")}
/> />
</Grid> </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}> <Grid item xs={6}>
<FormControlLabel <FormControlLabel
control={ control={
@@ -340,6 +326,20 @@ export default function Popup({ setShowPopup, translator }) {
label={i18n("richtext_alt")} label={i18n("richtext_alt")}
/> />
</Grid> </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}> <Grid item xs={6}>
<FormControlLabel <FormControlLabel
control={ control={

View File

@@ -10,7 +10,7 @@ import { apiTranslate } from "../../apis";
import { isValidWord } from "../../libs/utils"; import { isValidWord } from "../../libs/utils";
import CopyBtn from "./CopyBtn"; import CopyBtn from "./CopyBtn";
export default function DictCont({ text }) { function DictBaidu({ text, setCopyText }) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [dictResult, setDictResult] = useState(null); const [dictResult, setDictResult] = useState(null);
@@ -22,21 +22,21 @@ export default function DictCont({ text }) {
setError(""); setError("");
setDictResult(null); setDictResult(null);
if (!isValidWord(text)) { // if (!isValidWord(text)) {
return; // return;
} // }
// todo: 修复 // // todo: 修复
const dictRes = await apiTranslate({ // const dictRes = await apiTranslate({
text, // text,
apiSlug: OPT_TRANS_BAIDU, // apiSlug: OPT_TRANS_BAIDU,
fromLang: "en", // fromLang: "en",
toLang: "zh-CN", // toLang: "zh-CN",
}); // });
if (dictRes[2]?.type === 1) { // if (dictRes[2]?.type === 1) {
setDictResult(JSON.parse(dictRes[2].result)); // setDictResult(JSON.parse(dictRes[2].result));
} // }
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
} finally { } finally {
@@ -45,12 +45,12 @@ export default function DictCont({ text }) {
})(); })();
}, [text]); }, [text]);
const copyText = useMemo(() => { useEffect(() => {
if (!dictResult) { if (!dictResult) {
return text; return;
} }
return [ const copyText = [
dictResult.src, dictResult.src,
dictResult.voice dictResult.voice
?.map(Object.entries) ?.map(Object.entries)
@@ -63,29 +63,22 @@ export default function DictCont({ text }) {
}) })
.join("\n"), .join("\n"),
].join("\n"); ].join("\n");
}, [text, dictResult]);
setCopyText(copyText);
}, [dictResult, setCopyText]);
if (loading) { if (loading) {
return <CircularProgress size={16} />; return <CircularProgress size={16} />;
} }
return ( if (error) {
<Stack className="KT-transbox-dict" spacing={1}> return <Alert severity="error">{error}</Alert>;
{text && ( }
<Stack direction="row" justifyContent="space-between">
<Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
{dictResult?.src || text}
</Typography>
<Stack direction="row" justifyContent="space-between">
<CopyBtn text={copyText} />
<FavBtn word={dictResult?.src || text} />
</Stack>
</Stack>
)}
{error && <Alert severity="error">{error}</Alert>} return <Typography>baidu: {text}</Typography>;
{dictResult && ( {
/* {dictResult && (
<Typography component="div"> <Typography component="div">
<Typography component="div"> <Typography component="div">
{dictResult.voice {dictResult.voice
@@ -112,7 +105,32 @@ export default function DictCont({ text }) {
))} ))}
</Typography> </Typography>
</Typography> </Typography>
)} */
}
}
export default function DictCont({ text, enDict }) {
const [copyText, setCopyText] = useState(text);
const dictMap = {
[OPT_TRANS_BAIDU]: <DictBaidu text={text} setCopyText={setCopyText} />,
};
return (
<Stack spacing={1}>
{text && (
<Stack direction="row" justifyContent="space-between">
<Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
{text}
</Typography>
<Stack direction="row" justifyContent="space-between">
<CopyBtn text={copyText} />
<FavBtn word={text} />
</Stack>
</Stack>
)} )}
{dictMap[enDict] || <Typography>Dict not support</Typography>}
</Stack> </Stack>
); );
} }

View File

@@ -21,7 +21,7 @@ export default function SugCont({ text }) {
} }
return ( return (
<Stack className="KT-transbox-sug" spacing={1}> <Stack spacing={1}>
{sugs.map(({ k, v }) => ( {sugs.map(({ k, v }) => (
<Typography component="div" key={k}> <Typography component="div" key={k}>
<Typography>{k}</Typography> <Typography>{k}</Typography>

View File

@@ -2,13 +2,9 @@ import { SettingProvider } from "../../hooks/Setting";
import ThemeProvider from "../../hooks/Theme"; import ThemeProvider from "../../hooks/Theme";
import DraggableResizable from "./DraggableResizable"; import DraggableResizable from "./DraggableResizable";
import Stack from "@mui/material/Stack"; 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 Box from "@mui/material/Box";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import DoneIcon from "@mui/icons-material/Done";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
import UnfoldLessIcon from "@mui/icons-material/UnfoldLess"; import UnfoldLessIcon from "@mui/icons-material/UnfoldLess";
import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore"; import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore";
@@ -19,14 +15,9 @@ import LockOpenIcon from "@mui/icons-material/LockOpen";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import { OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config"; import { useState } from "react";
import { useState, useRef, useMemo } from "react";
import TranCont from "./TranCont";
import DictCont from "./DictCont";
import SugCont from "./SugCont";
import CopyBtn from "./CopyBtn";
import { isValidWord } from "../../libs/utils";
import { isMobile } from "../../libs/mobile"; import { isMobile } from "../../libs/mobile";
import TranForm from "./TranForm.js";
function Header({ function Header({
setShowPopup, setShowPopup,
@@ -46,7 +37,6 @@ function Header({
return ( return (
<Box <Box
className="KT-transbox-header"
onMouseUp={(e) => e.stopPropagation()} onMouseUp={(e) => e.stopPropagation()}
onTouchEnd={(e) => e.stopPropagation()} onTouchEnd={(e) => e.stopPropagation()}
> >
@@ -120,188 +110,11 @@ function Header({
); );
} }
function TranForm({
text,
setText,
tranboxSetting,
transApis,
simpleStyle,
langDetector,
enDict,
}) {
const i18n = useI18n();
const [editMode, setEditMode] = useState(false);
const [editText, setEditText] = useState("");
const [apiSlug, setApiSlug] = useState(tranboxSetting.apiSlug);
const [fromLang, setFromLang] = useState(tranboxSetting.fromLang);
const [toLang, setToLang] = useState(tranboxSetting.toLang);
const inputRef = useRef(null);
const optApis = useMemo(
() =>
transApis
.filter((api) => !api.isDisabled)
.map((api) => ({
key: api.apiSlug,
name: api.apiName || api.apiSlug,
})),
[transApis]
);
return (
<Stack
className="KT-transbox-container"
sx={{ p: simpleStyle ? 1 : 2 }}
spacing={simpleStyle ? 1 : 2}
>
{!simpleStyle && (
<>
<Box className="KT-transbox-select">
<Grid container spacing={simpleStyle ? 1 : 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={(e) => {
setFromLang(e.target.value);
}}
>
{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={(e) => {
setToLang(e.target.value);
}}
>
{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={apiSlug}
name="apiSlug"
label={i18n("translate_service")}
onChange={(e) => {
setApiSlug(e.target.value);
}}
>
{optApis.map(({ key, name }) => (
<MenuItem key={key} value={key}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
</Grid>
</Box>
<Box className="KT-transbox-origin">
<TextField
size="small"
label={i18n("original_text")}
inputRef={inputRef}
fullWidth
multiline
value={editMode ? editText : text}
onChange={(e) => {
setEditText(e.target.value);
}}
onFocus={() => {
setEditMode(true);
setEditText(text);
}}
onBlur={() => {
setEditMode(false);
setText(editText.trim());
}}
InputProps={{
endAdornment: (
<Stack
direction="row"
sx={{
position: "absolute",
right: 0,
top: 0,
}}
>
{editMode ? (
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
}}
>
<DoneIcon fontSize="inherit" />
</IconButton>
) : (
<CopyBtn text={text} />
)}
</Stack>
),
}}
/>
</Box>
</>
)}
{(!simpleStyle ||
!isValidWord(text) ||
!toLang.startsWith("zh") ||
enDict === "-") && (
<TranCont
text={text}
apiSlug={apiSlug}
fromLang={fromLang}
toLang={toLang}
toLang2={tranboxSetting.toLang2}
transApis={transApis}
simpleStyle={simpleStyle}
langDetector={langDetector}
/>
)}
{enDict !== "-" && isValidWord(text) && (
<>
<DictCont text={text} />
<SugCont text={text} />
</>
)}
</Stack>
);
}
export default function TranBox({ export default function TranBox({
text, text,
setText, setText,
setShowBox, setShowBox,
tranboxSetting, tranboxSetting: { apiSlugs, fromLang, toLang, toLang2 },
transApis, transApis,
boxSize, boxSize,
setBoxSize, setBoxSize,
@@ -313,7 +126,7 @@ export default function TranBox({
setHideClickAway, setHideClickAway,
followSelection, followSelection,
setFollowSelection, setFollowSelection,
extStyles, extStyles = "",
langDetector, langDetector,
enDict, enDict,
}) { }) {
@@ -343,15 +156,20 @@ export default function TranBox({
onMouseEnter={() => setMouseHover(true)} onMouseEnter={() => setMouseHover(true)}
onMouseLeave={() => setMouseHover(false)} onMouseLeave={() => setMouseHover(false)}
> >
<TranForm <Box sx={{ p: simpleStyle ? 1 : 2 }}>
text={text} <TranForm
setText={setText} text={text}
tranboxSetting={tranboxSetting} setText={setText}
transApis={transApis} apiSlugs={apiSlugs}
simpleStyle={simpleStyle} fromLang={fromLang}
langDetector={langDetector} toLang={toLang}
enDict={enDict} toLang2={toLang2}
/> transApis={transApis}
simpleStyle={simpleStyle}
langDetector={langDetector}
enDict={enDict}
/>
</Box>
</DraggableResizable> </DraggableResizable>
</ThemeProvider> </ThemeProvider>
</SettingProvider> </SettingProvider>

View File

@@ -3,31 +3,32 @@ import Box from "@mui/material/Box";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import { DEFAULT_API_SETTING } from "../../config"; import { useEffect, useState, useMemo } from "react";
import { useEffect, useState } from "react";
import { apiTranslate } from "../../apis"; import { apiTranslate } from "../../apis";
import CopyBtn from "./CopyBtn"; import CopyBtn from "./CopyBtn";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Alert from "@mui/material/Alert"; import Alert from "@mui/material/Alert";
import { tryDetectLang } from "../../libs/detect";
export default function TranCont({ export default function TranCont({
text, text,
apiSlug,
fromLang, fromLang,
toLang, toLang,
toLang2 = "en", apiSlug,
transApis, transApis,
simpleStyle = false, simpleStyle = false,
langDetector,
}) { }) {
const i18n = useI18n(); const i18n = useI18n();
const [trText, setTrText] = useState(""); const [trText, setTrText] = useState("");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const apiSetting = useMemo(
() => transApis.find((api) => api.apiSlug === apiSlug),
[transApis, apiSlug]
);
useEffect(() => { useEffect(() => {
if (!text?.trim()) { if (!text?.trim() || !apiSetting) {
return; return;
} }
@@ -37,24 +38,13 @@ export default function TranCont({
setTrText(""); setTrText("");
setError(""); setError("");
let to = toLang;
if (fromLang === "auto" && toLang !== toLang2 && toLang2 !== "none") {
const detectLang = await tryDetectLang(text, "true", langDetector);
if (detectLang === toLang) {
to = toLang2;
}
}
const apiSetting =
transApis.find((api) => api.apiSlug === apiSlug) ||
DEFAULT_API_SETTING;
const [trText] = await apiTranslate({ const [trText] = await apiTranslate({
text, text,
apiSlug,
fromLang, fromLang,
toLang: to, toLang,
apiSetting, apiSetting,
}); });
setTrText(trText); setTrText(trText);
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
@@ -62,11 +52,11 @@ export default function TranCont({
setLoading(false); setLoading(false);
} }
})(); })();
}, [text, apiSlug, fromLang, toLang, toLang2, transApis, langDetector]); }, [text, fromLang, toLang, apiSetting]);
if (simpleStyle) { if (simpleStyle) {
return ( return (
<Box className="KT-transbox-target KT-transbox-target_simple"> <Box>
{error ? ( {error ? (
<Alert severity="error">{error}</Alert> <Alert severity="error">{error}</Alert>
) : loading ? ( ) : loading ? (
@@ -79,10 +69,10 @@ export default function TranCont({
} }
return ( return (
<Box className="KT-transbox-target KT-transbox-target_default"> <Box>
<TextField <TextField
size="small" size="small"
label={i18n("translated_text")} label={`${i18n("translated_text")} - ${apiSetting.apiSlug}`}
// disabled // disabled
fullWidth fullWidth
multiline multiline

View File

@@ -0,0 +1,305 @@
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 IconButton from "@mui/material/IconButton";
import DoneIcon from "@mui/icons-material/Done";
import CircularProgress from "@mui/material/CircularProgress";
import { useI18n } from "../../hooks/I18n";
import {
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_LANGDETECTOR_ALL,
OPT_DICT_ALL,
OPT_LANGS_MAP,
OPT_DICT_MAP,
} from "../../config";
import { useState, useMemo, useEffect } from "react";
import TranCont from "./TranCont";
import DictCont from "./DictCont";
import SugCont from "./SugCont";
import CopyBtn from "./CopyBtn";
import { isValidWord } from "../../libs/utils";
import { kissLog } from "../../libs/log";
import { tryDetectLang } from "../../libs/detect";
export default function TranForm({
text,
setText,
apiSlugs: initApiSlugs,
fromLang: initFromLang,
toLang: initToLang,
toLang2,
transApis,
simpleStyle = false,
langDetector: initLangDetector = "-",
enDict: initEnDict = "-",
isPlaygound = false,
}) {
const i18n = useI18n();
const [editMode, setEditMode] = useState(false);
const [editText, setEditText] = useState(text);
const [apiSlugs, setApiSlugs] = useState(initApiSlugs);
const [fromLang, setFromLang] = useState(initFromLang);
const [toLang, setToLang] = useState(initToLang);
const [langDetector, setLangDetector] = useState(initLangDetector);
const [enDict, setEnDict] = useState(initEnDict);
const [deLang, setDeLang] = useState("");
const [deLoading, setDeLoading] = useState(false);
useEffect(() => {
if (!text.trim()) {
setDeLang("");
return;
}
(async () => {
try {
setDeLoading(true);
const deLang = await tryDetectLang(text, langDetector);
if (deLang) {
setDeLang(deLang);
}
} catch (err) {
kissLog("tranbox: detect lang", err);
} finally {
setDeLoading(false);
}
})();
}, [text, langDetector, setDeLang, setDeLoading]);
// todo: 语言变化后realToLang引发二次翻译请求
const realToLang = useMemo(() => {
if (
fromLang === "auto" &&
toLang !== toLang2 &&
toLang2 !== "-" &&
deLang === toLang
) {
return toLang2;
}
return toLang;
}, [fromLang, toLang, toLang2, deLang]);
const optApis = useMemo(
() =>
transApis
.filter((api) => !api.isDisabled)
.map((api) => ({
key: api.apiSlug,
name: api.apiName || api.apiSlug,
})),
[transApis]
);
const isWord = useMemo(() => isValidWord(text), [text]);
return (
<Stack spacing={simpleStyle ? 1 : 2}>
{!simpleStyle && (
<>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{
multiple: true,
MenuProps: { disablePortal: true },
}}
fullWidth
size="small"
value={apiSlugs}
name="apiSlugs"
label={i18n("translate_service")}
onChange={(e) => {
setApiSlugs(e.target.value);
}}
>
{optApis.map(({ key, name }) => (
<MenuItem key={key} value={key}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<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={(e) => {
setFromLang(e.target.value);
}}
>
{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={(e) => {
setToLang(e.target.value);
}}
>
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
</Grid>
</Box>
{isPlaygound && (
<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="enDict"
value={enDict}
label={i18n("english_dict")}
onChange={(e) => {
setEnDict(e.target.value);
}}
>
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
{OPT_DICT_ALL.map((item) => (
<MenuItem value={item} key={item}>
{item}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
name="langDetector"
value={langDetector}
label={i18n("detected_lang")}
onChange={(e) => {
setLangDetector(e.target.value);
}}
>
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
{OPT_LANGDETECTOR_ALL.map((item) => (
<MenuItem value={item} key={item}>
{item}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
fullWidth
size="small"
name="deLang"
value={deLang && OPT_LANGS_MAP.get(deLang)}
label={i18n("detected_result")}
disabled
InputProps={{
startAdornment: deLoading ? (
<CircularProgress size={16} />
) : null,
}}
/>
</Grid>
</Grid>
</Box>
)}
<Box>
<TextField
size="small"
label={i18n("original_text")}
fullWidth
multiline
minRows={isPlaygound ? 2 : 1}
maxRows={10}
value={editText}
onChange={(e) => {
setEditText(e.target.value);
}}
onFocus={() => {
setEditMode(true);
}}
onBlur={() => {
setEditMode(false);
setText(editText.trim());
}}
InputProps={{
endAdornment: (
<Stack
direction="row"
sx={{
position: "absolute",
right: 0,
top: 0,
}}
>
{editMode ? (
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
}}
>
<DoneIcon fontSize="inherit" />
</IconButton>
) : (
<CopyBtn text={text} />
)}
</Stack>
),
}}
/>
</Box>
</>
)}
{apiSlugs.map((slug) => (
<TranCont
key={slug}
text={text}
fromLang={fromLang}
toLang={realToLang}
simpleStyle={simpleStyle}
apiSlug={slug}
transApis={transApis}
/>
))}
{isWord && OPT_DICT_MAP.has(enDict) && (
<>
<DictCont text={text} enDict={enDict} />
<SugCont text={text} />
</>
)}
</Stack>
);
}

View File

@@ -30,7 +30,7 @@ export default function Slection({
followSelection: initFollowMouse = false, followSelection: initFollowMouse = false,
tranboxShortcut = DEFAULT_TRANBOX_SHORTCUT, tranboxShortcut = DEFAULT_TRANBOX_SHORTCUT,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK, triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
extStyles, // extStyles,
btnOffsetX, btnOffsetX,
btnOffsetY, btnOffsetY,
boxOffsetX = 0, boxOffsetX = 0,
@@ -236,7 +236,7 @@ export default function Slection({
setHideClickAway={setHideClickAway} setHideClickAway={setHideClickAway}
followSelection={followSelection} followSelection={followSelection}
setFollowSelection={setFollowSelection} setFollowSelection={setFollowSelection}
extStyles={extStyles} // extStyles={extStyles}
langDetector={langDetector} langDetector={langDetector}
enDict={enDict} enDict={enDict}
/> />