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,7 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { limitNumber } from "../../libs/utils";
import { isMobile } from "../../libs/mobile";
import { updateFab } from "../../libs/storage";
import { putFab } from "../../libs/storage";
import { debounce } from "../../libs/utils";
import Paper from "@mui/material/Paper";
@@ -61,7 +61,7 @@ export default function Draggable({
const [hover, setHover] = useState(false);
const [origin, setOrigin] = useState(null);
const [position, setPosition] = useState({ x: left, y: top });
const setFabPosition = useMemo(() => debounce(updateFab, 500), []);
const setFabPosition = useMemo(() => debounce(putFab, 500), []);
const handlePointerDown = (e) => {
!isMobile && e.target.setPointerCapture(e.pointerId);

View File

@@ -142,7 +142,7 @@ export default function Action({ translator, fab }) {
});
};
} catch (err) {
kissLog(err, "registerMenuCommand");
kissLog("registerMenuCommand", err);
}
}, [translator]);

View File

@@ -1,14 +0,0 @@
import { loadingSvg } from "../../libs/svg";
export default function LoadingIcon() {
return (
<span
style={{
display: "inline-block",
width: "1.2em",
height: "1em",
}}
dangerouslySetInnerHTML={{ __html: loadingSvg }}
/>
);
}

View File

@@ -1,208 +0,0 @@
import { useState, useEffect, useMemo } from "react";
import LoadingIcon from "./LoadingIcon";
import {
OPT_STYLE_LINE,
OPT_STYLE_DOTLINE,
OPT_STYLE_DASHLINE,
OPT_STYLE_WAVYLINE,
OPT_STYLE_DASHBOX,
OPT_STYLE_FUZZY,
OPT_STYLE_HIGHLIGHT,
OPT_STYLE_BLOCKQUOTE,
OPT_STYLE_DIY,
DEFAULT_COLOR,
MSG_TRANS_CURRULE,
} from "../../config";
import { useTranslate } from "../../hooks/Translate";
import { styled, css } from "@mui/material/styles";
import { APP_LCNAME } from "../../config";
import interpreter from "../../libs/interpreter";
const LINE_STYLES = {
[OPT_STYLE_LINE]: "solid",
[OPT_STYLE_DOTLINE]: "dotted",
[OPT_STYLE_DASHLINE]: "dashed",
[OPT_STYLE_WAVYLINE]: "wavy",
};
const StyledSpan = styled("span")`
${({ textStyle, textDiyStyle, bgColor }) => {
switch (textStyle) {
case OPT_STYLE_LINE: // 下划线
case OPT_STYLE_DOTLINE: // 点状线
case OPT_STYLE_DASHLINE: // 虚线
case OPT_STYLE_WAVYLINE: // 波浪线
return css`
opacity: 0.6;
-webkit-opacity: 0.6;
text-decoration-line: underline;
text-decoration-style: ${LINE_STYLES[textStyle]};
text-decoration-color: ${bgColor};
text-decoration-thickness: 2px;
text-underline-offset: 0.3em;
-webkit-text-decoration-line: underline;
-webkit-text-decoration-style: ${LINE_STYLES[textStyle]};
-webkit-text-decoration-color: ${bgColor};
-webkit-text-decoration-thickness: 2px;
-webkit-text-underline-offset: 0.3em;
&:hover {
opacity: 1;
-webkit-opacity: 1;
}
`;
case OPT_STYLE_DASHBOX: // 虚线框
return css`
color: ${bgColor || DEFAULT_COLOR};
border: 1px dashed ${bgColor || DEFAULT_COLOR};
background: transparent;
display: block;
padding: 0.2em;
box-sizing: border-box;
white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;
`;
case OPT_STYLE_FUZZY: // 模糊
return css`
filter: blur(0.2em);
-webkit-filter: blur(0.2em);
&:hover {
filter: none;
-webkit-filter: none;
}
`;
case OPT_STYLE_HIGHLIGHT: // 高亮
return css`
color: #fff;
background-color: ${bgColor || DEFAULT_COLOR};
`;
case OPT_STYLE_BLOCKQUOTE: // 引用
return css`
opacity: 0.6;
-webkit-opacity: 0.6;
display: block;
padding: 0 0.75em;
border-left: 0.25em solid ${bgColor || DEFAULT_COLOR};
&:hover {
opacity: 1;
-webkit-opacity: 1;
}
`;
case OPT_STYLE_DIY: // 自定义
return textDiyStyle;
default:
return ``;
}
}}
`;
export default function Content({ q, keeps, translator, $el }) {
const [rule, setRule] = useState(translator.rule);
const { text, sameLang, loading } = useTranslate(
q,
rule,
translator.setting,
translator.docInfo
);
const {
transOpen,
textStyle,
bgColor,
textDiyStyle,
transOnly,
transTag,
transEndHook,
} = rule;
const { newlineLength } = translator.setting;
const handleKissEvent = (e) => {
const { action, args } = e.detail;
switch (action) {
case MSG_TRANS_CURRULE:
setRule(args);
break;
default:
}
};
useEffect(() => {
window.addEventListener(translator.eventName, handleKissEvent);
return () => {
window.removeEventListener(translator.eventName, handleKissEvent);
};
}, [translator.eventName]);
const gap = useMemo(() => {
if (transOnly === "true") {
return "";
}
return q.length >= newlineLength ? <br /> : " ";
}, [q, transOnly, newlineLength]);
const styles = useMemo(
() => ({
textStyle,
textDiyStyle,
bgColor,
as: transTag,
}),
[textStyle, textDiyStyle, bgColor, transTag]
);
const trText = useMemo(() => {
if (loading || !transEndHook?.trim()) {
return text;
}
// 翻译完成钩子函数
interpreter.run(`exports.transEndHook = ${transEndHook}`);
return interpreter.exports.transEndHook($el, text, q, keeps);
}, [loading, $el, q, text, keeps, transEndHook]);
if (loading) {
return (
<>
{gap}
<LoadingIcon />
</>
);
}
if (!trText || sameLang) {
return;
}
if (
transOnly === "true" &&
transOpen === "true" &&
$el.querySelector(APP_LCNAME)
) {
Array.from($el.childNodes).forEach((el) => {
if (el.localName !== APP_LCNAME) {
el.remove();
}
});
}
if (keeps.length > 0) {
return (
<>
{gap}
<StyledSpan
{...styles}
dangerouslySetInnerHTML={{
__html: trText.replace(/\[(\d+)\]/g, (_, p) => keeps[parseInt(p)]),
}}
/>
</>
);
}
return (
<>
{gap}
<StyledSpan {...styles}>{trText}</StyledSpan>
</>
);
}

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>

