Files
kiss-translator/src/views/Options/Rules.js

476 lines
13 KiB
JavaScript
Raw Normal View History

2023-07-20 13:45:41 +08:00
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 {
2023-08-08 13:29:15 +08:00
GLOBAL_KEY,
2023-07-20 13:45:41 +08:00
DEFAULT_RULE,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_TRANS_ALL,
OPT_STYLE_ALL,
2023-08-17 15:55:44 +08:00
BUILTIN_RULES,
2023-07-20 13:45:41 +08:00
} from "../../config";
import { useState, useRef } from "react";
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";
2023-08-17 15:55:44 +08:00
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
2023-07-20 13:45:41 +08:00
2023-07-28 15:34:05 +08:00
function RuleFields({ rule, rules, setShow }) {
2023-08-08 14:09:35 +08:00
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
2023-07-20 13:45:41 +08:00
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,
2023-08-08 16:41:47 +08:00
bgColor,
2023-07-20 13:45:41 +08:00
} = formValues;
const hasSamePattern = (str) => {
2023-07-22 09:00:33 +08:00
for (const item of rules.list) {
2023-07-20 13:45:41 +08:00
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 (hasSamePattern(pattern)) {
errors.pattern = i18n("error_duplicate_values");
}
2023-08-08 15:36:42 +08:00
if (pattern === "*" && !errors.pattern && !selector.trim()) {
errors.selector = i18n("error_cant_be_blank");
}
2023-07-20 13:45:41 +08:00
if (Object.keys(errors).length > 0) {
setErrors(errors);
return;
}
if (editMode) {
// 编辑
setDisabled(true);
2023-07-28 15:34:05 +08:00
rules.put(rule.pattern, formValues);
2023-07-20 13:45:41 +08:00
} else {
// 添加
2023-07-22 09:00:33 +08:00
rules.add(formValues);
2023-07-20 13:45:41 +08:00
setShow(false);
setFormValues(initFormValues);
}
};
2023-08-08 13:29:15 +08:00
const globalItem = rule?.pattern !== "*" && (
<MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}>
{GLOBAL_KEY}
</MenuItem>
);
2023-07-20 13:45:41 +08:00
return (
<form onSubmit={handleSubmit}>
<Stack spacing={2}>
<TextField
size="small"
label={i18n("pattern")}
error={!!errors.pattern}
2023-08-08 11:31:11 +08:00
helperText={errors.pattern || i18n("pattern_helper")}
2023-07-20 13:45:41 +08:00
name="pattern"
value={pattern}
disabled={rule?.pattern === "*" || disabled}
onChange={handleChange}
onFocus={handleFocus}
2023-08-17 13:27:22 +08:00
multiline
2023-07-20 13:45:41 +08:00
/>
<TextField
size="small"
label={i18n("selector")}
error={!!errors.selector}
2023-08-08 11:31:11 +08:00
helperText={errors.selector || i18n("selector_helper")}
2023-07-20 13:45:41 +08:00
name="selector"
value={selector}
disabled={disabled}
onChange={handleChange}
onFocus={handleFocus}
multiline
/>
<Box>
2023-08-08 16:41:47 +08:00
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={3} lg={2}>
2023-07-20 13:45:41 +08:00
<TextField
select
size="small"
fullWidth
name="transOpen"
value={transOpen}
label={i18n("translate_switch")}
disabled={disabled}
onChange={handleChange}
>
2023-08-08 13:29:15 +08:00
{globalItem}
<MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem>
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
2023-07-20 13:45:41 +08:00
</TextField>
</Grid>
2023-08-08 16:41:47 +08:00
<Grid item xs={12} sm={6} md={3} lg={2}>
2023-07-20 13:45:41 +08:00
<TextField
select
size="small"
fullWidth
name="translator"
value={translator}
label={i18n("translate_service")}
disabled={disabled}
onChange={handleChange}
>
2023-08-08 13:29:15 +08:00
{globalItem}
2023-07-20 13:45:41 +08:00
{OPT_TRANS_ALL.map((item) => (
2023-08-04 10:08:54 +08:00
<MenuItem key={item} value={item}>
{item}
</MenuItem>
2023-07-20 13:45:41 +08:00
))}
</TextField>
</Grid>
2023-08-08 16:41:47 +08:00
<Grid item xs={12} sm={6} md={3} lg={2}>
2023-07-20 13:45:41 +08:00
<TextField
select
size="small"
fullWidth
name="fromLang"
value={fromLang}
label={i18n("from_lang")}
disabled={disabled}
onChange={handleChange}
>
2023-08-08 13:29:15 +08:00
{globalItem}
2023-07-20 13:45:41 +08:00
{OPT_LANGS_FROM.map(([lang, name]) => (
2023-08-08 13:29:15 +08:00
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
2023-07-20 13:45:41 +08:00
))}
</TextField>
</Grid>
2023-08-08 16:41:47 +08:00
<Grid item xs={12} sm={6} md={3} lg={2}>
2023-07-20 13:45:41 +08:00
<TextField
select
size="small"
fullWidth
name="toLang"
value={toLang}
label={i18n("to_lang")}
disabled={disabled}
onChange={handleChange}
>
2023-08-08 13:29:15 +08:00
{globalItem}
2023-07-20 13:45:41 +08:00
{OPT_LANGS_TO.map(([lang, name]) => (
2023-08-08 13:29:15 +08:00
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
2023-07-20 13:45:41 +08:00
))}
</TextField>
</Grid>
2023-08-08 16:41:47 +08:00
<Grid item xs={12} sm={6} md={3} lg={2}>
2023-07-20 13:45:41 +08:00
<TextField
select
size="small"
fullWidth
name="textStyle"
value={textStyle}
label={i18n("text_style")}
disabled={disabled}
onChange={handleChange}
>
2023-08-08 13:29:15 +08:00
{globalItem}
2023-07-20 13:45:41 +08:00
{OPT_STYLE_ALL.map((item) => (
2023-08-08 13:29:15 +08:00
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
2023-07-20 13:45:41 +08:00
))}
</TextField>
</Grid>
2023-08-08 16:41:47 +08:00
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
size="small"
fullWidth
name="bgColor"
value={bgColor}
label={i18n("bg_color")}
disabled={disabled}
onChange={handleChange}
/>
</Grid>
2023-07-20 13:45:41 +08:00
</Grid>
</Box>
2023-08-17 15:55:44 +08:00
{rules &&
(editMode ? (
// 编辑
<Stack direction="row" spacing={2}>
{disabled ? (
<>
2023-07-20 13:45:41 +08:00
<Button
size="small"
2023-08-17 15:55:44 +08:00
variant="contained"
2023-07-20 13:45:41 +08:00
onClick={(e) => {
e.preventDefault();
2023-08-17 15:55:44 +08:00
setDisabled(false);
2023-07-20 13:45:41 +08:00
}}
>
2023-08-17 15:55:44 +08:00
{i18n("edit")}
2023-07-20 13:45:41 +08:00
</Button>
2023-08-17 15:55:44 +08:00
{rule?.pattern !== "*" && (
<Button
size="small"
variant="outlined"
onClick={(e) => {
e.preventDefault();
rules.del(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>
))}
2023-07-20 13:45:41 +08:00
</Stack>
</form>
);
}
2023-08-17 13:27:22 +08:00
function RuleAccordion({ rule, rules }) {
const [expanded, setExpanded] = useState(false);
const handleChange = (e) => {
setExpanded((pre) => !pre);
};
return (
<Accordion expanded={expanded} onChange={handleChange}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
2023-08-17 15:55:44 +08:00
<Typography
style={{
opacity: rules ? 1 : 0.5,
}}
>
{rule.pattern}
</Typography>
2023-08-17 13:27:22 +08:00
</AccordionSummary>
<AccordionDetails>
{expanded && <RuleFields rule={rule} rules={rules} />}
</AccordionDetails>
</Accordion>
);
}
2023-07-20 13:45:41 +08:00
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();
2023-07-22 09:00:33 +08:00
const rules = useRules();
2023-07-20 13:45:41 +08:00
const [showAdd, setShowAdd] = useState(false);
2023-08-17 15:55:44 +08:00
const setting = useSetting();
const updateSetting = useSettingUpdate();
const injectRules = !!setting?.injectRules;
2023-07-20 13:45:41 +08:00
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 {
2023-07-22 09:00:33 +08:00
await rules.merge(JSON.parse(e.target.result));
2023-07-20 13:45:41 +08:00
} catch (err) {
console.log("[import rules]", err);
}
};
reader.readAsText(file);
};
2023-08-17 15:55:44 +08:00
const handleInject = () => {
updateSetting({
injectRules: !injectRules,
});
};
2023-07-20 13:45:41 +08:00
return (
<Box>
<Stack spacing={3}>
<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
2023-07-22 09:00:33 +08:00
data={JSON.stringify([...rules.list].reverse(), null, "\t")}
2023-07-20 13:45:41 +08:00
text={i18n("export")}
/>
2023-08-17 15:55:44 +08:00
<FormControlLabel
control={
<Switch
size="small"
checked={injectRules}
onChange={handleInject}
/>
}
label={i18n("inject_rules")}
/>
2023-07-20 13:45:41 +08:00
</Stack>
2023-07-22 09:00:33 +08:00
{showAdd && <RuleFields rules={rules} setShow={setShowAdd} />}
2023-07-20 13:45:41 +08:00
<Box>
2023-07-28 15:34:05 +08:00
{rules.list.map((rule) => (
2023-08-17 13:27:22 +08:00
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} />
2023-07-20 13:45:41 +08:00
))}
</Box>
2023-08-17 15:55:44 +08:00
{injectRules && (
<Box>
{BUILTIN_RULES.map((rule) => (
<RuleAccordion key={rule.pattern} rule={rule} />
))}
</Box>
)}
2023-07-20 13:45:41 +08:00
</Stack>
</Box>
);
}