feat: Extensive refactoring and modification to support any number of interfaces

This commit is contained in:
Gabe
2025-09-24 23:24:00 +08:00
parent 779c9fc850
commit 2a46939aa5
65 changed files with 2054 additions and 1947 deletions

View File

@@ -1,3 +1,4 @@
import { useState, useEffect, useMemo } from "react";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
@@ -5,58 +6,44 @@ import LoadingButton from "@mui/lab/LoadingButton";
import MenuItem from "@mui/material/MenuItem";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import {
OPT_TRANS_ALL,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLX,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_VOLCENGINE,
OPT_TRANS_OPENAI,
OPT_TRANS_OPENAI_2,
OPT_TRANS_OPENAI_3,
OPT_TRANS_GEMINI,
OPT_TRANS_GEMINI_2,
OPT_TRANS_CLAUDE,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_OLLAMA,
OPT_TRANS_OLLAMA_2,
OPT_TRANS_OLLAMA_3,
OPT_TRANS_OPENROUTER,
OPT_TRANS_CUSTOMIZE,
OPT_TRANS_CUSTOMIZE_2,
OPT_TRANS_CUSTOMIZE_3,
OPT_TRANS_CUSTOMIZE_4,
OPT_TRANS_CUSTOMIZE_5,
OPT_TRANS_NIUTRANS,
DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL,
DEFAULT_HTTP_TIMEOUT,
OPT_TRANS_BATCH,
OPT_TRANS_CONTEXT,
DEFAULT_BATCH_INTERVAL,
DEFAULT_BATCH_SIZE,
DEFAULT_BATCH_LENGTH,
DEFAULT_CONTEXT_SIZE,
} from "../../config";
import { useState } 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 AddIcon from "@mui/icons-material/Add";
import Alert from "@mui/material/Alert";
import Menu from "@mui/material/Menu";
import Grid from "@mui/material/Grid";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import { useAlert } from "../../hooks/Alert";
import { useApi } from "../../hooks/Api";
import { useApiList, useApiItem } from "../../hooks/Api";
import { useConfirm } from "../../hooks/Confirm";
import { apiTranslate } from "../../apis";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
import { limitNumber, limitFloat } from "../../libs/utils";
import ReusableAutocomplete from "./ReusableAutocomplete";
import {
OPT_TRANS_DEEPLX,
OPT_TRANS_OLLAMA,
OPT_TRANS_CUSTOMIZE,
OPT_TRANS_NIUTRANS,
DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL,
DEFAULT_HTTP_TIMEOUT,
DEFAULT_BATCH_INTERVAL,
DEFAULT_BATCH_SIZE,
DEFAULT_BATCH_LENGTH,
DEFAULT_CONTEXT_SIZE,
OPT_ALL_TYPES,
API_SPE_TYPES,
BUILTIN_STONES,
// BUILTIN_PLACEHOULDERS,
// BUILTIN_TAG_NAMES,
} from "../../config";
function TestButton({ translator, api }) {
function TestButton({ apiSlug, api }) {
const i18n = useI18n();
const alert = useAlert();
const [loading, setLoading] = useState(false);
@@ -64,7 +51,7 @@ function TestButton({ translator, api }) {
try {
setLoading(true);
const [text] = await apiTranslate({
translator,
apiSlug,
text: "hello world",
fromLang: "en",
toLang: "zh-CN",
@@ -114,7 +101,7 @@ function TestButton({ translator, api }) {
return (
<LoadingButton
size="small"
variant="contained"
variant="outlined"
onClick={handleApiTest}
loading={loading}
>
@@ -123,39 +110,34 @@ function TestButton({ translator, api }) {
);
}
function ApiFields({ translator, api, updateApi, resetApi }) {
function ApiFields({ apiSlug, isUserApi, deleteApi }) {
const { api, update, reset } = useApiItem(apiSlug);
const i18n = useI18n();
const {
url = "",
key = "",
model = "",
systemPrompt = "",
userPrompt = "",
customHeader = "",
customBody = "",
think = false,
thinkIgnore = "",
fetchLimit = DEFAULT_FETCH_LIMIT,
fetchInterval = DEFAULT_FETCH_INTERVAL,
httpTimeout = DEFAULT_HTTP_TIMEOUT,
dictNo = "",
memoryNo = "",
reqHook = "",
resHook = "",
temperature = 0,
maxTokens = 256,
apiName = "",
isDisabled = false,
useBatchFetch = false,
batchInterval = DEFAULT_BATCH_INTERVAL,
batchSize = DEFAULT_BATCH_SIZE,
batchLength = DEFAULT_BATCH_LENGTH,
useContext = false,
contextSize = DEFAULT_CONTEXT_SIZE,
} = api;
const [formData, setFormData] = useState({});
const [isModified, setIsModified] = useState(false);
const confirm = useConfirm();
useEffect(() => {
if (api) {
setFormData(api);
}
}, [api]);
useEffect(() => {
if (!api) return;
const hasChanged = JSON.stringify(api) !== JSON.stringify(formData);
setIsModified(hasChanged);
}, [api, formData]);
const handleChange = (e) => {
let { name, value } = e.target;
let { name, value, type, checked } = e.target;
if (type === "checkbox" || type === "switch") {
value = checked;
}
// if (value === "true") value = true;
// if (value === "false") value = false;
switch (name) {
case "fetchLimit":
value = limitNumber(value, 1, 100);
@@ -186,56 +168,78 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
break;
default:
}
updateApi({
setFormData((prevData) => ({
...prevData,
[name]: value,
});
}));
};
const builtinTranslators = [
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_VOLCENGINE,
];
const handleSave = () => {
// 过滤掉 api 对象中不存在的字段
// const updatedFields = Object.keys(formData).reduce((acc, key) => {
// if (api && Object.keys(api).includes(key)) {
// acc[key] = formData[key];
// }
// return acc;
// }, {});
// update(updatedFields);
update(formData);
};
const mulkeysTranslators = [
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI,
OPT_TRANS_OPENAI_2,
OPT_TRANS_OPENAI_3,
OPT_TRANS_GEMINI,
OPT_TRANS_GEMINI_2,
OPT_TRANS_CLAUDE,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_OLLAMA,
OPT_TRANS_OLLAMA_2,
OPT_TRANS_OLLAMA_3,
OPT_TRANS_OPENROUTER,
OPT_TRANS_NIUTRANS,
OPT_TRANS_CUSTOMIZE,
OPT_TRANS_CUSTOMIZE_2,
OPT_TRANS_CUSTOMIZE_3,
OPT_TRANS_CUSTOMIZE_4,
OPT_TRANS_CUSTOMIZE_5,
];
const handleReset = () => {
reset();
};
const keyHelper =
translator === OPT_TRANS_NIUTRANS ? (
<>
{i18n("mulkeys_help")}
<Link
href="https://niutrans.com/login?active=3&userSource=kiss-translator"
target="_blank"
>
{i18n("reg_niutrans")}
</Link>
</>
) : mulkeysTranslators.includes(translator) ? (
i18n("mulkeys_help")
) : (
""
);
const handleDelete = async () => {
const isConfirmed = await confirm({
confirmText: i18n("delete"),
cancelText: i18n("cancel"),
});
if (isConfirmed) {
deleteApi(apiSlug);
}
};
const {
url = "",
key = "",
model = "",
apiType,
systemPrompt = "",
// userPrompt = "",
customHeader = "",
customBody = "",
think = false,
thinkIgnore = "",
fetchLimit = DEFAULT_FETCH_LIMIT,
fetchInterval = DEFAULT_FETCH_INTERVAL,
httpTimeout = DEFAULT_HTTP_TIMEOUT,
dictNo = "",
memoryNo = "",
reqHook = "",
resHook = "",
temperature = 0,
maxTokens = 256,
apiName = "",
isDisabled = false,
useBatchFetch = false,
batchInterval = DEFAULT_BATCH_INTERVAL,
batchSize = DEFAULT_BATCH_SIZE,
batchLength = DEFAULT_BATCH_LENGTH,
useContext = false,
contextSize = DEFAULT_CONTEXT_SIZE,
tone = "neutral",
// placeholder = "{ }",
// tagName = "i",
// aiTerms = false,
} = formData;
const keyHelper = useMemo(
() => (API_SPE_TYPES.mulkeys.has(apiType) ? i18n("mulkeys_help") : ""),
[apiType, i18n]
);
return (
<Stack spacing={3}>
@@ -247,7 +251,7 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
onChange={handleChange}
/>
{!builtinTranslators.includes(translator) && (
{!API_SPE_TYPES.machine.has(apiType) && (
<>
<TextField
size="small"
@@ -255,10 +259,10 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
name="url"
value={url}
onChange={handleChange}
multiline={translator === OPT_TRANS_DEEPLX}
multiline={apiType === OPT_TRANS_DEEPLX}
maxRows={10}
helperText={
translator === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : ""
apiType === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : ""
}
/>
<TextField
@@ -267,26 +271,66 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
name="key"
value={key}
onChange={handleChange}
multiline={mulkeysTranslators.includes(translator)}
multiline={API_SPE_TYPES.mulkeys.has(apiType)}
maxRows={10}
helperText={keyHelper}
/>
</>
)}
{(translator.startsWith(OPT_TRANS_OPENAI) ||
translator.startsWith(OPT_TRANS_OLLAMA) ||
translator === OPT_TRANS_CLAUDE ||
translator === OPT_TRANS_OPENROUTER ||
translator.startsWith(OPT_TRANS_GEMINI)) && (
{API_SPE_TYPES.ai.has(apiType) && (
<>
<TextField
size="small"
label={"MODEL"}
name="model"
value={model}
onChange={handleChange}
/>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={6} sm={6} md={6} lg={3}>
{/* todo 改成 ReusableAutocomplete 可选择和填写模型 */}
<TextField
size="small"
fullWidth
label={"MODEL"}
name="model"
value={model}
onChange={handleChange}
/>
</Grid>
<Grid item xs={6} sm={6} md={6} lg={3}>
<ReusableAutocomplete
freeSolo
size="small"
fullWidth
options={BUILTIN_STONES}
name="tone"
label={i18n("translation_style")}
value={tone}
onChange={handleChange}
/>
</Grid>
<Grid item xs={6} sm={6} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={"Temperature"}
type="number"
name="temperature"
value={temperature}
onChange={handleChange}
/>
</Grid>
<Grid item xs={6} sm={6} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={"Max Tokens"}
type="number"
name="maxTokens"
value={maxTokens}
onChange={handleChange}
/>
</Grid>
<Grid item xs={6} sm={6} md={6} lg={3}></Grid>
</Grid>
</Box>
<TextField
size="small"
label={"SYSTEM PROMPT"}
@@ -295,8 +339,9 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
onChange={handleChange}
multiline
maxRows={10}
helperText={i18n("system_prompt_helper")}
/>
<TextField
{/* <TextField
size="small"
label={"USER PROMPT"}
name="userPrompt"
@@ -304,7 +349,51 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
onChange={handleChange}
multiline
maxRows={10}
/>
/> */}
{/* <Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={6} sm={6} md={6} lg={3}>
<ReusableAutocomplete
freeSolo
size="small"
fullWidth
options={BUILTIN_PLACEHOULDERS}
name="placeholder"
label={i18n("placeholder")}
value={placeholder}
onChange={handleChange}
/>
</Grid>
<Grid item xs={6} sm={6} md={6} lg={3}>
<ReusableAutocomplete
freeSolo
size="small"
fullWidth
options={BUILTIN_TAG_NAMES}
name="tagName"
label={i18n("tag_name")}
value={tagName}
onChange={handleChange}
/>
</Grid>
<Grid item xs={6} sm={6} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="aiTerms"
value={aiTerms}
label={i18n("ai_terms")}
onChange={handleChange}
>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
</TextField>
</Grid>
</Grid>
</Box> */}
<TextField
size="small"
label={i18n("custom_header")}
@@ -328,7 +417,7 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
</>
)}
{translator.startsWith(OPT_TRANS_OLLAMA) && (
{apiType === OPT_TRANS_OLLAMA && (
<>
<TextField
select
@@ -351,32 +440,7 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
</>
)}
{(translator.startsWith(OPT_TRANS_OPENAI) ||
translator === OPT_TRANS_CLAUDE ||
translator === OPT_TRANS_OPENROUTER ||
translator === OPT_TRANS_GEMINI ||
translator === OPT_TRANS_GEMINI_2) && (
<>
<TextField
size="small"
label={"Temperature"}
type="number"
name="temperature"
value={temperature}
onChange={handleChange}
/>
<TextField
size="small"
label={"Max Tokens"}
type="number"
name="maxTokens"
value={maxTokens}
onChange={handleChange}
/>
</>
)}
{translator === OPT_TRANS_NIUTRANS && (
{apiType === OPT_TRANS_NIUTRANS && (
<>
<TextField
size="small"
@@ -395,7 +459,7 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
</>
)}
{translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
{apiType === OPT_TRANS_CUSTOMIZE && (
<>
<TextField
size="small"
@@ -418,140 +482,180 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
</>
)}
{OPT_TRANS_BATCH.has(translator) && (
<>
<TextField
select
size="small"
name="useBatchFetch"
value={useBatchFetch}
label={i18n("use_batch_fetch")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
{useBatchFetch && (
<>
{API_SPE_TYPES.batch.has(api.apiType) && (
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={6} sm={6} md={6} lg={3}>
<TextField
select
fullWidth
size="small"
name="useBatchFetch"
value={useBatchFetch}
label={i18n("use_batch_fetch")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={6} sm={6} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={i18n("batch_interval")}
type="number"
name="batchInterval"
value={batchInterval}
onChange={handleChange}
/>
</Grid>
<Grid item xs={6} sm={6} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={i18n("batch_size")}
type="number"
name="batchSize"
value={batchSize}
onChange={handleChange}
/>
</Grid>
<Grid item xs={6} sm={6} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={i18n("batch_length")}
type="number"
name="batchLength"
value={batchLength}
onChange={handleChange}
/>
</>
)}
</Grid>
</Grid>
</Box>
)}
{API_SPE_TYPES.context.has(api.apiType) && (
<>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={6} sm={6} md={6} lg={3}>
{" "}
<TextField
select
size="small"
fullWidth
name="useContext"
value={useContext}
label={i18n("use_context")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={6} sm={6} md={6} lg={3}>
{" "}
<TextField
size="small"
fullWidth
label={i18n("context_size")}
type="number"
name="contextSize"
value={contextSize}
onChange={handleChange}
/>
</Grid>
</Grid>
</Box>
</>
)}
{OPT_TRANS_CONTEXT.has(translator) && (
<>
<TextField
select
size="small"
name="useContext"
value={useContext}
label={i18n("use_context")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
{useBatchFetch && (
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={6} sm={6} md={6} lg={3}>
<TextField
size="small"
label={i18n("context_size")}
fullWidth
label={i18n("fetch_limit")}
type="number"
name="contextSize"
value={contextSize}
name="fetchLimit"
value={fetchLimit}
onChange={handleChange}
/>
)}
</>
)}
<TextField
size="small"
label={i18n("fetch_limit")}
type="number"
name="fetchLimit"
value={fetchLimit}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("fetch_interval")}
type="number"
name="fetchInterval"
value={fetchInterval}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("http_timeout")}
type="number"
name="httpTimeout"
defaultValue={httpTimeout}
onChange={handleChange}
/>
<FormControlLabel
control={
<Switch
size="small"
name="isDisabled"
checked={isDisabled}
onChange={() => {
updateApi({ isDisabled: !isDisabled });
}}
/>
}
label={i18n("is_disabled")}
/>
</Grid>
<Grid item xs={6} sm={6} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={i18n("fetch_interval")}
type="number"
name="fetchInterval"
value={fetchInterval}
onChange={handleChange}
/>
</Grid>
<Grid item xs={6} sm={6} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={i18n("http_timeout")}
type="number"
name="httpTimeout"
value={httpTimeout}
onChange={handleChange}
/>
</Grid>
<Grid item xs={6} sm={6} md={6} lg={3}></Grid>
</Grid>
</Box>
<Stack direction="row" spacing={2}>
<TestButton translator={translator} api={api} />
<Button
size="small"
variant="outlined"
onClick={() => {
resetApi();
}}
variant="contained"
onClick={handleSave}
disabled={!isModified}
>
{i18n("save")}
</Button>
<TestButton apiSlug={apiSlug} api={api} />
<Button size="small" variant="outlined" onClick={handleReset}>
{i18n("restore_default")}
</Button>
{isUserApi && (
<Button
size="small"
variant="outlined"
color="error"
onClick={handleDelete}
>
{i18n("delete")}
</Button>
)}
<FormControlLabel
control={
<Switch
size="small"
fullWidth
name="isDisabled"
checked={isDisabled}
onChange={handleChange}
/>
}
label={i18n("is_disabled")}
/>
</Stack>
{translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
<pre>{i18n("custom_api_help")}</pre>
)}
{apiType === OPT_TRANS_CUSTOMIZE && <pre>{i18n("custom_api_help")}</pre>}
</Stack>
);
}
function ApiAccordion({ translator }) {
function ApiAccordion({ api, isUserApi, deleteApi }) {
const [expanded, setExpanded] = useState(false);
const { api, updateApi, resetApi } = useApi(translator);
const handleChange = (e) => {
setExpanded((pre) => !pre);
@@ -566,16 +670,15 @@ function ApiAccordion({ translator }) {
overflowWrap: "anywhere",
}}
>
{api.apiName ? `${translator} (${api.apiName})` : translator}
{`[${api.apiType}] ${api.apiName}`}
</Typography>
</AccordionSummary>
<AccordionDetails>
{expanded && (
<ApiFields
translator={translator}
api={api}
updateApi={updateApi}
resetApi={resetApi}
apiSlug={api.apiSlug}
isUserApi={isUserApi}
deleteApi={deleteApi}
/>
)}
</AccordionDetails>
@@ -585,14 +688,85 @@ function ApiAccordion({ translator }) {
export default function Apis() {
const i18n = useI18n();
const { userApis, builtinApis, addApi, deleteApi } = useApiList();
const apiTypes = useMemo(
() =>
OPT_ALL_TYPES.map((type) => ({
type,
label: type,
})),
[]
);
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleMenuItemClick = (apiType) => {
addApi(apiType);
handleClose();
};
return (
<Box>
<Stack spacing={3}>
<Alert severity="info">{i18n("about_api")}</Alert>
<Box>
{OPT_TRANS_ALL.map((translator) => (
<ApiAccordion key={translator} translator={translator} />
<Button
size="small"
id="add-api-button"
variant="contained"
onClick={handleClick}
aria-controls={open ? "add-api-menu" : undefined}
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
endIcon={<KeyboardArrowDownIcon />}
startIcon={<AddIcon />}
>
{i18n("add")}
</Button>
<Menu
id="add-api-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
"aria-labelledby": "add-api-button",
}}
>
{apiTypes.map((apiOption) => (
<MenuItem
key={apiOption.type}
onClick={() => handleMenuItemClick(apiOption.type)}
>
{apiOption.label}
</MenuItem>
))}
</Menu>
</Box>
<Box>
{userApis.map((api) => (
<ApiAccordion
key={api.apiSlug}
api={api}
isUserApi={true}
deleteApi={deleteApi}
/>
))}
</Box>
<Box>
{builtinApis.map((api) => (
<ApiAccordion key={api.apiSlug} api={api} />
))}
</Box>
</Stack>

View File

@@ -18,7 +18,7 @@ export default function DownloadButton({ handleData, text, fileName }) {
link.click();
link.remove();
} catch (err) {
kissLog(err, "download");
kissLog("download", err);
} finally {
setLoading(false);
}

View File

@@ -5,7 +5,7 @@ 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 CircularProgress from "@mui/material/CircularProgress";
// import CircularProgress from "@mui/material/CircularProgress";
import { useI18n } from "../../hooks/I18n";
import Box from "@mui/material/Box";
import { useFavWords } from "../../hooks/FavWords";
@@ -49,29 +49,25 @@ function FavAccordion({ word, index }) {
export default function FavWords() {
const i18n = useI18n();
const { loading, favWords, mergeWords, clearWords } = useFavWords();
const favList = Object.entries(favWords).sort((a, b) =>
a[0].localeCompare(b[0])
);
const downloadList = favList.map(([word]) => word);
const { favList, wordList, mergeWords, clearWords } = useFavWords();
const handleImport = async (data) => {
const handleImport = (data) => {
try {
const newWords = data
.split("\n")
.map((line) => line.split(",")[0].trim())
.filter(isValidWord);
await mergeWords(newWords);
mergeWords(newWords);
} catch (err) {
kissLog(err, "import rules");
kissLog("import rules", err);
}
};
const handleTranslation = async () => {
const tranList = [];
for (const text of downloadList) {
for (const text of wordList) {
try {
// todo
// todo: 修复
const dictRes = await apiTranslate({
text,
translator: OPT_TRANS_BAIDU,
@@ -122,7 +118,7 @@ export default function FavWords() {
fileExts={[".txt", ".csv"]}
/>
<DownloadButton
handleData={() => downloadList.join("\n")}
handleData={() => wordList.join("\n")}
text={i18n("export")}
fileName={`kiss-words_${Date.now()}.txt`}
/>
@@ -144,18 +140,14 @@ export default function FavWords() {
</Stack>
<Box>
{loading ? (
<CircularProgress size={24} />
) : (
favList.map(([word, { createdAt }], index) => (
<FavAccordion
key={word}
index={index}
word={word}
createdAt={createdAt}
/>
))
)}
{favList.map(([word, { createdAt }], index) => (
<FavAccordion
key={word}
index={index}
word={word}
createdAt={createdAt}
/>
))}
</Box>
</Stack>
</Box>

View File

@@ -4,7 +4,6 @@ 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,
OPT_INPUT_TRANS_SIGNS,
@@ -16,10 +15,12 @@ import { useInputRule } from "../../hooks/InputRule";
import { useCallback } from "react";
import Grid from "@mui/material/Grid";
import { limitNumber } from "../../libs/utils";
import { useApiList } from "../../hooks/Api";
export default function InputSetting() {
const i18n = useI18n();
const { inputRule, updateInputRule } = useInputRule();
const { enabledApis } = useApiList();
const handleChange = (e) => {
e.preventDefault();
@@ -44,7 +45,7 @@ export default function InputSetting() {
const {
transOpen,
translator,
apiSlug,
fromLang,
toLang,
triggerShortcut,
@@ -73,14 +74,14 @@ export default function InputSetting() {
<TextField
select
size="small"
name="translator"
value={translator}
name="apiSlug"
value={apiSlug}
label={i18n("translate_service")}
onChange={handleChange}
>
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
{enabledApis.map((api) => (
<MenuItem key={api.apiSlug} value={api.apiSlug}>
{api.apiName}
</MenuItem>
))}
</TextField>
@@ -166,7 +167,7 @@ export default function InputSetting() {
label={i18n("combo_timeout")}
type="number"
name="triggerTime"
defaultValue={triggerTime}
value={triggerTime}
onChange={handleChange}
/>
</Grid>

View File

@@ -7,6 +7,7 @@ import Switch from "@mui/material/Switch";
import { useMouseHoverSetting } from "../../hooks/MouseHover";
import { useCallback } from "react";
import Grid from "@mui/material/Grid";
import { DEFAULT_MOUSEHOVER_KEY } from "../../config";
export default function MouseHoverSetting() {
const i18n = useI18n();
@@ -19,7 +20,7 @@ export default function MouseHoverSetting() {
[updateMouseHoverSetting]
);
const { useMouseHover = true, mouseHoverKey = ["ControlLeft"] } =
const { useMouseHover = true, mouseHoverKey = DEFAULT_MOUSEHOVER_KEY } =
mouseHoverSetting;
return (

View File

@@ -6,7 +6,6 @@ import {
REMAIN_KEY,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_TRANS_ALL,
OPT_STYLE_ALL,
OPT_STYLE_DIY,
OPT_STYLE_USE_COLOR,
@@ -15,10 +14,12 @@ import { useI18n } from "../../hooks/I18n";
import MenuItem from "@mui/material/MenuItem";
import Grid from "@mui/material/Grid";
import { useOwSubRule } from "../../hooks/SubRules";
import { useApiList } from "../../hooks/Api";
export default function OwSubRule() {
const i18n = useI18n();
const { owSubrule, updateOwSubrule } = useOwSubRule();
const { enabledApis } = useApiList();
const handleChange = (e) => {
e.preventDefault();
@@ -27,7 +28,7 @@ export default function OwSubRule() {
};
const {
translator,
apiSlug,
fromLang,
toLang,
textStyle,
@@ -73,16 +74,16 @@ export default function OwSubRule() {
select
size="small"
fullWidth
name="translator"
value={translator}
name="apiSlug"
value={apiSlug}
label={i18n("translate_service")}
onChange={handleChange}
>
{RemainItem}
{GlobalItem}
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
{enabledApis.map((api) => (
<MenuItem key={api.apiSlug} value={api.apiSlug}>
{api.apiName}
</MenuItem>
))}
</TextField>

View File

@@ -0,0 +1,74 @@
import { useState, useEffect, useRef } from "react";
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
/**
* 一个可复用的 Autocomplete 组件,增加了 name 属性和标准化的 onChange 事件
* @param {object} props - 组件的 props
* @param {string} props.name - 表单字段的名称,会包含在 onChange 的 event.target 中
* @param {string} props.label - TextField 的标签
* @param {any} props.value - 受控组件的当前值
* @param {function} props.onChange - 值改变时的回调函数 (event) => {}
* @param {Array} props.options - Autocomplete 的选项列表
*/
export default function ReusableAutocomplete({
name,
label,
value,
onChange,
...rest
}) {
const [inputValue, setInputValue] = useState(value || "");
const isChangeCommitted = useRef(false);
useEffect(() => {
setInputValue(value || "");
}, [value]);
const triggerOnChange = (newValue) => {
if (onChange) {
const syntheticEvent = {
target: {
name: name,
value: newValue,
},
};
onChange(syntheticEvent);
}
};
const handleBlur = () => {
if (isChangeCommitted.current) {
isChangeCommitted.current = false;
return;
}
if (inputValue !== value) {
triggerOnChange(inputValue);
}
};
const handleChange = (event, newValue) => {
isChangeCommitted.current = true;
triggerOnChange(newValue);
};
const handleInputChange = (event, newInputValue) => {
isChangeCommitted.current = false;
setInputValue(newInputValue);
};
return (
<Autocomplete
value={value}
onChange={handleChange}
inputValue={inputValue}
onInputChange={handleInputChange}
onBlur={handleBlur}
{...rest}
renderInput={(params) => (
<TextField {...params} name={name} label={label} />
)}
/>
);
}

View File

@@ -10,7 +10,6 @@ import {
GLOBLA_RULE,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_TRANS_ALL,
OPT_STYLE_ALL,
OPT_STYLE_DIY,
OPT_STYLE_USE_COLOR,
@@ -58,19 +57,26 @@ import EditIcon from "@mui/icons-material/Edit";
import CancelIcon from "@mui/icons-material/Cancel";
import SaveIcon from "@mui/icons-material/Save";
import { kissLog } from "../../libs/log";
import { useApiList } from "../../hooks/Api";
function RuleFields({ rule, rules, setShow, setKeyword }) {
const initFormValues = {
...(rule?.pattern === "*" ? GLOBLA_RULE : DEFAULT_RULE),
...(rule || {}),
};
const editMode = !!rule;
const initFormValues = useMemo(
() => ({
...(rule?.pattern === "*" ? GLOBLA_RULE : DEFAULT_RULE),
...(rule || {}),
}),
[rule]
);
const editMode = useMemo(() => !!rule, [rule]);
const i18n = useI18n();
const [disabled, setDisabled] = useState(editMode);
const [errors, setErrors] = useState({});
const [formValues, setFormValues] = useState(initFormValues);
const [showMore, setShowMore] = useState(!rules);
const [isModified, setIsModified] = useState(false);
const { enabledApis } = useApiList();
const {
pattern,
selector,
@@ -82,7 +88,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
parentStyle = "",
injectJs = "",
injectCss = "",
translator,
apiSlug,
fromLang,
toLang,
textStyle,
@@ -106,6 +112,13 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
// transRemoveHook = "",
} = formValues;
useEffect(() => {
if (!initFormValues) return;
const hasChanged =
JSON.stringify(initFormValues) !== JSON.stringify(formValues);
setIsModified(hasChanged);
}, [initFormValues, formValues]);
const hasSamePattern = (str) => {
for (const item of rules.list) {
if (item.pattern === str && rule?.pattern !== str) {
@@ -417,16 +430,16 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
select
size="small"
fullWidth
name="translator"
value={translator}
name="apiSlug"
value={apiSlug}
label={i18n("translate_service")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
{enabledApis.map((api) => (
<MenuItem key={api.apiSlug} value={api.apiSlug}>
{api.apiName}
</MenuItem>
))}
</TextField>
@@ -738,6 +751,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
variant="contained"
type="submit"
startIcon={<SaveIcon />}
disabled={!isModified}
>
{i18n("save")}
</Button>
@@ -842,7 +856,7 @@ function ShareButton({ rules, injectRules, selectedUrl }) {
window.open(url, "_blank");
} catch (err) {
alert.warning(i18n("error_got_some_wrong"));
kissLog(err, "share rules");
kissLog("share rules", err);
}
};
@@ -871,7 +885,7 @@ function UserRules({ subRules, rules }) {
try {
await rules.merge(JSON.parse(data));
} catch (err) {
kissLog(err, "import rules");
kissLog("import rules", err);
}
};
@@ -1004,7 +1018,7 @@ function SubRulesItem({
await delSubRules(url);
await deleteDataCache(url);
} catch (err) {
kissLog(err, "del subrules");
kissLog("del subrules", err);
}
};
@@ -1017,7 +1031,7 @@ function SubRulesItem({
}
await updateDataCache(url);
} catch (err) {
kissLog(err, "sync sub rules");
kissLog("sync sub rules", err);
} finally {
setLoading(false);
}
@@ -1096,7 +1110,7 @@ function SubRulesEdit({ subList, addSub, updateDataCache }) {
setShowInput(false);
setInputText("");
} catch (err) {
kissLog(err, "fetch rules");
kissLog("fetch rules", err);
setInputError(i18n("error_fetch_url"));
} finally {
setLoading(false);

View File

@@ -96,7 +96,7 @@ export default function Settings() {
caches.delete(CACHE_NAME);
alert.success(i18n("clear_success"));
} catch (err) {
kissLog(err, "clear cache");
kissLog("clear cache", err);
}
};
@@ -104,7 +104,7 @@ export default function Settings() {
try {
await updateSetting(JSON.parse(data));
} catch (err) {
kissLog(err, "import setting");
kissLog("import setting", err);
}
};
@@ -119,10 +119,10 @@ export default function Settings() {
touchTranslate = 2,
blacklist = DEFAULT_BLACKLIST.join(",\n"),
csplist = DEFAULT_CSPLIST.join(",\n"),
transInterval = 200,
transInterval = 100,
langDetector = OPT_TRANS_MICROSOFT,
} = setting;
const { isHide = false, fabClickAction = 0 } = fab || {};
const { isHide = false, fabClickAction = 0 } = fab || {};
return (
<Box>
@@ -163,7 +163,7 @@ export default function Settings() {
label={i18n("min_translate_length")}
type="number"
name="minLength"
defaultValue={minLength}
value={minLength}
onChange={handleChange}
/>
@@ -172,7 +172,7 @@ export default function Settings() {
label={i18n("max_translate_length")}
type="number"
name="maxLength"
defaultValue={maxLength}
value={maxLength}
onChange={handleChange}
/>
@@ -181,7 +181,7 @@ export default function Settings() {
label={i18n("num_of_newline_characters")}
type="number"
name="newlineLength"
defaultValue={newlineLength}
value={newlineLength}
onChange={handleChange}
/>
@@ -190,7 +190,7 @@ export default function Settings() {
label={i18n("translate_interval")}
type="number"
name="transInterval"
defaultValue={transInterval}
value={transInterval}
onChange={handleChange}
/>
<TextField
@@ -198,7 +198,7 @@ export default function Settings() {
label={i18n("http_timeout")}
type="number"
name="httpTimeout"
defaultValue={httpTimeout}
value={httpTimeout}
onChange={handleChange}
/>
<FormControl size="small">
@@ -236,9 +236,9 @@ export default function Settings() {
<InputLabel>{i18n("fab_click_action")}</InputLabel>
<Select
name="fabClickAction"
value={fabClickAction}
value={fabClickAction}
label={i18n("fab_click_action")}
onChange= {(e) => updateFab({ fabClickAction: e.target.value })}
onChange={(e) => updateFab({ fabClickAction: e.target.value })}
>
<MenuItem value={0}>{i18n("fab_click_menu")}</MenuItem>
<MenuItem value={1}>{i18n("fab_click_translate")}</MenuItem>
@@ -302,7 +302,7 @@ export default function Settings() {
i18n("pattern_helper") + " " + i18n("disabled_csplist_helper")
}
name="csplist"
defaultValue={csplist}
value={csplist}
onChange={handleChange}
multiline
/>
@@ -345,7 +345,7 @@ export default function Settings() {
label={i18n("translate_blacklist")}
helperText={i18n("pattern_helper")}
name="blacklist"
defaultValue={blacklist}
value={blacklist}
onChange={handleChange}
maxRows={10}
multiline

View File

@@ -2,32 +2,61 @@ import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton";
import EditIcon from "@mui/icons-material/Edit";
import CheckIcon from "@mui/icons-material/Check";
import { useEffect, useState, useRef } from "react";
import { shortcutListener } from "../../libs/shortcut";
import { useI18n } from "../../hooks/I18n";
export default function ShortcutInput({ value, onChange, label, helperText }) {
const [disabled, setDisabled] = useState(true);
export default function ShortcutInput({
value: keys,
onChange,
label,
helperText,
}) {
const [isEditing, setIsEditing] = useState(false);
const [editingKeys, setEditingKeys] = useState([]);
const inputRef = useRef(null);
const i18n = useI18n();
const commitChanges = () => {
if (editingKeys.length > 0) {
onChange(editingKeys);
}
setIsEditing(false);
};
const handleBlur = () => {
commitChanges();
};
const handleEditClick = () => {
setEditingKeys([]);
setIsEditing(true);
};
useEffect(() => {
if (disabled) {
if (!isEditing) {
return;
}
inputRef.current.focus();
onChange([]);
const clearShortcut = shortcutListener((curkeys, allkeys) => {
onChange(allkeys);
if (curkeys.length === 0) {
setDisabled(true);
}
}, inputRef.current);
const inputElement = inputRef.current;
if (inputElement) {
inputElement.focus();
}
const clearShortcut = shortcutListener((pressedKeys, event) => {
event.preventDefault();
event.stopPropagation();
setEditingKeys([...pressedKeys]);
});
return () => {
clearShortcut();
};
}, [disabled, onChange]);
}, [isEditing]);
const displayValue = isEditing ? editingKeys : keys;
const formattedValue = displayValue
.map((item) => (item === " " ? "Space" : item))
.join(" + ");
return (
<Stack direction="row" alignItems="flex-start">
@@ -35,22 +64,22 @@ export default function ShortcutInput({ value, onChange, label, helperText }) {
size="small"
label={label}
name={label}
value={value.map((item) => (item === " " ? "Space" : item)).join(" + ")}
value={formattedValue}
fullWidth
inputRef={inputRef}
disabled={disabled}
onBlur={() => {
setDisabled(true);
}}
helperText={helperText}
disabled={!isEditing}
onBlur={handleBlur}
helperText={isEditing ? i18n("pls_press_shortcut") : helperText}
/>
<IconButton
onClick={() => {
setDisabled(false);
}}
>
{<EditIcon />}
</IconButton>
{isEditing ? (
<IconButton onClick={commitChanges} color="primary">
<CheckIcon />
</IconButton>
) : (
<IconButton onClick={handleEditClick}>
<EditIcon />
</IconButton>
)}
</Stack>
);
}

View File

@@ -21,8 +21,8 @@ import { useAlert } from "../../hooks/Alert";
import { useSetting } from "../../hooks/Setting";
import { kissLog } from "../../libs/log";
import SyncIcon from "@mui/icons-material/Sync";
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import ContentPasteIcon from "@mui/icons-material/ContentPaste";
export default function SyncSetting() {
const i18n = useI18n();
@@ -44,10 +44,10 @@ export default function SyncSetting() {
try {
setLoading(true);
await syncSettingAndRules();
await reloadSetting();
reloadSetting();
alert.success(i18n("sync_success"));
} catch (err) {
kissLog(err, "sync all");
kissLog("sync all", err);
alert.error(i18n("sync_failed"));
} finally {
setLoading(false);
@@ -56,37 +56,37 @@ export default function SyncSetting() {
const handleGenerateShareString = async () => {
try {
const base64Config = btoa(JSON.stringify({
syncType: syncType,
syncUrl: syncUrl,
syncUser: syncUser,
syncKey: syncKey,
}));
const base64Config = btoa(
JSON.stringify({
syncType: syncType,
syncUrl: syncUrl,
syncUser: syncUser,
syncKey: syncKey,
})
);
const shareString = `${OPT_SYNCTOKEN_PERFIX}${base64Config}`;
await navigator.clipboard.writeText(shareString);
console.debug("Share string copied to clipboard", shareString);
kissLog("Share string copied to clipboard", shareString);
} catch (error) {
console.error("Failed to copy share string to clipboard", error);
kissLog("Failed to copy share string to clipboard", error);
}
};
const handleImportFromClipboard = async () => {
try {
const text = await navigator.clipboard.readText();
console.debug('read_clipboard', text)
kissLog("read_clipboard", text);
if (text.startsWith(OPT_SYNCTOKEN_PERFIX)) {
const base64Config = text.slice(OPT_SYNCTOKEN_PERFIX.length);
const jsonString = atob(base64Config);
const updatedConfig = JSON.parse(jsonString);
if (!OPT_SYNCTYPE_ALL.includes(updatedConfig.syncType)) {
console.error('error syncType', updatedConfig.syncType)
kissLog("error syncType", updatedConfig.syncType);
return;
}
if (
updatedConfig.syncUrl
) {
if (updatedConfig.syncUrl) {
updateSync({
syncType: updatedConfig.syncType,
syncUrl: updatedConfig.syncUrl,
@@ -94,17 +94,16 @@ export default function SyncSetting() {
syncKey: updatedConfig.syncKey,
});
} else {
console.error("Invalid config structure");
kissLog("Invalid config structure");
}
} else {
console.error("Invalid share string", text);
kissLog("Invalid share string", text);
}
} catch (error) {
console.error("Failed to read from clipboard or parse JSON", error);
kissLog("Failed to read from clipboard or parse JSON", error);
}
};
if (!sync) {
return;
}

View File

@@ -4,7 +4,6 @@ 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,
OPT_TRANBOX_TRIGGER_CLICK,
@@ -16,11 +15,13 @@ import { useCallback } from "react";
import { limitNumber } from "../../libs/utils";
import { useTranbox } from "../../hooks/Tranbox";
import { isExt } from "../../libs/client";
import { useApiList } from "../../hooks/Api";
import Alert from "@mui/material/Alert";
export default function Tranbox() {
const i18n = useI18n();
const { tranboxSetting, updateTranbox } = useTranbox();
const { enabledApis } = useApiList();
const handleChange = (e) => {
e.preventDefault();
@@ -47,7 +48,7 @@ export default function Tranbox() {
);
const {
translator,
apiSlug,
fromLang,
toLang,
toLang2 = "en",
@@ -72,14 +73,14 @@ export default function Tranbox() {
<TextField
select
size="small"
name="translator"
value={translator}
name="apiSlug"
value={apiSlug}
label={i18n("translate_service")}
onChange={handleChange}
>
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
{enabledApis.map((api) => (
<MenuItem key={api.apiSlug} value={api.apiSlug}>
{api.apiName}
</MenuItem>
))}
</TextField>
@@ -147,7 +148,7 @@ export default function Tranbox() {
label={i18n("tranbtn_offset_x")}
type="number"
name="btnOffsetX"
defaultValue={btnOffsetX}
value={btnOffsetX}
onChange={handleChange}
/>
@@ -156,7 +157,7 @@ export default function Tranbox() {
label={i18n("tranbtn_offset_y")}
type="number"
name="btnOffsetY"
defaultValue={btnOffsetY}
value={btnOffsetY}
onChange={handleChange}
/>
@@ -165,7 +166,7 @@ export default function Tranbox() {
label={i18n("tranbox_offset_x")}
type="number"
name="boxOffsetX"
defaultValue={boxOffsetX}
value={boxOffsetX}
onChange={handleChange}
/>
@@ -174,7 +175,7 @@ export default function Tranbox() {
label={i18n("tranbox_offset_y")}
type="number"
name="boxOffsetY"
defaultValue={boxOffsetY}
value={boxOffsetY}
onChange={handleChange}
/>
@@ -245,7 +246,7 @@ export default function Tranbox() {
size="small"
label={i18n("extend_styles")}
name="extStyles"
defaultValue={extStyles}
value={extStyles}
onChange={handleChange}
maxRows={10}
multiline

View File

@@ -9,9 +9,9 @@ import ThemeProvider from "../../hooks/Theme";
import { useEffect, useState } from "react";
import { isGm } from "../../libs/client";
import { sleep } from "../../libs/utils";
import CircularProgress from "@mui/material/CircularProgress";
import { trySyncSettingAndRules } from "../../libs/sync";
import { AlertProvider } from "../../hooks/Alert";
import { ConfirmProvider } from "../../hooks/Confirm";
import Link from "@mui/material/Link";
import Divider from "@mui/material/Divider";
import Stack from "@mui/material/Stack";
@@ -22,6 +22,7 @@ import InputSetting from "./InputSetting";
import Tranbox from "./Tranbox";
import FavWords from "./FavWords";
import MouseHoverSetting from "./MouseHover";
import Loading from "../../hooks/Loading";
export default function Options() {
const [error, setError] = useState("");
@@ -91,37 +92,30 @@ export default function Options() {
}
if (!ready) {
return (
<center>
<Divider>
<Link
href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider>
<CircularProgress />
</center>
);
return <Loading />;
}
return (
<SettingProvider>
<ThemeProvider>
<AlertProvider>
<HashRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Setting />} />
<Route path="rules" element={<Rules />} />
<Route path="input" element={<InputSetting />} />
<Route path="tranbox" element={<Tranbox />} />
<Route path="mousehover" element={<MouseHoverSetting />} />
<Route path="apis" element={<Apis />} />
<Route path="sync" element={<SyncSetting />} />
<Route path="words" element={<FavWords />} />
<Route path="about" element={<About />} />
</Route>
</Routes>
</HashRouter>
<ConfirmProvider>
<HashRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Setting />} />
<Route path="rules" element={<Rules />} />
<Route path="input" element={<InputSetting />} />
<Route path="tranbox" element={<Tranbox />} />
<Route path="mousehover" element={<MouseHoverSetting />} />
<Route path="apis" element={<Apis />} />
<Route path="sync" element={<SyncSetting />} />
<Route path="words" element={<FavWords />} />
<Route path="about" element={<About />} />
</Route>
</Routes>
</HashRouter>
</ConfirmProvider>
</AlertProvider>
</ThemeProvider>
</SettingProvider>