View File

@@ -20,11 +20,9 @@ import {
MSG_OPEN_OPTIONS,
MSG_SAVE_RULE,
MSG_COMMAND_SHORTCUTS,
OPT_TRANS_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_STYLE_ALL,
DEFAULT_TRANS_APIS,
} from "../../config";
import { sendIframeMsg } from "../../libs/iframe";
import { saveRule } from "../../libs/rules";
@@ -33,14 +31,14 @@ import { kissLog } from "../../libs/log";
// 插件popup没有参数
// 网页弹框有
export default function Popup({ setShowPopup, translator: tran }) {
export default function Popup({ setShowPopup, translator }) {
const i18n = useI18n();
const [rule, setRule] = useState(tran?.rule);
const [transApis, setTransApis] = useState(tran?.setting?.transApis || []);
const [rule, setRule] = useState(translator?.rule);
const [transApis, setTransApis] = useState(translator?.setting?.transApis || []);
const [commands, setCommands] = useState({});
const handleOpenSetting = () => {
if (!tran) {
if (!translator) {
browser?.runtime.openOptionsPage();
} else if (isExt) {
sendBgMsg(MSG_OPEN_OPTIONS);
@@ -54,14 +52,14 @@ export default function Popup({ setShowPopup, translator: tran }) {
try {
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
if (!tran) {
if (!translator) {
await sendTabMsg(MSG_TRANS_TOGGLE);
} else {
tran.toggle();
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
}
} catch (err) {
kissLog(err, "toggle trans");
kissLog("toggle trans", err);
}
};
@@ -70,14 +68,14 @@ export default function Popup({ setShowPopup, translator: tran }) {
const { name, value } = e.target;
setRule((pre) => ({ ...pre, [name]: value }));
if (!tran) {
if (!translator) {
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
} else {
tran.updateRule({ [name]: value });
translator.updateRule({ [name]: value });
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
}
} catch (err) {
kissLog(err, "update rule");
kissLog("update rule", err);
}
};
@@ -88,23 +86,23 @@ export default function Popup({ setShowPopup, translator: tran }) {
const handleSaveRule = async () => {
try {
let href = window.location.href;
if (!tran) {
if (!translator) {
const tab = await getCurTab();
href = tab.url;
}
const newRule = { ...rule, pattern: href.split("/")[2] };
if (isExt && tran) {
if (isExt && translator) {
sendBgMsg(MSG_SAVE_RULE, newRule);
} else {
saveRule(newRule);
}
} catch (err) {
kissLog(err, "save rule");
kissLog("save rule", err);
}
};
useEffect(() => {
if (tran) {
if (translator) {
return;
}
(async () => {
@@ -115,10 +113,10 @@ export default function Popup({ setShowPopup, translator: tran }) {
setTransApis(res.setting.transApis);
}
} catch (err) {
kissLog(err, "query rule");
kissLog("query rule", err);
}
})();
}, [tran]);
}, [translator]);
useEffect(() => {
(async () => {
@@ -130,7 +128,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
commands[name] = shortcut;
});
} else {
const shortcuts = tran.setting.shortcuts;
const shortcuts = translator.setting.shortcuts;
if (shortcuts) {
Object.entries(shortcuts).forEach(([key, val]) => {
commands[key] = val.join("+");
@@ -139,21 +137,18 @@ export default function Popup({ setShowPopup, translator: tran }) {
}
setCommands(commands);
} catch (err) {
kissLog(err, "query cmds");
kissLog("query cmds", err);
}
})();
}, [tran]);
}, [translator]);
const optApis = useMemo(
() =>
OPT_TRANS_ALL.map((key) => ({
...(transApis[key] || DEFAULT_TRANS_APIS[key]),
apiKey: key,
}))
.filter((item) => !item.isDisabled)
.map(({ apiKey, apiName }) => ({
key: apiKey,
name: apiName?.trim() || apiKey,
transApis
.filter((api) => !api.isDisabled)
.map((api) => ({
key: api.apiSlug,
name: api.apiName || api.apiSlug,
})),
[transApis]
);
@@ -161,7 +156,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
if (!rule) {
return (
<Box minWidth={300}>
{!tran && (
{!translator && (
<>
<Header />
<Divider />
@@ -178,7 +173,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
const {
transOpen,
translator,
apiSlug,
fromLang,
toLang,
textStyle,
@@ -190,7 +185,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
return (
<Box width={320}>
{!tran && (
{!translator && (
<>
<Header />
<Divider />
@@ -275,8 +270,8 @@ export default function Popup({ setShowPopup, translator: tran }) {
select
SelectProps={{ MenuProps: { disablePortal: true } }}
size="small"
value={translator}
name="translator"
value={apiSlug}
name="apiSlug"
label={i18n("translate_service")}
onChange={handleChange}
>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo } from "react";
import Stack from "@mui/material/Stack";
import FavBtn from "./FavBtn";
import Typography from "@mui/material/Typography";
@@ -26,10 +26,10 @@ export default function DictCont({ text }) {
return;
}
// todo
// todo: 修复
const dictRes = await apiTranslate({
text,
translator: OPT_TRANS_BAIDU,
apiSlug: OPT_TRANS_BAIDU,
fromLang: "en",
toLang: "zh-CN",
});
@@ -45,70 +45,74 @@ export default function DictCont({ text }) {
})();
}, [text]);
if (error) {
return <Alert severity="error">{error}</Alert>;
}
const copyText = useMemo(() => {
if (!dictResult) {
return text;
}
return [
dictResult.src,
dictResult.voice
?.map(Object.entries)
.map((item) => item[0])
.map(([key, val]) => `${PHONIC_MAP[key]?.[0] || key} ${val}`)
.join(" "),
dictResult.content[0].mean
.map(({ pre, cont }) => {
return `${pre ? `[${pre}] ` : ""}${Object.keys(cont).join("; ")}`;
})
.join("\n"),
].join("\n");
}, [text, dictResult]);
if (loading) {
return <CircularProgress size={16} />;
}
if (!text || !dictResult) {
return;
}
const copyText = [
dictResult.src,
dictResult.voice
?.map(Object.entries)
.map((item) => item[0])
.map(([key, val]) => `${PHONIC_MAP[key]?.[0] || key} ${val}`)
.join(" "),
dictResult.content[0].mean
.map(({ pre, cont }) => {
return `${pre ? `[${pre}] ` : ""}${Object.keys(cont).join("; ")}`;
})
.join("\n"),
].join("\n");
return (
<Stack className="KT-transbox-dict" spacing={1}>
<Stack direction="row" justifyContent="space-between">
<Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
{dictResult.src}
</Typography>
{text && (
<Stack direction="row" justifyContent="space-between">
<CopyBtn text={copyText} />
<FavBtn word={dictResult.src} />
<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>
</Stack>
)}
<Typography component="div">
{error && <Alert severity="error">{error}</Alert>}
{dictResult && (
<Typography component="div">
{dictResult.voice
?.map(Object.entries)
.map((item) => item[0])
.map(([key, val]) => (
<Typography
component="div"
key={key}
style={{ display: "inline-block" }}
>
<Typography component="span">{`${PHONIC_MAP[key]?.[0] || key} ${val}`}</Typography>
<AudioBtn text={dictResult.src} lan={PHONIC_MAP[key]?.[1]} />
<Typography component="div">
{dictResult.voice
?.map(Object.entries)
.map((item) => item[0])
.map(([key, val]) => (
<Typography
component="div"
key={key}
style={{ display: "inline-block" }}
>
<Typography component="span">{`${PHONIC_MAP[key]?.[0] || key} ${val}`}</Typography>
<AudioBtn text={dictResult.src} lan={PHONIC_MAP[key]?.[1]} />
</Typography>
))}
</Typography>
<Typography component="ul">
{dictResult.content[0].mean.map(({ pre, cont }, idx) => (
<Typography component="li" key={idx}>
{pre && `[${pre}] `}
{Object.keys(cont).join("; ")}
</Typography>
))}
</Typography>
</Typography>
<Typography component="ul">
{dictResult.content[0].mean.map(({ pre, cont }, idx) => (
<Typography component="li" key={idx}>
{pre && `[${pre}] `}
{Object.keys(cont).join("; ")}
</Typography>
))}
</Typography>
</Typography>
)}
</Stack>
);
}

View File

@@ -9,12 +9,12 @@ export default function FavBtn({ word }) {
const { favWords, toggleFav } = useFavWords();
const [loading, setLoading] = useState(false);
const handleClick = async () => {
const handleClick = () => {
try {
setLoading(true);
await toggleFav(word);
toggleFav(word);
} catch (err) {
kissLog(err, "set fav");
kissLog("set fav", err);
} finally {
setLoading(false);
}

View File

@@ -18,12 +18,7 @@ import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen";
import CloseIcon from "@mui/icons-material/Close";
import { useI18n } from "../../hooks/I18n";
import {
OPT_TRANS_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
DEFAULT_TRANS_APIS,
} from "../../config";
import { OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
import { useState, useRef, useMemo } from "react";
import TranCont from "./TranCont";
import DictCont from "./DictCont";
@@ -119,21 +114,18 @@ function TranForm({
const [editMode, setEditMode] = useState(false);
const [editText, setEditText] = useState("");
const [translator, setTranslator] = useState(tranboxSetting.translator);
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(
() =>
OPT_TRANS_ALL.map((key) => ({
...(transApis[key] || DEFAULT_TRANS_APIS[key]),
apiKey: key,
}))
.filter((item) => !item.isDisabled)
.map(({ apiKey, apiName }) => ({
key: apiKey,
name: apiName?.trim() || apiKey,
transApis
.filter((api) => !api.isDisabled)
.map((api) => ({
key: api.apiSlug,
name: api.apiName || api.apiSlug,
})),
[transApis]
);
@@ -194,11 +186,11 @@ function TranForm({
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
value={translator}
name="translator"
value={apiSlug}
name="apiSlug"
label={i18n("translate_service")}
onChange={(e) => {
setTranslator(e.target.value);
setApiSlug(e.target.value);
}}
>
{optApis.map(({ key, name }) => (
@@ -266,7 +258,7 @@ function TranForm({
enDict === "-") && (
<TranCont
text={text}
translator={translator}
apiSlug={apiSlug}
fromLang={fromLang}
toLang={toLang}
toLang2={tranboxSetting.toLang2}
@@ -307,6 +299,7 @@ export default function TranBox({
enDict,
}) {
const [mouseHover, setMouseHover] = useState(false);
// todo: 这里的 SettingProvider 不应和 background 的共用
return (
<SettingProvider>
<ThemeProvider styles={extStyles}>

View File

@@ -3,7 +3,7 @@ import Box from "@mui/material/Box";
import CircularProgress from "@mui/material/CircularProgress";
import Stack from "@mui/material/Stack";
import { useI18n } from "../../hooks/I18n";
import { DEFAULT_TRANS_APIS } from "../../config";
import { DEFAULT_API_SETTING } from "../../config";
import { useEffect, useState } from "react";
import { apiTranslate } from "../../apis";
import CopyBtn from "./CopyBtn";
@@ -13,7 +13,7 @@ import { tryDetectLang } from "../../libs";
export default function TranCont({
text,
translator,
apiSlug,
fromLang,
toLang,
toLang2 = "en",
@@ -42,10 +42,11 @@ export default function TranCont({
}
const apiSetting =
transApis[translator] || DEFAULT_TRANS_APIS[translator];
transApis.find((api) => api.apiSlug === apiSlug) ||
DEFAULT_API_SETTING;
const [trText] = await apiTranslate({
text,
translator,
apiSlug,
fromLang,
toLang: to,
apiSetting,
@@ -57,7 +58,7 @@ export default function TranCont({
setLoading(false);
}
})();
}, [text, translator, fromLang, toLang, toLang2, transApis, langDetector]);
}, [text, apiSlug, fromLang, toLang, toLang2, transApis, langDetector]);
if (simpleStyle) {
return (

View File

@@ -201,7 +201,7 @@ export default function Slection({
});
};
} catch (err) {
kissLog(err, "registerMenuCommand");
kissLog("registerMenuCommand", err);
}
}, [handleTranbox, contextMenuType, langMap]);