add codes
This commit is contained in:
42
src/views/Content/LoadingIcon.js
Normal file
42
src/views/Content/LoadingIcon.js
Normal file
@@ -0,0 +1,42 @@
|
||||
export default function LoadingIcon() {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
style={{
|
||||
maxWidth: "1.2em",
|
||||
maxHeight: "1.2em",
|
||||
}}
|
||||
>
|
||||
<circle fill="#209CEE" stroke="none" cx="6" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 15 ; 0 -15; 0 15"
|
||||
repeatCount="indefinite"
|
||||
begin="0.1"
|
||||
/>
|
||||
</circle>
|
||||
<circle fill="#209CEE" stroke="none" cx="30" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 10 ; 0 -10; 0 10"
|
||||
repeatCount="indefinite"
|
||||
begin="0.2"
|
||||
/>
|
||||
</circle>
|
||||
<circle fill="#209CEE" stroke="none" cx="54" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 5 ; 0 -5; 0 5"
|
||||
repeatCount="indefinite"
|
||||
begin="0.3"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
59
src/views/Content/index.js
Normal file
59
src/views/Content/index.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import LoadingIcon from "./LoadingIcon";
|
||||
import { OPT_STYLE_FUZZY, OPT_STYLE_LINE } from "../../config";
|
||||
import { useTranslate } from "../../hooks/Translate";
|
||||
|
||||
export default function Content({ q, rule }) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const { text, sameLang, loading, textStyle } = useTranslate(q, rule);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setHover(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setHover(false);
|
||||
};
|
||||
|
||||
const style = useMemo(() => {
|
||||
switch (textStyle) {
|
||||
case OPT_STYLE_LINE:
|
||||
return {
|
||||
opacity: hover ? 1 : 0.6,
|
||||
textDecoration: "dashed underline 2px",
|
||||
textUnderlineOffset: "0.3em",
|
||||
};
|
||||
case OPT_STYLE_FUZZY:
|
||||
return {
|
||||
filter: hover ? "none" : "blur(5px)",
|
||||
transition: "filter 0.3s ease-in-out",
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}, [textStyle, hover]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
{q.length > 40 ? <br /> : " "}
|
||||
<LoadingIcon />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (text && !sameLang) {
|
||||
return (
|
||||
<>
|
||||
{q.length > 40 ? <br /> : " "}
|
||||
<span
|
||||
style={style}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
18
src/views/Options/About.js
Normal file
18
src/views/Options/About.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useI18n, useI18nMd } from "../../hooks/I18n";
|
||||
|
||||
export default function About() {
|
||||
const i18n = useI18n();
|
||||
const [md, loading, error] = useI18nMd("about_md");
|
||||
return (
|
||||
<Box>
|
||||
{loading ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
<ReactMarkdown children={error ? i18n("about_md_local") : md} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
51
src/views/Options/Header.js
Normal file
51
src/views/Options/Header.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import PropTypes from "prop-types";
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useDarkModeSwitch } from "../../hooks/ColorMode";
|
||||
import { useDarkMode } from "../../hooks/ColorMode";
|
||||
import LightModeIcon from "@mui/icons-material/LightMode";
|
||||
import DarkModeIcon from "@mui/icons-material/DarkMode";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
|
||||
function Header(props) {
|
||||
const i18n = useI18n();
|
||||
const { onDrawerToggle } = props;
|
||||
const switchColorMode = useDarkModeSwitch();
|
||||
const darkMode = useDarkMode();
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
color="primary"
|
||||
position="sticky"
|
||||
sx={{
|
||||
zIndex: 1300,
|
||||
}}
|
||||
>
|
||||
<Toolbar variant="dense">
|
||||
<Box sx={{ display: { sm: "none", xs: "block" } }}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
onClick={onDrawerToggle}
|
||||
edge="start"
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>{i18n("app_name")}</Box>
|
||||
<IconButton onClick={switchColorMode} color="inherit">
|
||||
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
onDrawerToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
49
src/views/Options/Layout.js
Normal file
49
src/views/Options/Layout.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import Box from "@mui/material/Box";
|
||||
import Navigator from "./Navigator";
|
||||
import Header from "./Header";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export default function Layout() {
|
||||
const navWidth = 256;
|
||||
const location = useLocation();
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = useState(false);
|
||||
const isSm = useMediaQuery(theme.breakpoints.up("sm"));
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<CssBaseline />
|
||||
<Header onDrawerToggle={handleDrawerToggle} />
|
||||
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<Box
|
||||
component="nav"
|
||||
sx={{ width: { sm: navWidth }, flexShrink: { sm: 0 } }}
|
||||
>
|
||||
<Navigator
|
||||
PaperProps={{ style: { width: navWidth } }}
|
||||
variant={isSm ? "permanent" : "temporary"}
|
||||
open={isSm ? true : open}
|
||||
onClose={handleDrawerToggle}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box component="main" sx={{ flex: 1, p: 2 }}>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
50
src/views/Options/Navigator.js
Normal file
50
src/views/Options/Navigator.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import Drawer from "@mui/material/Drawer";
|
||||
import List from "@mui/material/List";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import { NavLink, useMatch } from "react-router-dom";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import InfoIcon from "@mui/icons-material/Info";
|
||||
import DesignServicesIcon from "@mui/icons-material/DesignServices";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
|
||||
function LinkItem({ label, url, icon }) {
|
||||
const match = useMatch(url);
|
||||
return (
|
||||
<ListItemButton component={NavLink} to={url} selected={!!match}>
|
||||
<ListItemIcon>{icon}</ListItemIcon>
|
||||
<ListItemText>{label}</ListItemText>
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Navigator(props) {
|
||||
const i18n = useI18n();
|
||||
const memus = [
|
||||
{
|
||||
id: "basic_setting",
|
||||
label: i18n("basic_setting"),
|
||||
url: "/",
|
||||
icon: <SettingsIcon />,
|
||||
},
|
||||
{
|
||||
id: "rules_setting",
|
||||
label: i18n("rules_setting"),
|
||||
url: "/rules",
|
||||
icon: <DesignServicesIcon />,
|
||||
},
|
||||
{ id: "about", label: i18n("about"), url: "/about", icon: <InfoIcon /> },
|
||||
];
|
||||
return (
|
||||
<Drawer {...props}>
|
||||
<Toolbar variant="dense" />
|
||||
<List component="nav">
|
||||
{memus.map(({ id, label, url, icon }) => (
|
||||
<LinkItem key={id} label={label} url={url} icon={icon} />
|
||||
))}
|
||||
</List>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
412
src/views/Options/Rules.js
Normal file
412
src/views/Options/Rules.js
Normal file
@@ -0,0 +1,412 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Button from "@mui/material/Button";
|
||||
import {
|
||||
DEFAULT_RULE,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
} from "../../config";
|
||||
import { useState, useRef } from "react";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import { useRules } from "../../hooks/Rules";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import FileDownloadIcon from "@mui/icons-material/FileDownload";
|
||||
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
||||
|
||||
function RuleFields({
|
||||
rule,
|
||||
rules,
|
||||
index,
|
||||
addRule,
|
||||
delRule,
|
||||
putRule,
|
||||
setShow,
|
||||
}) {
|
||||
const initFormValues = rule || {
|
||||
...DEFAULT_RULE,
|
||||
pattern: "",
|
||||
transOpen: true,
|
||||
};
|
||||
const editMode = !!rule;
|
||||
|
||||
const i18n = useI18n();
|
||||
const [disabled, setDisabled] = useState(editMode);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [formValues, setFormValues] = useState(initFormValues);
|
||||
const {
|
||||
pattern,
|
||||
selector,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
textStyle,
|
||||
transOpen,
|
||||
} = formValues;
|
||||
|
||||
const hasSamePattern = (str) => {
|
||||
for (const item of rules) {
|
||||
if (item.pattern === str && rule?.pattern !== str) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleFocus = (e) => {
|
||||
e.preventDefault();
|
||||
const { name } = e.target;
|
||||
setErrors((pre) => ({ ...pre, [name]: "" }));
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
const { name, value } = e.target;
|
||||
setFormValues((pre) => ({ ...pre, [name]: value }));
|
||||
};
|
||||
|
||||
const handleCancel = (e) => {
|
||||
e.preventDefault();
|
||||
if (editMode) {
|
||||
setDisabled(true);
|
||||
} else {
|
||||
setShow(false);
|
||||
}
|
||||
setErrors({});
|
||||
setFormValues(initFormValues);
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const errors = {};
|
||||
if (!pattern.trim()) {
|
||||
errors.pattern = i18n("error_cant_be_blank");
|
||||
}
|
||||
if (!selector.trim()) {
|
||||
errors.selector = i18n("error_cant_be_blank");
|
||||
}
|
||||
if (hasSamePattern(pattern)) {
|
||||
errors.pattern = i18n("error_duplicate_values");
|
||||
}
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
if (editMode) {
|
||||
// 编辑
|
||||
setDisabled(true);
|
||||
putRule(index, formValues);
|
||||
} else {
|
||||
// 添加
|
||||
addRule(formValues);
|
||||
setShow(false);
|
||||
setFormValues(initFormValues);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("pattern")}
|
||||
error={!!errors.pattern}
|
||||
helperText={errors.pattern ?? i18n("pattern_helper")}
|
||||
name="pattern"
|
||||
value={pattern}
|
||||
disabled={rule?.pattern === "*" || disabled}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("selector")}
|
||||
error={!!errors.selector}
|
||||
helperText={errors.selector ?? i18n("selector_helper")}
|
||||
name="selector"
|
||||
value={selector}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
multiline
|
||||
minRows={2}
|
||||
maxRows={10}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={20}>
|
||||
<Grid item xs={10} md={4}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transOpen"
|
||||
value={transOpen}
|
||||
label={i18n("translate_switch")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={true}>{i18n("default_enabled")}</MenuItem>
|
||||
<MenuItem value={false}>{i18n("default_disabled")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={10} md={4}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="translator"
|
||||
value={translator}
|
||||
label={i18n("translate_service")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_TRANS_ALL.map((item) => (
|
||||
<MenuItem value={item}>{item}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={10} md={4}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="fromLang"
|
||||
value={fromLang}
|
||||
label={i18n("from_lang")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem value={lang}>{name}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={10} md={4}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="toLang"
|
||||
value={toLang}
|
||||
label={i18n("to_lang")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem value={lang}>{name}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={10} md={4}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="textStyle"
|
||||
value={textStyle}
|
||||
label={i18n("text_style")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
<MenuItem value={item}>{i18n(item)}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{editMode ? (
|
||||
// 编辑
|
||||
<Stack direction="row" spacing={2}>
|
||||
{disabled ? (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setDisabled(false);
|
||||
}}
|
||||
>
|
||||
{i18n("edit")}
|
||||
</Button>
|
||||
{rule?.pattern !== "*" && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
delRule(rule.pattern);
|
||||
}}
|
||||
>
|
||||
{i18n("delete")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button size="small" variant="contained" type="submit">
|
||||
{i18n("save")}
|
||||
</Button>
|
||||
<Button size="small" variant="outlined" onClick={handleCancel}>
|
||||
{i18n("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
) : (
|
||||
// 添加
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Button size="small" variant="contained" type="submit">
|
||||
{i18n("save")}
|
||||
</Button>
|
||||
<Button size="small" variant="outlined" onClick={handleCancel}>
|
||||
{i18n("cancel")}
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function DownloadButton({ data, text, fileName }) {
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
if (data) {
|
||||
const url = window.URL.createObjectURL(new Blob([data]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", fileName || `${Date.now()}.json`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleClick}
|
||||
startIcon={<FileDownloadIcon />}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function UploadButton({ onChange, text }) {
|
||||
const inputRef = useRef(null);
|
||||
const handleClick = () => {
|
||||
inputRef.current && inputRef.current.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleClick}
|
||||
startIcon={<FileUploadIcon />}
|
||||
>
|
||||
{text}
|
||||
<input
|
||||
type="file"
|
||||
accept=".json"
|
||||
ref={inputRef}
|
||||
onChange={onChange}
|
||||
hidden
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Rules() {
|
||||
const i18n = useI18n();
|
||||
const [rules, addRule, delRule, putRule, mergeRules] = useRules();
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
|
||||
const handleImport = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.type.includes("json")) {
|
||||
alert(i18n("error_wrong_file_type"));
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
await mergeRules(JSON.parse(e.target.result));
|
||||
} catch (err) {
|
||||
console.log("[import rules]", err);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Alert severity="warning">{i18n("advanced_warn")}</Alert>
|
||||
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
disabled={showAdd}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowAdd(true);
|
||||
}}
|
||||
>
|
||||
{i18n("add")}
|
||||
</Button>
|
||||
|
||||
<UploadButton text={i18n("import")} onChange={handleImport} />
|
||||
<DownloadButton
|
||||
data={JSON.stringify([...rules].reverse(), null, "\t")}
|
||||
text={i18n("export")}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{showAdd && (
|
||||
<RuleFields addRule={addRule} rules={rules} setShow={setShowAdd} />
|
||||
)}
|
||||
|
||||
<Box>
|
||||
{rules.map((rule, index) => (
|
||||
<Accordion key={rule.pattern}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>{rule.pattern}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<RuleFields
|
||||
rule={rule}
|
||||
index={index}
|
||||
putRule={putRule}
|
||||
delRule={delRule}
|
||||
rules={rules}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
141
src/views/Options/Setting.js
Normal file
141
src/views/Options/Setting.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import InputLabel from "@mui/material/InputLabel";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import Select from "@mui/material/Select";
|
||||
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
|
||||
import { limitNumber } from "../../libs/utils";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import { UI_LANGS } from "../../config";
|
||||
|
||||
export default function Settings() {
|
||||
const i18n = useI18n();
|
||||
const setting = useSetting();
|
||||
const updateSetting = useSettingUpdate();
|
||||
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
uiLang,
|
||||
googleUrl,
|
||||
fetchLimit,
|
||||
openaiUrl,
|
||||
openaiKey,
|
||||
openaiModel,
|
||||
openaiPrompt,
|
||||
clearCache,
|
||||
} = setting;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("ui_lang")}</InputLabel>
|
||||
<Select
|
||||
value={uiLang}
|
||||
label={i18n("ui_lang")}
|
||||
onChange={(e) => {
|
||||
updateSetting({
|
||||
uiLang: e.target.value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{UI_LANGS.map(([lang, name]) => (
|
||||
<MenuItem value={lang}>{name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("fetch_limit")}
|
||||
type="number"
|
||||
defaultValue={fetchLimit}
|
||||
onChange={(e) => {
|
||||
updateSetting({
|
||||
fetchLimit: limitNumber(e.target.value, 1, 10),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("clear_cache")}</InputLabel>
|
||||
<Select
|
||||
value={clearCache}
|
||||
label={i18n("clear_cache")}
|
||||
onChange={(e) => {
|
||||
updateSetting({
|
||||
clearCache: e.target.value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("google_api")}
|
||||
defaultValue={googleUrl}
|
||||
onChange={(e) => {
|
||||
updateSetting({
|
||||
googleUrl: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("openai_api")}
|
||||
defaultValue={openaiUrl}
|
||||
onChange={(e) => {
|
||||
updateSetting({
|
||||
openaiUrl: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("openai_key")}
|
||||
defaultValue={openaiKey}
|
||||
onChange={(e) => {
|
||||
updateSetting({
|
||||
openaiKey: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("openai_model")}
|
||||
defaultValue={openaiModel}
|
||||
onChange={(e) => {
|
||||
updateSetting({
|
||||
openaiModel: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("openai_prompt")}
|
||||
defaultValue={openaiPrompt}
|
||||
onChange={(e) => {
|
||||
updateSetting({
|
||||
openaiPrompt: e.target.value,
|
||||
});
|
||||
}}
|
||||
multiline
|
||||
minRows={2}
|
||||
maxRows={10}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
17
src/views/Options/index.js
Normal file
17
src/views/Options/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import About from "./About";
|
||||
import Rules from "./Rules";
|
||||
import Setting from "./Setting";
|
||||
import Layout from "./Layout";
|
||||
|
||||
export default function Options() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Setting />} />
|
||||
<Route path="rules" element={<Rules />} />
|
||||
<Route path="about" element={<About />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
142
src/views/Popup/index.js
Normal file
142
src/views/Popup/index.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
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 { sendTabMsg } from "../../libs/msg";
|
||||
import browser from "../../libs/browser";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_STYLE_ALL,
|
||||
} from "../../config";
|
||||
|
||||
export default function Popup() {
|
||||
const i18n = useI18n();
|
||||
const [rule, setRule] = useState(null);
|
||||
|
||||
const handleOpenSetting = () => {
|
||||
browser?.runtime.openOptionsPage();
|
||||
};
|
||||
|
||||
const handleTransToggle = async (e) => {
|
||||
try {
|
||||
setRule({ ...rule, transOpen: e.target.checked });
|
||||
await sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
} catch (err) {
|
||||
console.log("[toggle trans]", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = async (e) => {
|
||||
try {
|
||||
const { name, value } = e.target;
|
||||
setRule((pre) => ({ ...pre, [name]: value }));
|
||||
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||
} catch (err) {
|
||||
console.log("[update rule]", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await sendTabMsg(MSG_TRANS_GETRULE);
|
||||
if (!res.error) {
|
||||
setRule(res.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[query rule]", err);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (!rule) {
|
||||
return (
|
||||
<Box minWidth={300} sx={{ p: 2 }}>
|
||||
<Stack spacing={3}>
|
||||
<Button variant="text" onClick={handleOpenSetting}>
|
||||
{i18n("setting")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const { transOpen, translator, fromLang, toLang, textStyle } = rule;
|
||||
|
||||
return (
|
||||
<Box minWidth={300} sx={{ p: 2 }}>
|
||||
<Stack spacing={3}>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={transOpen} onChange={handleTransToggle} />}
|
||||
label={i18n("translate")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
value={translator}
|
||||
name="translator"
|
||||
label={i18n("translate_service")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_TRANS_ALL.map((item) => (
|
||||
<MenuItem value={item}>{item}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
value={fromLang}
|
||||
name="fromLang"
|
||||
label={i18n("from_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem value={lang}>{name}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
value={toLang}
|
||||
name="toLang"
|
||||
label={i18n("to_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem value={lang}>{name}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
value={textStyle}
|
||||
name="textStyle"
|
||||
label={i18n("text_style")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
<MenuItem value={item}>{i18n(item)}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<Button variant="text" onClick={handleOpenSetting}>
|
||||
{i18n("setting")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user