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__" "description": "__MSG_open_options__"
} }
}, },
"permissions": ["storage", "contextMenus", "scripting", "declarativeNetRequest"], "permissions": ["storage", "contextMenus", "scripting", "declarativeNetRequest", "declarativeNetRequestWithHostAccess"],
"host_permissions": ["<all_urls>"], "host_permissions": ["<all_urls>"],
"icons": { "icons": {
"16": "images/logo16.png", "16": "images/logo16.png",

View File

@@ -159,6 +159,77 @@ export const apiBaiduSuggest = async (text) => {
return []; 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 * @param {*} text

View File

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

View File

@@ -14,6 +14,14 @@ export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符 export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
export const OPT_DICT_BAIDU = "Baidu"; 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 = "Google";
export const OPT_TRANS_GOOGLE_2 = "Google2"; export const OPT_TRANS_GOOGLE_2 = "Google2";
@@ -63,9 +71,6 @@ export const OPT_LANGDETECTOR_ALL = [
OPT_TRANS_TENCENT, 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 = { export const API_SPE_TYPES = {
// 内置翻译 // 内置翻译

View File

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

View File

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

View File

@@ -1,40 +1,152 @@
import { useEffect, useState } from "react"; import { useEffect, useState, useCallback } from "react";
/** export const useAsync = () => {
* fetch data hook
* @returns
*/
export const useFetch = (url) => {
const [data, setData] = useState(null); const [data, setData] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
useEffect(() => { const execute = useCallback(async (fn, ...args) => {
if (!url) { if (!fn) {
return; return;
} }
(async () => { setLoading(true);
setLoading(true); setError(null);
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]);
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 { useSetting } from "./Setting";
import { I18N, URL_RAW_PREFIX } from "../config"; import { I18N, URL_RAW_PREFIX } from "../config";
import { useFetch } from "./Fetch"; import { useGet } from "./Fetch";
export const getI18n = (uiLang, key, defaultText = "") => { export const getI18n = (uiLang, key, defaultText = "") => {
return I18N?.[key]?.[uiLang] ?? defaultText; return I18N?.[key]?.[uiLang] ?? defaultText;
@@ -25,5 +25,5 @@ export const useI18nMd = (key) => {
const i18n = useI18n(); const i18n = useI18n();
const fileName = i18n(key); const fileName = i18n(key);
const url = fileName ? `${URL_RAW_PREFIX}/${fileName}` : ""; 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 Stack from "@mui/material/Stack";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import { useFetch } from "./hooks/Fetch"; import { useGet } from "./hooks/Fetch";
import { I18N, URL_RAW_PREFIX } from "./config"; import { I18N, URL_RAW_PREFIX } from "./config";
function App() { function App() {
const [lang, setLang] = useState("zh"); const [lang, setLang] = useState("zh");
const [data, loading, error] = useFetch( const { data, loading, error } = useGet(
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}` `${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}`
); );
return ( return (
<Paper sx={{ padding: 2, margin: 2 }}> <Paper sx={{ padding: 2, margin: 2 }}>
<Stack spacing={2} direction="row" justifyContent="flex-end"> <Stack spacing={2} direction="row" justifyContent="flex-end">
@@ -47,7 +48,7 @@ function App() {
<CircularProgress /> <CircularProgress />
</center> </center>
) : ( ) : (
<ReactMarkdown children={error ? error.message : data} /> <ReactMarkdown children={error || data} />
)} )}
</Paper> </Paper>
); );

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ import {
OPT_SHORTCUT_SETTING, OPT_SHORTCUT_SETTING,
DEFAULT_BLACKLIST, DEFAULT_BLACKLIST,
DEFAULT_CSPLIST, DEFAULT_CSPLIST,
DEFAULT_ORILIST,
MSG_CONTEXT_MENUS, MSG_CONTEXT_MENUS,
MSG_UPDATE_CSP, MSG_UPDATE_CSP,
DEFAULT_HTTP_TIMEOUT, DEFAULT_HTTP_TIMEOUT,
@@ -79,7 +80,10 @@ export default function Settings() {
isExt && sendBgMsg(MSG_CONTEXT_MENUS, value); isExt && sendBgMsg(MSG_CONTEXT_MENUS, value);
break; break;
case "csplist": 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; break;
default: default:
} }
@@ -116,6 +120,7 @@ export default function Settings() {
touchTranslate = 2, touchTranslate = 2,
blacklist = DEFAULT_BLACKLIST.join(",\n"), blacklist = DEFAULT_BLACKLIST.join(",\n"),
csplist = DEFAULT_CSPLIST.join(",\n"), csplist = DEFAULT_CSPLIST.join(",\n"),
orilist = DEFAULT_ORILIST.join(",\n"),
transInterval = 100, transInterval = 100,
langDetector = "-", langDetector = "-",
preInit = true, preInit = true,
@@ -399,6 +404,15 @@ export default function Settings() {
onChange={handleChange} onChange={handleChange}
multiline 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_TRANBOX_TRIGGER_ALL,
OPT_DICT_BAIDU, OPT_DICT_BAIDU,
OPT_DICT_ALL, OPT_DICT_ALL,
OPT_SUG_ALL,
OPT_SUG_BAIDU,
} from "../../config"; } from "../../config";
import ShortcutInput from "./ShortcutInput"; import ShortcutInput from "./ShortcutInput";
import FormControlLabel from "@mui/material/FormControlLabel"; import FormControlLabel from "@mui/material/FormControlLabel";
@@ -68,6 +70,7 @@ export default function Tranbox() {
triggerMode = OPT_TRANBOX_TRIGGER_CLICK, triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
// extStyles = "", // extStyles = "",
enDict = OPT_DICT_BAIDU, enDict = OPT_DICT_BAIDU,
enSug = OPT_SUG_BAIDU,
} = tranboxSetting; } = tranboxSetting;
return ( return (
@@ -89,7 +92,7 @@ export default function Tranbox() {
<Box> <Box>
<Grid container spacing={2} columns={12}> <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 <TextField
select select
fullWidth fullWidth
@@ -181,6 +184,24 @@ export default function Tranbox() {
))} ))}
</TextField> </TextField>
</Grid> </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}> <Grid item xs={12} sm={12} md={6} lg={3}>
<TextField <TextField
fullWidth 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 Stack from "@mui/material/Stack";
import FavBtn from "./FavBtn"; import FavBtn from "./FavBtn";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import AudioBtn from "./AudioBtn"; import AudioBtn from "./AudioBtn";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import Divider from "@mui/material/Divider";
import Alert from "@mui/material/Alert"; import Alert from "@mui/material/Alert";
import { OPT_TRANS_BAIDU, PHONIC_MAP } from "../../config"; import { OPT_DICT_BAIDU, OPT_DICT_YOUDAO, PHONIC_MAP } from "../../config";
import { apiTranslate } from "../../apis";
import { isValidWord } from "../../libs/utils";
import CopyBtn from "./CopyBtn"; import CopyBtn from "./CopyBtn";
import { useAsyncNow } from "../../hooks/Fetch";
import { apiYoudaoDict } from "../../apis";
function DictBaidu({ text, setCopyText }) { function DictBaidu({ text, setCopyText }) {
const [loading, setLoading] = useState(true); // useEffect(() => {
const [error, setError] = useState(""); // if (!data) {
const [dictResult, setDictResult] = useState(null); // return;
// }
useEffect(() => { // const copyText = [
(async () => { // data.src,
try { // data.voice
setLoading(true); // ?.map(Object.entries)
setError(""); // .map((item) => item[0])
setDictResult(null); // .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)) { // setCopyText(copyText);
// return; // }, [data, setCopyText]);
// }
// // todo: 修复 return <Typography>baidu dict not supported yet</Typography>;
// 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>;
{ {
/* {dictResult && ( /* {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 }) { export default function DictCont({ text, enDict }) {
const [copyText, setCopyText] = useState(text); const [copyText, setCopyText] = useState(text);
const dictMap = { 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 ( return (
@@ -130,6 +134,8 @@ export default function DictCont({ text, enDict }) {
</Stack> </Stack>
)} )}
<Divider />
{dictMap[enDict] || <Typography>Dict not support</Typography>} {dictMap[enDict] || <Typography>Dict not support</Typography>}
</Stack> </Stack>
); );

View File

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

View File

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

View File

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

View File

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