fix: api hooks

This commit is contained in:
Gabe
2025-09-25 23:08:39 +08:00
parent 533a0e2d5b
commit 6b9a1a49bb
12 changed files with 523 additions and 444 deletions

View File

@@ -214,10 +214,9 @@ export const apiTranslate = async ({
}
const { apiType, apiSlug, useBatchFetch } = apiSetting;
const from =
OPT_LANGS_SPECIAL[apiType].get(fromLang) ??
OPT_LANGS_SPECIAL[apiType].get("auto");
const to = OPT_LANGS_SPECIAL[apiType].get(toLang);
const langMap = OPT_LANGS_SPECIAL[apiType];
const from = langMap.get(fromLang) ?? langMap.get("auto");
const to = langMap.get(toLang);
if (!to) {
kissLog(`target lang: ${toLang} not support`);
return ["", false];
@@ -249,6 +248,9 @@ export const apiTranslate = async ({
const queue = getBatchQueue({
from,
to,
fromLang,
toLang,
langMap,
docInfo,
apiSetting,
usePool,
@@ -257,22 +259,32 @@ export const apiTranslate = async ({
const tranlation = await queue.addTask({ text });
if (Array.isArray(tranlation)) {
[trText, srLang = ""] = tranlation;
} else if (typeof tranlation === "string") {
trText = tranlation;
}
} else {
const translations = await handleTranslate({
texts: [text],
from,
to,
fromLang,
toLang,
langMap,
docInfo,
apiSetting,
usePool,
});
if (Array.isArray(translations?.[0])) {
[trText, srLang = ""] = translations[0];
if (Array.isArray(translations)) {
if (Array.isArray(translations[0])) {
[trText, srLang = ""] = translations[0];
} else {
[trText, srLang = ""] = translations;
}
}
}
const isSame = srLang && (to.includes(srLang) || srLang.includes(to));
// const isSame = srLang && (to.includes(srLang) || srLang.includes(to));
const isSame = srLang && srLang.slice(0, 2) === to.slice(0, 2);
// 插入缓存
if (useCache && trText) {

View File

@@ -111,7 +111,7 @@ const parseAIRes = (raw) => {
};
const genGoogle = ({ texts, from, to, url, key }) => {
const params = {
const params = queryString.stringify({
client: "gtx",
dt: "t",
dj: 1,
@@ -119,52 +119,42 @@ const genGoogle = ({ texts, from, to, url, key }) => {
sl: from,
tl: to,
q: texts.join(" "),
};
const input = `${url}?${queryString.stringify(params)}`;
const init = {
headers: {
"Content-type": "application/json",
},
});
url = `${url}?${params}`;
const headers = {
"Content-type": "application/json",
};
if (key) {
init.headers.Authorization = `Bearer ${key}`;
headers.Authorization = `Bearer ${key}`;
}
return [input, init];
return { url, headers, method: "GET" };
};
const genGoogle2 = ({ texts, from, to, url, key }) => {
const body = JSON.stringify([[texts, from, to], "wt_lib"]);
const init = {
method: "POST",
headers: {
"Content-Type": "application/json+protobuf",
"X-Goog-API-Key": key,
},
body,
const data = [[texts, from, to], "wt_lib"];
const headers = {
"Content-Type": "application/json+protobuf",
"X-Goog-API-Key": key,
};
return [url, init];
return { url, data, headers };
};
const genMicrosoft = async ({ texts, from, to }) => {
const [token] = await msAuth();
const params = {
const genMicrosoft = ({ texts, from, to, token }) => {
const params = queryString.stringify({
from,
to,
"api-version": "3.0",
});
const url = `https://api-edge.cognitive.microsofttranslator.com/translate?${params}`;
const headers = {
"Content-type": "application/json",
Authorization: `Bearer ${token}`,
};
const input = `https://api-edge.cognitive.microsofttranslator.com/translate?${queryString.stringify(params)}`;
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${token}`,
},
method: "POST",
body: JSON.stringify(texts.map((text) => ({ Text: text }))),
};
const data = texts.map((text) => ({ Text: text }));
return [input, init];
return { url, data, headers };
};
const genDeepl = ({ texts, from, to, url, key }) => {
@@ -174,16 +164,12 @@ const genDeepl = ({ texts, from, to, url, key }) => {
source_lang: from,
// split_sentences: "0",
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `DeepL-Auth-Key ${key}`,
},
method: "POST",
body: JSON.stringify(data),
const headers = {
"Content-type": "application/json",
Authorization: `DeepL-Auth-Key ${key}`,
};
return [url, init];
return { url, data, headers };
};
const genDeeplX = ({ texts, from, to, url, key }) => {
@@ -193,18 +179,14 @@ const genDeeplX = ({ texts, from, to, url, key }) => {
source_lang: from,
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
const headers = {
"Content-type": "application/json",
};
if (key) {
init.headers.Authorization = `Bearer ${key}`;
headers.Authorization = `Bearer ${key}`;
}
return [url, init];
return { url, data, headers };
};
const genNiuTrans = ({ texts, from, to, url, key, dictNo, memoryNo }) => {
@@ -217,15 +199,11 @@ const genNiuTrans = ({ texts, from, to, url, key, dictNo, memoryNo }) => {
memoryNo,
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
const headers = {
"Content-type": "application/json",
};
return [url, init];
return { url, data, headers };
};
const genTencent = ({ texts, from, to }) => {
@@ -246,19 +224,15 @@ const genTencent = ({ texts, from, to }) => {
},
};
const input = "https://transmart.qq.com/api/imt";
const init = {
headers: {
"Content-Type": "application/json",
"user-agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
referer: "https://transmart.qq.com/zh-CN/index",
},
method: "POST",
body: JSON.stringify(data),
const url = "https://transmart.qq.com/api/imt";
const headers = {
"Content-Type": "application/json",
"user-agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
referer: "https://transmart.qq.com/zh-CN/index",
};
return [input, init];
return { url, data, headers };
};
const genVolcengine = ({ texts, from, to }) => {
@@ -268,22 +242,15 @@ const genVolcengine = ({ texts, from, to }) => {
text: texts.join(" "),
};
const input = "https://translate.volcengine.com/crx/translate/v1";
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
const url = "https://translate.volcengine.com/crx/translate/v1";
const headers = {
"Content-type": "application/json",
};
return [input, init];
return { url, data, headers };
};
const genOpenAI = ({
texts,
from,
to,
url,
key,
systemPrompt,
@@ -291,16 +258,8 @@ const genOpenAI = ({
model,
temperature,
maxTokens,
customHeader,
customBody,
docInfo,
hisMsgs,
}) => {
systemPrompt = genSystemPrompt({ systemPrompt, from, to });
userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo });
customHeader = parseJsonObj(customHeader);
customBody = parseJsonObj(customBody);
const userMsg = {
role: "user",
content: userPrompt,
@@ -317,27 +276,18 @@ const genOpenAI = ({
],
temperature,
max_completion_tokens: maxTokens,
...customBody,
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${key}`, // OpenAI
"api-key": key, // Azure OpenAI
...customHeader,
},
method: "POST",
body: JSON.stringify(data),
const headers = {
"Content-type": "application/json",
Authorization: `Bearer ${key}`, // OpenAI
// "api-key": key, // Azure OpenAI
};
return [url, init, userMsg];
return { url, data, headers, userMsg };
};
const genGemini = ({
texts,
from,
to,
url,
key,
systemPrompt,
@@ -345,18 +295,11 @@ const genGemini = ({
model,
temperature,
maxTokens,
customHeader,
customBody,
docInfo,
hisMsgs,
}) => {
url = url
.replaceAll(INPUT_PLACE_MODEL, model)
.replaceAll(INPUT_PLACE_KEY, key);
systemPrompt = genSystemPrompt({ systemPrompt, from, to });
userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo });
customHeader = parseJsonObj(customHeader);
customBody = parseJsonObj(customBody);
const userMsg = { role: "user", parts: [{ text: userPrompt }] };
const data = {
@@ -393,25 +336,15 @@ const genGemini = ({
threshold: "BLOCK_NONE",
},
],
...customBody,
};
const headers = {
"Content-type": "application/json",
};
const init = {
headers: {
"Content-type": "application/json",
...customHeader,
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init, userMsg];
return { url, data, headers, userMsg };
};
const genGemini2 = ({
texts,
from,
to,
url,
key,
systemPrompt,
@@ -419,16 +352,8 @@ const genGemini2 = ({
model,
temperature,
maxTokens,
customHeader,
customBody,
docInfo,
hisMsgs,
}) => {
systemPrompt = genSystemPrompt({ systemPrompt, from, to });
userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo });
customHeader = parseJsonObj(customHeader);
customBody = parseJsonObj(customBody);
const userMsg = {
role: "user",
content: userPrompt,
@@ -445,26 +370,17 @@ const genGemini2 = ({
],
temperature,
max_tokens: maxTokens,
...customBody,
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
...customHeader,
},
method: "POST",
body: JSON.stringify(data),
const headers = {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
};
return [url, init, userMsg];
return { url, data, headers, userMsg };
};
const genClaude = ({
texts,
from,
to,
url,
key,
systemPrompt,
@@ -472,16 +388,8 @@ const genClaude = ({
model,
temperature,
maxTokens,
customHeader,
customBody,
docInfo,
hisMsgs,
}) => {
systemPrompt = genSystemPrompt({ systemPrompt, from, to });
userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo });
customHeader = parseJsonObj(customHeader);
customBody = parseJsonObj(customBody);
const userMsg = {
role: "user",
content: userPrompt,
@@ -492,28 +400,19 @@ const genClaude = ({
messages: [...hisMsgs, userMsg],
temperature,
max_tokens: maxTokens,
...customBody,
};
const init = {
headers: {
"Content-type": "application/json",
"anthropic-version": "2023-06-01",
"anthropic-dangerous-direct-browser-access": "true",
"x-api-key": key,
...customHeader,
},
method: "POST",
body: JSON.stringify(data),
const headers = {
"Content-type": "application/json",
"anthropic-version": "2023-06-01",
"anthropic-dangerous-direct-browser-access": "true",
"x-api-key": key,
};
return [url, init, userMsg];
return { url, data, headers, userMsg };
};
const genOpenRouter = ({
texts,
from,
to,
url,
key,
systemPrompt,
@@ -521,16 +420,8 @@ const genOpenRouter = ({
model,
temperature,
maxTokens,
customHeader,
customBody,
docInfo,
hisMsgs,
}) => {
systemPrompt = genSystemPrompt({ systemPrompt, from, to });
userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo });
customHeader = parseJsonObj(customHeader);
customBody = parseJsonObj(customBody);
const userMsg = {
role: "user",
content: userPrompt,
@@ -547,26 +438,17 @@ const genOpenRouter = ({
],
temperature,
max_tokens: maxTokens,
...customBody,
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
...customHeader,
},
method: "POST",
body: JSON.stringify(data),
const headers = {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
};
return [url, init, userMsg];
return { url, data, headers, userMsg };
};
const genOllama = ({
texts,
from,
to,
think,
url,
key,
@@ -575,16 +457,8 @@ const genOllama = ({
model,
temperature,
maxTokens,
customHeader,
customBody,
docInfo,
hisMsgs,
}) => {
systemPrompt = genSystemPrompt({ systemPrompt, from, to });
userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo });
customHeader = parseJsonObj(customHeader);
customBody = parseJsonObj(customBody);
const userMsg = {
role: "user",
content: userPrompt,
@@ -603,22 +477,16 @@ const genOllama = ({
max_tokens: maxTokens,
think,
stream: false,
...customBody,
};
const init = {
headers: {
"Content-type": "application/json",
...customHeader,
},
method: "POST",
body: JSON.stringify(data),
const headers = {
"Content-type": "application/json",
};
if (key) {
init.headers.Authorization = `Bearer ${key}`;
headers.Authorization = `Bearer ${key}`;
}
return [url, init, userMsg];
return { url, data, headers, userMsg };
};
const genCloudflareAI = ({ texts, from, to, url, key }) => {
@@ -628,52 +496,65 @@ const genCloudflareAI = ({ texts, from, to, url, key }) => {
target_lang: to,
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
},
method: "POST",
body: JSON.stringify(data),
const headers = {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
};
return [url, init];
return { url, data, headers };
};
const genCustom = ({
texts,
from,
to,
url,
key,
reqHook,
docInfo,
hisMsgs,
}) => {
if (reqHook?.trim()) {
interpreter.run(`exports.reqHook = ${reqHook}`);
return interpreter.exports.reqHook({
texts,
from,
to,
url,
key,
docInfo,
hisMsgs,
});
}
const genCustom = ({ texts, from, to, url, key }) => {
const data = { texts, from, to };
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
},
method: "POST",
body: JSON.stringify(data),
const headers = {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
};
return [url, init];
return { url, data, headers };
};
const genReqFuncs = {
[OPT_TRANS_GOOGLE]: genGoogle,
[OPT_TRANS_GOOGLE_2]: genGoogle2,
[OPT_TRANS_MICROSOFT]: genMicrosoft,
[OPT_TRANS_DEEPL]: genDeepl,
[OPT_TRANS_DEEPLFREE]: genDeeplFree,
[OPT_TRANS_DEEPLX]: genDeeplX,
[OPT_TRANS_NIUTRANS]: genNiuTrans,
[OPT_TRANS_BAIDU]: genBaidu,
[OPT_TRANS_TENCENT]: genTencent,
[OPT_TRANS_VOLCENGINE]: genVolcengine,
[OPT_TRANS_OPENAI]: genOpenAI,
[OPT_TRANS_GEMINI]: genGemini,
[OPT_TRANS_GEMINI_2]: genGemini2,
[OPT_TRANS_CLAUDE]: genClaude,
[OPT_TRANS_CLOUDFLAREAI]: genCloudflareAI,
[OPT_TRANS_OLLAMA]: genOllama,
[OPT_TRANS_OPENROUTER]: genOpenRouter,
[OPT_TRANS_CUSTOMIZE]: genCustom,
};
const genInit = ({
url = "",
data = null,
headers = {},
userMsg = null,
method = "POST",
}) => {
if (!url) {
throw new Error("genInit: url is empty");
}
const init = {
method,
headers,
};
if (method !== "GET" && method !== "HEAD" && data) {
Object.assign(init, { body: JSON.stringify(data) });
}
return [url, init, userMsg];
};
/**
@@ -681,66 +562,70 @@ const genCustom = ({
* @param {*}
* @returns
*/
export const genTransReq = ({ apiType, apiSlug, ...args }) => {
switch (apiType) {
case OPT_TRANS_DEEPL:
case OPT_TRANS_OPENAI:
case OPT_TRANS_GEMINI:
case OPT_TRANS_GEMINI_2:
case OPT_TRANS_CLAUDE:
case OPT_TRANS_CLOUDFLAREAI:
case OPT_TRANS_OLLAMA:
case OPT_TRANS_OPENROUTER:
case OPT_TRANS_NIUTRANS:
case OPT_TRANS_CUSTOMIZE:
args.key = keyPick(apiSlug, args.key, keyMap);
break;
case OPT_TRANS_DEEPLX:
args.url = keyPick(apiSlug, args.url, urlMap);
break;
default:
export const genTransReq = async ({ reqHook, resHook, ...args }) => {
const {
apiType,
apiSlug,
key,
systemPrompt,
userPrompt,
from,
to,
texts,
docInfo,
customHeader,
customBody,
} = args;
if (API_SPE_TYPES.mulkeys.has(apiType)) {
args.key = keyPick(apiSlug, key, keyMap);
}
switch (apiType) {
case OPT_TRANS_GOOGLE:
return genGoogle(args);
case OPT_TRANS_GOOGLE_2:
return genGoogle2(args);
case OPT_TRANS_MICROSOFT:
return genMicrosoft(args);
case OPT_TRANS_DEEPL:
return genDeepl(args);
case OPT_TRANS_DEEPLFREE:
return genDeeplFree(args);
case OPT_TRANS_DEEPLX:
return genDeeplX(args);
case OPT_TRANS_NIUTRANS:
return genNiuTrans(args);
case OPT_TRANS_BAIDU:
return genBaidu(args);
case OPT_TRANS_TENCENT:
return genTencent(args);
case OPT_TRANS_VOLCENGINE:
return genVolcengine(args);
case OPT_TRANS_OPENAI:
return genOpenAI(args);
case OPT_TRANS_GEMINI:
return genGemini(args);
case OPT_TRANS_GEMINI_2:
return genGemini2(args);
case OPT_TRANS_CLAUDE:
return genClaude(args);
case OPT_TRANS_CLOUDFLAREAI:
return genCloudflareAI(args);
case OPT_TRANS_OLLAMA:
return genOllama(args);
case OPT_TRANS_OPENROUTER:
return genOpenRouter(args);
case OPT_TRANS_CUSTOMIZE:
return genCustom(args);
default:
throw new Error(`[trans] ${apiType} not support`);
if (apiType === OPT_TRANS_DEEPLX) {
args.url = keyPick(apiSlug, args.url, urlMap);
}
if (API_SPE_TYPES.ai.has(apiType)) {
args.systemPrompt = genSystemPrompt({ systemPrompt, from, to });
args.userPrompt = genUserPrompt({ userPrompt, from, to, texts, docInfo });
}
const {
url = "",
data = null,
headers = {},
userMsg = null,
method = "POST",
} = genReqFuncs[apiType](args);
// 合并用户自定义headers和body
if (customHeader?.trim()) {
Object.assign(headers, parseJsonObj(customHeader));
}
if (customBody?.trim()) {
Object.assign(data, parseJsonObj(customBody));
}
// 执行 request hook
if (reqHook?.trim()) {
try {
interpreter.run(`exports.reqHook = ${reqHook}`);
const hookResult = await interpreter.exports.reqHook(args, {
url,
data,
headers,
userMsg,
method,
});
if (hookResult && hookResult.url) {
return genInit(hookResult);
}
} catch (err) {
kissLog("run req hook", err);
}
}
return genInit({ url, data, headers, userMsg, method });
};
/**
@@ -749,10 +634,48 @@ export const genTransReq = ({ apiType, apiSlug, ...args }) => {
* @param {*} param3
* @returns
*/
export const parseTransRes = (
export const parseTransRes = async (
res,
{ texts, from, to, resHook, thinkIgnore, history, userMsg, apiType }
{
texts,
from,
to,
fromLang,
toLang,
langMap,
resHook,
thinkIgnore,
history,
userMsg,
apiType,
}
) => {
// 执行 response hook
if (resHook?.trim()) {
try {
interpreter.run(`exports.resHook = ${resHook}`);
const hookResult = await interpreter.exports.resHook({
apiType,
userMsg,
res,
texts,
from,
to,
fromLang,
toLang,
langMap,
});
if (hookResult && Array.isArray(hookResult.translations)) {
if (history && userMsg && hookResult.modelMsg) {
history.add(userMsg, hookResult.modelMsg);
}
return hookResult.translations;
}
} catch (err) {
kissLog("run res hook", err);
}
}
let modelMsg = "";
switch (apiType) {
@@ -832,7 +755,9 @@ export const parseTransRes = (
case OPT_TRANS_OLLAMA:
modelMsg = res?.choices?.[0]?.message;
const deepModels = thinkIgnore.split(",").filter((model) => model.trim());
const deepModels = thinkIgnore
.split(",")
.filter((model) => model?.trim());
if (deepModels.some((model) => res?.model?.startsWith(model))) {
modelMsg?.content.replace(/<think>[\s\S]*<\/think>/i, "");
}
@@ -845,23 +770,7 @@ export const parseTransRes = (
}
return parseAIRes(modelMsg?.content);
case OPT_TRANS_CUSTOMIZE:
if (resHook?.trim()) {
interpreter.run(`exports.resHook = ${resHook}`);
if (history) {
const [translations, modelMsg] = interpreter.exports.resHook({
res,
texts,
from,
to,
});
userMsg && modelMsg && history.add(userMsg, modelMsg);
return translations;
} else {
return interpreter.exports.resHook({ res, texts, from, to });
}
} else {
return res?.map((item) => [item.text, item.src]);
}
return res?.map((item) => [item.text, item.src]);
default:
}
@@ -877,6 +786,9 @@ export const handleTranslate = async ({
texts,
from,
to,
fromLang,
toLang,
langMap,
docInfo,
apiSetting,
usePool,
@@ -897,12 +809,21 @@ export const handleTranslate = async ({
hisMsgs = history.getAll();
}
let token = "";
if (apiType === OPT_TRANS_MICROSOFT) {
[token] = await msAuth();
}
const [input, init, userMsg] = await genTransReq({
texts,
from,
to,
fromLang,
toLang,
langMap,
docInfo,
hisMsgs,
token,
...apiSetting,
});
@@ -921,6 +842,9 @@ export const handleTranslate = async ({
texts,
from,
to,
fromLang,
toLang,
langMap,
history,
userMsg,
...apiSetting,

View File

@@ -350,6 +350,18 @@ Output: {"translations":[{"id":1,"text":"一个<b>React</b>组件","sourceLangua
Fail-safe: On any error, return {"translations":[]}.`;
const defaultRequestHook = `async (args, { url, data, headers, userMsg, method } = {}) => {
console.log("request hook args:", args);
// return { url, data, headers, userMsg, method };
}`;
const defaultResponseHook = `async ({ res, ...args }) => {
console.log("reaponse hook args:", res, args);
// const translations = [["你好", "zh"]];
// const modelMsg = "";
// return { translations, modelMsg };
}`;
// 翻译接口默认参数
const defaultApi = {
apiSlug: "", // 唯一标识
@@ -473,16 +485,8 @@ const defaultApiOpts = {
[OPT_TRANS_CUSTOMIZE]: {
...defaultApi,
url: "https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN",
reqHook: `// Request Hook
(text, from, to, url, key) => [url, {
headers: {
"Content-type": "application/json",
},
method: "GET",
body: null,
}]`,
resHook: `// Response Hook
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]`,
reqHook: defaultRequestHook,
resHook: defaultResponseHook,
},
};

View File

@@ -136,6 +136,52 @@ https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-
${customApiLangs}
`;
const requestHookHelperZH = `1、第一个参数包含如下字段'texts', 'from', 'to', 'url', 'key', 'model', 'systemPrompt', ...
2、返回值必须是包含以下字段的对象 'url', 'data', 'headers', 'userMsg', 'method'
3、如返回空值则hook函数不会产生任何效果。
// 示例
async (args, { url, data, headers, userMsg, method } = {}) => {
console.log("request hook args:", args);
return { url, data, headers, userMsg, method };
}`;
const requestHookHelperEN = `1. The first parameter contains the following fields: 'texts', 'from', 'to', 'url', 'key', 'model', 'systemPrompt', ...
2. The return value must be an object containing the following fields: 'url', 'data', 'headers', 'userMsg', 'method'
3. If a null value is returned, the hook function will have no effect.
// Example
async (args, { url, data, headers, userMsg, method } = {}) => {
console.log("request hook args:", args);
return { url, data, headers, userMsg, method };
}`;
const responsetHookHelperZH = `1、第一个参数包含如下字段'res', ...
2、返回值必须是包含以下字段的对象 'translations', 'modelMsg'
'translations' 应为一个二维数组,表示 [译文,源语言] 的列表)
3、如返回空值则hook函数不会产生任何效果。
// 示例
async ({ res, ...args }) => {
console.log("reaponse hook args:", res, args);
const translations = [["你好", "zh"]];
const modelMsg = "";
return { translations, modelMsg };
}`;
const responsetHookHelperEN = `1. The first parameter contains the following fields: 'res', ...
2. The return value must be an object containing the following fields: 'translations', 'modelMsg'
('translations' should be a two-dimensional array representing a list of [translation, source language]).
3. If a null value is returned, the hook function will have no effect.
// Example
async ({ res, ...args }) => {
console.log("reaponse hook args:", res, args);
const translations = [["你好", "zh"]];
const modelMsg = "";
return { translations, modelMsg };
}`;
export const I18N = {
app_name: {
zh: `简约翻译`,
@@ -152,6 +198,16 @@ export const I18N = {
en: customApiHelpEN,
zh_TW: customApiHelpZH,
},
request_hook_helper: {
zh: requestHookHelperZH,
en: requestHookHelperEN,
zh_TW: requestHookHelperZH,
},
response_hook_helper: {
zh: responsetHookHelperZH,
en: responsetHookHelperEN,
zh_TW: responsetHookHelperZH,
},
translate_alt: {
zh: `翻译`,
en: `Translate`,
@@ -613,9 +669,9 @@ export const I18N = {
zh_TW: `選擇器節點樣式`,
},
selector_style_helper: {
zh: `开启翻译时注入,关闭翻译时不会移除`,
en: `It is injected when translation is turned on and will not be removed when translation is turned off.`,
zh_TW: `在開啟翻譯時注入,關閉翻譯時不會移除`,
zh: `开启翻译时注入。`,
en: `It is injected when translation is turned on.`,
zh_TW: `在開啟翻譯時注入。`,
},
selector_parent_style: {
zh: `选择器父节点样式`,
@@ -1213,9 +1269,9 @@ export const I18N = {
zh_TW: `翻譯開始 Hook`,
},
translate_start_hook_helper: {
zh: `翻译前时运行,入参为: 翻译节点列表。`,
en: `Run before translation, input parameters are: translation node list.`,
zh_TW: `翻譯前時運行,入參為: 翻譯節點清單。`,
zh: `翻译前时运行,入参为: ({hostNode, parentNode, nodes})`,
en: `Run before translation, input parameters are: ({hostNode, parentNode, nodes})`,
zh_TW: `翻譯前時運行,入參為: ({hostNode, parentNode, nodes})`,
},
translate_end_hook: {
zh: `翻译完成钩子函数`,
@@ -1223,9 +1279,9 @@ export const I18N = {
zh_TW: `翻譯完成 Hook`,
},
translate_end_hook_helper: {
zh: `翻译完成时运行,入参为: 翻译节点列表。`,
en: `Run when translation is complete, input parameters are: translation node list.`,
zh_TW: `翻譯完成時運行,入參為: 翻譯節點清單。`,
zh: `翻译完成时运行,入参为: ({hostNode, parentNode, nodes, wrapperNode, innerNode})`,
en: `Run when translation is complete, input parameters are: ({hostNode, parentNode, nodes, wrapperNode, innerNode})`,
zh_TW: `翻譯完成時運行,入參為: ({hostNode, parentNode, nodes, wrapperNode, innerNode})`,
},
translate_remove_hook: {
zh: `翻译移除钩子函数`,

View File

@@ -74,7 +74,7 @@ export const DEFAULT_SELECTOR =
"h1, h2, h3, h4, h5, h6, li, p, dd, blockquote, figcaption, label, legend";
export const DEFAULT_IGNORE_SELECTOR =
"button, code, footer, form, header, mark, nav, pre";
export const DEFAULT_KEEP_SELECTOR = `code, img, svg, pre`;
export const DEFAULT_KEEP_SELECTOR = `code, img, svg, pre, a:has(code)`;
export const DEFAULT_RULE = {
pattern: "", // 匹配网址
selector: "", // 选择器

View File

@@ -70,6 +70,9 @@ const BatchQueue = (
try {
const payloads = tasksToProcess.map((item) => item.payload);
const responses = await sendBatchRequest(payloads);
if (!Array.isArray(responses)) {
throw new Error("responses format error");
}
tasksToProcess.forEach((taskItem, index) => {
const response = responses[index];

View File

@@ -62,10 +62,11 @@ async function trySetObj(key, obj) {
async function getObj(key) {
const val = await get(key);
if (val === null || val === undefined) return null;
try {
return JSON.parse(val);
} catch (err) {
kissLog("parse json: ", key);
kissLog("parse json in storage err: ", key);
}
return null;
}

View File

@@ -255,6 +255,7 @@ export class Translator {
#setting; // 设置选项
#rule; // 规则
#isInitialized = false; // 初始化状态
#isJsInjected = false; // 注入用户JS
#mouseHoverEnabled = false; // 鼠标悬停翻译
#enabled = false; // 全局默认状态
#runId = 0; // 用于中止过期的异步请求
@@ -1248,6 +1249,11 @@ export class Translator {
// 注入JS/CSS
#initInjector() {
if (this.#isJsInjected) {
return;
}
this.#isJsInjected = true;
try {
const { injectJs, injectCss } = this.#rule;
if (isExt) {
@@ -1258,7 +1264,7 @@ export class Translator {
injectCss && injectInternalCss(injectCss);
}
} catch (err) {
kissLog("inject js");
kissLog("inject js", err);
}
}

View File

@@ -24,6 +24,7 @@ import { apiTranslate } from "../../apis";
import Box from "@mui/material/Box";
import { limitNumber, limitFloat } from "../../libs/utils";
import ReusableAutocomplete from "./ReusableAutocomplete";
import ShowMoreButton from "./ShowMoreButton";
import {
OPT_TRANS_DEEPLX,
OPT_TRANS_OLLAMA,
@@ -115,6 +116,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
const i18n = useI18n();
const [formData, setFormData] = useState({});
const [isModified, setIsModified] = useState(false);
const [showMore, setShowMore] = useState(false);
const confirm = useConfirm();
useEffect(() => {
@@ -282,7 +284,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
<>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
{/* todo 改成 ReusableAutocomplete 可选择和填写模型 */}
<TextField
size="small"
@@ -293,7 +295,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<ReusableAutocomplete
freeSolo
size="small"
@@ -305,7 +307,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
@@ -316,7 +318,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
@@ -327,7 +329,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}></Grid>
<Grid item xs={12} sm={12} md={6} lg={3}></Grid>
</Grid>
</Box>
@@ -393,27 +395,6 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
</Grid>
</Grid>
</Box> */}
<TextField
size="small"
label={i18n("custom_header")}
name="customHeader"
value={customHeader}
onChange={handleChange}
multiline
maxRows={10}
helperText={i18n("custom_header_help")}
/>
<TextField
size="small"
label={i18n("custom_body")}
name="customBody"
value={customBody}
onChange={handleChange}
multiline
maxRows={10}
helperText={i18n("custom_body_help")}
/>
</>
)}
@@ -469,6 +450,14 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange}
multiline
maxRows={10}
FormHelperTextProps={{
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("request_hook_helper")}
</Box>
}
/>
<TextField
size="small"
@@ -478,6 +467,14 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange}
multiline
maxRows={10}
FormHelperTextProps={{
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("response_hook_helper")}
</Box>
}
/>
</>
)}
@@ -485,7 +482,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
{API_SPE_TYPES.batch.has(api.apiType) && (
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
fullWidth
@@ -499,7 +496,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
@@ -510,7 +507,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
@@ -521,7 +518,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
@@ -540,7 +537,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
<>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
{" "}
<TextField
select
@@ -555,7 +552,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
{" "}
<TextField
size="small"
@@ -574,7 +571,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
@@ -585,7 +582,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
@@ -596,7 +593,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
@@ -607,10 +604,74 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}></Grid>
<Grid item xs={12} sm={12} md={6} lg={3}></Grid>
</Grid>
</Box>
{showMore && (
<>
<TextField
size="small"
label={i18n("custom_header")}
name="customHeader"
value={customHeader}
onChange={handleChange}
multiline
maxRows={10}
helperText={i18n("custom_header_help")}
/>
<TextField
size="small"
label={i18n("custom_body")}
name="customBody"
value={customBody}
onChange={handleChange}
multiline
maxRows={10}
helperText={i18n("custom_body_help")}
/>
{apiType !== OPT_TRANS_CUSTOMIZE && (
<>
<TextField
size="small"
label={"Request Hook"}
name="reqHook"
value={reqHook}
onChange={handleChange}
multiline
maxRows={10}
FormHelperTextProps={{
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("request_hook_helper")}
</Box>
}
/>
<TextField
size="small"
label={"Response Hook"}
name="resHook"
value={resHook}
onChange={handleChange}
multiline
maxRows={10}
FormHelperTextProps={{
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("response_hook_helper")}
</Box>
}
/>
</>
)}
</>
)}
<Stack
direction="row"
alignItems="center"
@@ -652,9 +713,11 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
}
label={i18n("is_disabled")}
/>
<ShowMoreButton showMore={showMore} onChange={setShowMore} />
</Stack>
{apiType === OPT_TRANS_CUSTOMIZE && <pre>{i18n("custom_api_help")}</pre>}
{/* {apiType === OPT_TRANS_CUSTOMIZE && <pre>{i18n("custom_api_help")}</pre>} */}
</Stack>
);
}

View File

@@ -24,7 +24,6 @@ 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 ExpandLessIcon from "@mui/icons-material/ExpandLess";
import { useRules } from "../../hooks/Rules";
import MenuItem from "@mui/material/MenuItem";
import Grid from "@mui/material/Grid";
@@ -62,6 +61,7 @@ import CancelIcon from "@mui/icons-material/Cancel";
import SaveIcon from "@mui/icons-material/Save";
import { kissLog } from "../../libs/log";
import { useApiList } from "../../hooks/Api";
import ShowMoreButton from "./ShowMoreButton";
function RuleFields({ rule, rules, setShow, setKeyword }) {
const initFormValues = useMemo(
@@ -209,30 +209,6 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
</MenuItem>
);
const ShowMoreButton = showMore ? (
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(false);
}}
startIcon={<ExpandLessIcon />}
>
{i18n("less")}
</Button>
) : (
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
startIcon={<ExpandMoreIcon />}
>
{i18n("more")}
</Button>
);
return (
<form onSubmit={handleSubmit}>
<Stack spacing={2}>
@@ -293,7 +269,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -309,7 +285,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -325,7 +301,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -341,7 +317,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -358,7 +334,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -374,7 +350,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -390,7 +366,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -406,7 +382,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<MenuItem value={"font"}>{`<font>`}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -423,7 +399,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -442,7 +418,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -461,7 +437,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -480,7 +456,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
))}
</TextField>
</Grid>
{/* <Grid item xs={12} sm={6} md={6} lg={3}>
{/* <Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -500,7 +476,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
</TextField>
</Grid> */}
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -521,7 +497,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
@@ -540,7 +516,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
@@ -714,7 +690,6 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
{i18n("delete")}
</Button>
)}
{ShowMoreButton}
</>
) : (
<>
@@ -742,9 +717,9 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
>
{i18n("restore_default")}
</Button>
{ShowMoreButton}
</>
)}
<ShowMoreButton showMore={showMore} onChange={setShowMore} />
</Stack>
) : (
// 添加
@@ -765,7 +740,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
>
{i18n("cancel")}
</Button>
{ShowMoreButton}
<ShowMoreButton showMore={showMore} onChange={setShowMore} />
</Stack>
))}
</Stack>
@@ -1078,7 +1053,7 @@ function SubRulesEdit({ subList, addSub, updateDataCache }) {
return;
}
if (subList.find((item) => item.url === url)) {
if (subList.some((item) => item.url === url)) {
setInputError(i18n("error_duplicate_values"));
return;
}

View File

@@ -149,7 +149,7 @@ export default function Settings() {
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
fullWidth
@@ -166,7 +166,7 @@ export default function Settings() {
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
fullWidth
@@ -180,7 +180,7 @@ export default function Settings() {
<MenuItem value={false}>{i18n("disable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
fullWidth
@@ -196,7 +196,7 @@ export default function Settings() {
<MenuItem value={true}>{i18n("hide")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
fullWidth
@@ -210,7 +210,7 @@ export default function Settings() {
<MenuItem value={1}>{i18n("fab_click_translate")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
fullWidth
size="small"
@@ -221,7 +221,7 @@ export default function Settings() {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
fullWidth
size="small"
@@ -232,7 +232,7 @@ export default function Settings() {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
fullWidth
size="small"
@@ -243,7 +243,7 @@ export default function Settings() {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
fullWidth
size="small"
@@ -254,7 +254,7 @@ export default function Settings() {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
fullWidth
size="small"
@@ -265,7 +265,7 @@ export default function Settings() {
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
fullWidth
@@ -282,7 +282,7 @@ export default function Settings() {
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
fullWidth
@@ -297,7 +297,7 @@ export default function Settings() {
<MenuItem value={2}>{i18n("secondary_context_menus")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
fullWidth
@@ -353,25 +353,25 @@ export default function Settings() {
<>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_TRANSLATE}
label={i18n("toggle_translate_shortcut")}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_STYLE}
label={i18n("toggle_style_shortcut")}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_POPUP}
label={i18n("toggle_popup_shortcut")}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={3}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_SETTING}
label={i18n("open_setting_shortcut")}

View File

@@ -0,0 +1,35 @@
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import Button from "@mui/material/Button";
import { useI18n } from "../../hooks/I18n";
export default function ShowMoreButton({ onChange, showMore }) {
const i18n = useI18n();
const handleClick = () => {
onChange((prev) => !prev);
};
if (showMore) {
return (
<Button
size="small"
variant="text"
onClick={handleClick}
startIcon={<ExpandLessIcon />}
>
{i18n("less")}
</Button>
);
}
return (
<Button
size="small"
variant="text"
onClick={handleClick}
startIcon={<ExpandMoreIcon />}
>
{i18n("more")}
</Button>
);
}