feat: support youdao dict

This commit is contained in:
Gabe
2025-10-03 18:28:50 +08:00
parent 65e8fabe7d
commit 171dbb7509
19 changed files with 631 additions and 228 deletions

View File

@@ -45,7 +45,7 @@
"description": "__MSG_open_options__"
}
},
"permissions": ["storage", "contextMenus", "scripting", "declarativeNetRequest"],
"permissions": ["storage", "contextMenus", "scripting", "declarativeNetRequest", "declarativeNetRequestWithHostAccess"],
"host_permissions": ["<all_urls>"],
"icons": {
"16": "images/logo16.png",

View File

@@ -159,6 +159,77 @@ export const apiBaiduSuggest = async (text) => {
return [];
};
/**
* 有道翻译建议
* @param {*} text
* @returns
*/
export const apiYoudaoSuggest = async (text) => {
const params = {
num: 5,
ver: 3.0,
doctype: "json",
cache: false,
le: "en",
q: text,
};
const input = `https://dict.youdao.com/suggest?${queryString.stringify(params)}`;
const init = {
headers: {
accept: "application/json, text/plain, */*",
"accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,ja;q=0.6",
"content-type": "application/x-www-form-urlencoded",
},
method: "GET",
};
const res = await fetchData(input, init, { useCache: true });
if (res?.result?.code === 200) {
await putHttpCachePolyfill(input, init, res);
return res.data.entries;
}
return [];
};
/**
* 有道词典
* @param {*} text
* @returns
*/
export const apiYoudaoDict = async (text) => {
const params = {
doctype: "json",
jsonversion: 4,
};
const input = `https://dict.youdao.com/jsonapi_s?${queryString.stringify(params)}`;
const body = queryString.stringify({
q: "search",
le: "en",
t: 3,
client: "web",
// sign: "",
keyfrom: "webdict",
});
const init = {
headers: {
accept: "application/json, text/plain, */*",
"accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,ja;q=0.6",
"content-type": "application/x-www-form-urlencoded",
},
method: "POST",
body,
};
const res = await fetchData(input, init, { useCache: true });
if (res) {
await putHttpCachePolyfill(input, init, res);
return res;
}
return null;
};
/**
* 百度语音
* @param {*} text

View File

@@ -14,6 +14,7 @@ import {
MSG_INJECT_CSS,
MSG_UPDATE_CSP,
DEFAULT_CSPLIST,
DEFAULT_ORILIST,
CMD_TOGGLE_TRANSLATE,
CMD_TOGGLE_STYLE,
CMD_OPEN_OPTIONS,
@@ -33,7 +34,9 @@ import { kissLog } from "./libs/log";
globalThis.ContextType = "BACKGROUND";
const REMOVE_HEADERS = [
const CSP_RULE_START_ID = 1;
const ORI_RULE_START_ID = 10000;
const CSP_REMOVE_HEADERS = [
`content-security-policy`,
`content-security-policy-report-only`,
`x-webkit-csp`,
@@ -94,17 +97,34 @@ async function addContextMenus(contextMenuType = 1) {
* 更新CSP策略
* @param {*} csplist
*/
async function updateCspRules(csplist = DEFAULT_CSPLIST.join(",\n")) {
async function updateCspRules({ csplist, orilist }) {
try {
const newRules = csplist
.split(/\n|,/)
.map((url) => url.trim())
.filter(Boolean)
.map((url, idx) => ({
id: idx + 1,
const oldRules = await browser.declarativeNetRequest.getDynamicRules();
const rulesToAdd = [];
const idsToRemove = [];
if (csplist !== undefined) {
let processedCspList = csplist;
if (typeof processedCspList === "string") {
processedCspList = processedCspList
.split(/\n|,/)
.map((url) => url.trim())
.filter(Boolean);
}
const oldCspRuleIds = oldRules
.filter(
(rule) => rule.id >= CSP_RULE_START_ID && rule.id < ORI_RULE_START_ID
)
.map((rule) => rule.id);
idsToRemove.push(...oldCspRuleIds);
const newCspRules = processedCspList.map((url, index) => ({
id: CSP_RULE_START_ID + index,
action: {
type: "modifyHeaders",
responseHeaders: REMOVE_HEADERS.map((header) => ({
responseHeaders: CSP_REMOVE_HEADERS.map((header) => ({
operation: "remove",
header,
})),
@@ -114,12 +134,43 @@ async function updateCspRules(csplist = DEFAULT_CSPLIST.join(",\n")) {
resourceTypes: ["main_frame", "sub_frame"],
},
}));
const oldRules = await browser.declarativeNetRequest.getDynamicRules();
const oldRuleIds = oldRules.map((rule) => rule.id);
await browser.declarativeNetRequest.updateDynamicRules({
removeRuleIds: oldRuleIds,
addRules: newRules,
});
rulesToAdd.push(...newCspRules);
}
if (orilist !== undefined) {
let processedOriList = orilist;
if (typeof processedOriList === "string") {
processedOriList = processedOriList
.split(/\n|,/)
.map((url) => url.trim())
.filter(Boolean);
}
const oldOriRuleIds = oldRules
.filter((rule) => rule.id >= ORI_RULE_START_ID)
.map((rule) => rule.id);
idsToRemove.push(...oldOriRuleIds);
const newOriRules = processedOriList.map((url, index) => ({
id: ORI_RULE_START_ID + index,
action: {
type: "modifyHeaders",
requestHeaders: [{ header: "Origin", operation: "remove" }],
},
condition: {
urlFilter: url,
resourceTypes: ["xmlhttprequest"],
},
}));
rulesToAdd.push(...newOriRules);
}
if (idsToRemove.length > 0 || rulesToAdd.length > 0) {
await browser.declarativeNetRequest.updateDynamicRules({
removeRuleIds: idsToRemove,
addRules: rulesToAdd,
});
}
} catch (err) {
kissLog("update csp rules", err);
}
@@ -149,7 +200,7 @@ browser.runtime.onInstalled.addListener(() => {
addContextMenus();
// 禁用CSP
updateCspRules();
updateCspRules({ csplist: DEFAULT_CSPLIST, orilist: DEFAULT_ORILIST });
});
/**
@@ -159,7 +210,7 @@ browser.runtime.onStartup.addListener(async () => {
// 同步数据
await trySyncSettingAndRules();
const { clearCache, contextMenuType, subrulesList, csplist } =
const { clearCache, contextMenuType, subrulesList, csplist, orilist } =
await getSettingWithDefault();
// 清除缓存
@@ -177,7 +228,7 @@ browser.runtime.onStartup.addListener(async () => {
addContextMenus(contextMenuType);
// 禁用CSP
updateCspRules(csplist);
updateCspRules({ csplist, orilist });
// 同步订阅规则
trySyncAllSubRules({ subrulesList });

View File

@@ -14,6 +14,14 @@ export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
export const OPT_DICT_BAIDU = "Baidu";
export const OPT_DICT_YOUDAO = "Youdao";
export const OPT_DICT_ALL = [OPT_DICT_BAIDU, OPT_DICT_YOUDAO];
export const OPT_DICT_MAP = new Set(OPT_DICT_ALL);
export const OPT_SUG_BAIDU = "Baidu";
export const OPT_SUG_YOUDAO = "Youdao";
export const OPT_SUG_ALL = [OPT_SUG_BAIDU, OPT_SUG_YOUDAO];
export const OPT_SUG_MAP = new Set(OPT_SUG_ALL);
export const OPT_TRANS_GOOGLE = "Google";
export const OPT_TRANS_GOOGLE_2 = "Google2";
@@ -63,9 +71,6 @@ export const OPT_LANGDETECTOR_ALL = [
OPT_TRANS_TENCENT,
];
export const OPT_DICT_ALL = [OPT_TRANS_BAIDU];
export const OPT_DICT_MAP = new Set(OPT_DICT_ALL);
// 翻译引擎特殊集合
export const API_SPE_TYPES = {
// 内置翻译

View File

@@ -1148,6 +1148,11 @@ export const I18N = {
en: `Translate Blacklist`,
zh_TW: `停用翻譯名單`,
},
disabled_orilist: {
zh: `禁用Origin名单`,
en: `Disabled Origin List`,
zh_TW: `停用 Origin 名單`,
},
disabled_csplist: {
zh: `禁用CSP名单`,
en: `Disabled CSP List`,
@@ -1323,6 +1328,11 @@ export const I18N = {
en: `English Dictionary`,
zh_TW: `英文字典`,
},
english_suggest: {
zh: `英文建议`,
en: `English Suggest`,
zh_TW: `英文建議`,
},
api_name: {
zh: `接口名称`,
en: `API Name`,

View File

@@ -1,5 +1,6 @@
import {
OPT_DICT_BAIDU,
OPT_SUG_BAIDU,
DEFAULT_HTTP_TIMEOUT,
OPT_TRANS_MICROSOFT,
DEFAULT_API_LIST,
@@ -28,6 +29,7 @@ export const DEFAULT_BLACKLIST = [
"login.dingtalk.com",
]; // 禁用翻译名单
export const DEFAULT_CSPLIST = ["https://github.com"]; // 禁用CSP名单
export const DEFAULT_ORILIST = ["https://dict.youdao.com"]; // 移除Origin名单
// 同步设置
export const OPT_SYNCTYPE_WORKER = "KISS-Worker";
@@ -90,6 +92,7 @@ export const DEFAULT_TRANBOX_SETTING = {
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
// extStyles: "", // 附加样式
enDict: OPT_DICT_BAIDU, // 英文词典
enSug: OPT_SUG_BAIDU, // 英文建议
};
// 订阅列表
@@ -143,6 +146,7 @@ export const DEFAULT_SETTING = {
touchTranslate: 2, // 触屏翻译
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
orilist: DEFAULT_ORILIST.join(",\n"), // 禁用CSP名单
// disableLangs: [], // 不翻译的语言(移至rule作废)
skipLangs: [], // 不翻译的语言从rule移回
transInterval: 100, // 翻译等待时间

View File

@@ -1,40 +1,152 @@
import { useEffect, useState } from "react";
import { useEffect, useState, useCallback } from "react";
/**
* fetch data hook
* @returns
*/
export const useFetch = (url) => {
export const useAsync = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (!url) {
const execute = useCallback(async (fn, ...args) => {
if (!fn) {
return;
}
(async () => {
setLoading(true);
try {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`[${res.status}] ${res.statusText}`);
}
let data;
if (res.headers.get("Content-Type")?.includes("json")) {
data = await res.json();
} else {
data = await res.text();
}
setData(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
})();
}, [url]);
setLoading(true);
setError(null);
return [data, loading, error];
try {
const res = await fn(...args);
setData(res);
setLoading(false);
return res;
} catch (err) {
setError(err?.message || "An unknown error occurred");
setLoading(false);
// throw err;
}
}, []);
const reset = useCallback(() => {
setData(null);
setLoading(false);
setError(null);
}, []);
return { data, loading, error, execute, reset };
};
export const useAsyncNow = (fn, arg) => {
const { execute, ...asyncState } = useAsync();
useEffect(() => {
if (fn) {
execute(fn, arg);
}
}, [execute, fn, arg]);
return { ...asyncState };
};
export const useFetch = () => {
const { execute, ...asyncState } = useAsync();
const requester = useCallback(async (url, options) => {
const response = await fetch(url, options);
if (!response.ok) {
const errorInfo = await response.text();
throw new Error(
`Request failed: ${response.status} ${response.statusText} - ${errorInfo}`
);
}
if (response.status === 204) {
return null;
}
if (response.headers.get("Content-Type")?.includes("json")) {
return response.json();
}
return response.text();
}, []);
const get = useCallback(
async (url, options = {}) => {
try {
const result = await execute(requester, url, {
...options,
method: "GET",
});
return result;
} catch (err) {
return null;
}
},
[execute, requester]
);
const post = useCallback(
async (url, body, options = {}) => {
try {
const result = await execute(requester, url, {
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options.headers },
body: JSON.stringify(body),
});
return result;
} catch (err) {
return null;
}
},
[execute, requester]
);
const put = useCallback(
async (url, body, options = {}) => {
try {
const result = await execute(requester, url, {
...options,
method: "PUT",
headers: { "Content-Type": "application/json", ...options.headers },
body: JSON.stringify(body),
});
return result;
} catch (err) {
return null;
}
},
[execute, requester]
);
const del = useCallback(
async (url, options = {}) => {
try {
const result = await execute(requester, url, {
...options,
method: "DELETE",
});
return result;
} catch (err) {
return null;
}
},
[execute, requester]
);
return {
...asyncState,
get,
post,
put,
del,
};
};
export const useGet = (url) => {
const { get, ...fetchState } = useFetch();
useEffect(() => {
if (url) get(url);
}, [url, get]);
return { ...fetchState };
};

View File

@@ -1,6 +1,6 @@
import { useSetting } from "./Setting";
import { I18N, URL_RAW_PREFIX } from "../config";
import { useFetch } from "./Fetch";
import { useGet } from "./Fetch";
export const getI18n = (uiLang, key, defaultText = "") => {
return I18N?.[key]?.[uiLang] ?? defaultText;
@@ -25,5 +25,5 @@ export const useI18nMd = (key) => {
const i18n = useI18n();
const fileName = i18n(key);
const url = fileName ? `${URL_RAW_PREFIX}/${fileName}` : "";
return useFetch(url);
return useGet(url);
};

View File

@@ -7,14 +7,15 @@ import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Button from "@mui/material/Button";
import Link from "@mui/material/Link";
import { useFetch } from "./hooks/Fetch";
import { useGet } from "./hooks/Fetch";
import { I18N, URL_RAW_PREFIX } from "./config";
function App() {
const [lang, setLang] = useState("zh");
const [data, loading, error] = useFetch(
const { data, loading, error } = useGet(
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}`
);
return (
<Paper sx={{ padding: 2, margin: 2 }}>
<Stack spacing={2} direction="row" justifyContent="flex-end">
@@ -47,7 +48,7 @@ function App() {
<CircularProgress />
</center>
) : (
<ReactMarkdown children={error ? error.message : data} />
<ReactMarkdown children={error || data} />
)}
</Paper>
);

View File

@@ -5,7 +5,7 @@ import { useI18n, useI18nMd } from "../../hooks/I18n";
export default function About() {
const i18n = useI18n();
const [data, loading, error] = useI18nMd("about_md");
const { data, loading, error } = useI18nMd("about_md");
return (
<Box>
{loading ? (

View File

@@ -21,9 +21,12 @@ import { kissLog } from "../../libs/log";
import { apiTranslate } from "../../apis";
import { OPT_TRANS_BAIDU, PHONIC_MAP } from "../../config";
import { useConfirm } from "../../hooks/Confirm";
import { useSetting } from "../../hooks/Setting";
function FavAccordion({ word, index }) {
const [expanded, setExpanded] = useState(false);
const { setting } = useSetting();
const { enDict, enSug } = setting?.tranboxSetting || {};
const handleChange = (e) => {
setExpanded((pre) => !pre);
@@ -40,8 +43,8 @@ function FavAccordion({ word, index }) {
<AccordionDetails>
{expanded && (
<Stack spacing={2}>
<DictCont text={word} />
<SugCont text={word} />
<DictCont text={word} enDict={enDict} />
<SugCont text={word} enSug={enSug} />
</Stack>
)}
</AccordionDetails>

View File

@@ -8,7 +8,7 @@ export default function Playgound() {
const { setting } = useSetting();
const { transApis, langDetector, tranboxSetting } =
setting || DEFAULT_SETTING;
const { apiSlugs, fromLang, toLang, toLang2, enDict } =
const { apiSlugs, fromLang, toLang, toLang2, enDict, enSug } =
tranboxSetting || DEFAULT_TRANBOX_SETTING;
return (
<TranForm
@@ -22,6 +22,7 @@ export default function Playgound() {
simpleStyle={false}
langDetector={langDetector}
enDict={enDict}
enSug={enSug}
isPlaygound={true}
/>
);

View File

@@ -20,6 +20,7 @@ import {
OPT_SHORTCUT_SETTING,
DEFAULT_BLACKLIST,
DEFAULT_CSPLIST,
DEFAULT_ORILIST,
MSG_CONTEXT_MENUS,
MSG_UPDATE_CSP,
DEFAULT_HTTP_TIMEOUT,
@@ -79,7 +80,10 @@ export default function Settings() {
isExt && sendBgMsg(MSG_CONTEXT_MENUS, value);
break;
case "csplist":
isExt && sendBgMsg(MSG_UPDATE_CSP, value);
isExt && sendBgMsg(MSG_UPDATE_CSP, { csplist: value });
break;
case "orilist":
isExt && sendBgMsg(MSG_UPDATE_CSP, { orilist: value });
break;
default:
}
@@ -116,6 +120,7 @@ export default function Settings() {
touchTranslate = 2,
blacklist = DEFAULT_BLACKLIST.join(",\n"),
csplist = DEFAULT_CSPLIST.join(",\n"),
orilist = DEFAULT_ORILIST.join(",\n"),
transInterval = 100,
langDetector = "-",
preInit = true,
@@ -399,6 +404,15 @@ export default function Settings() {
onChange={handleChange}
multiline
/>
<TextField
size="small"
label={i18n("disabled_orilist")}
helperText={i18n("pattern_helper")}
name="orilist"
value={orilist}
onChange={handleChange}
multiline
/>
</>
) : (
<>

View File

@@ -11,6 +11,8 @@ import {
OPT_TRANBOX_TRIGGER_ALL,
OPT_DICT_BAIDU,
OPT_DICT_ALL,
OPT_SUG_ALL,
OPT_SUG_BAIDU,
} from "../../config";
import ShortcutInput from "./ShortcutInput";
import FormControlLabel from "@mui/material/FormControlLabel";
@@ -68,6 +70,7 @@ export default function Tranbox() {
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
// extStyles = "",
enDict = OPT_DICT_BAIDU,
enSug = OPT_SUG_BAIDU,
} = tranboxSetting;
return (
@@ -89,7 +92,7 @@ export default function Tranbox() {
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={12} md={12} lg={6}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
fullWidth
@@ -181,6 +184,24 @@ export default function Tranbox() {
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
fullWidth
select
size="small"
name="enSug"
value={enSug}
label={i18n("english_suggest")}
onChange={handleChange}
>
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
{OPT_SUG_ALL.map((item) => (
<MenuItem value={item} key={item}>
{item}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
fullWidth

View File

@@ -1,81 +1,40 @@
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect } from "react";
import Stack from "@mui/material/Stack";
import FavBtn from "./FavBtn";
import Typography from "@mui/material/Typography";
import AudioBtn from "./AudioBtn";
import CircularProgress from "@mui/material/CircularProgress";
import Divider from "@mui/material/Divider";
import Alert from "@mui/material/Alert";
import { OPT_TRANS_BAIDU, PHONIC_MAP } from "../../config";
import { apiTranslate } from "../../apis";
import { isValidWord } from "../../libs/utils";
import { OPT_DICT_BAIDU, OPT_DICT_YOUDAO, PHONIC_MAP } from "../../config";
import CopyBtn from "./CopyBtn";
import { useAsyncNow } from "../../hooks/Fetch";
import { apiYoudaoDict } from "../../apis";
function DictBaidu({ text, setCopyText }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [dictResult, setDictResult] = useState(null);
// useEffect(() => {
// if (!data) {
// return;
// }
useEffect(() => {
(async () => {
try {
setLoading(true);
setError("");
setDictResult(null);
// const copyText = [
// data.src,
// data.voice
// ?.map(Object.entries)
// .map((item) => item[0])
// .map(([key, val]) => `${PHONIC_MAP[key]?.[0] || key} ${val}`)
// .join(" "),
// data.content[0].mean
// .map(({ pre, cont }) => {
// return `${pre ? `[${pre}] ` : ""}${Object.keys(cont).join("; ")}`;
// })
// .join("\n"),
// ].join("\n");
// if (!isValidWord(text)) {
// return;
// }
// setCopyText(copyText);
// }, [data, setCopyText]);
// // todo: 修复
// const dictRes = await apiTranslate({
// text,
// apiSlug: OPT_TRANS_BAIDU,
// fromLang: "en",
// toLang: "zh-CN",
// });
// if (dictRes[2]?.type === 1) {
// setDictResult(JSON.parse(dictRes[2].result));
// }
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
})();
}, [text]);
useEffect(() => {
if (!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");
setCopyText(copyText);
}, [dictResult, setCopyText]);
if (loading) {
return <CircularProgress size={16} />;
}
if (error) {
return <Alert severity="error">{error}</Alert>;
}
return <Typography>baidu: {text}</Typography>;
return <Typography>baidu dict not supported yet</Typography>;
{
/* {dictResult && (
@@ -109,11 +68,56 @@ function DictBaidu({ text, setCopyText }) {
}
}
function DictYoudao({ text, setCopyText }) {
const { loading, error, data } = useAsyncNow(apiYoudaoDict, text);
useEffect(() => {
if (!data) {
return;
}
const copyText = [
text,
data?.ec?.word?.trs
?.map(({ pos, tran }) => `${pos ? `[${pos}] ` : ""}${tran}`)
.join("\n"),
].join("\n");
setCopyText(copyText);
}, [data, setCopyText]);
if (loading) {
return <CircularProgress size={16} />;
}
if (error) {
return <Alert severity="error">{error}</Alert>;
}
if (!data) {
return;
}
return (
<Typography component="div">
<Typography component="ul">
{data?.ec?.word?.trs?.map(({ pos, tran }, idx) => (
<Typography component="li" key={idx}>
{pos && `[${pos}] `}
{tran}
</Typography>
))}
</Typography>
</Typography>
);
}
export default function DictCont({ text, enDict }) {
const [copyText, setCopyText] = useState(text);
const dictMap = {
[OPT_TRANS_BAIDU]: <DictBaidu text={text} setCopyText={setCopyText} />,
[OPT_DICT_BAIDU]: <DictBaidu text={text} setCopyText={setCopyText} />,
[OPT_DICT_YOUDAO]: <DictYoudao text={text} setCopyText={setCopyText} />,
};
return (
@@ -130,6 +134,8 @@ export default function DictCont({ text, enDict }) {
</Stack>
)}
<Divider />
{dictMap[enDict] || <Typography>Dict not support</Typography>}
</Stack>
);

View File

@@ -1,28 +1,30 @@
import { useState, useEffect } from "react";
import Typography from "@mui/material/Typography";
import { apiBaiduSuggest } from "../../apis";
import CircularProgress from "@mui/material/CircularProgress";
import Divider from "@mui/material/Divider";
import Alert from "@mui/material/Alert";
import { apiBaiduSuggest, apiYoudaoSuggest } from "../../apis";
import Stack from "@mui/material/Stack";
import { OPT_SUG_BAIDU, OPT_SUG_YOUDAO } from "../../config";
import { useAsyncNow } from "../../hooks/Fetch";
export default function SugCont({ text }) {
const [sugs, setSugs] = useState([]);
function SugBaidu({ text }) {
const { loading, error, data } = useAsyncNow(apiBaiduSuggest, text);
useEffect(() => {
(async () => {
try {
setSugs(await apiBaiduSuggest(text));
} catch (err) {
// skip
}
})();
}, [text]);
if (loading) {
return <CircularProgress size={16} />;
}
if (sugs.length === 0) {
return;
if (error) {
return <Alert severity="error">{error}</Alert>;
}
if (!data) {
return null;
}
return (
<Stack spacing={1}>
{sugs.map(({ k, v }) => (
<>
{data.map(({ k, v }) => (
<Typography component="div" key={k}>
<Typography>{k}</Typography>
<Typography component="ul" style={{ margin: "0" }}>
@@ -30,6 +32,49 @@ export default function SugCont({ text }) {
</Typography>
</Typography>
))}
</>
);
}
function SugYoudao({ text }) {
const { loading, error, data } = useAsyncNow(apiYoudaoSuggest, text);
if (loading) {
return <CircularProgress size={16} />;
}
if (error) {
return <Alert severity="error">{error}</Alert>;
}
if (!data) {
return null;
}
return (
<>
{data.map(({ entry, explain }) => (
<Typography component="div" key={entry}>
<Typography>{entry}</Typography>
<Typography component="ul" style={{ margin: "0" }}>
<Typography component="li">{explain}</Typography>
</Typography>
</Typography>
))}
</>
);
}
export default function SugCont({ text, enSug }) {
const sugMap = {
[OPT_SUG_BAIDU]: <SugBaidu text={text} />,
[OPT_SUG_YOUDAO]: <SugYoudao text={text} />,
};
return (
<Stack spacing={1}>
<Divider />
{sugMap[enSug] || <Typography>Sug not support</Typography>}
</Stack>
);
}

View File

@@ -114,7 +114,7 @@ export default function TranBox({
text,
setText,
setShowBox,
tranboxSetting: { apiSlugs, fromLang, toLang, toLang2 },
tranboxSetting: { enDict, enSug, apiSlugs, fromLang, toLang, toLang2 },
transApis,
boxSize,
setBoxSize,
@@ -128,7 +128,6 @@ export default function TranBox({
setFollowSelection,
extStyles = "",
langDetector,
enDict,
}) {
const [mouseHover, setMouseHover] = useState(false);
// todo: 这里的 SettingProvider 不应和 background 的共用
@@ -168,6 +167,7 @@ export default function TranBox({
simpleStyle={simpleStyle}
langDetector={langDetector}
enDict={enDict}
enSug={enSug}
/>
</Box>
</DraggableResizable>

View File

@@ -12,8 +12,10 @@ import {
OPT_LANGS_TO,
OPT_LANGDETECTOR_ALL,
OPT_DICT_ALL,
OPT_SUG_ALL,
OPT_LANGS_MAP,
OPT_DICT_MAP,
OPT_SUG_MAP,
} from "../../config";
import { useState, useMemo, useEffect } from "react";
import TranCont from "./TranCont";
@@ -30,11 +32,12 @@ export default function TranForm({
apiSlugs: initApiSlugs,
fromLang: initFromLang,
toLang: initToLang,
toLang2,
toLang2: initToLang2,
transApis,
simpleStyle = false,
langDetector: initLangDetector = "-",
enDict: initEnDict = "-",
enSug: initEnSug = "-",
isPlaygound = false,
}) {
const i18n = useI18n();
@@ -44,11 +47,19 @@ export default function TranForm({
const [apiSlugs, setApiSlugs] = useState(initApiSlugs);
const [fromLang, setFromLang] = useState(initFromLang);
const [toLang, setToLang] = useState(initToLang);
const [toLang2, setToLang2] = useState(initToLang2);
const [langDetector, setLangDetector] = useState(initLangDetector);
const [enDict, setEnDict] = useState(initEnDict);
const [enSug, setEnSug] = useState(initEnSug);
const [deLang, setDeLang] = useState("");
const [deLoading, setDeLoading] = useState(false);
useEffect(() => {
if (!editMode) {
setEditText(text);
}
}, [text, editMode]);
useEffect(() => {
if (!text.trim()) {
setDeLang("");
@@ -96,6 +107,7 @@ export default function TranForm({
);
const isWord = useMemo(() => isValidWord(text), [text]);
const xs = useMemo(() => (isPlaygound ? 3 : 4), [isPlaygound]);
return (
<Stack spacing={simpleStyle ? 1 : 2}>
@@ -103,18 +115,18 @@ export default function TranForm({
<>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={4} sm={4} md={4} lg={4}>
<Grid item xs={xs}>
<TextField
select
SelectProps={{
multiple: true,
MenuProps: { disablePortal: true },
MenuProps: { disablePortal: !isPlaygound },
}}
fullWidth
size="small"
value={apiSlugs}
name="apiSlugs"
label={i18n("translate_service")}
label={i18n("translate_service_multiple")}
onChange={(e) => {
setApiSlugs(e.target.value);
}}
@@ -126,10 +138,10 @@ export default function TranForm({
))}
</TextField>
</Grid>
<Grid item xs={4} sm={4} md={4} lg={4}>
<Grid item xs={xs}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
SelectProps={{ MenuProps: { disablePortal: !isPlaygound } }}
fullWidth
size="small"
name="fromLang"
@@ -146,10 +158,10 @@ export default function TranForm({
))}
</TextField>
</Grid>
<Grid item xs={4} sm={4} md={4} lg={4}>
<Grid item xs={xs}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
SelectProps={{ MenuProps: { disablePortal: !isPlaygound } }}
fullWidth
size="small"
name="toLang"
@@ -166,73 +178,120 @@ export default function TranForm({
))}
</TextField>
</Grid>
{isPlaygound && (
<>
<Grid item xs={xs}>
<TextField
select
SelectProps={{
MenuProps: { disablePortal: !isPlaygound },
}}
fullWidth
size="small"
name="toLang2"
value={toLang2}
label={i18n("to_lang2")}
onChange={(e) => {
setToLang2(e.target.value);
}}
>
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={xs}>
<TextField
select
SelectProps={{
MenuProps: { disablePortal: !isPlaygound },
}}
fullWidth
size="small"
name="enDict"
value={enDict}
label={i18n("english_dict")}
onChange={(e) => {
setEnDict(e.target.value);
}}
>
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
{OPT_DICT_ALL.map((item) => (
<MenuItem value={item} key={item}>
{item}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={xs}>
<TextField
select
SelectProps={{
MenuProps: { disablePortal: !isPlaygound },
}}
fullWidth
size="small"
name="enSug"
value={enSug}
label={i18n("english_suggest")}
onChange={(e) => {
setEnSug(e.target.value);
}}
>
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
{OPT_SUG_ALL.map((item) => (
<MenuItem value={item} key={item}>
{item}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={xs}>
<TextField
select
SelectProps={{
MenuProps: { disablePortal: !isPlaygound },
}}
fullWidth
size="small"
name="langDetector"
value={langDetector}
label={i18n("detected_lang")}
onChange={(e) => {
setLangDetector(e.target.value);
}}
>
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
{OPT_LANGDETECTOR_ALL.map((item) => (
<MenuItem value={item} key={item}>
{item}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={xs}>
<TextField
fullWidth
size="small"
name="deLang"
value={deLang && OPT_LANGS_MAP.get(deLang)}
label={i18n("detected_result")}
disabled
InputProps={{
startAdornment: deLoading ? (
<CircularProgress size={16} />
) : null,
}}
/>
</Grid>
</>
)}
</Grid>
</Box>
{isPlaygound && (
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
name="enDict"
value={enDict}
label={i18n("english_dict")}
onChange={(e) => {
setEnDict(e.target.value);
}}
>
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
{OPT_DICT_ALL.map((item) => (
<MenuItem value={item} key={item}>
{item}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
name="langDetector"
value={langDetector}
label={i18n("detected_lang")}
onChange={(e) => {
setLangDetector(e.target.value);
}}
>
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
{OPT_LANGDETECTOR_ALL.map((item) => (
<MenuItem value={item} key={item}>
{item}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
fullWidth
size="small"
name="deLang"
value={deLang && OPT_LANGS_MAP.get(deLang)}
label={i18n("detected_result")}
disabled
InputProps={{
startAdornment: deLoading ? (
<CircularProgress size={16} />
) : null,
}}
/>
</Grid>
</Grid>
</Box>
)}
<Box>
<TextField
size="small"
@@ -267,6 +326,8 @@ export default function TranForm({
size="small"
onClick={(e) => {
e.stopPropagation();
setEditMode(false);
setText(editText.trim());
}}
>
<DoneIcon fontSize="inherit" />
@@ -295,10 +356,11 @@ export default function TranForm({
))}
{isWord && OPT_DICT_MAP.has(enDict) && (
<>
<DictCont text={text} enDict={enDict} />
<SugCont text={text} />
</>
<DictCont text={text} enDict={enDict} />
)}
{isWord && OPT_SUG_MAP.has(enSug) && (
<SugCont text={text} enSug={enSug} />
)}
</Stack>
);

View File

@@ -10,7 +10,6 @@ import {
OPT_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_HOVER,
OPT_TRANBOX_TRIGGER_SELECT,
OPT_DICT_BAIDU,
} from "../../config";
import { isMobile } from "../../libs/mobile";
import { kissLog } from "../../libs/log";
@@ -35,7 +34,6 @@ export default function Slection({
btnOffsetY,
boxOffsetX = 0,
boxOffsetY = 10,
enDict = OPT_DICT_BAIDU,
} = tranboxSetting;
const boxWidth =
@@ -238,7 +236,6 @@ export default function Slection({
setFollowSelection={setFollowSelection}
// extStyles={extStyles}
langDetector={langDetector}
enDict={enDict}
/>
)}