feat: Extensive refactoring and modification to support any number of interfaces
This commit is contained in:
@@ -28,12 +28,12 @@ const MsgHistory = (maxSize = DEFAULT_CONTEXT_SIZE) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMsgHistory = (translator, maxSize) => {
|
export const getMsgHistory = (apiSlug, maxSize) => {
|
||||||
if (historyMap.has(translator)) {
|
if (historyMap.has(apiSlug)) {
|
||||||
return historyMap.get(translator);
|
return historyMap.get(apiSlug);
|
||||||
}
|
}
|
||||||
|
|
||||||
const msgHistory = MsgHistory(maxSize);
|
const msgHistory = MsgHistory(maxSize);
|
||||||
historyMap.set(translator, msgHistory);
|
historyMap.set(apiSlug, msgHistory);
|
||||||
return msgHistory;
|
return msgHistory;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
OPT_LANGS_TENCENT,
|
OPT_LANGS_TENCENT,
|
||||||
OPT_LANGS_SPECIAL,
|
OPT_LANGS_SPECIAL,
|
||||||
OPT_LANGS_MICROSOFT,
|
OPT_LANGS_MICROSOFT,
|
||||||
OPT_TRANS_BATCH,
|
API_SPE_TYPES,
|
||||||
|
DEFAULT_API_SETTING,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { sha256 } from "../libs/utils";
|
import { sha256 } from "../libs/utils";
|
||||||
import { msAuth } from "../libs/auth";
|
import { msAuth } from "../libs/auth";
|
||||||
@@ -200,11 +201,10 @@ export const apiTencentLangdetect = async (text) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiTranslate = async ({
|
export const apiTranslate = async ({
|
||||||
translator,
|
|
||||||
text,
|
text,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
apiSetting = {},
|
apiSetting = DEFAULT_API_SETTING,
|
||||||
docInfo = {},
|
docInfo = {},
|
||||||
useCache = true,
|
useCache = true,
|
||||||
usePool = true,
|
usePool = true,
|
||||||
@@ -213,23 +213,23 @@ export const apiTranslate = async ({
|
|||||||
return ["", false];
|
return ["", false];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { apiType, apiSlug, useBatchFetch } = apiSetting;
|
||||||
const from =
|
const from =
|
||||||
OPT_LANGS_SPECIAL[translator].get(fromLang) ??
|
OPT_LANGS_SPECIAL[apiType].get(fromLang) ??
|
||||||
OPT_LANGS_SPECIAL[translator].get("auto");
|
OPT_LANGS_SPECIAL[apiType].get("auto");
|
||||||
const to = OPT_LANGS_SPECIAL[translator].get(toLang);
|
const to = OPT_LANGS_SPECIAL[apiType].get(toLang);
|
||||||
if (!to) {
|
if (!to) {
|
||||||
kissLog(`target lang: ${toLang} not support`, "translate");
|
kissLog(`target lang: ${toLang} not support`);
|
||||||
return ["", false];
|
return ["", false];
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 优化缓存失效因素
|
// todo: 优化缓存失效因素
|
||||||
const [v1, v2] = process.env.REACT_APP_VERSION.split(".");
|
const [v1, v2] = process.env.REACT_APP_VERSION.split(".");
|
||||||
const cacheOpts = {
|
const cacheOpts = {
|
||||||
translator,
|
apiSlug,
|
||||||
text,
|
text,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
model: apiSetting.model, // model改变,缓存失效
|
|
||||||
version: [v1, v2].join("."),
|
version: [v1, v2].join("."),
|
||||||
};
|
};
|
||||||
const cacheInput = `${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`;
|
const cacheInput = `${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`;
|
||||||
@@ -245,26 +245,21 @@ export const apiTranslate = async ({
|
|||||||
// 请求接口数据
|
// 请求接口数据
|
||||||
let trText = "";
|
let trText = "";
|
||||||
let srLang = "";
|
let srLang = "";
|
||||||
if (apiSetting.useBatchFetch && OPT_TRANS_BATCH.has(translator)) {
|
if (useBatchFetch && API_SPE_TYPES.batch.has(apiType)) {
|
||||||
const queue = getBatchQueue(
|
const queue = getBatchQueue({
|
||||||
{
|
|
||||||
translator,
|
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
docInfo,
|
docInfo,
|
||||||
apiSetting,
|
apiSetting,
|
||||||
usePool,
|
usePool,
|
||||||
taskFn: handleTranslate,
|
taskFn: handleTranslate,
|
||||||
},
|
});
|
||||||
apiSetting
|
|
||||||
);
|
|
||||||
const tranlation = await queue.addTask({ text });
|
const tranlation = await queue.addTask({ text });
|
||||||
if (Array.isArray(tranlation)) {
|
if (Array.isArray(tranlation)) {
|
||||||
[trText, srLang = ""] = tranlation;
|
[trText, srLang = ""] = tranlation;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const translations = await handleTranslate({
|
const translations = await handleTranslate({
|
||||||
translator,
|
|
||||||
texts: [text],
|
texts: [text],
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
@@ -281,7 +276,7 @@ export const apiTranslate = async ({
|
|||||||
|
|
||||||
// 插入缓存
|
// 插入缓存
|
||||||
if (useCache && trText) {
|
if (useCache && trText) {
|
||||||
await putHttpCachePolyfill(cacheInput, null, { trText, isSame, srLang });
|
putHttpCachePolyfill(cacheInput, null, { trText, isSame, srLang });
|
||||||
}
|
}
|
||||||
|
|
||||||
return [trText, isSame];
|
return [trText, isSame];
|
||||||
|
|||||||
@@ -11,25 +11,17 @@ import {
|
|||||||
OPT_TRANS_TENCENT,
|
OPT_TRANS_TENCENT,
|
||||||
OPT_TRANS_VOLCENGINE,
|
OPT_TRANS_VOLCENGINE,
|
||||||
OPT_TRANS_OPENAI,
|
OPT_TRANS_OPENAI,
|
||||||
OPT_TRANS_OPENAI_2,
|
|
||||||
OPT_TRANS_OPENAI_3,
|
|
||||||
OPT_TRANS_GEMINI,
|
OPT_TRANS_GEMINI,
|
||||||
OPT_TRANS_GEMINI_2,
|
OPT_TRANS_GEMINI_2,
|
||||||
OPT_TRANS_CLAUDE,
|
OPT_TRANS_CLAUDE,
|
||||||
OPT_TRANS_CLOUDFLAREAI,
|
OPT_TRANS_CLOUDFLAREAI,
|
||||||
OPT_TRANS_OLLAMA,
|
OPT_TRANS_OLLAMA,
|
||||||
OPT_TRANS_OLLAMA_2,
|
|
||||||
OPT_TRANS_OLLAMA_3,
|
|
||||||
OPT_TRANS_OPENROUTER,
|
OPT_TRANS_OPENROUTER,
|
||||||
OPT_TRANS_CUSTOMIZE,
|
OPT_TRANS_CUSTOMIZE,
|
||||||
OPT_TRANS_CUSTOMIZE_2,
|
API_SPE_TYPES,
|
||||||
OPT_TRANS_CUSTOMIZE_3,
|
|
||||||
OPT_TRANS_CUSTOMIZE_4,
|
|
||||||
OPT_TRANS_CUSTOMIZE_5,
|
|
||||||
OPT_TRANS_CONTEXT,
|
|
||||||
INPUT_PLACE_FROM,
|
INPUT_PLACE_FROM,
|
||||||
INPUT_PLACE_TO,
|
INPUT_PLACE_TO,
|
||||||
INPUT_PLACE_TEXT,
|
// INPUT_PLACE_TEXT,
|
||||||
INPUT_PLACE_KEY,
|
INPUT_PLACE_KEY,
|
||||||
INPUT_PLACE_MODEL,
|
INPUT_PLACE_MODEL,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
@@ -46,7 +38,7 @@ const keyMap = new Map();
|
|||||||
const urlMap = new Map();
|
const urlMap = new Map();
|
||||||
|
|
||||||
// 轮询key/url
|
// 轮询key/url
|
||||||
const keyPick = (translator, key = "", cacheMap) => {
|
const keyPick = (apiSlug, key = "", cacheMap) => {
|
||||||
const keys = key
|
const keys = key
|
||||||
.split(/\n|,/)
|
.split(/\n|,/)
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
@@ -56,9 +48,9 @@ const keyPick = (translator, key = "", cacheMap) => {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const preIndex = cacheMap.get(translator) ?? -1;
|
const preIndex = cacheMap.get(apiSlug) ?? -1;
|
||||||
const curIndex = (preIndex + 1) % keys.length;
|
const curIndex = (preIndex + 1) % keys.length;
|
||||||
cacheMap.set(translator, curIndex);
|
cacheMap.set(apiSlug, curIndex);
|
||||||
|
|
||||||
return keys[curIndex];
|
return keys[curIndex];
|
||||||
};
|
};
|
||||||
@@ -68,20 +60,30 @@ const genSystemPrompt = ({ systemPrompt, from, to }) =>
|
|||||||
.replaceAll(INPUT_PLACE_FROM, from)
|
.replaceAll(INPUT_PLACE_FROM, from)
|
||||||
.replaceAll(INPUT_PLACE_TO, to);
|
.replaceAll(INPUT_PLACE_TO, to);
|
||||||
|
|
||||||
const genUserPrompt = ({ userPrompt, from, to, texts, docInfo }) => {
|
const genUserPrompt = ({
|
||||||
|
// userPrompt,
|
||||||
|
tone,
|
||||||
|
glossary = {},
|
||||||
|
// from,
|
||||||
|
to,
|
||||||
|
texts,
|
||||||
|
docInfo,
|
||||||
|
}) => {
|
||||||
const prompt = JSON.stringify({
|
const prompt = JSON.stringify({
|
||||||
targetLanguage: to,
|
targetLanguage: to,
|
||||||
title: docInfo.title,
|
title: docInfo.title,
|
||||||
description: docInfo.description,
|
description: docInfo.description,
|
||||||
segments: texts.map((text, i) => ({ id: i, text })),
|
segments: texts.map((text, i) => ({ id: i, text })),
|
||||||
|
glossary,
|
||||||
|
tone,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userPrompt.includes(INPUT_PLACE_TEXT)) {
|
// if (userPrompt.includes(INPUT_PLACE_TEXT)) {
|
||||||
return userPrompt
|
// return userPrompt
|
||||||
.replaceAll(INPUT_PLACE_FROM, from)
|
// .replaceAll(INPUT_PLACE_FROM, from)
|
||||||
.replaceAll(INPUT_PLACE_TO, to)
|
// .replaceAll(INPUT_PLACE_TO, to)
|
||||||
.replaceAll(INPUT_PLACE_TEXT, prompt);
|
// .replaceAll(INPUT_PLACE_TEXT, prompt);
|
||||||
}
|
// }
|
||||||
|
|
||||||
return prompt;
|
return prompt;
|
||||||
};
|
};
|
||||||
@@ -93,15 +95,19 @@ const parseAIRes = (raw) => {
|
|||||||
const jsonString = extractJson(raw);
|
const jsonString = extractJson(raw);
|
||||||
data = JSON.parse(jsonString);
|
data = JSON.parse(jsonString);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "parseAIRes");
|
kissLog("parseAIRes", err);
|
||||||
data = { translations: [] };
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(data.translations)) {
|
if (!Array.isArray(data.translations)) {
|
||||||
data.translations = [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.translations.map((item) => [item.text]);
|
// todo: 考虑序号id可能会打乱
|
||||||
|
return data.translations.map((item) => [
|
||||||
|
item?.text ?? "",
|
||||||
|
item?.sourceLanguage ?? "",
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const genGoogle = ({ texts, from, to, url, key }) => {
|
const genGoogle = ({ texts, from, to, url, key }) => {
|
||||||
@@ -675,35 +681,27 @@ const genCustom = ({
|
|||||||
* @param {*}
|
* @param {*}
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const genTransReq = (translator, args) => {
|
export const genTransReq = ({ apiType, apiSlug, ...args }) => {
|
||||||
switch (translator) {
|
switch (apiType) {
|
||||||
case OPT_TRANS_DEEPL:
|
case OPT_TRANS_DEEPL:
|
||||||
case OPT_TRANS_OPENAI:
|
case OPT_TRANS_OPENAI:
|
||||||
case OPT_TRANS_OPENAI_2:
|
|
||||||
case OPT_TRANS_OPENAI_3:
|
|
||||||
case OPT_TRANS_GEMINI:
|
case OPT_TRANS_GEMINI:
|
||||||
case OPT_TRANS_GEMINI_2:
|
case OPT_TRANS_GEMINI_2:
|
||||||
case OPT_TRANS_CLAUDE:
|
case OPT_TRANS_CLAUDE:
|
||||||
case OPT_TRANS_CLOUDFLAREAI:
|
case OPT_TRANS_CLOUDFLAREAI:
|
||||||
case OPT_TRANS_OLLAMA:
|
case OPT_TRANS_OLLAMA:
|
||||||
case OPT_TRANS_OLLAMA_2:
|
|
||||||
case OPT_TRANS_OLLAMA_3:
|
|
||||||
case OPT_TRANS_OPENROUTER:
|
case OPT_TRANS_OPENROUTER:
|
||||||
case OPT_TRANS_NIUTRANS:
|
case OPT_TRANS_NIUTRANS:
|
||||||
case OPT_TRANS_CUSTOMIZE:
|
case OPT_TRANS_CUSTOMIZE:
|
||||||
case OPT_TRANS_CUSTOMIZE_2:
|
args.key = keyPick(apiSlug, args.key, keyMap);
|
||||||
case OPT_TRANS_CUSTOMIZE_3:
|
|
||||||
case OPT_TRANS_CUSTOMIZE_4:
|
|
||||||
case OPT_TRANS_CUSTOMIZE_5:
|
|
||||||
args.key = keyPick(translator, args.key, keyMap);
|
|
||||||
break;
|
break;
|
||||||
case OPT_TRANS_DEEPLX:
|
case OPT_TRANS_DEEPLX:
|
||||||
args.url = keyPick(translator, args.url, urlMap);
|
args.url = keyPick(apiSlug, args.url, urlMap);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (translator) {
|
switch (apiType) {
|
||||||
case OPT_TRANS_GOOGLE:
|
case OPT_TRANS_GOOGLE:
|
||||||
return genGoogle(args);
|
return genGoogle(args);
|
||||||
case OPT_TRANS_GOOGLE_2:
|
case OPT_TRANS_GOOGLE_2:
|
||||||
@@ -725,8 +723,6 @@ export const genTransReq = (translator, args) => {
|
|||||||
case OPT_TRANS_VOLCENGINE:
|
case OPT_TRANS_VOLCENGINE:
|
||||||
return genVolcengine(args);
|
return genVolcengine(args);
|
||||||
case OPT_TRANS_OPENAI:
|
case OPT_TRANS_OPENAI:
|
||||||
case OPT_TRANS_OPENAI_2:
|
|
||||||
case OPT_TRANS_OPENAI_3:
|
|
||||||
return genOpenAI(args);
|
return genOpenAI(args);
|
||||||
case OPT_TRANS_GEMINI:
|
case OPT_TRANS_GEMINI:
|
||||||
return genGemini(args);
|
return genGemini(args);
|
||||||
@@ -737,37 +733,29 @@ export const genTransReq = (translator, args) => {
|
|||||||
case OPT_TRANS_CLOUDFLAREAI:
|
case OPT_TRANS_CLOUDFLAREAI:
|
||||||
return genCloudflareAI(args);
|
return genCloudflareAI(args);
|
||||||
case OPT_TRANS_OLLAMA:
|
case OPT_TRANS_OLLAMA:
|
||||||
case OPT_TRANS_OLLAMA_2:
|
|
||||||
case OPT_TRANS_OLLAMA_3:
|
|
||||||
return genOllama(args);
|
return genOllama(args);
|
||||||
case OPT_TRANS_OPENROUTER:
|
case OPT_TRANS_OPENROUTER:
|
||||||
return genOpenRouter(args);
|
return genOpenRouter(args);
|
||||||
case OPT_TRANS_CUSTOMIZE:
|
case OPT_TRANS_CUSTOMIZE:
|
||||||
case OPT_TRANS_CUSTOMIZE_2:
|
|
||||||
case OPT_TRANS_CUSTOMIZE_3:
|
|
||||||
case OPT_TRANS_CUSTOMIZE_4:
|
|
||||||
case OPT_TRANS_CUSTOMIZE_5:
|
|
||||||
return genCustom(args);
|
return genCustom(args);
|
||||||
default:
|
default:
|
||||||
throw new Error(`[trans] translator: ${translator} not support`);
|
throw new Error(`[trans] ${apiType} not support`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析翻译接口返回数据
|
* 解析翻译接口返回数据
|
||||||
* @param {*} translator
|
|
||||||
* @param {*} res
|
* @param {*} res
|
||||||
* @param {*} param3
|
* @param {*} param3
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const parseTransRes = (
|
export const parseTransRes = (
|
||||||
translator,
|
|
||||||
res,
|
res,
|
||||||
{ texts, from, to, resHook, thinkIgnore, history, userMsg }
|
{ texts, from, to, resHook, thinkIgnore, history, userMsg, apiType }
|
||||||
) => {
|
) => {
|
||||||
let modelMsg = "";
|
let modelMsg = "";
|
||||||
|
|
||||||
switch (translator) {
|
switch (apiType) {
|
||||||
case OPT_TRANS_GOOGLE:
|
case OPT_TRANS_GOOGLE:
|
||||||
return [[res?.sentences?.map((item) => item.trans).join(" "), res?.src]];
|
return [[res?.sentences?.map((item) => item.trans).join(" "), res?.src]];
|
||||||
case OPT_TRANS_GOOGLE_2:
|
case OPT_TRANS_GOOGLE_2:
|
||||||
@@ -814,8 +802,6 @@ export const parseTransRes = (
|
|||||||
case OPT_TRANS_VOLCENGINE:
|
case OPT_TRANS_VOLCENGINE:
|
||||||
return [[res?.translation, res?.detected_language]];
|
return [[res?.translation, res?.detected_language]];
|
||||||
case OPT_TRANS_OPENAI:
|
case OPT_TRANS_OPENAI:
|
||||||
case OPT_TRANS_OPENAI_2:
|
|
||||||
case OPT_TRANS_OPENAI_3:
|
|
||||||
case OPT_TRANS_GEMINI_2:
|
case OPT_TRANS_GEMINI_2:
|
||||||
case OPT_TRANS_OPENROUTER:
|
case OPT_TRANS_OPENROUTER:
|
||||||
modelMsg = res?.choices?.[0]?.message;
|
modelMsg = res?.choices?.[0]?.message;
|
||||||
@@ -844,8 +830,6 @@ export const parseTransRes = (
|
|||||||
case OPT_TRANS_CLOUDFLAREAI:
|
case OPT_TRANS_CLOUDFLAREAI:
|
||||||
return [[res?.result?.translated_text]];
|
return [[res?.result?.translated_text]];
|
||||||
case OPT_TRANS_OLLAMA:
|
case OPT_TRANS_OLLAMA:
|
||||||
case OPT_TRANS_OLLAMA_2:
|
|
||||||
case OPT_TRANS_OLLAMA_3:
|
|
||||||
modelMsg = res?.choices?.[0]?.message;
|
modelMsg = res?.choices?.[0]?.message;
|
||||||
|
|
||||||
const deepModels = thinkIgnore.split(",").filter((model) => model.trim());
|
const deepModels = thinkIgnore.split(",").filter((model) => model.trim());
|
||||||
@@ -861,10 +845,6 @@ export const parseTransRes = (
|
|||||||
}
|
}
|
||||||
return parseAIRes(modelMsg?.content);
|
return parseAIRes(modelMsg?.content);
|
||||||
case OPT_TRANS_CUSTOMIZE:
|
case OPT_TRANS_CUSTOMIZE:
|
||||||
case OPT_TRANS_CUSTOMIZE_2:
|
|
||||||
case OPT_TRANS_CUSTOMIZE_3:
|
|
||||||
case OPT_TRANS_CUSTOMIZE_4:
|
|
||||||
case OPT_TRANS_CUSTOMIZE_5:
|
|
||||||
if (resHook?.trim()) {
|
if (resHook?.trim()) {
|
||||||
interpreter.run(`exports.resHook = ${resHook}`);
|
interpreter.run(`exports.resHook = ${resHook}`);
|
||||||
if (history) {
|
if (history) {
|
||||||
@@ -894,7 +874,6 @@ export const parseTransRes = (
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const handleTranslate = async ({
|
export const handleTranslate = async ({
|
||||||
translator,
|
|
||||||
texts,
|
texts,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
@@ -904,12 +883,21 @@ export const handleTranslate = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
let history = null;
|
let history = null;
|
||||||
let hisMsgs = [];
|
let hisMsgs = [];
|
||||||
if (apiSetting.useContext && OPT_TRANS_CONTEXT.has(translator)) {
|
const {
|
||||||
history = getMsgHistory(translator, apiSetting.contextSize);
|
apiType,
|
||||||
|
apiSlug,
|
||||||
|
contextSize,
|
||||||
|
useContext,
|
||||||
|
fetchInterval,
|
||||||
|
fetchLimit,
|
||||||
|
httpTimeout,
|
||||||
|
} = apiSetting;
|
||||||
|
if (useContext && API_SPE_TYPES.context.has(apiType)) {
|
||||||
|
history = getMsgHistory(apiSlug, contextSize);
|
||||||
hisMsgs = history.getAll();
|
hisMsgs = history.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
const [input, init, userMsg] = await genTransReq(translator, {
|
const [input, init, userMsg] = await genTransReq({
|
||||||
texts,
|
texts,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
@@ -921,15 +909,15 @@ export const handleTranslate = async ({
|
|||||||
const res = await fetchData(input, init, {
|
const res = await fetchData(input, init, {
|
||||||
useCache: false,
|
useCache: false,
|
||||||
usePool,
|
usePool,
|
||||||
fetchInterval: apiSetting.fetchInterval,
|
fetchInterval,
|
||||||
fetchLimit: apiSetting.fetchLimit,
|
fetchLimit,
|
||||||
httpTimeout: apiSetting.httpTimeout,
|
httpTimeout,
|
||||||
});
|
});
|
||||||
if (!res) {
|
if (!res) {
|
||||||
throw new Error("tranlate got empty response");
|
throw new Error("tranlate got empty response");
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseTransRes(translator, res, {
|
return parseTransRes(res, {
|
||||||
texts,
|
texts,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ async function addContextMenus(contextMenuType = 1) {
|
|||||||
try {
|
try {
|
||||||
await browser.contextMenus.removeAll();
|
await browser.contextMenus.removeAll();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "remove contextMenus");
|
kissLog("remove contextMenus", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (contextMenuType) {
|
switch (contextMenuType) {
|
||||||
@@ -122,7 +122,7 @@ async function updateCspRules(csplist = DEFAULT_CSPLIST.join(",\n")) {
|
|||||||
addRules: newRules,
|
addRules: newRules,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "update csp rules");
|
kissLog("update csp rules", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,22 +26,16 @@ export const OPT_TRANS_BAIDU = "Baidu";
|
|||||||
export const OPT_TRANS_TENCENT = "Tencent";
|
export const OPT_TRANS_TENCENT = "Tencent";
|
||||||
export const OPT_TRANS_VOLCENGINE = "Volcengine";
|
export const OPT_TRANS_VOLCENGINE = "Volcengine";
|
||||||
export const OPT_TRANS_OPENAI = "OpenAI";
|
export const OPT_TRANS_OPENAI = "OpenAI";
|
||||||
export const OPT_TRANS_OPENAI_2 = "OpenAI2";
|
|
||||||
export const OPT_TRANS_OPENAI_3 = "OpenAI3";
|
|
||||||
export const OPT_TRANS_GEMINI = "Gemini";
|
export const OPT_TRANS_GEMINI = "Gemini";
|
||||||
export const OPT_TRANS_GEMINI_2 = "Gemini2";
|
export const OPT_TRANS_GEMINI_2 = "Gemini2";
|
||||||
export const OPT_TRANS_CLAUDE = "Claude";
|
export const OPT_TRANS_CLAUDE = "Claude";
|
||||||
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
|
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
|
||||||
export const OPT_TRANS_OLLAMA = "Ollama";
|
export const OPT_TRANS_OLLAMA = "Ollama";
|
||||||
export const OPT_TRANS_OLLAMA_2 = "Ollama2";
|
|
||||||
export const OPT_TRANS_OLLAMA_3 = "Ollama3";
|
|
||||||
export const OPT_TRANS_OPENROUTER = "OpenRouter";
|
export const OPT_TRANS_OPENROUTER = "OpenRouter";
|
||||||
export const OPT_TRANS_CUSTOMIZE = "Custom";
|
export const OPT_TRANS_CUSTOMIZE = "Custom";
|
||||||
export const OPT_TRANS_CUSTOMIZE_2 = "Custom2";
|
|
||||||
export const OPT_TRANS_CUSTOMIZE_3 = "Custom3";
|
// 内置支持的翻译引擎
|
||||||
export const OPT_TRANS_CUSTOMIZE_4 = "Custom4";
|
export const OPT_ALL_TYPES = [
|
||||||
export const OPT_TRANS_CUSTOMIZE_5 = "Custom5";
|
|
||||||
export const OPT_TRANS_ALL = [
|
|
||||||
OPT_TRANS_GOOGLE,
|
OPT_TRANS_GOOGLE,
|
||||||
OPT_TRANS_GOOGLE_2,
|
OPT_TRANS_GOOGLE_2,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
@@ -53,64 +47,74 @@ export const OPT_TRANS_ALL = [
|
|||||||
OPT_TRANS_DEEPLX,
|
OPT_TRANS_DEEPLX,
|
||||||
OPT_TRANS_NIUTRANS,
|
OPT_TRANS_NIUTRANS,
|
||||||
OPT_TRANS_OPENAI,
|
OPT_TRANS_OPENAI,
|
||||||
OPT_TRANS_OPENAI_2,
|
|
||||||
OPT_TRANS_OPENAI_3,
|
|
||||||
OPT_TRANS_GEMINI,
|
OPT_TRANS_GEMINI,
|
||||||
OPT_TRANS_GEMINI_2,
|
OPT_TRANS_GEMINI_2,
|
||||||
OPT_TRANS_CLAUDE,
|
OPT_TRANS_CLAUDE,
|
||||||
OPT_TRANS_CLOUDFLAREAI,
|
OPT_TRANS_CLOUDFLAREAI,
|
||||||
OPT_TRANS_OLLAMA,
|
OPT_TRANS_OLLAMA,
|
||||||
OPT_TRANS_OLLAMA_2,
|
|
||||||
OPT_TRANS_OLLAMA_3,
|
|
||||||
OPT_TRANS_OPENROUTER,
|
OPT_TRANS_OPENROUTER,
|
||||||
OPT_TRANS_CUSTOMIZE,
|
OPT_TRANS_CUSTOMIZE,
|
||||||
OPT_TRANS_CUSTOMIZE_2,
|
|
||||||
OPT_TRANS_CUSTOMIZE_3,
|
|
||||||
OPT_TRANS_CUSTOMIZE_4,
|
|
||||||
OPT_TRANS_CUSTOMIZE_5,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// 可使用批处理的翻译引擎
|
// 翻译引擎特殊集合
|
||||||
export const OPT_TRANS_BATCH = new Set([
|
export const API_SPE_TYPES = {
|
||||||
|
// 内置翻译
|
||||||
|
builtin: new Set(OPT_ALL_TYPES),
|
||||||
|
// 机器翻译
|
||||||
|
machine: new Set([
|
||||||
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_DEEPLFREE,
|
||||||
|
OPT_TRANS_BAIDU,
|
||||||
|
OPT_TRANS_TENCENT,
|
||||||
|
OPT_TRANS_VOLCENGINE,
|
||||||
|
]),
|
||||||
|
// AI翻译
|
||||||
|
ai: new Set([
|
||||||
|
OPT_TRANS_OPENAI,
|
||||||
|
OPT_TRANS_GEMINI,
|
||||||
|
OPT_TRANS_GEMINI_2,
|
||||||
|
OPT_TRANS_CLAUDE,
|
||||||
|
OPT_TRANS_OLLAMA,
|
||||||
|
OPT_TRANS_OPENROUTER,
|
||||||
|
]),
|
||||||
|
// 支持多key
|
||||||
|
mulkeys: new Set([
|
||||||
|
OPT_TRANS_DEEPL,
|
||||||
|
OPT_TRANS_OPENAI,
|
||||||
|
OPT_TRANS_GEMINI,
|
||||||
|
OPT_TRANS_GEMINI_2,
|
||||||
|
OPT_TRANS_CLAUDE,
|
||||||
|
OPT_TRANS_CLOUDFLAREAI,
|
||||||
|
OPT_TRANS_OLLAMA,
|
||||||
|
OPT_TRANS_OPENROUTER,
|
||||||
|
OPT_TRANS_NIUTRANS,
|
||||||
|
OPT_TRANS_CUSTOMIZE,
|
||||||
|
]),
|
||||||
|
// 支持批处理
|
||||||
|
batch: new Set([
|
||||||
OPT_TRANS_GOOGLE_2,
|
OPT_TRANS_GOOGLE_2,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
OPT_TRANS_TENCENT,
|
OPT_TRANS_TENCENT,
|
||||||
OPT_TRANS_DEEPL,
|
OPT_TRANS_DEEPL,
|
||||||
OPT_TRANS_OPENAI,
|
OPT_TRANS_OPENAI,
|
||||||
OPT_TRANS_OPENAI_2,
|
|
||||||
OPT_TRANS_OPENAI_3,
|
|
||||||
OPT_TRANS_GEMINI,
|
OPT_TRANS_GEMINI,
|
||||||
OPT_TRANS_GEMINI_2,
|
OPT_TRANS_GEMINI_2,
|
||||||
OPT_TRANS_CLAUDE,
|
OPT_TRANS_CLAUDE,
|
||||||
OPT_TRANS_OLLAMA,
|
OPT_TRANS_OLLAMA,
|
||||||
OPT_TRANS_OLLAMA_2,
|
|
||||||
OPT_TRANS_OLLAMA_3,
|
|
||||||
OPT_TRANS_OPENROUTER,
|
OPT_TRANS_OPENROUTER,
|
||||||
OPT_TRANS_CUSTOMIZE,
|
OPT_TRANS_CUSTOMIZE,
|
||||||
OPT_TRANS_CUSTOMIZE_2,
|
]),
|
||||||
OPT_TRANS_CUSTOMIZE_3,
|
// 支持上下文
|
||||||
OPT_TRANS_CUSTOMIZE_4,
|
context: new Set([
|
||||||
OPT_TRANS_CUSTOMIZE_5,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 可使用上下文的翻译引擎
|
|
||||||
export const OPT_TRANS_CONTEXT = new Set([
|
|
||||||
OPT_TRANS_OPENAI,
|
OPT_TRANS_OPENAI,
|
||||||
OPT_TRANS_OPENAI_2,
|
|
||||||
OPT_TRANS_OPENAI_3,
|
|
||||||
OPT_TRANS_GEMINI,
|
OPT_TRANS_GEMINI,
|
||||||
OPT_TRANS_GEMINI_2,
|
OPT_TRANS_GEMINI_2,
|
||||||
OPT_TRANS_CLAUDE,
|
OPT_TRANS_CLAUDE,
|
||||||
OPT_TRANS_OLLAMA,
|
OPT_TRANS_OLLAMA,
|
||||||
OPT_TRANS_OLLAMA_2,
|
|
||||||
OPT_TRANS_OLLAMA_3,
|
|
||||||
OPT_TRANS_OPENROUTER,
|
OPT_TRANS_OPENROUTER,
|
||||||
OPT_TRANS_CUSTOMIZE,
|
OPT_TRANS_CUSTOMIZE,
|
||||||
OPT_TRANS_CUSTOMIZE_2,
|
]),
|
||||||
OPT_TRANS_CUSTOMIZE_3,
|
};
|
||||||
OPT_TRANS_CUSTOMIZE_4,
|
|
||||||
OPT_TRANS_CUSTOMIZE_5,
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const OPT_LANGDETECTOR_ALL = [
|
export const OPT_LANGDETECTOR_ALL = [
|
||||||
OPT_TRANS_GOOGLE,
|
OPT_TRANS_GOOGLE,
|
||||||
@@ -119,6 +123,24 @@ export const OPT_LANGDETECTOR_ALL = [
|
|||||||
OPT_TRANS_TENCENT,
|
OPT_TRANS_TENCENT,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const BUILTIN_STONES = [
|
||||||
|
"formal", // 正式风格
|
||||||
|
"casual", // 口语风格
|
||||||
|
"neutral", // 中性风格
|
||||||
|
"technical", // 技术风格
|
||||||
|
"marketing", // 营销风格
|
||||||
|
"Literary", // 文学风格
|
||||||
|
"academic", // 学术风格
|
||||||
|
"legal", // 法律风格
|
||||||
|
"literal", // 直译风格
|
||||||
|
"ldiomatic", // 意译风格
|
||||||
|
"transcreation", // 创译风格
|
||||||
|
"machine-like", // 机器风格
|
||||||
|
"concise", // 简明风格
|
||||||
|
];
|
||||||
|
export const BUILTIN_PLACEHOULDERS = ["{ }", "{{ }}", "[ ]", "[[ ]]"];
|
||||||
|
export const BUILTIN_TAG_NAMES = ["i", "a", "span"];
|
||||||
|
|
||||||
export const OPT_LANGS_TO = [
|
export const OPT_LANGS_TO = [
|
||||||
["en", "English - English"],
|
["en", "English - English"],
|
||||||
["zh-CN", "Simplified Chinese - 简体中文"],
|
["zh-CN", "Simplified Chinese - 简体中文"],
|
||||||
@@ -250,12 +272,6 @@ export const OPT_LANGS_SPECIAL = {
|
|||||||
[OPT_TRANS_OPENAI]: new Map(
|
[OPT_TRANS_OPENAI]: new Map(
|
||||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
),
|
),
|
||||||
[OPT_TRANS_OPENAI_2]: new Map(
|
|
||||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
|
||||||
),
|
|
||||||
[OPT_TRANS_OPENAI_3]: new Map(
|
|
||||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
|
||||||
),
|
|
||||||
[OPT_TRANS_GEMINI]: new Map(
|
[OPT_TRANS_GEMINI]: new Map(
|
||||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
),
|
),
|
||||||
@@ -268,12 +284,6 @@ export const OPT_LANGS_SPECIAL = {
|
|||||||
[OPT_TRANS_OLLAMA]: new Map(
|
[OPT_TRANS_OLLAMA]: new Map(
|
||||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
),
|
),
|
||||||
[OPT_TRANS_OLLAMA_2]: new Map(
|
|
||||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
|
||||||
),
|
|
||||||
[OPT_TRANS_OLLAMA_3]: new Map(
|
|
||||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
|
||||||
),
|
|
||||||
[OPT_TRANS_OPENROUTER]: new Map(
|
[OPT_TRANS_OPENROUTER]: new Map(
|
||||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
),
|
),
|
||||||
@@ -294,18 +304,6 @@ export const OPT_LANGS_SPECIAL = {
|
|||||||
[OPT_TRANS_CUSTOMIZE]: new Map([
|
[OPT_TRANS_CUSTOMIZE]: new Map([
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||||
]),
|
]),
|
||||||
[OPT_TRANS_CUSTOMIZE_2]: new Map([
|
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
|
||||||
]),
|
|
||||||
[OPT_TRANS_CUSTOMIZE_3]: new Map([
|
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
|
||||||
]),
|
|
||||||
[OPT_TRANS_CUSTOMIZE_4]: new Map([
|
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
|
||||||
]),
|
|
||||||
[OPT_TRANS_CUSTOMIZE_5]: new Map([
|
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
|
||||||
]),
|
|
||||||
};
|
};
|
||||||
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
|
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
|
||||||
export const OPT_LANGS_MICROSOFT = new Map(
|
export const OPT_LANGS_MICROSOFT = new Map(
|
||||||
@@ -328,37 +326,44 @@ export const OPT_LANGS_TENCENT = new Map(
|
|||||||
);
|
);
|
||||||
OPT_LANGS_TENCENT.set("zh", "zh-CN");
|
OPT_LANGS_TENCENT.set("zh", "zh-CN");
|
||||||
|
|
||||||
// 翻译接口
|
const defaultSystemPrompt = `Act as a translation API. Output a single raw JSON object only. No extra text or fences.
|
||||||
|
|
||||||
|
Input:
|
||||||
|
{"targetLanguage":"<lang>","title":"<context>","description":"<context>","segments":[{"id":1,"text":"..."}],"glossary":{"sourceTerm":"targetTerm"},"tone":"<formal|casual>"}
|
||||||
|
|
||||||
|
Output:
|
||||||
|
{"translations":[{"id":1,"text":"...","sourceLanguage":"<detected>"}]}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. Use title/description for context only; do not output them.
|
||||||
|
2. Keep id, order, and count of segments.
|
||||||
|
3. Preserve whitespace, HTML entities, and all HTML-like tags (e.g., <i1>, <a1>). Translate inner text only.
|
||||||
|
4. Highest priority: Follow 'glossary'. Use value for translation; if value is "", keep the key.
|
||||||
|
5. Do not translate: content in <code>, <pre>, text enclosed in backticks, or placeholders like {1}, {{1}}, [1], [[1]].
|
||||||
|
6. Apply the specified tone to the translation.
|
||||||
|
7. Detect sourceLanguage for each segment.
|
||||||
|
8. Return empty or unchanged inputs as is.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Input: {"targetLanguage":"zh-CN","segments":[{"id":1,"text":"A <b>React</b> component."}],"glossary":{"component":"组件","React":""}}
|
||||||
|
Output: {"translations":[{"id":1,"text":"一个<b>React</b>组件","sourceLanguage":"en"}]}
|
||||||
|
|
||||||
|
Fail-safe: On any error, return {"translations":[]}.`;
|
||||||
|
|
||||||
|
// 翻译接口默认参数
|
||||||
const defaultApi = {
|
const defaultApi = {
|
||||||
apiSlug: "", // 唯一标识
|
apiSlug: "", // 唯一标识
|
||||||
apiName: "", // 接口名称
|
apiName: "", // 接口名称
|
||||||
|
apiType: "", // 接口类型
|
||||||
url: "",
|
url: "",
|
||||||
key: "",
|
key: "",
|
||||||
model: "", // 模型名称
|
model: "", // 模型名称
|
||||||
systemPrompt: `You are a translation API.
|
systemPrompt: defaultSystemPrompt,
|
||||||
|
userPrompt: "",
|
||||||
Output:
|
tone: "neutral", // 翻译风格
|
||||||
- Return one raw JSON object only.
|
placeholder: "{ }", // 占位符(todo: 备用)
|
||||||
- Start with "{" and end with "}".
|
tagName: "i", // 标签符 (todo: 备用)
|
||||||
- No fences or extra text.
|
aiTerms: false, // AI智能专业术语 (todo: 备用)
|
||||||
|
|
||||||
Input JSON:
|
|
||||||
{"targetLanguage":"<lang>","title":"<title>","description":"<desc>","segments":[{"id":1,"text":"..."}]}
|
|
||||||
|
|
||||||
Output JSON:
|
|
||||||
{"translations":[{"id":1,"text":"...","sourceLanguage":"<detected-language>"}]}
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
1. Use title/description as context only, do not output them.
|
|
||||||
2. Keep ids/order/count.
|
|
||||||
3. Translate inner text only, not HTML tags.
|
|
||||||
4. Do not translate <code>, <pre>, backticks, or terms like React, Docker, JavaScript, API.
|
|
||||||
5. Preserve whitespace & entities.
|
|
||||||
6. Automatically detect the source language of each segment and add it in the "sourceLanguage" field.
|
|
||||||
7. Empty/unchanged input → unchanged.
|
|
||||||
|
|
||||||
Fail-safe: {"translations":[]}`,
|
|
||||||
userPrompt: `${INPUT_PLACE_TEXT}`,
|
|
||||||
customHeader: "",
|
customHeader: "",
|
||||||
customBody: "",
|
customBody: "",
|
||||||
reqHook: "", // request 钩子函数
|
reqHook: "", // request 钩子函数
|
||||||
@@ -370,7 +375,6 @@ Fail-safe: {"translations":[]}`,
|
|||||||
batchSize: DEFAULT_BATCH_SIZE, // 每次最多发送段落数量
|
batchSize: DEFAULT_BATCH_SIZE, // 每次最多发送段落数量
|
||||||
batchLength: DEFAULT_BATCH_LENGTH, // 每次发送最大文字数量
|
batchLength: DEFAULT_BATCH_LENGTH, // 每次发送最大文字数量
|
||||||
useBatchFetch: false, // 是否启用聚合发送请求
|
useBatchFetch: false, // 是否启用聚合发送请求
|
||||||
useRichText: false, // 是否启用富文本翻译
|
|
||||||
useContext: false, // 是否启用智能上下文
|
useContext: false, // 是否启用智能上下文
|
||||||
contextSize: DEFAULT_CONTEXT_SIZE, // 智能上下文保留会话数
|
contextSize: DEFAULT_CONTEXT_SIZE, // 智能上下文保留会话数
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
@@ -379,7 +383,94 @@ Fail-safe: {"translations":[]}`,
|
|||||||
thinkIgnore: "qwen3,deepseek-r1",
|
thinkIgnore: "qwen3,deepseek-r1",
|
||||||
isDisabled: false, // 是否不显示
|
isDisabled: false, // 是否不显示
|
||||||
};
|
};
|
||||||
const defaultCustomApi = {
|
|
||||||
|
const defaultApiOpts = {
|
||||||
|
[OPT_TRANS_GOOGLE]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://translate.googleapis.com/translate_a/single",
|
||||||
|
},
|
||||||
|
[OPT_TRANS_GOOGLE_2]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://translate-pa.googleapis.com/v1/translateHtml",
|
||||||
|
key: "AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_MICROSOFT]: {
|
||||||
|
...defaultApi,
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_BAIDU]: {
|
||||||
|
...defaultApi,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_TENCENT]: {
|
||||||
|
...defaultApi,
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_VOLCENGINE]: {
|
||||||
|
...defaultApi,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_DEEPL]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://api-free.deepl.com/v2/translate",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_DEEPLFREE]: {
|
||||||
|
...defaultApi,
|
||||||
|
fetchLimit: 1,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_DEEPLX]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "http://localhost:1188/translate",
|
||||||
|
fetchLimit: 1,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_NIUTRANS]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://api.niutrans.com/NiuTransServer/translation",
|
||||||
|
dictNo: "",
|
||||||
|
memoryNo: "",
|
||||||
|
},
|
||||||
|
[OPT_TRANS_OPENAI]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://api.openai.com/v1/chat/completions",
|
||||||
|
model: "gpt-4",
|
||||||
|
useBatchFetch: true,
|
||||||
|
fetchLimit: 1,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_GEMINI]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: `https://generativelanguage.googleapis.com/v1/models/${INPUT_PLACE_MODEL}:generateContent?key=${INPUT_PLACE_KEY}`,
|
||||||
|
model: "gemini-2.5-flash",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_GEMINI_2]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`,
|
||||||
|
model: "gemini-2.0-flash",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_CLAUDE]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://api.anthropic.com/v1/messages",
|
||||||
|
model: "claude-3-haiku-20240307",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_CLOUDFLAREAI]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://api.cloudflare.com/client/v4/accounts/{{ACCOUNT_ID}}/ai/run/@cf/meta/m2m100-1.2b",
|
||||||
|
},
|
||||||
|
[OPT_TRANS_OLLAMA]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "http://localhost:11434/v1/chat/completions",
|
||||||
|
model: "llama3.1",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_OPENROUTER]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://openrouter.ai/api/v1/chat/completions",
|
||||||
|
model: "openai/gpt-4o",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_CUSTOMIZE]: {
|
||||||
...defaultApi,
|
...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",
|
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
|
reqHook: `// Request Hook
|
||||||
@@ -392,170 +483,16 @@ const defaultCustomApi = {
|
|||||||
}]`,
|
}]`,
|
||||||
resHook: `// Response Hook
|
resHook: `// Response Hook
|
||||||
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]`,
|
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]`,
|
||||||
};
|
|
||||||
const defaultOpenaiApi = {
|
|
||||||
...defaultApi,
|
|
||||||
url: "https://api.openai.com/v1/chat/completions",
|
|
||||||
model: "gpt-4",
|
|
||||||
fetchLimit: 1,
|
|
||||||
};
|
|
||||||
const defaultOllamaApi = {
|
|
||||||
...defaultApi,
|
|
||||||
url: "http://localhost:11434/v1/chat/completions",
|
|
||||||
model: "llama3.1",
|
|
||||||
};
|
|
||||||
export const DEFAULT_TRANS_APIS = {
|
|
||||||
[OPT_TRANS_GOOGLE]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_GOOGLE,
|
|
||||||
apiName: OPT_TRANS_GOOGLE,
|
|
||||||
url: "https://translate.googleapis.com/translate_a/single",
|
|
||||||
},
|
|
||||||
[OPT_TRANS_GOOGLE_2]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_GOOGLE_2,
|
|
||||||
apiName: OPT_TRANS_GOOGLE_2,
|
|
||||||
url: "https://translate-pa.googleapis.com/v1/translateHtml",
|
|
||||||
key: "AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520",
|
|
||||||
useBatchFetch: true,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_MICROSOFT]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_MICROSOFT,
|
|
||||||
apiName: OPT_TRANS_MICROSOFT,
|
|
||||||
useBatchFetch: true,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_BAIDU]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_BAIDU,
|
|
||||||
apiName: OPT_TRANS_BAIDU,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_TENCENT]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_TENCENT,
|
|
||||||
apiName: OPT_TRANS_TENCENT,
|
|
||||||
useBatchFetch: true,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_VOLCENGINE]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_VOLCENGINE,
|
|
||||||
apiName: OPT_TRANS_VOLCENGINE,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_DEEPL]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_DEEPL,
|
|
||||||
apiName: OPT_TRANS_DEEPL,
|
|
||||||
url: "https://api-free.deepl.com/v2/translate",
|
|
||||||
useBatchFetch: true,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_DEEPLFREE]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_DEEPLFREE,
|
|
||||||
apiName: OPT_TRANS_DEEPLFREE,
|
|
||||||
fetchLimit: 1,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_DEEPLX]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_DEEPLX,
|
|
||||||
apiName: OPT_TRANS_DEEPLX,
|
|
||||||
url: "http://localhost:1188/translate",
|
|
||||||
fetchLimit: 1,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_NIUTRANS]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_NIUTRANS,
|
|
||||||
apiName: OPT_TRANS_NIUTRANS,
|
|
||||||
url: "https://api.niutrans.com/NiuTransServer/translation",
|
|
||||||
dictNo: "",
|
|
||||||
memoryNo: "",
|
|
||||||
},
|
|
||||||
[OPT_TRANS_OPENAI]: {
|
|
||||||
...defaultOpenaiApi,
|
|
||||||
apiSlug: OPT_TRANS_OPENAI,
|
|
||||||
apiName: OPT_TRANS_OPENAI,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_OPENAI_2]: {
|
|
||||||
...defaultOpenaiApi,
|
|
||||||
apiSlug: OPT_TRANS_OPENAI_2,
|
|
||||||
apiName: OPT_TRANS_OPENAI_2,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_OPENAI_3]: {
|
|
||||||
...defaultOpenaiApi,
|
|
||||||
apiSlug: OPT_TRANS_OPENAI_3,
|
|
||||||
apiName: OPT_TRANS_OPENAI_3,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_GEMINI]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_GEMINI,
|
|
||||||
apiName: OPT_TRANS_GEMINI,
|
|
||||||
url: `https://generativelanguage.googleapis.com/v1/models/${INPUT_PLACE_MODEL}:generateContent?key=${INPUT_PLACE_KEY}`,
|
|
||||||
model: "gemini-2.5-flash",
|
|
||||||
},
|
|
||||||
[OPT_TRANS_GEMINI_2]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_GEMINI_2,
|
|
||||||
apiName: OPT_TRANS_GEMINI_2,
|
|
||||||
url: `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`,
|
|
||||||
model: "gemini-2.0-flash",
|
|
||||||
},
|
|
||||||
[OPT_TRANS_CLAUDE]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_CLAUDE,
|
|
||||||
apiName: OPT_TRANS_CLAUDE,
|
|
||||||
url: "https://api.anthropic.com/v1/messages",
|
|
||||||
model: "claude-3-haiku-20240307",
|
|
||||||
},
|
|
||||||
[OPT_TRANS_CLOUDFLAREAI]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_CLOUDFLAREAI,
|
|
||||||
apiName: OPT_TRANS_CLOUDFLAREAI,
|
|
||||||
url: "https://api.cloudflare.com/client/v4/accounts/{{ACCOUNT_ID}}/ai/run/@cf/meta/m2m100-1.2b",
|
|
||||||
},
|
|
||||||
[OPT_TRANS_OLLAMA]: {
|
|
||||||
...defaultOllamaApi,
|
|
||||||
apiSlug: OPT_TRANS_OLLAMA,
|
|
||||||
apiName: OPT_TRANS_OLLAMA,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_OLLAMA_2]: {
|
|
||||||
...defaultOllamaApi,
|
|
||||||
apiSlug: OPT_TRANS_OLLAMA_2,
|
|
||||||
apiName: OPT_TRANS_OLLAMA_2,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_OLLAMA_3]: {
|
|
||||||
...defaultOllamaApi,
|
|
||||||
apiSlug: OPT_TRANS_OLLAMA_3,
|
|
||||||
apiName: OPT_TRANS_OLLAMA_3,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_OPENROUTER]: {
|
|
||||||
...defaultApi,
|
|
||||||
apiSlug: OPT_TRANS_OPENROUTER,
|
|
||||||
apiName: "",
|
|
||||||
url: "https://openrouter.ai/api/v1/chat/completions",
|
|
||||||
model: "openai/gpt-4o",
|
|
||||||
},
|
|
||||||
[OPT_TRANS_CUSTOMIZE]: {
|
|
||||||
...defaultCustomApi,
|
|
||||||
apiSlug: OPT_TRANS_CUSTOMIZE,
|
|
||||||
apiName: OPT_TRANS_CUSTOMIZE,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_CUSTOMIZE_2]: {
|
|
||||||
...defaultCustomApi,
|
|
||||||
apiSlug: OPT_TRANS_CUSTOMIZE_2,
|
|
||||||
apiName: OPT_TRANS_CUSTOMIZE_2,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_CUSTOMIZE_3]: {
|
|
||||||
...defaultCustomApi,
|
|
||||||
apiSlug: OPT_TRANS_CUSTOMIZE_3,
|
|
||||||
apiName: OPT_TRANS_CUSTOMIZE_3,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_CUSTOMIZE_4]: {
|
|
||||||
...defaultCustomApi,
|
|
||||||
apiSlug: OPT_TRANS_CUSTOMIZE_4,
|
|
||||||
apiName: OPT_TRANS_CUSTOMIZE_4,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_CUSTOMIZE_5]: {
|
|
||||||
...defaultCustomApi,
|
|
||||||
apiSlug: OPT_TRANS_CUSTOMIZE_5,
|
|
||||||
apiName: OPT_TRANS_CUSTOMIZE_5,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 内置翻译接口列表(带参数)
|
||||||
|
export const DEFAULT_API_LIST = OPT_ALL_TYPES.map((apiType) => ({
|
||||||
|
...defaultApiOpts[apiType],
|
||||||
|
apiSlug: apiType,
|
||||||
|
apiName: apiType,
|
||||||
|
apiType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const DEFAULT_API_TYPE = OPT_TRANS_MICROSOFT;
|
||||||
|
export const DEFAULT_API_SETTING = DEFAULT_API_LIST[DEFAULT_API_TYPE];
|
||||||
|
|||||||
@@ -1342,4 +1342,59 @@ export const I18N = {
|
|||||||
en: `Hide Original`,
|
en: `Hide Original`,
|
||||||
zh_TW: `隱藏原文`,
|
zh_TW: `隱藏原文`,
|
||||||
},
|
},
|
||||||
|
confirm_title: {
|
||||||
|
zh: `确认`,
|
||||||
|
en: `Confirm`,
|
||||||
|
zh_TW: `確認`,
|
||||||
|
},
|
||||||
|
confirm_message: {
|
||||||
|
zh: `确定操作吗?`,
|
||||||
|
en: `Are you sure you want to proceed?`,
|
||||||
|
zh_TW: `確定操作嗎?`,
|
||||||
|
},
|
||||||
|
confirm_action: {
|
||||||
|
zh: `确定`,
|
||||||
|
en: `Confirm`,
|
||||||
|
zh_TW: `確定`,
|
||||||
|
},
|
||||||
|
cancel_action: {
|
||||||
|
zh: `取消`,
|
||||||
|
en: `Cancel`,
|
||||||
|
zh_TW: `取消`,
|
||||||
|
},
|
||||||
|
pls_press_shortcut: {
|
||||||
|
zh: `请按下快捷键组合`,
|
||||||
|
en: `Please press the shortcut key combination`,
|
||||||
|
zh_TW: `請按下快速鍵組合`,
|
||||||
|
},
|
||||||
|
load_setting_err: {
|
||||||
|
zh: `数据加载出错,请刷新页面或卸载后重新安装。`,
|
||||||
|
en: `Please press the shortcut key combination`,
|
||||||
|
zh_TW: `請按下快速鍵組合`,
|
||||||
|
},
|
||||||
|
translation_style: {
|
||||||
|
zh: `翻译风格`,
|
||||||
|
en: `Translation style`,
|
||||||
|
zh_TW: `翻譯風格`,
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
zh: `占位符`,
|
||||||
|
en: `Placeholder`,
|
||||||
|
zh_TW: `佔位符`,
|
||||||
|
},
|
||||||
|
tag_name: {
|
||||||
|
zh: `占位标签名`,
|
||||||
|
en: `Placeholder tag name`,
|
||||||
|
zh_TW: `佔位標名`,
|
||||||
|
},
|
||||||
|
ai_terms: {
|
||||||
|
zh: `AI识别术语表`,
|
||||||
|
en: `AI Identification Glossary`,
|
||||||
|
zh_TW: `AI辨識術語表`,
|
||||||
|
},
|
||||||
|
system_prompt_helper: {
|
||||||
|
zh: `在未完全理解默认Prompt的情况下,请勿随意修改,否则可能翻译失败。`,
|
||||||
|
en: `If you do not fully understand the default prompt, please do not modify it at will, otherwise the translation may fail.`,
|
||||||
|
zh_TW: `在未完全理解預設Prompt的情況下,請勿隨意修改,否則可能翻譯失敗。`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { FIXER_BR, FIXER_BN, FIXER_BR_DIV, FIXER_BN_DIV } from "../libs/webfix";
|
|
||||||
import { OPT_TRANS_MICROSOFT } from "./api";
|
import { OPT_TRANS_MICROSOFT } from "./api";
|
||||||
|
|
||||||
export const GLOBAL_KEY = "*";
|
export const GLOBAL_KEY = "*";
|
||||||
@@ -58,6 +57,19 @@ export const OPT_TIMING_ALL = [
|
|||||||
OPT_TIMING_ALT,
|
OPT_TIMING_ALT,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DEFAULT_DIY_STYLE = `color: #666;
|
||||||
|
background: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
LightGreen 20%,
|
||||||
|
LightPink 20% 40%,
|
||||||
|
LightSalmon 40% 60%,
|
||||||
|
LightSeaGreen 60% 80%,
|
||||||
|
LightSkyBlue 80%
|
||||||
|
);
|
||||||
|
&:hover {
|
||||||
|
color: #333;
|
||||||
|
};`;
|
||||||
|
|
||||||
export const DEFAULT_SELECTOR =
|
export const DEFAULT_SELECTOR =
|
||||||
"h1, h2, h3, h4, h5, h6, li, p, dd, blockquote, figcaption, label, legend";
|
"h1, h2, h3, h4, h5, h6, li, p, dd, blockquote, figcaption, label, legend";
|
||||||
export const DEFAULT_IGNORE_SELECTOR =
|
export const DEFAULT_IGNORE_SELECTOR =
|
||||||
@@ -68,13 +80,13 @@ export const DEFAULT_RULE = {
|
|||||||
selector: "", // 选择器
|
selector: "", // 选择器
|
||||||
keepSelector: "", // 保留元素选择器
|
keepSelector: "", // 保留元素选择器
|
||||||
terms: "", // 专业术语
|
terms: "", // 专业术语
|
||||||
translator: GLOBAL_KEY, // 翻译服务
|
apiSlug: GLOBAL_KEY, // 翻译服务
|
||||||
fromLang: GLOBAL_KEY, // 源语言
|
fromLang: GLOBAL_KEY, // 源语言
|
||||||
toLang: GLOBAL_KEY, // 目标语言
|
toLang: GLOBAL_KEY, // 目标语言
|
||||||
textStyle: GLOBAL_KEY, // 译文样式
|
textStyle: GLOBAL_KEY, // 译文样式
|
||||||
transOpen: GLOBAL_KEY, // 开启翻译
|
transOpen: GLOBAL_KEY, // 开启翻译
|
||||||
bgColor: "", // 译文颜色
|
bgColor: "", // 译文颜色
|
||||||
textDiyStyle: "", // 自定义译文样式
|
textDiyStyle: DEFAULT_DIY_STYLE, // 自定义译文样式
|
||||||
selectStyle: "", // 选择器节点样式
|
selectStyle: "", // 选择器节点样式
|
||||||
parentStyle: "", // 选择器父节点样式
|
parentStyle: "", // 选择器父节点样式
|
||||||
injectJs: "", // 注入JS
|
injectJs: "", // 注入JS
|
||||||
@@ -104,7 +116,7 @@ export const GLOBLA_RULE = {
|
|||||||
selector: DEFAULT_SELECTOR, // 选择器
|
selector: DEFAULT_SELECTOR, // 选择器
|
||||||
keepSelector: DEFAULT_KEEP_SELECTOR, // 保留元素选择器
|
keepSelector: DEFAULT_KEEP_SELECTOR, // 保留元素选择器
|
||||||
terms: "", // 专业术语
|
terms: "", // 专业术语
|
||||||
translator: OPT_TRANS_MICROSOFT, // 翻译服务
|
apiSlug: OPT_TRANS_MICROSOFT, // 翻译服务
|
||||||
fromLang: "auto", // 源语言
|
fromLang: "auto", // 源语言
|
||||||
toLang: "zh-CN", // 目标语言
|
toLang: "zh-CN", // 目标语言
|
||||||
textStyle: OPT_STYLE_NONE, // 译文样式
|
textStyle: OPT_STYLE_NONE, // 译文样式
|
||||||
@@ -136,21 +148,8 @@ export const GLOBLA_RULE = {
|
|||||||
|
|
||||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||||
|
|
||||||
const DEFAULT_DIY_STYLE = `color: #666;
|
|
||||||
background: linear-gradient(
|
|
||||||
45deg,
|
|
||||||
LightGreen 20%,
|
|
||||||
LightPink 20% 40%,
|
|
||||||
LightSalmon 40% 60%,
|
|
||||||
LightSeaGreen 60% 80%,
|
|
||||||
LightSkyBlue 80%
|
|
||||||
);
|
|
||||||
&:hover {
|
|
||||||
color: #333;
|
|
||||||
};`;
|
|
||||||
|
|
||||||
export const DEFAULT_OW_RULE = {
|
export const DEFAULT_OW_RULE = {
|
||||||
translator: REMAIN_KEY,
|
apiSlug: REMAIN_KEY,
|
||||||
fromLang: REMAIN_KEY,
|
fromLang: REMAIN_KEY,
|
||||||
toLang: REMAIN_KEY,
|
toLang: REMAIN_KEY,
|
||||||
textStyle: REMAIN_KEY,
|
textStyle: REMAIN_KEY,
|
||||||
@@ -159,258 +158,33 @@ export const DEFAULT_OW_RULE = {
|
|||||||
textDiyStyle: DEFAULT_DIY_STYLE,
|
textDiyStyle: DEFAULT_DIY_STYLE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// todo: 校验几个内置规则
|
||||||
const RULES_MAP = {
|
const RULES_MAP = {
|
||||||
"www.google.com/search": {
|
"www.google.com/search": {
|
||||||
selector: `h3, .IsZvec, .VwiC3b`,
|
selector: `h3, .IsZvec, .VwiC3b`,
|
||||||
},
|
},
|
||||||
"news.google.com": {
|
|
||||||
selector: `[data-n-tid], ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"www.foxnews.com": {
|
|
||||||
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
|
|
||||||
},
|
|
||||||
"bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php": {
|
|
||||||
selector: `${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"themessenger.com": {
|
|
||||||
selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"www.telegraph.co.uk, go.dev/doc/": {
|
|
||||||
selector: `article ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"www.theguardian.com": {
|
|
||||||
selector: `.show-underline, .dcr-hup5wm div, .dcr-7vl6y8 div, .dcr-12evv1c, figcaption, article ${DEFAULT_SELECTOR}, [data-cy="mostviewed-footer"] h4`,
|
|
||||||
},
|
|
||||||
"www.semafor.com": {
|
|
||||||
selector: `${DEFAULT_SELECTOR}, .styles_intro__IYj__, [class*="styles_description"]`,
|
|
||||||
},
|
|
||||||
"www.noemamag.com": {
|
|
||||||
selector: `.splash__title, .single-card__title, .single-card__type, .single-card__topic, .highlighted-content__title, .single-card__author, article ${DEFAULT_SELECTOR}, .quote__text, .wp-caption-text div`,
|
|
||||||
},
|
|
||||||
"restofworld.org": {
|
|
||||||
selector: `${DEFAULT_SELECTOR}, .recirc-story__headline, .recirc-story__dek`,
|
|
||||||
},
|
|
||||||
"www.axios.com": {
|
|
||||||
selector: `.h7, ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"www.newyorker.com": {
|
|
||||||
selector: `.summary-item__hed, .summary-item__dek, .summary-collection-grid__dek, .dqtvfu, .rubric__link, .caption, article ${DEFAULT_SELECTOR}, .HEhan ${DEFAULT_SELECTOR}, .ContributorBioBio-fBolsO, .BaseText-ewhhUZ`,
|
|
||||||
},
|
|
||||||
"time.com": {
|
|
||||||
selector: `h1, h3, .summary, .video-title, #article-body ${DEFAULT_SELECTOR}, .image-wrap-container .credit.body-caption, .media-heading`,
|
|
||||||
},
|
|
||||||
"www.dw.com": {
|
|
||||||
selector: `.ts-teaser-title a, .news-title a, .title a, .teaser-description a, .hbudab h3, .hbudab p, figcaption ,article ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"www.bbc.com": {
|
|
||||||
selector: `h1, h2, .media__link, .media__summary, article ${DEFAULT_SELECTOR}, .ssrcss-y7krbn-Stack, .ssrcss-17zglt8-PromoHeadline, .ssrcss-18cjaf3-Headline, .gs-c-promo-heading__title, .gs-c-promo-summary, .media__content h3, .article__intro, .lx-c-summary-points>li`,
|
|
||||||
},
|
|
||||||
"www.chinadaily.com.cn": {
|
|
||||||
selector: `h1, .tMain [shape="rect"], .cMain [shape="rect"], .photo_art [shape="rect"], .mai_r [shape="rect"], .lisBox li, #Content ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"www.facebook.com": {
|
|
||||||
selector: `[role="main"] [dir="auto"]`,
|
|
||||||
},
|
|
||||||
"www.reddit.com, new.reddit.com, sh.reddit.com": {
|
|
||||||
selector: `:is(#AppRouter-main-content, #overlayScrollContainer) :is([class^=tbIA],[class^=_1zP],[class^=ULWj],[class^=_2Jj], [class^=_334],[class^=_2Gr],[class^=_7T4],[class^=_1WO], ${DEFAULT_SELECTOR}); [id^="post-title"], :is([slot="text-body"], [slot="comment"]) ${DEFAULT_SELECTOR}, recent-posts h3, aside :is(span:has(>h2), p); shreddit-subreddit-header >>> :is(#title, #description)`,
|
|
||||||
},
|
|
||||||
"www.quora.com": {
|
|
||||||
selector: `.qu-wordBreak--break-word`,
|
|
||||||
},
|
|
||||||
"edition.cnn.com": {
|
|
||||||
selector: `.container__title, .container__headline, .headline__text, .image__caption, [data-type="Title"], .article__content ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"www.reuters.com": {
|
|
||||||
selector: `#main-content [data-testid="Heading"], #main-content [data-testid="Body"], .article-body__content__17Yit ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"www.bloomberg.com": {
|
|
||||||
selector: `[data-component="headline"], [data-component="related-item-headline"], [data-component="title"], article ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"deno.land, docs.github.com": {
|
|
||||||
selector: `main ${DEFAULT_SELECTOR}`,
|
|
||||||
keepSelector: DEFAULT_KEEP_SELECTOR,
|
|
||||||
},
|
|
||||||
"doc.rust-lang.org": {
|
|
||||||
selector: `.content ${DEFAULT_SELECTOR}`,
|
|
||||||
keepSelector: DEFAULT_KEEP_SELECTOR,
|
|
||||||
},
|
|
||||||
"www.indiehackers.com": {
|
|
||||||
selector: `h1, h3, .content ${DEFAULT_SELECTOR}, .feed-item__title-link`,
|
|
||||||
},
|
|
||||||
"platform.openai.com/docs": {
|
|
||||||
selector: `.docs-body ${DEFAULT_SELECTOR}`,
|
|
||||||
keepSelector: DEFAULT_KEEP_SELECTOR,
|
|
||||||
},
|
|
||||||
"en.wikipedia.org": {
|
"en.wikipedia.org": {
|
||||||
selector: `h1, .mw-parser-output ${DEFAULT_SELECTOR}`,
|
selector: `h1, .mw-parser-output ${DEFAULT_SELECTOR}`,
|
||||||
keepSelector: `.mwe-math-element`,
|
keepSelector: `.mwe-math-element`,
|
||||||
},
|
},
|
||||||
"stackoverflow.com, serverfault.com, superuser.com, stackexchange.com, askubuntu.com, stackapps.com, mathoverflow.net":
|
|
||||||
{
|
|
||||||
selector: `.s-prose ${DEFAULT_SELECTOR}, .comment-copy, .question-hyperlink, .s-post-summary--content-title, .s-post-summary--content-excerpt`,
|
|
||||||
keepSelector: `${DEFAULT_KEEP_SELECTOR}, .math-container`,
|
|
||||||
},
|
|
||||||
"www.npmjs.com/package, developer.chrome.com/docs, medium.com, react.dev, create-react-app.dev, pytorch.org":
|
|
||||||
{
|
|
||||||
selector: `article ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"news.ycombinator.com": {
|
"news.ycombinator.com": {
|
||||||
selector: `.title, p`,
|
selector: `.title, p`,
|
||||||
fixerSelector: `.toptext, .commtext`,
|
fixerSelector: `.toptext, .commtext`,
|
||||||
fixerFunc: FIXER_BR,
|
|
||||||
},
|
},
|
||||||
"github.com": {
|
"github.com": {
|
||||||
selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop="description"], .markdown-title, bdi, .ws-pre-wrap, .status-meta, span.status-meta, .col-10.color-fg-muted, .TimelineItem-body, .pinned-item-list-item-content .color-fg-muted, .markdown-body td, .markdown-body th`,
|
selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop="description"], .markdown-title, bdi, .ws-pre-wrap, .status-meta, span.status-meta, .col-10.color-fg-muted, .TimelineItem-body, .pinned-item-list-item-content .color-fg-muted, .markdown-body td, .markdown-body th`,
|
||||||
keepSelector: DEFAULT_KEEP_SELECTOR,
|
keepSelector: DEFAULT_KEEP_SELECTOR,
|
||||||
},
|
},
|
||||||
"twitter.com": {
|
"twitter.com, https://x.com": {
|
||||||
selector: `[data-testid="tweetText"], [data-testid="birdwatch-pivot"]>div.css-1rynq56`,
|
selector: `[data-testid="tweetText"], [data-testid="birdwatch-pivot"]>div.css-1rynq56`,
|
||||||
keepSelector: `img, a, .r-18u37iz, .css-175oi2r`,
|
keepSelector: `img, a, .r-18u37iz, .css-175oi2r`,
|
||||||
},
|
},
|
||||||
"m.youtube.com": {
|
|
||||||
selector: `.slim-video-information-title .yt-core-attributed-string, .media-item-headline .yt-core-attributed-string, .comment-text .yt-core-attributed-string, .typography-body-2b .yt-core-attributed-string, #ytp-caption-window-container .ytp-caption-segment`,
|
|
||||||
selectStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
|
||||||
parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
|
||||||
keepSelector: `img, #content-text>a`,
|
|
||||||
},
|
|
||||||
"www.youtube.com": {
|
"www.youtube.com": {
|
||||||
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span, #ytp-caption-window-container .ytp-caption-segment`,
|
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span, #ytp-caption-window-container .ytp-caption-segment`,
|
||||||
selectStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
selectStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||||
parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||||
keepSelector: `img, #content-text>a`,
|
keepSelector: `img, #content-text>a`,
|
||||||
},
|
},
|
||||||
"bard.google.com": {
|
|
||||||
selector: `.query-content ${DEFAULT_SELECTOR}, message-content ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"www.bing.com, copilot.microsoft.com": {
|
|
||||||
selector: `.b_algoSlug, .rwrl_padref; .cib-serp-main >>> .ac-textBlock ${DEFAULT_SELECTOR}, .text-message-content div`,
|
|
||||||
},
|
|
||||||
"www.phoronix.com": {
|
|
||||||
selector: `article ${DEFAULT_SELECTOR}`,
|
|
||||||
fixerSelector: `.content`,
|
|
||||||
fixerFunc: FIXER_BR,
|
|
||||||
},
|
|
||||||
"wx2.qq.com": {
|
|
||||||
selector: `.js_message_plain`,
|
|
||||||
},
|
|
||||||
"app.slack.com/client/": {
|
|
||||||
selector: `.p-rich_text_section, .c-message_attachment__text, .p-rich_text_list li`,
|
|
||||||
},
|
|
||||||
"discord.com/channels/": {
|
|
||||||
selector: `div[class^=message], div[class^=headerText], div[class^=name_], section[aria-label='Search Results'] div[id^=message-content], div[id^=message]`,
|
|
||||||
keepSelector: `li[class^='card'] div[class^='message'], [class^='embedFieldValue'], [data-list-item-id^='forum-channel-list'] div[class^='headerText']`,
|
|
||||||
},
|
|
||||||
"t.me/s/": {
|
|
||||||
selector: `.js-message_text ${DEFAULT_SELECTOR}`,
|
|
||||||
fixerSelector: `.tgme_widget_message_text`,
|
|
||||||
fixerFunc: FIXER_BR,
|
|
||||||
},
|
|
||||||
"web.telegram.org/k": {
|
|
||||||
selector: `div.kiss-p`,
|
|
||||||
keepSelector: `div[class^=time], .peer-title, .document-wrapper, .message.spoilers-container custom-emoji-element, reactions-element`,
|
|
||||||
fixerSelector: `.message`,
|
|
||||||
fixerFunc: FIXER_BN_DIV,
|
|
||||||
},
|
|
||||||
"web.telegram.org/a": {
|
|
||||||
selector: `.text-content > .kiss-p`,
|
|
||||||
keepSelector: `.Reactions, .time, .peer-title, .document-wrapper, .message.spoilers-container custom-emoji-element`,
|
|
||||||
fixerSelector: `.text-content`,
|
|
||||||
fixerFunc: FIXER_BR_DIV,
|
|
||||||
},
|
|
||||||
"www.instagram.com/": {
|
|
||||||
selector: `h1, article span[dir=auto] > span[dir=auto], ._ab1y`,
|
|
||||||
},
|
|
||||||
"www.instagram.com/p/,www.instagram.com/reels/": {
|
|
||||||
selector: `h1, div[class='x9f619 xjbqb8w x78zum5 x168nmei x13lgxp2 x5pf9jr xo71vjh x1uhb9sk x1plvlek xryxfnj x1c4vz4f x2lah0s xdt5ytf xqjyukv x1cy8zhl x1oa3qoh x1nhvcw1'] > span[class='x1lliihq x1plvlek xryxfnj x1n2onr6 x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs x1s928wv xhkezso x1gmr53x x1cpjm7i x1fgarty x1943h6x x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj'], span[class='x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs xt0psk2 x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj']`,
|
|
||||||
},
|
|
||||||
"mail.google.com": {
|
|
||||||
selector: `.a3s.aiL ${DEFAULT_SELECTOR}, span[data-thread-id]`,
|
|
||||||
fixerSelector: `.a3s.aiL`,
|
|
||||||
fixerFunc: FIXER_BR,
|
|
||||||
},
|
|
||||||
"web.whatsapp.com": {
|
|
||||||
selector: `.copyable-text > span`,
|
|
||||||
},
|
|
||||||
"chat.openai.com": {
|
|
||||||
selector: `div[data-message-author-role] > div ${DEFAULT_SELECTOR}`,
|
|
||||||
fixerSelector: `div[data-message-author-role='user'] > div`,
|
|
||||||
fixerFunc: FIXER_BN,
|
|
||||||
},
|
|
||||||
"forum.ru-board.com": {
|
|
||||||
selector: `.tit, .dats, .kiss-p, .lgf ${DEFAULT_SELECTOR}`,
|
|
||||||
fixerSelector: `span.post`,
|
|
||||||
fixerFunc: FIXER_BR,
|
|
||||||
},
|
|
||||||
"education.github.com": {
|
|
||||||
selector: `${DEFAULT_SELECTOR}, a, summary, span.Button-content`,
|
|
||||||
},
|
|
||||||
"blogs.windows.com": {
|
|
||||||
selector: `${DEFAULT_SELECTOR}, .c-uhf-nav-link, figcaption`,
|
|
||||||
fixerSelector: `.t-content>div>ul>li`,
|
|
||||||
fixerFunc: FIXER_BR,
|
|
||||||
},
|
|
||||||
"developer.apple.com/documentation/": {
|
|
||||||
selector: `#main ${DEFAULT_SELECTOR}, #main .abstract .content, #main .abstract.content, #main .link span`,
|
|
||||||
keepSelector: DEFAULT_KEEP_SELECTOR,
|
|
||||||
},
|
|
||||||
"greasyfork.org": {
|
|
||||||
selector: `h2, .script-link, .script-description, #additional-info ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"www.fmkorea.com": {
|
|
||||||
selector: `#container ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"forum.arduino.cc": {
|
|
||||||
selector: `.top-row>.title, .featured-topic>.title, .link-top-line>.title, .category-description, .topic-excerpt, .fancy-title, .cooked ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"docs.arduino.cc": {
|
|
||||||
selector: `[class^="tutorial-module--left"] ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"www.historydefined.net": {
|
|
||||||
selector: `.wp-element-caption, ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"gobyexample.com": {
|
|
||||||
selector: `.docs p`,
|
|
||||||
keepSelector: `code`,
|
|
||||||
},
|
|
||||||
"go.dev/tour": {
|
|
||||||
selector: `#left-side ${DEFAULT_SELECTOR}`,
|
|
||||||
keepSelector: `code, img, svg >>> code`,
|
|
||||||
},
|
|
||||||
"pkg.go.dev": {
|
|
||||||
selector: `.Documentation-content ${DEFAULT_SELECTOR}`,
|
|
||||||
keepSelector: `${DEFAULT_KEEP_SELECTOR}, a, span`,
|
|
||||||
},
|
|
||||||
"docs.rs": {
|
|
||||||
selector: `.docblock ${DEFAULT_SELECTOR}, .docblock-short`,
|
|
||||||
keepSelector: `code >>> code`,
|
|
||||||
},
|
|
||||||
"randomnerdtutorials.com": {
|
|
||||||
selector: `article ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
"notebooks.githubusercontent.com/view/ipynb": {
|
|
||||||
selector: `#notebook-container ${DEFAULT_SELECTOR}`,
|
|
||||||
keepSelector: DEFAULT_KEEP_SELECTOR,
|
|
||||||
},
|
|
||||||
"developers.cloudflare.com": {
|
|
||||||
selector: `article ${DEFAULT_SELECTOR}, .WorkerStarter--description`,
|
|
||||||
keepSelector: `a[rel='noopener'], code`,
|
|
||||||
},
|
|
||||||
"ubuntuforums.org": {
|
|
||||||
fixerSelector: `.postcontent`,
|
|
||||||
fixerFunc: FIXER_BR,
|
|
||||||
},
|
|
||||||
"play.google.com/store/apps/details": {
|
|
||||||
fixerSelector: `[data-g-id="description"]`,
|
|
||||||
fixerFunc: FIXER_BR,
|
|
||||||
},
|
|
||||||
"news.yahoo.co.jp/articles/": {
|
|
||||||
fixerSelector: `.sc-cTsKDU`,
|
|
||||||
fixerFunc: FIXER_BN,
|
|
||||||
},
|
|
||||||
"chromereleases.googleblog.com": {
|
|
||||||
fixerSelector: `.post-content, .post-content > span, li > span`,
|
|
||||||
fixerFunc: FIXER_BR,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BUILTIN_RULES = Object.entries(RULES_MAP)
|
export const BUILTIN_RULES = Object.entries(RULES_MAP)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
OPT_DICT_BAIDU,
|
OPT_DICT_BAIDU,
|
||||||
DEFAULT_HTTP_TIMEOUT,
|
DEFAULT_HTTP_TIMEOUT,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
DEFAULT_TRANS_APIS,
|
DEFAULT_API_LIST,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import { DEFAULT_OW_RULE } from "./rules";
|
import { DEFAULT_OW_RULE } from "./rules";
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ export const OPT_INPUT_TRANS_SIGNS = ["/", "//", "\\", "\\\\", ">", ">>"];
|
|||||||
export const DEFAULT_INPUT_SHORTCUT = ["AltLeft", "KeyI"];
|
export const DEFAULT_INPUT_SHORTCUT = ["AltLeft", "KeyI"];
|
||||||
export const DEFAULT_INPUT_RULE = {
|
export const DEFAULT_INPUT_RULE = {
|
||||||
transOpen: true,
|
transOpen: true,
|
||||||
translator: OPT_TRANS_MICROSOFT,
|
apiSlug: OPT_TRANS_MICROSOFT,
|
||||||
fromLang: "auto",
|
fromLang: "auto",
|
||||||
toLang: "en",
|
toLang: "en",
|
||||||
triggerShortcut: DEFAULT_INPUT_SHORTCUT,
|
triggerShortcut: DEFAULT_INPUT_SHORTCUT,
|
||||||
@@ -75,7 +75,7 @@ export const OPT_TRANBOX_TRIGGER_ALL = [
|
|||||||
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
|
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
|
||||||
export const DEFAULT_TRANBOX_SETTING = {
|
export const DEFAULT_TRANBOX_SETTING = {
|
||||||
// transOpen: true, // 是否启用划词翻译(作废,移至rule)
|
// transOpen: true, // 是否启用划词翻译(作废,移至rule)
|
||||||
translator: OPT_TRANS_MICROSOFT,
|
apiSlug: OPT_TRANS_MICROSOFT,
|
||||||
fromLang: "auto",
|
fromLang: "auto",
|
||||||
toLang: "zh-CN",
|
toLang: "zh-CN",
|
||||||
toLang2: "en",
|
toLang2: "en",
|
||||||
@@ -109,17 +109,17 @@ export const DEFAULT_SUBRULES_LIST = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT__MOUSEHOVER_KEY = ["ControlLeft"];
|
export const DEFAULT_MOUSEHOVER_KEY = ["ControlLeft"];
|
||||||
export const DEFAULT_MOUSE_HOVER_SETTING = {
|
export const DEFAULT_MOUSE_HOVER_SETTING = {
|
||||||
useMouseHover: true, // 是否启用鼠标悬停翻译
|
useMouseHover: true, // 是否启用鼠标悬停翻译
|
||||||
mouseHoverKey: DEFAULT__MOUSEHOVER_KEY, // 鼠标悬停翻译组合键
|
mouseHoverKey: DEFAULT_MOUSEHOVER_KEY, // 鼠标悬停翻译组合键
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_SETTING = {
|
export const DEFAULT_SETTING = {
|
||||||
darkMode: false, // 深色模式
|
darkMode: false, // 深色模式
|
||||||
uiLang: "en", // 界面语言
|
uiLang: "en", // 界面语言
|
||||||
// fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量(移至transApis,作废)
|
// fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量(移至rule,作废)
|
||||||
// fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间(移至transApis,作废)
|
// fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间(移至rule,作废)
|
||||||
minLength: TRANS_MIN_LENGTH,
|
minLength: TRANS_MIN_LENGTH,
|
||||||
maxLength: TRANS_MAX_LENGTH,
|
maxLength: TRANS_MAX_LENGTH,
|
||||||
newlineLength: TRANS_NEWLINE_LENGTH,
|
newlineLength: TRANS_NEWLINE_LENGTH,
|
||||||
@@ -136,7 +136,7 @@ export const DEFAULT_SETTING = {
|
|||||||
// transTitle: false, // 是否同时翻译页面标题(移至rule,作废)
|
// transTitle: false, // 是否同时翻译页面标题(移至rule,作废)
|
||||||
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
||||||
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
|
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
|
||||||
transApis: DEFAULT_TRANS_APIS, // 翻译接口
|
transApis: DEFAULT_API_LIST, // 翻译接口 (v2.0 对象改为数组)
|
||||||
// mouseKey: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译(移至rule,作废)
|
// mouseKey: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译(移至rule,作废)
|
||||||
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
||||||
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
||||||
@@ -145,7 +145,7 @@ export const DEFAULT_SETTING = {
|
|||||||
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
|
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
|
||||||
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
|
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
|
||||||
// disableLangs: [], // 不翻译的语言(移至rule,作废)
|
// disableLangs: [], // 不翻译的语言(移至rule,作废)
|
||||||
transInterval: 200, // 翻译等待时间
|
transInterval: 100, // 翻译等待时间
|
||||||
langDetector: OPT_TRANS_MICROSOFT, // 远程语言识别服务
|
langDetector: OPT_TRANS_MICROSOFT, // 远程语言识别服务
|
||||||
mouseHoverSetting: DEFAULT_MOUSE_HOVER_SETTING, // 鼠标悬停翻译
|
mouseHoverSetting: DEFAULT_MOUSE_HOVER_SETTING, // 鼠标悬停翻译
|
||||||
};
|
};
|
||||||
|
|||||||
123
src/hooks/Api.js
123
src/hooks/Api.js
@@ -1,34 +1,107 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { DEFAULT_TRANS_APIS } from "../config";
|
import { DEFAULT_API_LIST, API_SPE_TYPES } from "../config";
|
||||||
import { useSetting } from "./Setting";
|
import { useSetting } from "./Setting";
|
||||||
|
|
||||||
export function useApi(translator) {
|
function useApiState() {
|
||||||
const { setting, updateSetting } = useSetting();
|
const { setting, updateSetting } = useSetting();
|
||||||
const transApis = setting?.transApis || DEFAULT_TRANS_APIS;
|
const transApis = setting?.transApis || [];
|
||||||
|
|
||||||
const updateApi = useCallback(
|
return { transApis, updateSetting };
|
||||||
async (obj) => {
|
}
|
||||||
const api = {
|
|
||||||
...DEFAULT_TRANS_APIS[translator],
|
export function useApiList() {
|
||||||
...(transApis[translator] || {}),
|
const { transApis, updateSetting } = useApiState();
|
||||||
};
|
|
||||||
Object.assign(transApis, { [translator]: { ...api, ...obj } });
|
const userApis = useMemo(
|
||||||
await updateSetting({ transApis });
|
() =>
|
||||||
},
|
transApis
|
||||||
[translator, transApis, updateSetting]
|
.filter((api) => !API_SPE_TYPES.builtin.has(api.apiSlug))
|
||||||
|
.sort((a, b) => a.apiSlug.localeCompare(b.apiSlug)),
|
||||||
|
[transApis]
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetApi = useCallback(async () => {
|
const builtinApis = useMemo(
|
||||||
Object.assign(transApis, { [translator]: DEFAULT_TRANS_APIS[translator] });
|
() => transApis.filter((api) => API_SPE_TYPES.builtin.has(api.apiSlug)),
|
||||||
await updateSetting({ transApis });
|
[transApis]
|
||||||
}, [translator, transApis, updateSetting]);
|
);
|
||||||
|
|
||||||
return {
|
const enabledApis = useMemo(
|
||||||
api: {
|
() => transApis.filter((api) => !api.isDisabled),
|
||||||
...DEFAULT_TRANS_APIS[translator],
|
[transApis]
|
||||||
...(transApis[translator] || {}),
|
);
|
||||||
},
|
|
||||||
updateApi,
|
const addApi = useCallback(
|
||||||
resetApi,
|
(apiType) => {
|
||||||
|
const defaultApiOpt =
|
||||||
|
DEFAULT_API_LIST.find((da) => da.apiType === apiType) || {};
|
||||||
|
const uuid = crypto.randomUUID();
|
||||||
|
const apiSlug = `${apiType}_${crypto.randomUUID()}`;
|
||||||
|
const apiName = `${apiType}_${uuid.slice(0, 8)}`;
|
||||||
|
const newApi = {
|
||||||
|
...defaultApiOpt,
|
||||||
|
apiSlug,
|
||||||
|
apiName,
|
||||||
|
apiType,
|
||||||
};
|
};
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
transApis: [...(prev?.transApis || []), newApi],
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[updateSetting]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteApi = useCallback(
|
||||||
|
(apiSlug) => {
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
transApis: (prev?.transApis || []).filter((api) => api.apiSlug !== apiSlug),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[updateSetting]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { transApis, userApis, builtinApis, enabledApis, addApi, deleteApi };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApiItem(apiSlug) {
|
||||||
|
const { transApis, updateSetting } = useApiState();
|
||||||
|
|
||||||
|
const api = useMemo(
|
||||||
|
() => transApis.find((a) => a.apiSlug === apiSlug),
|
||||||
|
[transApis, apiSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
const update = useCallback(
|
||||||
|
(updateData) => {
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
transApis: (prev?.transApis || []).map((item) =>
|
||||||
|
item.apiSlug === apiSlug ? { ...item, ...updateData, apiSlug } : item
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[apiSlug, updateSetting]
|
||||||
|
);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
transApis: (prev?.transApis || []).map((item) => {
|
||||||
|
if (item.apiSlug === apiSlug) {
|
||||||
|
const defaultApiOpt =
|
||||||
|
DEFAULT_API_LIST.find((da) => da.apiType === item.apiType) || {};
|
||||||
|
return {
|
||||||
|
...defaultApiOpt,
|
||||||
|
apiSlug: item.apiSlug,
|
||||||
|
apiName: item.apiName,
|
||||||
|
apiType: item.apiType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
}, [apiSlug, updateSetting]);
|
||||||
|
|
||||||
|
return { api, update, reset };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function useTextAudio(text, lan = "uk", spd = 3) {
|
|||||||
try {
|
try {
|
||||||
setSrc(await apiBaiduTTS(text, lan, spd));
|
setSrc(await apiBaiduTTS(text, lan, spd));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "baidu tts");
|
kissLog("baidu tts", err);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [text, lan, spd]);
|
}, [text, lan, spd]);
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export function useDarkMode() {
|
|||||||
updateSetting,
|
updateSetting,
|
||||||
} = useSetting();
|
} = useSetting();
|
||||||
|
|
||||||
const toggleDarkMode = useCallback(async () => {
|
const toggleDarkMode = useCallback(() => {
|
||||||
await updateSetting({ darkMode: !darkMode });
|
updateSetting({ darkMode: !darkMode });
|
||||||
}, [darkMode, updateSetting]);
|
}, [darkMode, updateSetting]);
|
||||||
|
|
||||||
return { darkMode, toggleDarkMode };
|
return { darkMode, toggleDarkMode };
|
||||||
|
|||||||
97
src/hooks/Confirm.js
Normal file
97
src/hooks/Confirm.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
useState,
|
||||||
|
useContext,
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import DialogContentText from "@mui/material/DialogContentText";
|
||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import { useI18n } from "./I18n";
|
||||||
|
|
||||||
|
const ConfirmContext = createContext(null);
|
||||||
|
|
||||||
|
export function ConfirmProvider({ children }) {
|
||||||
|
const [dialogConfig, setDialogConfig] = useState(null);
|
||||||
|
const resolveRef = useRef(null);
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const translatedDefaults = useMemo(
|
||||||
|
() => ({
|
||||||
|
title: i18n("confirm_title", "Confirm"),
|
||||||
|
message: i18n("confirm_message", "Are you sure you want to proceed?"),
|
||||||
|
confirmText: i18n("confirm_action", "Confirm"),
|
||||||
|
cancelText: i18n("cancel_action", "Cancel"),
|
||||||
|
}),
|
||||||
|
[i18n]
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirm = useCallback(
|
||||||
|
(config) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setDialogConfig({ ...translatedDefaults, ...config });
|
||||||
|
resolveRef.current = resolve;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[translatedDefaults]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (resolveRef.current) {
|
||||||
|
resolveRef.current(false);
|
||||||
|
}
|
||||||
|
setDialogConfig(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (resolveRef.current) {
|
||||||
|
resolveRef.current(true);
|
||||||
|
}
|
||||||
|
setDialogConfig(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmContext.Provider value={confirm}>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={!!dialogConfig}
|
||||||
|
onClose={handleClose}
|
||||||
|
aria-labelledby="confirm-dialog-title"
|
||||||
|
aria-describedby="confirm-dialog-description"
|
||||||
|
>
|
||||||
|
{dialogConfig && (
|
||||||
|
<>
|
||||||
|
<DialogTitle id="confirm-dialog-title">
|
||||||
|
{dialogConfig.title}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="confirm-dialog-description">
|
||||||
|
{dialogConfig.message}
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClose}>{dialogConfig.cancelText}</Button>
|
||||||
|
<Button onClick={handleConfirm} color="primary" autoFocus>
|
||||||
|
{dialogConfig.confirmText}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</ConfirmContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConfirm() {
|
||||||
|
const context = useContext(ConfirmContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useConfirm must be used within a ConfirmProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
17
src/hooks/DebouncedCallback.js
Normal file
17
src/hooks/DebouncedCallback.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useMemo, useEffect, useRef } from "react";
|
||||||
|
import { debounce } from "../libs/utils";
|
||||||
|
|
||||||
|
export function useDebouncedCallback(callback, delay) {
|
||||||
|
const callbackRef = useRef(callback);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
callbackRef.current = callback;
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
const debouncedCallback = useMemo(
|
||||||
|
() => debounce((...args) => callbackRef.current(...args), delay),
|
||||||
|
[delay]
|
||||||
|
);
|
||||||
|
|
||||||
|
return debouncedCallback;
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { STOKEY_FAB } from "../config";
|
import { STOKEY_FAB } from "../config";
|
||||||
import { useStorage } from "./Storage";
|
import { useStorage } from "./Storage";
|
||||||
|
|
||||||
|
const DEFAULT_FAB = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* fab hook
|
* fab hook
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useFab() {
|
export function useFab() {
|
||||||
const { data, update } = useStorage(STOKEY_FAB);
|
const { data, update } = useStorage(STOKEY_FAB, DEFAULT_FAB);
|
||||||
return { fab: data, updateFab: update };
|
return { fab: data, updateFab: update };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,55 @@
|
|||||||
import { KV_WORDS_KEY } from "../config";
|
import { STOKEY_WORDS, KV_WORDS_KEY } from "../config";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { trySyncWords } from "../libs/sync";
|
import { useStorage } from "./Storage";
|
||||||
import { getWordsWithDefault, setWords } from "../libs/storage";
|
|
||||||
import { useSyncMeta } from "./Sync";
|
const DEFAULT_FAVWORDS = {};
|
||||||
import { kissLog } from "../libs/log";
|
|
||||||
|
|
||||||
export function useFavWords() {
|
export function useFavWords() {
|
||||||
const [loading, setLoading] = useState(false);
|
const { data: favWords, save } = useStorage(
|
||||||
const [favWords, setFavWords] = useState({});
|
STOKEY_WORDS,
|
||||||
const { updateSyncMeta } = useSyncMeta();
|
DEFAULT_FAVWORDS,
|
||||||
|
KV_WORDS_KEY
|
||||||
|
);
|
||||||
|
|
||||||
const toggleFav = useCallback(
|
const toggleFav = useCallback(
|
||||||
async (word) => {
|
(word) => {
|
||||||
const favs = { ...favWords };
|
save((prev) => {
|
||||||
if (favs[word]) {
|
if (!prev[word]) {
|
||||||
delete favs[word];
|
return { ...prev, [word]: { createdAt: Date.now() } };
|
||||||
} else {
|
|
||||||
favs[word] = { createdAt: Date.now() };
|
|
||||||
}
|
}
|
||||||
await setWords(favs);
|
|
||||||
await updateSyncMeta(KV_WORDS_KEY);
|
const favs = { ...prev };
|
||||||
await trySyncWords();
|
delete favs[word];
|
||||||
setFavWords(favs);
|
return favs;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[updateSyncMeta, favWords]
|
[save]
|
||||||
);
|
);
|
||||||
|
|
||||||
const mergeWords = useCallback(
|
const mergeWords = useCallback(
|
||||||
async (newWords) => {
|
(words) => {
|
||||||
const favs = { ...favWords };
|
save((prev) => ({
|
||||||
newWords.forEach((word) => {
|
...words.reduce((acc, key) => {
|
||||||
if (!favs[word]) {
|
acc[key] = { createdAt: Date.now() };
|
||||||
favs[word] = { createdAt: Date.now() };
|
return acc;
|
||||||
}
|
}, {}),
|
||||||
});
|
...prev,
|
||||||
await setWords(favs);
|
}));
|
||||||
await updateSyncMeta(KV_WORDS_KEY);
|
|
||||||
await trySyncWords();
|
|
||||||
setFavWords(favs);
|
|
||||||
},
|
},
|
||||||
[updateSyncMeta, favWords]
|
[save]
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearWords = useCallback(async () => {
|
const clearWords = useCallback(() => {
|
||||||
await setWords({});
|
save({});
|
||||||
await updateSyncMeta(KV_WORDS_KEY);
|
}, [save]);
|
||||||
await trySyncWords();
|
|
||||||
setFavWords({});
|
|
||||||
}, [updateSyncMeta]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const favList = useMemo(
|
||||||
(async () => {
|
() =>
|
||||||
try {
|
Object.entries(favWords || {}).sort((a, b) => a[0].localeCompare(b[0])),
|
||||||
setLoading(true);
|
[favWords]
|
||||||
await trySyncWords();
|
);
|
||||||
const favWords = await getWordsWithDefault();
|
|
||||||
setFavWords(favWords);
|
|
||||||
} catch (err) {
|
|
||||||
kissLog(err, "query fav");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { loading, favWords, toggleFav, mergeWords, clearWords };
|
const wordList = useMemo(() => favList.map(([word]) => word), [favList]);
|
||||||
|
|
||||||
|
return { favWords, favList, wordList, toggleFav, mergeWords, clearWords };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { DEFAULT_INPUT_RULE } from "../config";
|
import { DEFAULT_INPUT_RULE } from "../config";
|
||||||
import { useSetting } from "./Setting";
|
import { useSetting } from "./Setting";
|
||||||
|
|
||||||
export function useInputRule() {
|
export function useInputRule() {
|
||||||
const { setting, updateSetting } = useSetting();
|
const { setting, updateChild } = useSetting();
|
||||||
const inputRule = setting?.inputRule || DEFAULT_INPUT_RULE;
|
const inputRule = setting?.inputRule || DEFAULT_INPUT_RULE;
|
||||||
|
const updateInputRule = updateChild("inputRule");
|
||||||
const updateInputRule = useCallback(
|
|
||||||
async (obj) => {
|
|
||||||
Object.assign(inputRule, obj);
|
|
||||||
await updateSetting({ inputRule });
|
|
||||||
},
|
|
||||||
[inputRule, updateSetting]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { inputRule, updateInputRule };
|
return { inputRule, updateInputRule };
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/hooks/Loading.js
Normal file
16
src/hooks/Loading.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return (
|
||||||
|
<center>
|
||||||
|
<Divider>
|
||||||
|
<Link
|
||||||
|
href={process.env.REACT_APP_HOMEPAGE}
|
||||||
|
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||||
|
</Divider>
|
||||||
|
<CircularProgress />
|
||||||
|
</center>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,19 +1,11 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { DEFAULT_MOUSE_HOVER_SETTING } from "../config";
|
import { DEFAULT_MOUSE_HOVER_SETTING } from "../config";
|
||||||
import { useSetting } from "./Setting";
|
import { useSetting } from "./Setting";
|
||||||
|
|
||||||
export function useMouseHoverSetting() {
|
export function useMouseHoverSetting() {
|
||||||
const { setting, updateSetting } = useSetting();
|
const { setting, updateChild } = useSetting();
|
||||||
const mouseHoverSetting =
|
const mouseHoverSetting =
|
||||||
setting?.mouseHoverSetting || DEFAULT_MOUSE_HOVER_SETTING;
|
setting?.mouseHoverSetting || DEFAULT_MOUSE_HOVER_SETTING;
|
||||||
|
const updateMouseHoverSetting = updateChild("mouseHoverSetting");
|
||||||
const updateMouseHoverSetting = useCallback(
|
|
||||||
async (obj) => {
|
|
||||||
Object.assign(mouseHoverSetting, obj);
|
|
||||||
await updateSetting({ mouseHoverSetting });
|
|
||||||
},
|
|
||||||
[mouseHoverSetting, updateSetting]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { mouseHoverSetting, updateMouseHoverSetting };
|
return { mouseHoverSetting, updateMouseHoverSetting };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +1,88 @@
|
|||||||
import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
|
import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
|
||||||
import { useStorage } from "./Storage";
|
import { useStorage } from "./Storage";
|
||||||
import { trySyncRules } from "../libs/sync";
|
|
||||||
import { checkRules } from "../libs/rules";
|
import { checkRules } from "../libs/rules";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useSyncMeta } from "./Sync";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 规则 hook
|
* 规则 hook
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useRules() {
|
export function useRules() {
|
||||||
const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
|
const { data: list, save } = useStorage(
|
||||||
const { updateSyncMeta } = useSyncMeta();
|
STOKEY_RULES,
|
||||||
|
DEFAULT_RULES,
|
||||||
const updateRules = useCallback(
|
KV_RULES_KEY
|
||||||
async (rules) => {
|
|
||||||
await save(rules);
|
|
||||||
await updateSyncMeta(KV_RULES_KEY);
|
|
||||||
trySyncRules();
|
|
||||||
},
|
|
||||||
[save, updateSyncMeta]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const add = useCallback(
|
const add = useCallback(
|
||||||
async (rule) => {
|
(rule) => {
|
||||||
const rules = [...list];
|
save((prev) => {
|
||||||
if (rule.pattern === "*") {
|
if (
|
||||||
return;
|
rule.pattern === "*" ||
|
||||||
|
prev.some((item) => item.pattern === rule.pattern)
|
||||||
|
) {
|
||||||
|
return prev;
|
||||||
}
|
}
|
||||||
if (rules.map((item) => item.pattern).includes(rule.pattern)) {
|
return [rule, ...prev];
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
rules.unshift(rule);
|
|
||||||
await updateRules(rules);
|
|
||||||
},
|
},
|
||||||
[list, updateRules]
|
[save]
|
||||||
);
|
);
|
||||||
|
|
||||||
const del = useCallback(
|
const del = useCallback(
|
||||||
async (pattern) => {
|
(pattern) => {
|
||||||
let rules = [...list];
|
save((prev) => {
|
||||||
if (pattern === "*") {
|
if (pattern === "*") {
|
||||||
return;
|
return prev;
|
||||||
}
|
}
|
||||||
rules = rules.filter((item) => item.pattern !== pattern);
|
return prev.filter((item) => item.pattern !== pattern);
|
||||||
await updateRules(rules);
|
});
|
||||||
},
|
},
|
||||||
[list, updateRules]
|
[save]
|
||||||
);
|
);
|
||||||
|
|
||||||
const clear = useCallback(async () => {
|
const clear = useCallback(() => {
|
||||||
let rules = [...list];
|
save((prev) => prev.filter((item) => item.pattern === "*"));
|
||||||
rules = rules.filter((item) => item.pattern === "*");
|
}, [save]);
|
||||||
await updateRules(rules);
|
|
||||||
}, [list, updateRules]);
|
|
||||||
|
|
||||||
const put = useCallback(
|
const put = useCallback(
|
||||||
async (pattern, obj) => {
|
(pattern, obj) => {
|
||||||
const rules = [...list];
|
save((prev) => {
|
||||||
if (pattern === "*") {
|
if (
|
||||||
obj.pattern = "*";
|
prev.some(
|
||||||
|
(item) => item.pattern === obj.pattern && item.pattern !== pattern
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return prev;
|
||||||
}
|
}
|
||||||
const rule = rules.find((r) => r.pattern === pattern);
|
return prev.map((item) =>
|
||||||
rule && Object.assign(rule, obj);
|
item.pattern === pattern ? { ...item, ...obj } : item
|
||||||
await updateRules(rules);
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[list, updateRules]
|
[save]
|
||||||
);
|
);
|
||||||
|
|
||||||
const merge = useCallback(
|
const merge = useCallback(
|
||||||
async (newRules) => {
|
(rules) => {
|
||||||
const rules = [...list];
|
save((prev) => {
|
||||||
newRules = checkRules(newRules);
|
const adds = checkRules(rules);
|
||||||
newRules.forEach((newRule) => {
|
if (adds.length === 0) {
|
||||||
const rule = rules.find(
|
return prev;
|
||||||
(oldRule) => oldRule.pattern === newRule.pattern
|
|
||||||
);
|
|
||||||
if (rule) {
|
|
||||||
Object.assign(rule, newRule);
|
|
||||||
} else {
|
|
||||||
rules.unshift(newRule);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const map = new Map();
|
||||||
|
// 不进行深度合并
|
||||||
|
// [...prev, ...adds].forEach((item) => {
|
||||||
|
// const k = item.pattern;
|
||||||
|
// map.set(k, { ...(map.get(k) || {}), ...item });
|
||||||
|
// });
|
||||||
|
prev.forEach((item) => map.set(item.pattern, item));
|
||||||
|
adds.forEach((item) => map.set(item.pattern, item));
|
||||||
|
return [...map.values()];
|
||||||
});
|
});
|
||||||
await updateRules(rules);
|
|
||||||
},
|
},
|
||||||
[list, updateRules]
|
[save]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { list, add, del, clear, put, merge };
|
return { list, add, del, clear, put, merge };
|
||||||
|
|||||||
@@ -1,51 +1,70 @@
|
|||||||
|
import { createContext, useCallback, useContext, useMemo } from "react";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
import { STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY } from "../config";
|
import { STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY } from "../config";
|
||||||
import { useStorage } from "./Storage";
|
import { useStorage } from "./Storage";
|
||||||
import { trySyncSetting } from "../libs/sync";
|
import { debounceSyncMeta } from "../libs/storage";
|
||||||
import { createContext, useCallback, useContext, useMemo } from "react";
|
import Loading from "./Loading";
|
||||||
import { debounce } from "../libs/utils";
|
|
||||||
import { useSyncMeta } from "./Sync";
|
|
||||||
|
|
||||||
const SettingContext = createContext({
|
const SettingContext = createContext({
|
||||||
setting: null,
|
setting: DEFAULT_SETTING,
|
||||||
updateSetting: async () => {},
|
updateSetting: () => {},
|
||||||
reloadSetting: async () => {},
|
reloadSetting: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function SettingProvider({ children }) {
|
export function SettingProvider({ children }) {
|
||||||
const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
|
const {
|
||||||
const { updateSyncMeta } = useSyncMeta();
|
data: setting,
|
||||||
|
isLoading,
|
||||||
const syncSetting = useMemo(
|
update,
|
||||||
() =>
|
reload,
|
||||||
debounce(() => {
|
} = useStorage(STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY);
|
||||||
trySyncSetting();
|
|
||||||
}, [2000]),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateSetting = useCallback(
|
const updateSetting = useCallback(
|
||||||
async (obj) => {
|
(objOrFn) => {
|
||||||
await update(obj);
|
update(objOrFn);
|
||||||
await updateSyncMeta(KV_SETTING_KEY);
|
debounceSyncMeta(KV_SETTING_KEY);
|
||||||
syncSetting();
|
|
||||||
},
|
},
|
||||||
[update, syncSetting, updateSyncMeta]
|
[update]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data) {
|
const updateChild = useCallback(
|
||||||
return;
|
(key) => async (obj) => {
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: { ...(prev?.[key] || {}), ...obj },
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[updateSetting]
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
setting,
|
||||||
|
updateSetting,
|
||||||
|
updateChild,
|
||||||
|
reloadSetting: reload,
|
||||||
|
}),
|
||||||
|
[setting, updateSetting, updateChild, reload]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!setting) {
|
||||||
|
<center>
|
||||||
|
<Alert severity="error" sx={{ maxWidth: 600, margin: "60px auto" }}>
|
||||||
|
<p>数据加载出错,请刷新页面或卸载后重新安装。</p>
|
||||||
|
<p>
|
||||||
|
Data loading error, please refresh the page or uninstall and
|
||||||
|
reinstall.
|
||||||
|
</p>
|
||||||
|
</Alert>
|
||||||
|
</center>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingContext.Provider
|
<SettingContext.Provider value={value}>{children}</SettingContext.Provider>
|
||||||
value={{
|
|
||||||
setting: data,
|
|
||||||
updateSetting,
|
|
||||||
reloadSetting: reload,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SettingContext.Provider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ export function useShortcut(action) {
|
|||||||
const { setting, updateSetting } = useSetting();
|
const { setting, updateSetting } = useSetting();
|
||||||
const shortcuts = setting?.shortcuts || DEFAULT_SHORTCUTS;
|
const shortcuts = setting?.shortcuts || DEFAULT_SHORTCUTS;
|
||||||
const shortcut = shortcuts[action] || [];
|
const shortcut = shortcuts[action] || [];
|
||||||
|
|
||||||
const setShortcut = useCallback(
|
const setShortcut = useCallback(
|
||||||
async (val) => {
|
(val) => {
|
||||||
Object.assign(shortcuts, { [action]: val });
|
updateSetting((prev) => ({
|
||||||
await updateSetting({ shortcuts });
|
...prev,
|
||||||
|
shortcuts: { ...(prev?.shortcuts || {}), [action]: val },
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
[action, shortcuts, updateSetting]
|
[action, updateSetting]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { shortcut, setShortcut };
|
return { shortcut, setShortcut };
|
||||||
|
|||||||
@@ -1,70 +1,144 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { storage } from "../libs/storage";
|
import { storage } from "../libs/storage";
|
||||||
import { kissLog } from "../libs/log";
|
import { kissLog } from "../libs/log";
|
||||||
|
import { syncData } from "../libs/sync";
|
||||||
|
import { useDebouncedCallback } from "./DebouncedCallback";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* 用于将组件状态与 Storage 同步
|
||||||
*
|
*
|
||||||
* @param {*} key
|
* @param {string} key 用于在 Storage 中存取值的键
|
||||||
* @param {*} defaultVal 需为调用hook外的常量
|
* @param {*} defaultVal 默认值。建议在组件外定义为常量。
|
||||||
* @returns
|
* @param {string} [syncKey=""] 用于远端同步的可选键名
|
||||||
|
* @returns {{
|
||||||
|
* data: *,
|
||||||
|
* save: (valueOrFn: any | ((prevData: any) => any)) => void,
|
||||||
|
* update: (partialDataOrFn: object | ((prevData: object) => object)) => void,
|
||||||
|
* remove: () => Promise<void>,
|
||||||
|
* reload: () => Promise<void>
|
||||||
|
* }}
|
||||||
*/
|
*/
|
||||||
export function useStorage(key, defaultVal) {
|
export function useStorage(key, defaultVal = null, syncKey = "") {
|
||||||
const [loading, setLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(defaultVal);
|
||||||
|
|
||||||
const save = useCallback(
|
|
||||||
async (val) => {
|
|
||||||
setData(val);
|
|
||||||
await storage.setObj(key, val);
|
|
||||||
},
|
|
||||||
[key]
|
|
||||||
);
|
|
||||||
|
|
||||||
const update = useCallback(
|
|
||||||
async (obj) => {
|
|
||||||
setData((pre = {}) => ({ ...pre, ...obj }));
|
|
||||||
await storage.putObj(key, obj);
|
|
||||||
},
|
|
||||||
[key]
|
|
||||||
);
|
|
||||||
|
|
||||||
const remove = useCallback(async () => {
|
|
||||||
setData(null);
|
|
||||||
await storage.del(key);
|
|
||||||
}, [key]);
|
|
||||||
|
|
||||||
const reload = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const val = await storage.getObj(key);
|
|
||||||
if (val) {
|
|
||||||
setData(val);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
kissLog(err, "storage reload");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [key]);
|
|
||||||
|
|
||||||
|
// 首次加载数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
let isMounted = true;
|
||||||
|
|
||||||
|
const loadInitialData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
const storedVal = await storage.getObj(key);
|
||||||
const val = await storage.getObj(key);
|
if (storedVal === undefined || storedVal === null) {
|
||||||
if (val) {
|
|
||||||
setData(val);
|
|
||||||
} else if (defaultVal) {
|
|
||||||
setData(defaultVal);
|
|
||||||
await storage.setObj(key, defaultVal);
|
await storage.setObj(key, defaultVal);
|
||||||
|
} else if (isMounted) {
|
||||||
|
setData(storedVal);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "storage load");
|
kissLog(`storage load error for key: ${key}`, err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (isMounted) {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadInitialData();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
}, [key, defaultVal]);
|
}, [key, defaultVal]);
|
||||||
|
|
||||||
return { data, save, update, remove, reload, loading };
|
// 远端同步
|
||||||
|
const runSync = useCallback(async (keyToSync, valueToSync) => {
|
||||||
|
try {
|
||||||
|
const { value, isNew } = await syncData(keyToSync, valueToSync);
|
||||||
|
if (isNew) {
|
||||||
|
setData(value);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
kissLog("Sync failed", keyToSync);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const debouncedSync = useDebouncedCallback(runSync, 3000);
|
||||||
|
|
||||||
|
// 持久化
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.setObj(key, data).catch((err) => {
|
||||||
|
kissLog(`storage save error for key: ${key}`, err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 触发远端同步
|
||||||
|
if (syncKey) {
|
||||||
|
debouncedSync(syncKey, data);
|
||||||
|
}
|
||||||
|
}, [key, syncKey, isLoading, data, debouncedSync]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全量替换状态值
|
||||||
|
* @param {any | ((prevData: any) => any)} valueOrFn 新的值或一个返回新值的函数。
|
||||||
|
*/
|
||||||
|
const save = useCallback((valueOrFn) => {
|
||||||
|
// kissLog("save storage:", valueOrFn);
|
||||||
|
setData((prevData) =>
|
||||||
|
typeof valueOrFn === "function" ? valueOrFn(prevData) : valueOrFn
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并对象到当前状态(假设状态是一个对象)。
|
||||||
|
* @param {object | ((prevData: object) => object)} partialDataOrFn 要合并的对象或一个返回该对象的函数。
|
||||||
|
*/
|
||||||
|
const update = useCallback((partialDataOrFn) => {
|
||||||
|
// kissLog("update storage:", partialDataOrFn);
|
||||||
|
setData((prevData) => {
|
||||||
|
const partialData =
|
||||||
|
typeof partialDataOrFn === "function"
|
||||||
|
? partialDataOrFn(prevData)
|
||||||
|
: partialDataOrFn;
|
||||||
|
// 确保 preData 是一个对象,避免展开 null 或 undefined
|
||||||
|
const baseObj =
|
||||||
|
typeof prevData === "object" && prevData !== null ? prevData : {};
|
||||||
|
return { ...baseObj, ...partialData };
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Storage 中删除该值,并将状态重置为 null。
|
||||||
|
*/
|
||||||
|
const remove = useCallback(async () => {
|
||||||
|
// kissLog("remove storage:");
|
||||||
|
try {
|
||||||
|
await storage.del(key);
|
||||||
|
setData(null);
|
||||||
|
} catch (err) {
|
||||||
|
kissLog(`storage remove error for key: ${key}`, err);
|
||||||
|
}
|
||||||
|
}, [key]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Storage 重新加载数据以覆盖当前状态。
|
||||||
|
*/
|
||||||
|
const reload = useCallback(async () => {
|
||||||
|
// kissLog("reload storage:");
|
||||||
|
try {
|
||||||
|
const storedVal = await storage.getObj(key);
|
||||||
|
setData(storedVal ?? defaultVal);
|
||||||
|
} catch (err) {
|
||||||
|
kissLog(`storage reload error for key: ${key}`, err);
|
||||||
|
// setData(defaultVal);
|
||||||
|
}
|
||||||
|
}, [key, defaultVal]);
|
||||||
|
|
||||||
|
return { data, save, update, remove, reload, isLoading };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { DEFAULT_SUBRULES_LIST, DEFAULT_OW_RULE } from "../config";
|
|||||||
import { useSetting } from "./Setting";
|
import { useSetting } from "./Setting";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { loadOrFetchSubRules } from "../libs/subRules";
|
import { loadOrFetchSubRules } from "../libs/subRules";
|
||||||
import { delSubRules } from "../libs/storage";
|
|
||||||
import { kissLog } from "../libs/log";
|
import { kissLog } from "../libs/log";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,50 +18,36 @@ export function useSubRules() {
|
|||||||
const selectedUrl = selectedSub.url;
|
const selectedUrl = selectedSub.url;
|
||||||
|
|
||||||
const selectSub = useCallback(
|
const selectSub = useCallback(
|
||||||
async (url) => {
|
(url) => {
|
||||||
const subrulesList = [...list];
|
updateSetting((prev) => ({
|
||||||
subrulesList.forEach((item) => {
|
...prev,
|
||||||
if (item.url === url) {
|
subrulesList: prev.subrulesList.map((item) => ({
|
||||||
item.selected = true;
|
...item,
|
||||||
} else {
|
selected: item.url === url,
|
||||||
item.selected = false;
|
})),
|
||||||
}
|
}));
|
||||||
});
|
|
||||||
await updateSetting({ subrulesList });
|
|
||||||
},
|
},
|
||||||
[list, updateSetting]
|
[updateSetting]
|
||||||
);
|
|
||||||
|
|
||||||
const updateSub = useCallback(
|
|
||||||
async (url, obj) => {
|
|
||||||
const subrulesList = [...list];
|
|
||||||
subrulesList.forEach((item) => {
|
|
||||||
if (item.url === url) {
|
|
||||||
Object.assign(item, obj);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await updateSetting({ subrulesList });
|
|
||||||
},
|
|
||||||
[list, updateSetting]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const addSub = useCallback(
|
const addSub = useCallback(
|
||||||
async (url) => {
|
(url) => {
|
||||||
const subrulesList = [...list];
|
updateSetting((prev) => ({
|
||||||
subrulesList.push({ url, selected: false });
|
...prev,
|
||||||
await updateSetting({ subrulesList });
|
subrulesList: [...prev.subrulesList, { url, selected: false }],
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
[list, updateSetting]
|
[updateSetting]
|
||||||
);
|
);
|
||||||
|
|
||||||
const delSub = useCallback(
|
const delSub = useCallback(
|
||||||
async (url) => {
|
(url) => {
|
||||||
let subrulesList = [...list];
|
updateSetting((prev) => ({
|
||||||
subrulesList = subrulesList.filter((item) => item.url !== url);
|
...prev,
|
||||||
await updateSetting({ subrulesList });
|
subrulesList: prev.subrulesList.filter((item) => item.url !== url),
|
||||||
await delSubRules(url);
|
}));
|
||||||
},
|
},
|
||||||
[list, updateSetting]
|
[updateSetting]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,7 +58,7 @@ export function useSubRules() {
|
|||||||
const rules = await loadOrFetchSubRules(selectedUrl);
|
const rules = await loadOrFetchSubRules(selectedUrl);
|
||||||
setSelectedRules(rules);
|
setSelectedRules(rules);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "loadOrFetchSubRules");
|
kissLog("loadOrFetchSubRules", err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -84,7 +69,6 @@ export function useSubRules() {
|
|||||||
return {
|
return {
|
||||||
subList: list,
|
subList: list,
|
||||||
selectSub,
|
selectSub,
|
||||||
updateSub,
|
|
||||||
addSub,
|
addSub,
|
||||||
delSub,
|
delSub,
|
||||||
selectedSub,
|
selectedSub,
|
||||||
@@ -100,15 +84,9 @@ export function useSubRules() {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useOwSubRule() {
|
export function useOwSubRule() {
|
||||||
const { setting, updateSetting } = useSetting();
|
const { setting, updateChild } = useSetting();
|
||||||
const { owSubrule = DEFAULT_OW_RULE } = setting;
|
const owSubrule = setting?.owSubrule || DEFAULT_OW_RULE;
|
||||||
|
const updateOwSubrule = updateChild("owSubrule");
|
||||||
const updateOwSubrule = useCallback(
|
|
||||||
async (obj) => {
|
|
||||||
await updateSetting({ owSubrule: { ...owSubrule, ...obj } });
|
|
||||||
},
|
|
||||||
[owSubrule, updateSetting]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { owSubrule, updateOwSubrule };
|
return { owSubrule, updateOwSubrule };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
|
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
|
||||||
import { useStorage } from "./Storage";
|
import { useStorage } from "./Storage";
|
||||||
|
|
||||||
@@ -16,15 +16,24 @@ export function useSync() {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useSyncMeta() {
|
export function useSyncMeta() {
|
||||||
const { sync, updateSync } = useSync();
|
const { updateSync } = useSync();
|
||||||
|
|
||||||
const updateSyncMeta = useCallback(
|
const updateSyncMeta = useCallback(
|
||||||
async (key) => {
|
(key) => {
|
||||||
const syncMeta = sync?.syncMeta || {};
|
updateSync((prevSync) => {
|
||||||
syncMeta[key] = { ...(syncMeta[key] || {}), updateAt: Date.now() };
|
const newSyncMeta = {
|
||||||
await updateSync({ syncMeta });
|
...(prevSync?.syncMeta || {}),
|
||||||
|
[key]: {
|
||||||
|
...(prevSync?.syncMeta?.[key] || {}),
|
||||||
|
updateAt: Date.now(),
|
||||||
},
|
},
|
||||||
[sync?.syncMeta, updateSync]
|
};
|
||||||
|
return { syncMeta: newSyncMeta };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updateSync]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { updateSyncMeta };
|
return { updateSyncMeta };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,25 +46,32 @@ export function useSyncCaches() {
|
|||||||
const { sync, updateSync, reloadSync } = useSync();
|
const { sync, updateSync, reloadSync } = useSync();
|
||||||
|
|
||||||
const updateDataCache = useCallback(
|
const updateDataCache = useCallback(
|
||||||
async (url) => {
|
(url) => {
|
||||||
const dataCaches = sync?.dataCaches || {};
|
updateSync((prevSync) => ({
|
||||||
dataCaches[url] = Date.now();
|
dataCaches: {
|
||||||
await updateSync({ dataCaches });
|
...(prevSync?.dataCaches || {}),
|
||||||
|
[url]: Date.now(),
|
||||||
},
|
},
|
||||||
[sync, updateSync]
|
}));
|
||||||
|
},
|
||||||
|
[updateSync]
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteDataCache = useCallback(
|
const deleteDataCache = useCallback(
|
||||||
async (url) => {
|
(url) => {
|
||||||
const dataCaches = sync?.dataCaches || {};
|
updateSync((prevSync) => {
|
||||||
delete dataCaches[url];
|
const newDataCaches = { ...(prevSync?.dataCaches || {}) };
|
||||||
await updateSync({ dataCaches });
|
delete newDataCaches[url];
|
||||||
|
return { dataCaches: newDataCaches };
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[sync, updateSync]
|
[updateSync]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dataCaches = useMemo(() => sync?.dataCaches || {}, [sync?.dataCaches]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dataCaches: sync?.dataCaches || {},
|
dataCaches,
|
||||||
updateDataCache,
|
updateDataCache,
|
||||||
deleteDataCache,
|
deleteDataCache,
|
||||||
reloadSync,
|
reloadSync,
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { DEFAULT_TRANBOX_SETTING } from "../config";
|
import { DEFAULT_TRANBOX_SETTING } from "../config";
|
||||||
import { useSetting } from "./Setting";
|
import { useSetting } from "./Setting";
|
||||||
|
|
||||||
export function useTranbox() {
|
export function useTranbox() {
|
||||||
const { setting, updateSetting } = useSetting();
|
const { setting, updateChild } = useSetting();
|
||||||
const tranboxSetting = setting?.tranboxSetting || DEFAULT_TRANBOX_SETTING;
|
const tranboxSetting = setting?.tranboxSetting || DEFAULT_TRANBOX_SETTING;
|
||||||
|
const updateTranbox = updateChild("tranboxSetting");
|
||||||
const updateTranbox = useCallback(
|
|
||||||
async (obj) => {
|
|
||||||
Object.assign(tranboxSetting, obj);
|
|
||||||
await updateSetting({ tranboxSetting });
|
|
||||||
},
|
|
||||||
[tranboxSetting, updateSetting]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { tranboxSetting, updateTranbox };
|
return { tranboxSetting, updateTranbox };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { tryDetectLang } from "../libs";
|
|
||||||
import { apiTranslate } from "../apis";
|
|
||||||
import { DEFAULT_TRANS_APIS } from "../config";
|
|
||||||
import { kissLog } from "../libs/log";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 翻译hook
|
|
||||||
* @param {*} q
|
|
||||||
* @param {*} rule
|
|
||||||
* @param {*} setting
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function useTranslate(q, rule, setting, docInfo) {
|
|
||||||
const [text, setText] = useState("");
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [sameLang, setSamelang] = useState(false);
|
|
||||||
|
|
||||||
const { translator, fromLang, toLang, detectRemote, skipLangs = [] } = rule;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
if (!q.replace(/\[(\d+)\]/g, "").trim()) {
|
|
||||||
setText(q);
|
|
||||||
setSamelang(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let deLang = "";
|
|
||||||
if (fromLang === "auto") {
|
|
||||||
deLang = await tryDetectLang(
|
|
||||||
q,
|
|
||||||
detectRemote === "true",
|
|
||||||
setting.langDetector
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (deLang && (toLang.includes(deLang) || skipLangs.includes(deLang))) {
|
|
||||||
setSamelang(true);
|
|
||||||
} else {
|
|
||||||
const [trText, isSame] = await apiTranslate({
|
|
||||||
translator,
|
|
||||||
text: q,
|
|
||||||
fromLang,
|
|
||||||
toLang,
|
|
||||||
apiSetting: {
|
|
||||||
...DEFAULT_TRANS_APIS[translator],
|
|
||||||
...(setting.transApis[translator] || {}),
|
|
||||||
},
|
|
||||||
docInfo,
|
|
||||||
});
|
|
||||||
setText(trText);
|
|
||||||
setSamelang(isSame);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
kissLog(err, "translate");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [
|
|
||||||
q,
|
|
||||||
translator,
|
|
||||||
fromLang,
|
|
||||||
toLang,
|
|
||||||
detectRemote,
|
|
||||||
skipLangs,
|
|
||||||
setting,
|
|
||||||
docInfo,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { text, sameLang, loading };
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ const parseMSToken = (token) => {
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(atob(token.split(".")[1])).exp;
|
return JSON.parse(atob(token.split(".")[1])).exp;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "parseMSToken");
|
kissLog("parseMSToken", err);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -131,17 +131,15 @@ const queueMap = new Map();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取批处理实例
|
* 获取批处理实例
|
||||||
* @param {*} translator
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export const getBatchQueue = (args, opts) => {
|
export const getBatchQueue = (args) => {
|
||||||
const { translator, from, to } = args;
|
const { from, to, apiSetting } = args;
|
||||||
const key = `${translator}_${from}_${to}`;
|
const key = `${apiSetting.apiSlug}_${from}_${to}`;
|
||||||
if (queueMap.has(key)) {
|
if (queueMap.has(key)) {
|
||||||
return queueMap.get(key);
|
return queueMap.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
const queue = BatchQueue(args, opts);
|
const queue = BatchQueue(args, apiSetting);
|
||||||
queueMap.set(key, queue);
|
queueMap.set(key, queue);
|
||||||
return queue;
|
return queue;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function _browser() {
|
|||||||
try {
|
try {
|
||||||
return require("webextension-polyfill");
|
return require("webextension-polyfill");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// kissLog(err, "browser");
|
// kissLog("browser", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const getHttpCache = async (input, init) => {
|
|||||||
return await parseResponse(res);
|
return await parseResponse(res);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "get cache");
|
kissLog("get cache", err);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -54,7 +54,12 @@ export const getHttpCache = async (input, init) => {
|
|||||||
* @param {*} init
|
* @param {*} init
|
||||||
* @param {*} data
|
* @param {*} data
|
||||||
*/
|
*/
|
||||||
export const putHttpCache = async (input, init, data) => {
|
export const putHttpCache = async (
|
||||||
|
input,
|
||||||
|
init,
|
||||||
|
data,
|
||||||
|
maxAge = DEFAULT_CACHE_TIMEOUT // todo: 从设置里面读取最大缓存时间
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const req = await newCacheReq(input, init);
|
const req = await newCacheReq(input, init);
|
||||||
const cache = await caches.open(CACHE_NAME);
|
const cache = await caches.open(CACHE_NAME);
|
||||||
@@ -62,13 +67,13 @@ export const putHttpCache = async (input, init, data) => {
|
|||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Cache-Control": `max-age=${DEFAULT_CACHE_TIMEOUT}`,
|
"Cache-Control": `max-age=${maxAge}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// res.headers.set("Cache-Control", `max-age=${DEFAULT_CACHE_TIMEOUT}`);
|
// res.headers.set("Cache-Control", `max-age=${maxAge}`);
|
||||||
await cache.put(req, res);
|
await cache.put(req, res);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "put cache");
|
kissLog("put cache", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const fetchPatcher = async (input, init = {}, opts) => {
|
|||||||
try {
|
try {
|
||||||
timeout = (await getSettingWithDefault()).httpTimeout;
|
timeout = (await getSettingWithDefault()).httpTimeout;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "getSettingWithDefault");
|
kissLog("getSettingWithDefault", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!timeout) {
|
if (!timeout) {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const tryClearCaches = async () => {
|
|||||||
try {
|
try {
|
||||||
caches.delete(CACHE_NAME);
|
caches.delete(CACHE_NAME);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "clean caches");
|
kissLog("clean caches", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export const tryDetectLang = async (
|
|||||||
try {
|
try {
|
||||||
lang = await langdetectMap[langDetector](q);
|
lang = await langdetectMap[langDetector](q);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "detect lang remote");
|
kissLog("detect lang remote", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ export const tryDetectLang = async (
|
|||||||
const res = await browser?.i18n?.detectLanguage(q);
|
const res = await browser?.i18n?.detectLanguage(q);
|
||||||
lang = res?.languages?.[0]?.language;
|
lang = res?.languages?.[0]?.language;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "detect lang local");
|
kissLog("detect lang local", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
DEFAULT_INPUT_RULE,
|
DEFAULT_INPUT_RULE,
|
||||||
DEFAULT_TRANS_APIS,
|
|
||||||
DEFAULT_INPUT_SHORTCUT,
|
DEFAULT_INPUT_SHORTCUT,
|
||||||
OPT_LANGS_LIST,
|
OPT_LANGS_LIST,
|
||||||
|
DEFAULT_API_SETTING,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
|
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
|
||||||
import { stepShortcutRegister } from "./shortcut";
|
import { stepShortcutRegister } from "./shortcut";
|
||||||
@@ -87,7 +87,7 @@ export default function inputTranslate({
|
|||||||
inputRule: {
|
inputRule: {
|
||||||
transOpen,
|
transOpen,
|
||||||
triggerShortcut,
|
triggerShortcut,
|
||||||
translator,
|
apiSlug,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
triggerCount,
|
triggerCount,
|
||||||
@@ -100,7 +100,8 @@ export default function inputTranslate({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiSetting = transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
|
const apiSetting =
|
||||||
|
transApis.find((api) => api.apiSlug === apiSlug) || DEFAULT_API_SETTING;
|
||||||
if (triggerShortcut.length === 0) {
|
if (triggerShortcut.length === 0) {
|
||||||
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
||||||
triggerCount = 1;
|
triggerCount = 1;
|
||||||
@@ -156,7 +157,7 @@ export default function inputTranslate({
|
|||||||
addLoading(node, loadingId);
|
addLoading(node, loadingId);
|
||||||
|
|
||||||
const [trText, isSame] = await apiTranslate({
|
const [trText, isSame] = await apiTranslate({
|
||||||
translator,
|
apiSlug,
|
||||||
text,
|
text,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
@@ -188,7 +189,7 @@ export default function inputTranslate({
|
|||||||
collapseToEnd(node);
|
collapseToEnd(node);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "translate input");
|
kissLog("translate input", err);
|
||||||
} finally {
|
} finally {
|
||||||
removeLoading(node, loadingId);
|
removeLoading(node, loadingId);
|
||||||
}
|
}
|
||||||
|
|||||||
136
src/libs/log.js
136
src/libs/log.js
@@ -1,12 +1,126 @@
|
|||||||
/**
|
// 定义日志级别
|
||||||
* 日志函数
|
export const LogLevel = {
|
||||||
* @param {*} msg
|
DEBUG: { value: 0, name: "DEBUG", color: "#6495ED" }, // 宝蓝色
|
||||||
* @param {*} type
|
INFO: { value: 1, name: "INFO", color: "#4CAF50" }, // 绿色
|
||||||
*/
|
WARN: { value: 2, name: "WARN", color: "#FFC107" }, // 琥珀色
|
||||||
export const kissLog = (msg, type) => {
|
ERROR: { value: 3, name: "ERROR", color: "#F44336" }, // 红色
|
||||||
let prefix = `[KISS-Translator]`;
|
SILENT: { value: 4, name: "SILENT" }, // 特殊级别,用于关闭所有日志
|
||||||
if (type) {
|
|
||||||
prefix += `[${type}]`;
|
|
||||||
}
|
|
||||||
console.log(`${prefix} ${msg}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
/**
|
||||||
|
* @param {object} [options={}] 配置选项
|
||||||
|
* @param {LogLevel} [options.level=LogLevel.INFO] 要显示的最低日志级别
|
||||||
|
* @param {string} [options.prefix='App'] 日志前缀,用于区分模块
|
||||||
|
*/
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.config = {
|
||||||
|
level: options.level || LogLevel.INFO,
|
||||||
|
prefix: options.prefix || "KISS-Translator",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态设置日志级别
|
||||||
|
* @param {LogLevel} level - 新的日志级别
|
||||||
|
*/
|
||||||
|
setLevel(level) {
|
||||||
|
if (level && typeof level.value === "number") {
|
||||||
|
this.config.level = level;
|
||||||
|
console.log(`[${this.config.prefix}] Log level set to ${level.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核心日志记录方法
|
||||||
|
* @private
|
||||||
|
* @param {LogLevel} level - 当前消息的日志级别
|
||||||
|
* @param {...any} args - 要记录的多个参数,可以是任何类型
|
||||||
|
*/
|
||||||
|
_log(level, ...args) {
|
||||||
|
// 如果当前级别低于配置的最低级别,则不打印
|
||||||
|
if (level.value < this.config.level.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const prefixStr = `[${this.config.prefix}]`;
|
||||||
|
const levelStr = `[${level.name}]`;
|
||||||
|
|
||||||
|
// 判断是否在浏览器环境并且浏览器支持 console 样式
|
||||||
|
const isBrowser =
|
||||||
|
typeof window !== "undefined" && typeof window.document !== "undefined";
|
||||||
|
|
||||||
|
if (isBrowser) {
|
||||||
|
// 在浏览器中使用颜色高亮
|
||||||
|
const consoleMethod = this._getConsoleMethod(level);
|
||||||
|
consoleMethod(
|
||||||
|
`%c${timestamp} %c${prefixStr} %c${levelStr}`,
|
||||||
|
"color: gray; font-weight: lighter;", // 时间戳样式
|
||||||
|
"color: #7c57e0; font-weight: bold;", // 前缀样式 (紫色)
|
||||||
|
`color: ${level.color}; font-weight: bold;`, // 日志级别样式
|
||||||
|
...args
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 在 Node.js 或不支持样式的环境中,输出纯文本
|
||||||
|
const consoleMethod = this._getConsoleMethod(level);
|
||||||
|
consoleMethod(timestamp, prefixStr, levelStr, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据日志级别获取对应的 console 方法
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getConsoleMethod(level) {
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.ERROR:
|
||||||
|
return console.error;
|
||||||
|
case LogLevel.WARN:
|
||||||
|
return console.warn;
|
||||||
|
case LogLevel.INFO:
|
||||||
|
return console.info;
|
||||||
|
default:
|
||||||
|
return console.log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 DEBUG 级别的日志
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
|
debug(...args) {
|
||||||
|
this._log(LogLevel.DEBUG, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 INFO 级别的日志
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
|
info(...args) {
|
||||||
|
this._log(LogLevel.INFO, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 WARN 级别的日志
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
|
warn(...args) {
|
||||||
|
this._log(LogLevel.WARN, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 ERROR 级别的日志
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
|
error(...args) {
|
||||||
|
this._log(LogLevel.ERROR, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDevelopment =
|
||||||
|
typeof process === "undefined" || process.env.NODE_ENV !== "development";
|
||||||
|
const defaultLevel = isDevelopment ? LogLevel.DEBUG : LogLevel.INFO;
|
||||||
|
|
||||||
|
export const logger = new Logger({ level: defaultLevel });
|
||||||
|
export const kissLog = logger.info.bind(logger);
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class TaskPool {
|
|||||||
const res = await fn(args);
|
const res = await fn(args);
|
||||||
resolve(res);
|
resolve(res);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "task");
|
kissLog("task pool", err);
|
||||||
if (retry < this.#maxRetry) {
|
if (retry < this.#maxRetry) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.#pool.unshift({ ...task, retry: retry + 1 }); // unshift 保证重试任务优先
|
this.#pool.unshift({ ...task, retry: retry + 1 }); // unshift 保证重试任务优先
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { matchValue, type, isMatch } from "./utils";
|
|||||||
import {
|
import {
|
||||||
GLOBAL_KEY,
|
GLOBAL_KEY,
|
||||||
REMAIN_KEY,
|
REMAIN_KEY,
|
||||||
OPT_TRANS_ALL,
|
|
||||||
OPT_STYLE_ALL,
|
OPT_STYLE_ALL,
|
||||||
OPT_LANGS_FROM,
|
OPT_LANGS_FROM,
|
||||||
OPT_LANGS_TO,
|
OPT_LANGS_TO,
|
||||||
// OPT_TIMING_ALL,
|
// OPT_TIMING_ALL,
|
||||||
GLOBLA_RULE,
|
GLOBLA_RULE,
|
||||||
|
DEFAULT_API_TYPE,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { loadOrFetchSubRules } from "./subRules";
|
import { loadOrFetchSubRules } from "./subRules";
|
||||||
import { getRulesWithDefault, setRules } from "./storage";
|
import { getRulesWithDefault, setRules } from "./storage";
|
||||||
@@ -50,7 +50,7 @@ export const matchRule = async (
|
|||||||
rules.splice(-1, 0, ...subRules);
|
rules.splice(-1, 0, ...subRules);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "load injectRules");
|
kissLog("load injectRules", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export const matchRule = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
[
|
[
|
||||||
"translator",
|
"apiSlug",
|
||||||
"fromLang",
|
"fromLang",
|
||||||
"toLang",
|
"toLang",
|
||||||
"transOpen",
|
"transOpen",
|
||||||
@@ -158,7 +158,7 @@ export const checkRules = (rules) => {
|
|||||||
parentStyle,
|
parentStyle,
|
||||||
injectJs,
|
injectJs,
|
||||||
injectCss,
|
injectCss,
|
||||||
translator,
|
apiSlug,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
textStyle,
|
textStyle,
|
||||||
@@ -193,7 +193,7 @@ export const checkRules = (rules) => {
|
|||||||
injectCss: type(injectCss) === "string" ? injectCss : "",
|
injectCss: type(injectCss) === "string" ? injectCss : "",
|
||||||
bgColor: type(bgColor) === "string" ? bgColor : "",
|
bgColor: type(bgColor) === "string" ? bgColor : "",
|
||||||
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
|
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
|
||||||
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
|
apiSlug: apiSlug?.trim() || DEFAULT_API_TYPE,
|
||||||
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
|
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
|
||||||
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
|
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
|
||||||
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
|
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export class ShadowRootMonitor {
|
|||||||
try {
|
try {
|
||||||
monitorInstance.callback(shadowRoot);
|
monitorInstance.callback(shadowRoot);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
kissLog(error, "Error in ShadowRootMonitor callback");
|
kissLog("Error in ShadowRootMonitor callback", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return shadowRoot;
|
return shadowRoot;
|
||||||
|
|||||||
@@ -1,112 +1,106 @@
|
|||||||
import { isSameSet } from "./utils";
|
import { isSameSet } from "./utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 键盘快捷键监听
|
* 键盘快捷键监听器
|
||||||
* @param {*} fn
|
* @param {(pressedKeys: Set<string>, event: KeyboardEvent) => void} onKeyDown - Keydown 回调
|
||||||
* @param {*} target
|
* @param {(pressedKeys: Set<string>, event: KeyboardEvent) => void} onKeyUp - Keyup 回调
|
||||||
* @param {*} timeout
|
* @param {EventTarget} target - 监听的目标元素
|
||||||
* @returns
|
* @returns {() => void} - 用于注销监听的函数
|
||||||
*/
|
*/
|
||||||
export const shortcutListener = (fn, target = document, timeout = 3000) => {
|
export const shortcutListener = (
|
||||||
const allkeys = new Set();
|
onKeyDown = () => {},
|
||||||
const curkeys = new Set();
|
onKeyUp = () => {},
|
||||||
let timer = null;
|
target = document
|
||||||
|
) => {
|
||||||
|
const pressedKeys = new Set();
|
||||||
|
|
||||||
const handleKeydown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
timer && clearTimeout(timer);
|
if (pressedKeys.has(e.code)) return;
|
||||||
timer = setTimeout(() => {
|
pressedKeys.add(e.code);
|
||||||
allkeys.clear();
|
onKeyDown(new Set(pressedKeys), e);
|
||||||
curkeys.clear();
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = null;
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
if (e.code) {
|
|
||||||
allkeys.add(e.code);
|
|
||||||
curkeys.add(e.code);
|
|
||||||
fn([...curkeys], [...allkeys]);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyup = (e) => {
|
const handleKeyUp = (e) => {
|
||||||
curkeys.delete(e.code);
|
// onKeyUp 应该在 key 从集合中移除前触发,以便判断组合键
|
||||||
if (curkeys.size === 0) {
|
onKeyUp(new Set(pressedKeys), e);
|
||||||
fn([...curkeys], [...allkeys]);
|
pressedKeys.delete(e.code);
|
||||||
allkeys.clear();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
target.addEventListener("keydown", handleKeydown, true);
|
target.addEventListener("keydown", handleKeyDown);
|
||||||
target.addEventListener("keyup", handleKeyup, true);
|
target.addEventListener("keyup", handleKeyUp);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (timer) {
|
target.removeEventListener("keydown", handleKeyDown);
|
||||||
clearTimeout(timer);
|
target.removeEventListener("keyup", handleKeyUp);
|
||||||
timer = null;
|
pressedKeys.clear();
|
||||||
}
|
|
||||||
target.removeEventListener("keydown", handleKeydown);
|
|
||||||
target.removeEventListener("keyup", handleKeyup);
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册键盘快捷键
|
* 注册键盘快捷键
|
||||||
* @param {*} targetKeys
|
* @param {string[]} targetKeys - 目标快捷键数组
|
||||||
* @param {*} fn
|
* @param {() => void} fn - 匹配成功后执行的回调
|
||||||
* @param {*} target
|
* @param {EventTarget} target - 监听目标
|
||||||
* @returns
|
* @returns {() => void} - 注销函数
|
||||||
*/
|
*/
|
||||||
export const shortcutRegister = (targetKeys = [], fn, target = document) => {
|
export const shortcutRegister = (targetKeys = [], fn, target = document) => {
|
||||||
return shortcutListener((curkeys) => {
|
if (targetKeys.length === 0) return () => {};
|
||||||
if (
|
|
||||||
targetKeys.length > 0 &&
|
const targetKeySet = new Set(targetKeys);
|
||||||
isSameSet(new Set(targetKeys), new Set(curkeys))
|
const onKeyDown = (pressedKeys, event) => {
|
||||||
) {
|
if (targetKeySet.size > 0 && isSameSet(targetKeySet, pressedKeys)) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
fn();
|
fn();
|
||||||
}
|
}
|
||||||
}, target);
|
};
|
||||||
|
const onKeyUp = () => {};
|
||||||
|
|
||||||
|
return shortcutListener(onKeyDown, onKeyUp, target);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高阶函数:为目标函数增加计次和超时重置功能
|
||||||
|
* @param {() => void} fn - 需要被包装的函数
|
||||||
|
* @param {number} step - 需要触发的次数
|
||||||
|
* @param {number} timeout - 超时毫秒数
|
||||||
|
* @returns {() => void} - 包装后的新函数
|
||||||
|
*/
|
||||||
|
const withStepCounter = (fn, step, timeout) => {
|
||||||
|
let count = 0;
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
timer && clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
count = 0;
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
count++;
|
||||||
|
if (count === step) {
|
||||||
|
count = 0;
|
||||||
|
clearTimeout(timer);
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册连续快捷键
|
* 注册连续快捷键
|
||||||
* @param {*} targetKeys
|
* @param {string[]} targetKeys - 目标快捷键数组
|
||||||
* @param {*} fn
|
* @param {() => void} fn - 成功回调
|
||||||
* @param {*} step
|
* @param {number} step - 连续触发次数
|
||||||
* @param {*} timeout
|
* @param {number} timeout - 每次触发的间隔超时
|
||||||
* @param {*} target
|
* @param {EventTarget} target - 监听目标
|
||||||
* @returns
|
* @returns {() => void} - 注销函数
|
||||||
*/
|
*/
|
||||||
export const stepShortcutRegister = (
|
export const stepShortcutRegister = (
|
||||||
targetKeys = [],
|
targetKeys = [],
|
||||||
fn,
|
fn,
|
||||||
step = 3,
|
step = 2,
|
||||||
timeout = 500,
|
timeout = 500,
|
||||||
target = document
|
target = document
|
||||||
) => {
|
) => {
|
||||||
let count = 0;
|
const steppedFn = withStepCounter(fn, step, timeout);
|
||||||
let pre = Date.now();
|
return shortcutRegister(targetKeys, steppedFn, target);
|
||||||
let timer;
|
|
||||||
return shortcutListener((curkeys, allkeys) => {
|
|
||||||
timer && clearTimeout(timer);
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
count = 0;
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
if (targetKeys.length > 0 && curkeys.length === 0) {
|
|
||||||
const now = Date.now();
|
|
||||||
if (
|
|
||||||
(count === 0 || now - pre < timeout) &&
|
|
||||||
isSameSet(new Set(targetKeys), new Set(allkeys))
|
|
||||||
) {
|
|
||||||
count++;
|
|
||||||
if (count === step) {
|
|
||||||
count = 0;
|
|
||||||
fn();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
count = 0;
|
|
||||||
}
|
|
||||||
pre = now;
|
|
||||||
}
|
|
||||||
}, target);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { isExt, isGm } from "./client";
|
import { isExt, isGm } from "./client";
|
||||||
import { browser } from "./browser";
|
import { browser } from "./browser";
|
||||||
import { kissLog } from "./log";
|
import { kissLog } from "./log";
|
||||||
|
import { debounce } from "./utils";
|
||||||
|
|
||||||
async function set(key, val) {
|
async function set(key, val) {
|
||||||
if (isExt) {
|
if (isExt) {
|
||||||
@@ -90,7 +91,7 @@ export const getSettingWithDefault = async () => ({
|
|||||||
...((await getSetting()) || {}),
|
...((await getSetting()) || {}),
|
||||||
});
|
});
|
||||||
export const setSetting = (val) => setObj(STOKEY_SETTING, val);
|
export const setSetting = (val) => setObj(STOKEY_SETTING, val);
|
||||||
export const updateSetting = (obj) => putObj(STOKEY_SETTING, obj);
|
export const putSetting = (obj) => putObj(STOKEY_SETTING, obj);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 规则列表
|
* 规则列表
|
||||||
@@ -122,14 +123,20 @@ export const setSubRules = (url, val) =>
|
|||||||
export const getFab = () => getObj(STOKEY_FAB);
|
export const getFab = () => getObj(STOKEY_FAB);
|
||||||
export const getFabWithDefault = async () => (await getFab()) || {};
|
export const getFabWithDefault = async () => (await getFab()) || {};
|
||||||
export const setFab = (obj) => setObj(STOKEY_FAB, obj);
|
export const setFab = (obj) => setObj(STOKEY_FAB, obj);
|
||||||
export const updateFab = (obj) => putObj(STOKEY_FAB, obj);
|
export const putFab = (obj) => putObj(STOKEY_FAB, obj);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据同步
|
* 数据同步
|
||||||
*/
|
*/
|
||||||
export const getSync = () => getObj(STOKEY_SYNC);
|
export const getSync = () => getObj(STOKEY_SYNC);
|
||||||
export const getSyncWithDefault = async () => (await getSync()) || DEFAULT_SYNC;
|
export const getSyncWithDefault = async () => (await getSync()) || DEFAULT_SYNC;
|
||||||
export const updateSync = (obj) => putObj(STOKEY_SYNC, obj);
|
export const putSync = (obj) => putObj(STOKEY_SYNC, obj);
|
||||||
|
export const putSyncMeta = async (key) => {
|
||||||
|
const { syncMeta = {} } = await getSyncWithDefault();
|
||||||
|
syncMeta[key] = { ...(syncMeta[key] || {}), updateAt: Date.now() };
|
||||||
|
await putSync({ syncMeta });
|
||||||
|
};
|
||||||
|
export const debounceSyncMeta = debounce(putSyncMeta, 300);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ms auth
|
* ms auth
|
||||||
@@ -156,6 +163,6 @@ export const tryInitDefaultData = async () => {
|
|||||||
BUILTIN_RULES
|
BUILTIN_RULES
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "init default");
|
kissLog("init default", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { GLOBAL_KEY } from "../config";
|
import { GLOBAL_KEY } from "../config";
|
||||||
import {
|
import {
|
||||||
getSyncWithDefault,
|
getSyncWithDefault,
|
||||||
updateSync,
|
putSync,
|
||||||
setSubRules,
|
setSubRules,
|
||||||
getSubRules,
|
getSubRules,
|
||||||
} from "./storage";
|
} from "./storage";
|
||||||
@@ -17,7 +17,7 @@ import { kissLog } from "./log";
|
|||||||
const updateSyncDataCache = async (url) => {
|
const updateSyncDataCache = async (url) => {
|
||||||
const { dataCaches = {} } = await getSyncWithDefault();
|
const { dataCaches = {} } = await getSyncWithDefault();
|
||||||
dataCaches[url] = Date.now();
|
dataCaches[url] = Date.now();
|
||||||
await updateSync({ dataCaches });
|
await putSync({ dataCaches });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,7 +47,7 @@ export const syncAllSubRules = async (subrulesList) => {
|
|||||||
await syncSubRules(subrules.url);
|
await syncSubRules(subrules.url);
|
||||||
await updateSyncDataCache(subrules.url);
|
await updateSyncDataCache(subrules.url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, `sync subrule error: ${subrules.url}`);
|
kissLog(`sync subrule error: ${subrules.url}`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -65,10 +65,10 @@ export const trySyncAllSubRules = async ({ subrulesList }) => {
|
|||||||
if (now - subRulesSyncAt > interval) {
|
if (now - subRulesSyncAt > interval) {
|
||||||
// 同步订阅规则
|
// 同步订阅规则
|
||||||
await syncAllSubRules(subrulesList);
|
await syncAllSubRules(subrulesList);
|
||||||
await updateSync({ subRulesSyncAt: now });
|
await putSync({ subRulesSyncAt: now });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "try sync all subrules");
|
kissLog("try sync all subrules", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "../config";
|
} from "../config";
|
||||||
import {
|
import {
|
||||||
getSyncWithDefault,
|
getSyncWithDefault,
|
||||||
updateSync,
|
putSync,
|
||||||
getSettingWithDefault,
|
getSettingWithDefault,
|
||||||
getRulesWithDefault,
|
getRulesWithDefault,
|
||||||
getWordsWithDefault,
|
getWordsWithDefault,
|
||||||
@@ -61,7 +61,7 @@ const syncByWorker = async (data, { syncUrl, syncKey }) => {
|
|||||||
return await apiSyncData(`${syncUrl}/sync`, syncKey, data);
|
return await apiSyncData(`${syncUrl}/sync`, syncKey, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncData = async (key, valueFn) => {
|
export const syncData = async (key, value) => {
|
||||||
const {
|
const {
|
||||||
syncType,
|
syncType,
|
||||||
syncUrl,
|
syncUrl,
|
||||||
@@ -70,13 +70,14 @@ const syncData = async (key, valueFn) => {
|
|||||||
syncMeta = {},
|
syncMeta = {},
|
||||||
} = await getSyncWithDefault();
|
} = await getSyncWithDefault();
|
||||||
if (!syncUrl || !syncKey || (syncType === OPT_SYNCTYPE_WEBDAV && !syncUser)) {
|
if (!syncUrl || !syncKey || (syncType === OPT_SYNCTYPE_WEBDAV && !syncUser)) {
|
||||||
return;
|
throw new Error("sync args err");
|
||||||
}
|
}
|
||||||
|
|
||||||
let { updateAt = 0, syncAt = 0 } = syncMeta[key] || {};
|
let { updateAt = 0, syncAt = 0 } = syncMeta[key] || {};
|
||||||
syncAt === 0 && (updateAt = 0);
|
if (syncAt === 0) {
|
||||||
|
updateAt = 0; // 没有同步过,更新时间置零
|
||||||
|
}
|
||||||
|
|
||||||
const value = await valueFn();
|
|
||||||
const data = {
|
const data = {
|
||||||
key,
|
key,
|
||||||
value: JSON.stringify(value),
|
value: JSON.stringify(value),
|
||||||
@@ -93,13 +94,20 @@ const syncData = async (key, valueFn) => {
|
|||||||
? await syncByWebdav(data, args)
|
? await syncByWebdav(data, args)
|
||||||
: await syncByWorker(data, args);
|
: await syncByWorker(data, args);
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
throw new Error("sync data got err", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVal = JSON.parse(res.value);
|
||||||
|
const isNew = res.updateAt > updateAt;
|
||||||
|
|
||||||
syncMeta[key] = {
|
syncMeta[key] = {
|
||||||
updateAt: res.updateAt,
|
updateAt: res.updateAt,
|
||||||
syncAt: Date.now(),
|
syncAt: Date.now(),
|
||||||
};
|
};
|
||||||
await updateSync({ syncMeta });
|
await putSync({ syncMeta });
|
||||||
|
|
||||||
return { value: JSON.parse(res.value), isNew: res.updateAt > updateAt };
|
return { value: newVal, isNew };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,7 +115,8 @@ const syncData = async (key, valueFn) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const syncSetting = async () => {
|
const syncSetting = async () => {
|
||||||
const res = await syncData(KV_SETTING_KEY, getSettingWithDefault);
|
const value = await getSettingWithDefault();
|
||||||
|
const res = await syncData(KV_SETTING_KEY, value);
|
||||||
if (res?.isNew) {
|
if (res?.isNew) {
|
||||||
await setSetting(res.value);
|
await setSetting(res.value);
|
||||||
}
|
}
|
||||||
@@ -117,7 +126,7 @@ export const trySyncSetting = async () => {
|
|||||||
try {
|
try {
|
||||||
await syncSetting();
|
await syncSetting();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "sync setting");
|
kissLog("sync setting", err.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,7 +135,8 @@ export const trySyncSetting = async () => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const syncRules = async () => {
|
const syncRules = async () => {
|
||||||
const res = await syncData(KV_RULES_KEY, getRulesWithDefault);
|
const value = await getRulesWithDefault();
|
||||||
|
const res = await syncData(KV_RULES_KEY, value);
|
||||||
if (res?.isNew) {
|
if (res?.isNew) {
|
||||||
await setRules(res.value);
|
await setRules(res.value);
|
||||||
}
|
}
|
||||||
@@ -136,7 +146,7 @@ export const trySyncRules = async () => {
|
|||||||
try {
|
try {
|
||||||
await syncRules();
|
await syncRules();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "sync user rules");
|
kissLog("sync user rules", err.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,7 +155,8 @@ export const trySyncRules = async () => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const syncWords = async () => {
|
const syncWords = async () => {
|
||||||
const res = await syncData(KV_WORDS_KEY, getWordsWithDefault);
|
const value = await getWordsWithDefault();
|
||||||
|
const res = await syncData(KV_WORDS_KEY, value);
|
||||||
if (res?.isNew) {
|
if (res?.isNew) {
|
||||||
await setWords(res.value);
|
await setWords(res.value);
|
||||||
}
|
}
|
||||||
@@ -155,7 +166,7 @@ export const trySyncWords = async () => {
|
|||||||
try {
|
try {
|
||||||
await syncWords();
|
await syncWords();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "sync fav words");
|
kissLog("sync fav words", err.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
OPT_STYLE_FUZZY,
|
OPT_STYLE_FUZZY,
|
||||||
GLOBLA_RULE,
|
GLOBLA_RULE,
|
||||||
DEFAULT_SETTING,
|
DEFAULT_SETTING,
|
||||||
DEFAULT_TRANS_APIS,
|
DEFAULT_MOUSEHOVER_KEY,
|
||||||
DEFAULT__MOUSEHOVER_KEY,
|
|
||||||
OPT_STYLE_NONE,
|
OPT_STYLE_NONE,
|
||||||
|
DEFAULT_API_SETTING,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import interpreter from "./interpreter";
|
import interpreter from "./interpreter";
|
||||||
import { ShadowRootMonitor } from "./shadowroot";
|
import { ShadowRootMonitor } from "./shadowroot";
|
||||||
@@ -356,7 +356,7 @@ export class Translator {
|
|||||||
this.#startObserveShadowRoot(shadowRoot);
|
this.#startObserveShadowRoot(shadowRoot);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "findAllShadowRoots");
|
kissLog("findAllShadowRoots", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -419,7 +419,7 @@ export class Translator {
|
|||||||
termPatterns.push(`(${key})`);
|
termPatterns.push(`(${key})`);
|
||||||
this.#termValues.push(value);
|
this.#termValues.push(value);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, `Invalid RegExp for term: "${key}"`);
|
kissLog(`Invalid RegExp for term: "${key}"`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -556,7 +556,7 @@ export class Translator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "无法访问某个 shadowRoot");
|
kissLog("无法访问某个 shadowRoot", err);
|
||||||
}
|
}
|
||||||
// const end = performance.now();
|
// const end = performance.now();
|
||||||
// const duration = end - start;
|
// const duration = end - start;
|
||||||
@@ -839,7 +839,7 @@ export class Translator {
|
|||||||
nodes,
|
nodes,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "transStartHook");
|
kissLog("transStartHook", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -913,14 +913,14 @@ export class Translator {
|
|||||||
innerNode: inner,
|
innerNode: inner,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "transEndHook");
|
kissLog("transEndHook", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// inner.textContent = `[失败]...`;
|
// inner.textContent = `[失败]...`;
|
||||||
// todo: 失败重试按钮
|
// todo: 失败重试按钮
|
||||||
wrapper.remove();
|
wrapper.remove();
|
||||||
kissLog(err, "translateNodeGroup");
|
kissLog("translateNodeGroup", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1037,16 +1037,13 @@ export class Translator {
|
|||||||
|
|
||||||
// 发起翻译请求
|
// 发起翻译请求
|
||||||
#translateFetch(text) {
|
#translateFetch(text) {
|
||||||
const { translator, fromLang, toLang } = this.#rule;
|
const { apiSlug, fromLang, toLang } = this.#rule;
|
||||||
// const apiSetting = this.#setting.transApis[translator];
|
const apiSetting =
|
||||||
const apiSetting = {
|
this.#setting.transApis.find((api) => api.apiSlug === apiSlug) ||
|
||||||
...DEFAULT_TRANS_APIS[translator],
|
DEFAULT_API_SETTING;
|
||||||
...(this.#setting.transApis[translator] || {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
return apiTranslate({
|
return apiTranslate({
|
||||||
text,
|
text,
|
||||||
translator,
|
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
apiSetting,
|
apiSetting,
|
||||||
@@ -1150,11 +1147,11 @@ export class Translator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { translator, fromLang, toLang, hasRichText, textStyle, transOnly } =
|
const { apiSlug, fromLang, toLang, hasRichText, textStyle, transOnly } =
|
||||||
this.#rule;
|
this.#rule;
|
||||||
|
|
||||||
const needsRefresh =
|
const needsRefresh =
|
||||||
appliedRule.translator !== translator ||
|
appliedRule.apiSlug !== apiSlug ||
|
||||||
appliedRule.fromLang !== fromLang ||
|
appliedRule.fromLang !== fromLang ||
|
||||||
appliedRule.toLang !== toLang ||
|
appliedRule.toLang !== toLang ||
|
||||||
appliedRule.hasRichText !== hasRichText;
|
appliedRule.hasRichText !== hasRichText;
|
||||||
@@ -1162,7 +1159,7 @@ export class Translator {
|
|||||||
// 需要重新翻译
|
// 需要重新翻译
|
||||||
if (needsRefresh) {
|
if (needsRefresh) {
|
||||||
Object.assign(appliedRule, {
|
Object.assign(appliedRule, {
|
||||||
translator,
|
apiSlug,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
hasRichText,
|
hasRichText,
|
||||||
@@ -1207,7 +1204,7 @@ export class Translator {
|
|||||||
document.addEventListener("mousemove", this.#boundMouseMoveHandler);
|
document.addEventListener("mousemove", this.#boundMouseMoveHandler);
|
||||||
let { mouseHoverKey } = this.#setting.mouseHoverSetting;
|
let { mouseHoverKey } = this.#setting.mouseHoverSetting;
|
||||||
if (mouseHoverKey.length === 0) {
|
if (mouseHoverKey.length === 0) {
|
||||||
mouseHoverKey = DEFAULT__MOUSEHOVER_KEY;
|
mouseHoverKey = DEFAULT_MOUSEHOVER_KEY;
|
||||||
}
|
}
|
||||||
this.#removeKeydownHandler = shortcutRegister(
|
this.#removeKeydownHandler = shortcutRegister(
|
||||||
mouseHoverKey,
|
mouseHoverKey,
|
||||||
@@ -1273,7 +1270,7 @@ export class Translator {
|
|||||||
document.title = trText || title;
|
document.title = trText || title;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
kissLog(err, "tanslate title");
|
kissLog("tanslate title", err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { limitNumber } from "../../libs/utils";
|
import { limitNumber } from "../../libs/utils";
|
||||||
import { isMobile } from "../../libs/mobile";
|
import { isMobile } from "../../libs/mobile";
|
||||||
import { updateFab } from "../../libs/storage";
|
import { putFab } from "../../libs/storage";
|
||||||
import { debounce } from "../../libs/utils";
|
import { debounce } from "../../libs/utils";
|
||||||
import Paper from "@mui/material/Paper";
|
import Paper from "@mui/material/Paper";
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export default function Draggable({
|
|||||||
const [hover, setHover] = useState(false);
|
const [hover, setHover] = useState(false);
|
||||||
const [origin, setOrigin] = useState(null);
|
const [origin, setOrigin] = useState(null);
|
||||||
const [position, setPosition] = useState({ x: left, y: top });
|
const [position, setPosition] = useState({ x: left, y: top });
|
||||||
const setFabPosition = useMemo(() => debounce(updateFab, 500), []);
|
const setFabPosition = useMemo(() => debounce(putFab, 500), []);
|
||||||
|
|
||||||
const handlePointerDown = (e) => {
|
const handlePointerDown = (e) => {
|
||||||
!isMobile && e.target.setPointerCapture(e.pointerId);
|
!isMobile && e.target.setPointerCapture(e.pointerId);
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export default function Action({ translator, fab }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "registerMenuCommand");
|
kissLog("registerMenuCommand", err);
|
||||||
}
|
}
|
||||||
}, [translator]);
|
}, [translator]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { loadingSvg } from "../../libs/svg";
|
|
||||||
|
|
||||||
export default function LoadingIcon() {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: "inline-block",
|
|
||||||
width: "1.2em",
|
|
||||||
height: "1em",
|
|
||||||
}}
|
|
||||||
dangerouslySetInnerHTML={{ __html: loadingSvg }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
|
||||||
import LoadingIcon from "./LoadingIcon";
|
|
||||||
import {
|
|
||||||
OPT_STYLE_LINE,
|
|
||||||
OPT_STYLE_DOTLINE,
|
|
||||||
OPT_STYLE_DASHLINE,
|
|
||||||
OPT_STYLE_WAVYLINE,
|
|
||||||
OPT_STYLE_DASHBOX,
|
|
||||||
OPT_STYLE_FUZZY,
|
|
||||||
OPT_STYLE_HIGHLIGHT,
|
|
||||||
OPT_STYLE_BLOCKQUOTE,
|
|
||||||
OPT_STYLE_DIY,
|
|
||||||
DEFAULT_COLOR,
|
|
||||||
MSG_TRANS_CURRULE,
|
|
||||||
} from "../../config";
|
|
||||||
import { useTranslate } from "../../hooks/Translate";
|
|
||||||
import { styled, css } from "@mui/material/styles";
|
|
||||||
import { APP_LCNAME } from "../../config";
|
|
||||||
import interpreter from "../../libs/interpreter";
|
|
||||||
|
|
||||||
const LINE_STYLES = {
|
|
||||||
[OPT_STYLE_LINE]: "solid",
|
|
||||||
[OPT_STYLE_DOTLINE]: "dotted",
|
|
||||||
[OPT_STYLE_DASHLINE]: "dashed",
|
|
||||||
[OPT_STYLE_WAVYLINE]: "wavy",
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledSpan = styled("span")`
|
|
||||||
${({ textStyle, textDiyStyle, bgColor }) => {
|
|
||||||
switch (textStyle) {
|
|
||||||
case OPT_STYLE_LINE: // 下划线
|
|
||||||
case OPT_STYLE_DOTLINE: // 点状线
|
|
||||||
case OPT_STYLE_DASHLINE: // 虚线
|
|
||||||
case OPT_STYLE_WAVYLINE: // 波浪线
|
|
||||||
return css`
|
|
||||||
opacity: 0.6;
|
|
||||||
-webkit-opacity: 0.6;
|
|
||||||
text-decoration-line: underline;
|
|
||||||
text-decoration-style: ${LINE_STYLES[textStyle]};
|
|
||||||
text-decoration-color: ${bgColor};
|
|
||||||
text-decoration-thickness: 2px;
|
|
||||||
text-underline-offset: 0.3em;
|
|
||||||
-webkit-text-decoration-line: underline;
|
|
||||||
-webkit-text-decoration-style: ${LINE_STYLES[textStyle]};
|
|
||||||
-webkit-text-decoration-color: ${bgColor};
|
|
||||||
-webkit-text-decoration-thickness: 2px;
|
|
||||||
-webkit-text-underline-offset: 0.3em;
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-opacity: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
case OPT_STYLE_DASHBOX: // 虚线框
|
|
||||||
return css`
|
|
||||||
color: ${bgColor || DEFAULT_COLOR};
|
|
||||||
border: 1px dashed ${bgColor || DEFAULT_COLOR};
|
|
||||||
background: transparent;
|
|
||||||
display: block;
|
|
||||||
padding: 0.2em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
white-space: normal;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
`;
|
|
||||||
case OPT_STYLE_FUZZY: // 模糊
|
|
||||||
return css`
|
|
||||||
filter: blur(0.2em);
|
|
||||||
-webkit-filter: blur(0.2em);
|
|
||||||
&:hover {
|
|
||||||
filter: none;
|
|
||||||
-webkit-filter: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
case OPT_STYLE_HIGHLIGHT: // 高亮
|
|
||||||
return css`
|
|
||||||
color: #fff;
|
|
||||||
background-color: ${bgColor || DEFAULT_COLOR};
|
|
||||||
`;
|
|
||||||
case OPT_STYLE_BLOCKQUOTE: // 引用
|
|
||||||
return css`
|
|
||||||
opacity: 0.6;
|
|
||||||
-webkit-opacity: 0.6;
|
|
||||||
display: block;
|
|
||||||
padding: 0 0.75em;
|
|
||||||
border-left: 0.25em solid ${bgColor || DEFAULT_COLOR};
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-opacity: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
case OPT_STYLE_DIY: // 自定义
|
|
||||||
return textDiyStyle;
|
|
||||||
default:
|
|
||||||
return ``;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function Content({ q, keeps, translator, $el }) {
|
|
||||||
const [rule, setRule] = useState(translator.rule);
|
|
||||||
const { text, sameLang, loading } = useTranslate(
|
|
||||||
q,
|
|
||||||
rule,
|
|
||||||
translator.setting,
|
|
||||||
translator.docInfo
|
|
||||||
);
|
|
||||||
const {
|
|
||||||
transOpen,
|
|
||||||
textStyle,
|
|
||||||
bgColor,
|
|
||||||
textDiyStyle,
|
|
||||||
transOnly,
|
|
||||||
transTag,
|
|
||||||
transEndHook,
|
|
||||||
} = rule;
|
|
||||||
|
|
||||||
const { newlineLength } = translator.setting;
|
|
||||||
|
|
||||||
const handleKissEvent = (e) => {
|
|
||||||
const { action, args } = e.detail;
|
|
||||||
switch (action) {
|
|
||||||
case MSG_TRANS_CURRULE:
|
|
||||||
setRule(args);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener(translator.eventName, handleKissEvent);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener(translator.eventName, handleKissEvent);
|
|
||||||
};
|
|
||||||
}, [translator.eventName]);
|
|
||||||
|
|
||||||
const gap = useMemo(() => {
|
|
||||||
if (transOnly === "true") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return q.length >= newlineLength ? <br /> : " ";
|
|
||||||
}, [q, transOnly, newlineLength]);
|
|
||||||
|
|
||||||
const styles = useMemo(
|
|
||||||
() => ({
|
|
||||||
textStyle,
|
|
||||||
textDiyStyle,
|
|
||||||
bgColor,
|
|
||||||
as: transTag,
|
|
||||||
}),
|
|
||||||
[textStyle, textDiyStyle, bgColor, transTag]
|
|
||||||
);
|
|
||||||
|
|
||||||
const trText = useMemo(() => {
|
|
||||||
if (loading || !transEndHook?.trim()) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 翻译完成钩子函数
|
|
||||||
interpreter.run(`exports.transEndHook = ${transEndHook}`);
|
|
||||||
return interpreter.exports.transEndHook($el, text, q, keeps);
|
|
||||||
}, [loading, $el, q, text, keeps, transEndHook]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{gap}
|
|
||||||
<LoadingIcon />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!trText || sameLang) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
transOnly === "true" &&
|
|
||||||
transOpen === "true" &&
|
|
||||||
$el.querySelector(APP_LCNAME)
|
|
||||||
) {
|
|
||||||
Array.from($el.childNodes).forEach((el) => {
|
|
||||||
if (el.localName !== APP_LCNAME) {
|
|
||||||
el.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keeps.length > 0) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{gap}
|
|
||||||
<StyledSpan
|
|
||||||
{...styles}
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: trText.replace(/\[(\d+)\]/g, (_, p) => keeps[parseInt(p)]),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{gap}
|
|
||||||
<StyledSpan {...styles}>{trText}</StyledSpan>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
@@ -5,58 +6,44 @@ import LoadingButton from "@mui/lab/LoadingButton";
|
|||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
import Switch from "@mui/material/Switch";
|
import Switch from "@mui/material/Switch";
|
||||||
import {
|
|
||||||
OPT_TRANS_ALL,
|
|
||||||
OPT_TRANS_MICROSOFT,
|
|
||||||
OPT_TRANS_DEEPL,
|
|
||||||
OPT_TRANS_DEEPLX,
|
|
||||||
OPT_TRANS_DEEPLFREE,
|
|
||||||
OPT_TRANS_BAIDU,
|
|
||||||
OPT_TRANS_TENCENT,
|
|
||||||
OPT_TRANS_VOLCENGINE,
|
|
||||||
OPT_TRANS_OPENAI,
|
|
||||||
OPT_TRANS_OPENAI_2,
|
|
||||||
OPT_TRANS_OPENAI_3,
|
|
||||||
OPT_TRANS_GEMINI,
|
|
||||||
OPT_TRANS_GEMINI_2,
|
|
||||||
OPT_TRANS_CLAUDE,
|
|
||||||
OPT_TRANS_CLOUDFLAREAI,
|
|
||||||
OPT_TRANS_OLLAMA,
|
|
||||||
OPT_TRANS_OLLAMA_2,
|
|
||||||
OPT_TRANS_OLLAMA_3,
|
|
||||||
OPT_TRANS_OPENROUTER,
|
|
||||||
OPT_TRANS_CUSTOMIZE,
|
|
||||||
OPT_TRANS_CUSTOMIZE_2,
|
|
||||||
OPT_TRANS_CUSTOMIZE_3,
|
|
||||||
OPT_TRANS_CUSTOMIZE_4,
|
|
||||||
OPT_TRANS_CUSTOMIZE_5,
|
|
||||||
OPT_TRANS_NIUTRANS,
|
|
||||||
DEFAULT_FETCH_LIMIT,
|
|
||||||
DEFAULT_FETCH_INTERVAL,
|
|
||||||
DEFAULT_HTTP_TIMEOUT,
|
|
||||||
OPT_TRANS_BATCH,
|
|
||||||
OPT_TRANS_CONTEXT,
|
|
||||||
DEFAULT_BATCH_INTERVAL,
|
|
||||||
DEFAULT_BATCH_SIZE,
|
|
||||||
DEFAULT_BATCH_LENGTH,
|
|
||||||
DEFAULT_CONTEXT_SIZE,
|
|
||||||
} from "../../config";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Accordion from "@mui/material/Accordion";
|
import Accordion from "@mui/material/Accordion";
|
||||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
|
import Menu from "@mui/material/Menu";
|
||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
import { useAlert } from "../../hooks/Alert";
|
import { useAlert } from "../../hooks/Alert";
|
||||||
import { useApi } from "../../hooks/Api";
|
import { useApiList, useApiItem } from "../../hooks/Api";
|
||||||
|
import { useConfirm } from "../../hooks/Confirm";
|
||||||
import { apiTranslate } from "../../apis";
|
import { apiTranslate } from "../../apis";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Link from "@mui/material/Link";
|
|
||||||
import { limitNumber, limitFloat } from "../../libs/utils";
|
import { limitNumber, limitFloat } from "../../libs/utils";
|
||||||
|
import ReusableAutocomplete from "./ReusableAutocomplete";
|
||||||
|
import {
|
||||||
|
OPT_TRANS_DEEPLX,
|
||||||
|
OPT_TRANS_OLLAMA,
|
||||||
|
OPT_TRANS_CUSTOMIZE,
|
||||||
|
OPT_TRANS_NIUTRANS,
|
||||||
|
DEFAULT_FETCH_LIMIT,
|
||||||
|
DEFAULT_FETCH_INTERVAL,
|
||||||
|
DEFAULT_HTTP_TIMEOUT,
|
||||||
|
DEFAULT_BATCH_INTERVAL,
|
||||||
|
DEFAULT_BATCH_SIZE,
|
||||||
|
DEFAULT_BATCH_LENGTH,
|
||||||
|
DEFAULT_CONTEXT_SIZE,
|
||||||
|
OPT_ALL_TYPES,
|
||||||
|
API_SPE_TYPES,
|
||||||
|
BUILTIN_STONES,
|
||||||
|
// BUILTIN_PLACEHOULDERS,
|
||||||
|
// BUILTIN_TAG_NAMES,
|
||||||
|
} from "../../config";
|
||||||
|
|
||||||
function TestButton({ translator, api }) {
|
function TestButton({ apiSlug, api }) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -64,7 +51,7 @@ function TestButton({ translator, api }) {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [text] = await apiTranslate({
|
const [text] = await apiTranslate({
|
||||||
translator,
|
apiSlug,
|
||||||
text: "hello world",
|
text: "hello world",
|
||||||
fromLang: "en",
|
fromLang: "en",
|
||||||
toLang: "zh-CN",
|
toLang: "zh-CN",
|
||||||
@@ -114,7 +101,7 @@ function TestButton({ translator, api }) {
|
|||||||
return (
|
return (
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
size="small"
|
size="small"
|
||||||
variant="contained"
|
variant="outlined"
|
||||||
onClick={handleApiTest}
|
onClick={handleApiTest}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
>
|
||||||
@@ -123,39 +110,34 @@ function TestButton({ translator, api }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ApiFields({ translator, api, updateApi, resetApi }) {
|
function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||||
|
const { api, update, reset } = useApiItem(apiSlug);
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const {
|
const [formData, setFormData] = useState({});
|
||||||
url = "",
|
const [isModified, setIsModified] = useState(false);
|
||||||
key = "",
|
const confirm = useConfirm();
|
||||||
model = "",
|
|
||||||
systemPrompt = "",
|
useEffect(() => {
|
||||||
userPrompt = "",
|
if (api) {
|
||||||
customHeader = "",
|
setFormData(api);
|
||||||
customBody = "",
|
}
|
||||||
think = false,
|
}, [api]);
|
||||||
thinkIgnore = "",
|
|
||||||
fetchLimit = DEFAULT_FETCH_LIMIT,
|
useEffect(() => {
|
||||||
fetchInterval = DEFAULT_FETCH_INTERVAL,
|
if (!api) return;
|
||||||
httpTimeout = DEFAULT_HTTP_TIMEOUT,
|
const hasChanged = JSON.stringify(api) !== JSON.stringify(formData);
|
||||||
dictNo = "",
|
setIsModified(hasChanged);
|
||||||
memoryNo = "",
|
}, [api, formData]);
|
||||||
reqHook = "",
|
|
||||||
resHook = "",
|
|
||||||
temperature = 0,
|
|
||||||
maxTokens = 256,
|
|
||||||
apiName = "",
|
|
||||||
isDisabled = false,
|
|
||||||
useBatchFetch = false,
|
|
||||||
batchInterval = DEFAULT_BATCH_INTERVAL,
|
|
||||||
batchSize = DEFAULT_BATCH_SIZE,
|
|
||||||
batchLength = DEFAULT_BATCH_LENGTH,
|
|
||||||
useContext = false,
|
|
||||||
contextSize = DEFAULT_CONTEXT_SIZE,
|
|
||||||
} = api;
|
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
let { name, value } = e.target;
|
let { name, value, type, checked } = e.target;
|
||||||
|
|
||||||
|
if (type === "checkbox" || type === "switch") {
|
||||||
|
value = checked;
|
||||||
|
}
|
||||||
|
// if (value === "true") value = true;
|
||||||
|
// if (value === "false") value = false;
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "fetchLimit":
|
case "fetchLimit":
|
||||||
value = limitNumber(value, 1, 100);
|
value = limitNumber(value, 1, 100);
|
||||||
@@ -186,55 +168,77 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
updateApi({
|
|
||||||
|
setFormData((prevData) => ({
|
||||||
|
...prevData,
|
||||||
[name]: value,
|
[name]: value,
|
||||||
});
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const builtinTranslators = [
|
const handleSave = () => {
|
||||||
OPT_TRANS_MICROSOFT,
|
// 过滤掉 api 对象中不存在的字段
|
||||||
OPT_TRANS_DEEPLFREE,
|
// const updatedFields = Object.keys(formData).reduce((acc, key) => {
|
||||||
OPT_TRANS_BAIDU,
|
// if (api && Object.keys(api).includes(key)) {
|
||||||
OPT_TRANS_TENCENT,
|
// acc[key] = formData[key];
|
||||||
OPT_TRANS_VOLCENGINE,
|
// }
|
||||||
];
|
// return acc;
|
||||||
|
// }, {});
|
||||||
|
// update(updatedFields);
|
||||||
|
update(formData);
|
||||||
|
};
|
||||||
|
|
||||||
const mulkeysTranslators = [
|
const handleReset = () => {
|
||||||
OPT_TRANS_DEEPL,
|
reset();
|
||||||
OPT_TRANS_OPENAI,
|
};
|
||||||
OPT_TRANS_OPENAI_2,
|
|
||||||
OPT_TRANS_OPENAI_3,
|
|
||||||
OPT_TRANS_GEMINI,
|
|
||||||
OPT_TRANS_GEMINI_2,
|
|
||||||
OPT_TRANS_CLAUDE,
|
|
||||||
OPT_TRANS_CLOUDFLAREAI,
|
|
||||||
OPT_TRANS_OLLAMA,
|
|
||||||
OPT_TRANS_OLLAMA_2,
|
|
||||||
OPT_TRANS_OLLAMA_3,
|
|
||||||
OPT_TRANS_OPENROUTER,
|
|
||||||
OPT_TRANS_NIUTRANS,
|
|
||||||
OPT_TRANS_CUSTOMIZE,
|
|
||||||
OPT_TRANS_CUSTOMIZE_2,
|
|
||||||
OPT_TRANS_CUSTOMIZE_3,
|
|
||||||
OPT_TRANS_CUSTOMIZE_4,
|
|
||||||
OPT_TRANS_CUSTOMIZE_5,
|
|
||||||
];
|
|
||||||
|
|
||||||
const keyHelper =
|
const handleDelete = async () => {
|
||||||
translator === OPT_TRANS_NIUTRANS ? (
|
const isConfirmed = await confirm({
|
||||||
<>
|
confirmText: i18n("delete"),
|
||||||
{i18n("mulkeys_help")}
|
cancelText: i18n("cancel"),
|
||||||
<Link
|
});
|
||||||
href="https://niutrans.com/login?active=3&userSource=kiss-translator"
|
|
||||||
target="_blank"
|
if (isConfirmed) {
|
||||||
>
|
deleteApi(apiSlug);
|
||||||
{i18n("reg_niutrans")}
|
}
|
||||||
</Link>
|
};
|
||||||
</>
|
|
||||||
) : mulkeysTranslators.includes(translator) ? (
|
const {
|
||||||
i18n("mulkeys_help")
|
url = "",
|
||||||
) : (
|
key = "",
|
||||||
""
|
model = "",
|
||||||
|
apiType,
|
||||||
|
systemPrompt = "",
|
||||||
|
// userPrompt = "",
|
||||||
|
customHeader = "",
|
||||||
|
customBody = "",
|
||||||
|
think = false,
|
||||||
|
thinkIgnore = "",
|
||||||
|
fetchLimit = DEFAULT_FETCH_LIMIT,
|
||||||
|
fetchInterval = DEFAULT_FETCH_INTERVAL,
|
||||||
|
httpTimeout = DEFAULT_HTTP_TIMEOUT,
|
||||||
|
dictNo = "",
|
||||||
|
memoryNo = "",
|
||||||
|
reqHook = "",
|
||||||
|
resHook = "",
|
||||||
|
temperature = 0,
|
||||||
|
maxTokens = 256,
|
||||||
|
apiName = "",
|
||||||
|
isDisabled = false,
|
||||||
|
useBatchFetch = false,
|
||||||
|
batchInterval = DEFAULT_BATCH_INTERVAL,
|
||||||
|
batchSize = DEFAULT_BATCH_SIZE,
|
||||||
|
batchLength = DEFAULT_BATCH_LENGTH,
|
||||||
|
useContext = false,
|
||||||
|
contextSize = DEFAULT_CONTEXT_SIZE,
|
||||||
|
tone = "neutral",
|
||||||
|
// placeholder = "{ }",
|
||||||
|
// tagName = "i",
|
||||||
|
// aiTerms = false,
|
||||||
|
} = formData;
|
||||||
|
|
||||||
|
const keyHelper = useMemo(
|
||||||
|
() => (API_SPE_TYPES.mulkeys.has(apiType) ? i18n("mulkeys_help") : ""),
|
||||||
|
[apiType, i18n]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -247,7 +251,7 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!builtinTranslators.includes(translator) && (
|
{!API_SPE_TYPES.machine.has(apiType) && (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -255,10 +259,10 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
|
|||||||
name="url"
|
name="url"
|
||||||
value={url}
|
value={url}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
multiline={translator === OPT_TRANS_DEEPLX}
|
multiline={apiType === OPT_TRANS_DEEPLX}
|
||||||
maxRows={10}
|
maxRows={10}
|
||||||
helperText={
|
helperText={
|
||||||
translator === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : ""
|
apiType === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : ""
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -267,26 +271,66 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
|
|||||||
name="key"
|
name="key"
|
||||||
value={key}
|
value={key}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
multiline={mulkeysTranslators.includes(translator)}
|
multiline={API_SPE_TYPES.mulkeys.has(apiType)}
|
||||||
maxRows={10}
|
maxRows={10}
|
||||||
helperText={keyHelper}
|
helperText={keyHelper}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(translator.startsWith(OPT_TRANS_OPENAI) ||
|
{API_SPE_TYPES.ai.has(apiType) && (
|
||||||
translator.startsWith(OPT_TRANS_OLLAMA) ||
|
|
||||||
translator === OPT_TRANS_CLAUDE ||
|
|
||||||
translator === OPT_TRANS_OPENROUTER ||
|
|
||||||
translator.startsWith(OPT_TRANS_GEMINI)) && (
|
|
||||||
<>
|
<>
|
||||||
|
<Box>
|
||||||
|
<Grid container spacing={2} columns={12}>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
|
{/* todo: 改成 ReusableAutocomplete 可选择和填写模型 */}
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
|
fullWidth
|
||||||
label={"MODEL"}
|
label={"MODEL"}
|
||||||
name="model"
|
name="model"
|
||||||
value={model}
|
value={model}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
|
<ReusableAutocomplete
|
||||||
|
freeSolo
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
options={BUILTIN_STONES}
|
||||||
|
name="tone"
|
||||||
|
label={i18n("translation_style")}
|
||||||
|
value={tone}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
label={"Temperature"}
|
||||||
|
type="number"
|
||||||
|
name="temperature"
|
||||||
|
value={temperature}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
label={"Max Tokens"}
|
||||||
|
type="number"
|
||||||
|
name="maxTokens"
|
||||||
|
value={maxTokens}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}></Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={"SYSTEM PROMPT"}
|
label={"SYSTEM PROMPT"}
|
||||||
@@ -295,8 +339,9 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
multiline
|
multiline
|
||||||
maxRows={10}
|
maxRows={10}
|
||||||
|
helperText={i18n("system_prompt_helper")}
|
||||||
/>
|
/>
|
||||||
<TextField
|
{/* <TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={"USER PROMPT"}
|
label={"USER PROMPT"}
|
||||||
name="userPrompt"
|
name="userPrompt"
|
||||||
@@ -304,7 +349,51 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
multiline
|
multiline
|
||||||
maxRows={10}
|
maxRows={10}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
{/* <Box>
|
||||||
|
<Grid container spacing={2} columns={12}>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
|
<ReusableAutocomplete
|
||||||
|
freeSolo
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
options={BUILTIN_PLACEHOULDERS}
|
||||||
|
name="placeholder"
|
||||||
|
label={i18n("placeholder")}
|
||||||
|
value={placeholder}
|
||||||
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
|
<ReusableAutocomplete
|
||||||
|
freeSolo
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
options={BUILTIN_TAG_NAMES}
|
||||||
|
name="tagName"
|
||||||
|
label={i18n("tag_name")}
|
||||||
|
value={tagName}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
name="aiTerms"
|
||||||
|
value={aiTerms}
|
||||||
|
label={i18n("ai_terms")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
||||||
|
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box> */}
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("custom_header")}
|
label={i18n("custom_header")}
|
||||||
@@ -328,7 +417,7 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{translator.startsWith(OPT_TRANS_OLLAMA) && (
|
{apiType === OPT_TRANS_OLLAMA && (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
@@ -351,32 +440,7 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(translator.startsWith(OPT_TRANS_OPENAI) ||
|
{apiType === OPT_TRANS_NIUTRANS && (
|
||||||
translator === OPT_TRANS_CLAUDE ||
|
|
||||||
translator === OPT_TRANS_OPENROUTER ||
|
|
||||||
translator === OPT_TRANS_GEMINI ||
|
|
||||||
translator === OPT_TRANS_GEMINI_2) && (
|
|
||||||
<>
|
|
||||||
<TextField
|
|
||||||
size="small"
|
|
||||||
label={"Temperature"}
|
|
||||||
type="number"
|
|
||||||
name="temperature"
|
|
||||||
value={temperature}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
size="small"
|
|
||||||
label={"Max Tokens"}
|
|
||||||
type="number"
|
|
||||||
name="maxTokens"
|
|
||||||
value={maxTokens}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{translator === OPT_TRANS_NIUTRANS && (
|
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -395,7 +459,7 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
|
{apiType === OPT_TRANS_CUSTOMIZE && (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -418,10 +482,13 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{OPT_TRANS_BATCH.has(translator) && (
|
{API_SPE_TYPES.batch.has(api.apiType) && (
|
||||||
<>
|
<Box>
|
||||||
|
<Grid container spacing={2} columns={12}>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
name="useBatchFetch"
|
name="useBatchFetch"
|
||||||
value={useBatchFetch}
|
value={useBatchFetch}
|
||||||
@@ -431,42 +498,54 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
|
|||||||
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||||
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
{useBatchFetch && (
|
</Grid>
|
||||||
<>
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
|
fullWidth
|
||||||
label={i18n("batch_interval")}
|
label={i18n("batch_interval")}
|
||||||
type="number"
|
type="number"
|
||||||
name="batchInterval"
|
name="batchInterval"
|
||||||
value={batchInterval}
|
value={batchInterval}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
|
fullWidth
|
||||||
label={i18n("batch_size")}
|
label={i18n("batch_size")}
|
||||||
type="number"
|
type="number"
|
||||||
name="batchSize"
|
name="batchSize"
|
||||||
value={batchSize}
|
value={batchSize}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
|
fullWidth
|
||||||
label={i18n("batch_length")}
|
label={i18n("batch_length")}
|
||||||
type="number"
|
type="number"
|
||||||
name="batchLength"
|
name="batchLength"
|
||||||
value={batchLength}
|
value={batchLength}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
</>
|
</Grid>
|
||||||
)}
|
</Grid>
|
||||||
</>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{OPT_TRANS_CONTEXT.has(translator) && (
|
{API_SPE_TYPES.context.has(api.apiType) && (
|
||||||
<>
|
<>
|
||||||
|
<Box>
|
||||||
|
<Grid container spacing={2} columns={12}>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
|
{" "}
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
size="small"
|
size="small"
|
||||||
|
fullWidth
|
||||||
name="useContext"
|
name="useContext"
|
||||||
value={useContext}
|
value={useContext}
|
||||||
label={i18n("use_context")}
|
label={i18n("use_context")}
|
||||||
@@ -475,83 +554,108 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
|
|||||||
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||||
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
{useBatchFetch && (
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
|
{" "}
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
|
fullWidth
|
||||||
label={i18n("context_size")}
|
label={i18n("context_size")}
|
||||||
type="number"
|
type="number"
|
||||||
name="contextSize"
|
name="contextSize"
|
||||||
value={contextSize}
|
value={contextSize}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
)}
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Grid container spacing={2} columns={12}>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
|
fullWidth
|
||||||
label={i18n("fetch_limit")}
|
label={i18n("fetch_limit")}
|
||||||
type="number"
|
type="number"
|
||||||
name="fetchLimit"
|
name="fetchLimit"
|
||||||
value={fetchLimit}
|
value={fetchLimit}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
|
fullWidth
|
||||||
label={i18n("fetch_interval")}
|
label={i18n("fetch_interval")}
|
||||||
type="number"
|
type="number"
|
||||||
name="fetchInterval"
|
name="fetchInterval"
|
||||||
value={fetchInterval}
|
value={fetchInterval}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
|
fullWidth
|
||||||
label={i18n("http_timeout")}
|
label={i18n("http_timeout")}
|
||||||
type="number"
|
type="number"
|
||||||
name="httpTimeout"
|
name="httpTimeout"
|
||||||
defaultValue={httpTimeout}
|
value={httpTimeout}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={6} lg={3}></Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!isModified}
|
||||||
|
>
|
||||||
|
{i18n("save")}
|
||||||
|
</Button>
|
||||||
|
<TestButton apiSlug={apiSlug} api={api} />
|
||||||
|
<Button size="small" variant="outlined" onClick={handleReset}>
|
||||||
|
{i18n("restore_default")}
|
||||||
|
</Button>
|
||||||
|
{isUserApi && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
{i18n("delete")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
|
fullWidth
|
||||||
name="isDisabled"
|
name="isDisabled"
|
||||||
checked={isDisabled}
|
checked={isDisabled}
|
||||||
onChange={() => {
|
onChange={handleChange}
|
||||||
updateApi({ isDisabled: !isDisabled });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={i18n("is_disabled")}
|
label={i18n("is_disabled")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack direction="row" spacing={2}>
|
|
||||||
<TestButton translator={translator} api={api} />
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => {
|
|
||||||
resetApi();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{i18n("restore_default")}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
|
{apiType === OPT_TRANS_CUSTOMIZE && <pre>{i18n("custom_api_help")}</pre>}
|
||||||
<pre>{i18n("custom_api_help")}</pre>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ApiAccordion({ translator }) {
|
function ApiAccordion({ api, isUserApi, deleteApi }) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const { api, updateApi, resetApi } = useApi(translator);
|
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
setExpanded((pre) => !pre);
|
setExpanded((pre) => !pre);
|
||||||
@@ -566,16 +670,15 @@ function ApiAccordion({ translator }) {
|
|||||||
overflowWrap: "anywhere",
|
overflowWrap: "anywhere",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{api.apiName ? `${translator} (${api.apiName})` : translator}
|
{`[${api.apiType}] ${api.apiName}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<ApiFields
|
<ApiFields
|
||||||
translator={translator}
|
apiSlug={api.apiSlug}
|
||||||
api={api}
|
isUserApi={isUserApi}
|
||||||
updateApi={updateApi}
|
deleteApi={deleteApi}
|
||||||
resetApi={resetApi}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
@@ -585,14 +688,85 @@ function ApiAccordion({ translator }) {
|
|||||||
|
|
||||||
export default function Apis() {
|
export default function Apis() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const { userApis, builtinApis, addApi, deleteApi } = useApiList();
|
||||||
|
|
||||||
|
const apiTypes = useMemo(
|
||||||
|
() =>
|
||||||
|
OPT_ALL_TYPES.map((type) => ({
|
||||||
|
type,
|
||||||
|
label: type,
|
||||||
|
})),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
|
const handleClick = (event) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuItemClick = (apiType) => {
|
||||||
|
addApi(apiType);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Alert severity="info">{i18n("about_api")}</Alert>
|
<Alert severity="info">{i18n("about_api")}</Alert>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
{OPT_TRANS_ALL.map((translator) => (
|
<Button
|
||||||
<ApiAccordion key={translator} translator={translator} />
|
size="small"
|
||||||
|
id="add-api-button"
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleClick}
|
||||||
|
aria-controls={open ? "add-api-menu" : undefined}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={open ? "true" : undefined}
|
||||||
|
endIcon={<KeyboardArrowDownIcon />}
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
>
|
||||||
|
{i18n("add")}
|
||||||
|
</Button>
|
||||||
|
<Menu
|
||||||
|
id="add-api-menu"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
MenuListProps={{
|
||||||
|
"aria-labelledby": "add-api-button",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{apiTypes.map((apiOption) => (
|
||||||
|
<MenuItem
|
||||||
|
key={apiOption.type}
|
||||||
|
onClick={() => handleMenuItemClick(apiOption.type)}
|
||||||
|
>
|
||||||
|
{apiOption.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
{userApis.map((api) => (
|
||||||
|
<ApiAccordion
|
||||||
|
key={api.apiSlug}
|
||||||
|
api={api}
|
||||||
|
isUserApi={true}
|
||||||
|
deleteApi={deleteApi}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
{builtinApis.map((api) => (
|
||||||
|
<ApiAccordion key={api.apiSlug} api={api} />
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function DownloadButton({ handleData, text, fileName }) {
|
|||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "download");
|
kissLog("download", err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Accordion from "@mui/material/Accordion";
|
|||||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
// import CircularProgress from "@mui/material/CircularProgress";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useFavWords } from "../../hooks/FavWords";
|
import { useFavWords } from "../../hooks/FavWords";
|
||||||
@@ -49,29 +49,25 @@ function FavAccordion({ word, index }) {
|
|||||||
|
|
||||||
export default function FavWords() {
|
export default function FavWords() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { loading, favWords, mergeWords, clearWords } = useFavWords();
|
const { favList, wordList, mergeWords, clearWords } = useFavWords();
|
||||||
const favList = Object.entries(favWords).sort((a, b) =>
|
|
||||||
a[0].localeCompare(b[0])
|
|
||||||
);
|
|
||||||
const downloadList = favList.map(([word]) => word);
|
|
||||||
|
|
||||||
const handleImport = async (data) => {
|
const handleImport = (data) => {
|
||||||
try {
|
try {
|
||||||
const newWords = data
|
const newWords = data
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((line) => line.split(",")[0].trim())
|
.map((line) => line.split(",")[0].trim())
|
||||||
.filter(isValidWord);
|
.filter(isValidWord);
|
||||||
await mergeWords(newWords);
|
mergeWords(newWords);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "import rules");
|
kissLog("import rules", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTranslation = async () => {
|
const handleTranslation = async () => {
|
||||||
const tranList = [];
|
const tranList = [];
|
||||||
for (const text of downloadList) {
|
for (const text of wordList) {
|
||||||
try {
|
try {
|
||||||
// todo
|
// todo: 修复
|
||||||
const dictRes = await apiTranslate({
|
const dictRes = await apiTranslate({
|
||||||
text,
|
text,
|
||||||
translator: OPT_TRANS_BAIDU,
|
translator: OPT_TRANS_BAIDU,
|
||||||
@@ -122,7 +118,7 @@ export default function FavWords() {
|
|||||||
fileExts={[".txt", ".csv"]}
|
fileExts={[".txt", ".csv"]}
|
||||||
/>
|
/>
|
||||||
<DownloadButton
|
<DownloadButton
|
||||||
handleData={() => downloadList.join("\n")}
|
handleData={() => wordList.join("\n")}
|
||||||
text={i18n("export")}
|
text={i18n("export")}
|
||||||
fileName={`kiss-words_${Date.now()}.txt`}
|
fileName={`kiss-words_${Date.now()}.txt`}
|
||||||
/>
|
/>
|
||||||
@@ -144,18 +140,14 @@ export default function FavWords() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
{loading ? (
|
{favList.map(([word, { createdAt }], index) => (
|
||||||
<CircularProgress size={24} />
|
|
||||||
) : (
|
|
||||||
favList.map(([word, { createdAt }], index) => (
|
|
||||||
<FavAccordion
|
<FavAccordion
|
||||||
key={word}
|
key={word}
|
||||||
index={index}
|
index={index}
|
||||||
word={word}
|
word={word}
|
||||||
createdAt={createdAt}
|
createdAt={createdAt}
|
||||||
/>
|
/>
|
||||||
))
|
))}
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import TextField from "@mui/material/TextField";
|
|||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import {
|
import {
|
||||||
OPT_TRANS_ALL,
|
|
||||||
OPT_LANGS_FROM,
|
OPT_LANGS_FROM,
|
||||||
OPT_LANGS_TO,
|
OPT_LANGS_TO,
|
||||||
OPT_INPUT_TRANS_SIGNS,
|
OPT_INPUT_TRANS_SIGNS,
|
||||||
@@ -16,10 +15,12 @@ import { useInputRule } from "../../hooks/InputRule";
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import { limitNumber } from "../../libs/utils";
|
import { limitNumber } from "../../libs/utils";
|
||||||
|
import { useApiList } from "../../hooks/Api";
|
||||||
|
|
||||||
export default function InputSetting() {
|
export default function InputSetting() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { inputRule, updateInputRule } = useInputRule();
|
const { inputRule, updateInputRule } = useInputRule();
|
||||||
|
const { enabledApis } = useApiList();
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -44,7 +45,7 @@ export default function InputSetting() {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
transOpen,
|
transOpen,
|
||||||
translator,
|
apiSlug,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
triggerShortcut,
|
triggerShortcut,
|
||||||
@@ -73,14 +74,14 @@ export default function InputSetting() {
|
|||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
size="small"
|
size="small"
|
||||||
name="translator"
|
name="apiSlug"
|
||||||
value={translator}
|
value={apiSlug}
|
||||||
label={i18n("translate_service")}
|
label={i18n("translate_service")}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{OPT_TRANS_ALL.map((item) => (
|
{enabledApis.map((api) => (
|
||||||
<MenuItem key={item} value={item}>
|
<MenuItem key={api.apiSlug} value={api.apiSlug}>
|
||||||
{item}
|
{api.apiName}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
@@ -166,7 +167,7 @@ export default function InputSetting() {
|
|||||||
label={i18n("combo_timeout")}
|
label={i18n("combo_timeout")}
|
||||||
type="number"
|
type="number"
|
||||||
name="triggerTime"
|
name="triggerTime"
|
||||||
defaultValue={triggerTime}
|
value={triggerTime}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Switch from "@mui/material/Switch";
|
|||||||
import { useMouseHoverSetting } from "../../hooks/MouseHover";
|
import { useMouseHoverSetting } from "../../hooks/MouseHover";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
|
import { DEFAULT_MOUSEHOVER_KEY } from "../../config";
|
||||||
|
|
||||||
export default function MouseHoverSetting() {
|
export default function MouseHoverSetting() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -19,7 +20,7 @@ export default function MouseHoverSetting() {
|
|||||||
[updateMouseHoverSetting]
|
[updateMouseHoverSetting]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { useMouseHover = true, mouseHoverKey = ["ControlLeft"] } =
|
const { useMouseHover = true, mouseHoverKey = DEFAULT_MOUSEHOVER_KEY } =
|
||||||
mouseHoverSetting;
|
mouseHoverSetting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
REMAIN_KEY,
|
REMAIN_KEY,
|
||||||
OPT_LANGS_FROM,
|
OPT_LANGS_FROM,
|
||||||
OPT_LANGS_TO,
|
OPT_LANGS_TO,
|
||||||
OPT_TRANS_ALL,
|
|
||||||
OPT_STYLE_ALL,
|
OPT_STYLE_ALL,
|
||||||
OPT_STYLE_DIY,
|
OPT_STYLE_DIY,
|
||||||
OPT_STYLE_USE_COLOR,
|
OPT_STYLE_USE_COLOR,
|
||||||
@@ -15,10 +14,12 @@ import { useI18n } from "../../hooks/I18n";
|
|||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import { useOwSubRule } from "../../hooks/SubRules";
|
import { useOwSubRule } from "../../hooks/SubRules";
|
||||||
|
import { useApiList } from "../../hooks/Api";
|
||||||
|
|
||||||
export default function OwSubRule() {
|
export default function OwSubRule() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { owSubrule, updateOwSubrule } = useOwSubRule();
|
const { owSubrule, updateOwSubrule } = useOwSubRule();
|
||||||
|
const { enabledApis } = useApiList();
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -27,7 +28,7 @@ export default function OwSubRule() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
translator,
|
apiSlug,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
textStyle,
|
textStyle,
|
||||||
@@ -73,16 +74,16 @@ export default function OwSubRule() {
|
|||||||
select
|
select
|
||||||
size="small"
|
size="small"
|
||||||
fullWidth
|
fullWidth
|
||||||
name="translator"
|
name="apiSlug"
|
||||||
value={translator}
|
value={apiSlug}
|
||||||
label={i18n("translate_service")}
|
label={i18n("translate_service")}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{RemainItem}
|
{RemainItem}
|
||||||
{GlobalItem}
|
{GlobalItem}
|
||||||
{OPT_TRANS_ALL.map((item) => (
|
{enabledApis.map((api) => (
|
||||||
<MenuItem key={item} value={item}>
|
<MenuItem key={api.apiSlug} value={api.apiSlug}>
|
||||||
{item}
|
{api.apiName}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|||||||
74
src/views/Options/ReusableAutocomplete.js
Normal file
74
src/views/Options/ReusableAutocomplete.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一个可复用的 Autocomplete 组件,增加了 name 属性和标准化的 onChange 事件
|
||||||
|
* @param {object} props - 组件的 props
|
||||||
|
* @param {string} props.name - 表单字段的名称,会包含在 onChange 的 event.target 中
|
||||||
|
* @param {string} props.label - TextField 的标签
|
||||||
|
* @param {any} props.value - 受控组件的当前值
|
||||||
|
* @param {function} props.onChange - 值改变时的回调函数 (event) => {}
|
||||||
|
* @param {Array} props.options - Autocomplete 的选项列表
|
||||||
|
*/
|
||||||
|
export default function ReusableAutocomplete({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
const [inputValue, setInputValue] = useState(value || "");
|
||||||
|
const isChangeCommitted = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputValue(value || "");
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const triggerOnChange = (newValue) => {
|
||||||
|
if (onChange) {
|
||||||
|
const syntheticEvent = {
|
||||||
|
target: {
|
||||||
|
name: name,
|
||||||
|
value: newValue,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
onChange(syntheticEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
if (isChangeCommitted.current) {
|
||||||
|
isChangeCommitted.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputValue !== value) {
|
||||||
|
triggerOnChange(inputValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (event, newValue) => {
|
||||||
|
isChangeCommitted.current = true;
|
||||||
|
triggerOnChange(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (event, newInputValue) => {
|
||||||
|
isChangeCommitted.current = false;
|
||||||
|
setInputValue(newInputValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
inputValue={inputValue}
|
||||||
|
onInputChange={handleInputChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
{...rest}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} name={name} label={label} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
GLOBLA_RULE,
|
GLOBLA_RULE,
|
||||||
OPT_LANGS_FROM,
|
OPT_LANGS_FROM,
|
||||||
OPT_LANGS_TO,
|
OPT_LANGS_TO,
|
||||||
OPT_TRANS_ALL,
|
|
||||||
OPT_STYLE_ALL,
|
OPT_STYLE_ALL,
|
||||||
OPT_STYLE_DIY,
|
OPT_STYLE_DIY,
|
||||||
OPT_STYLE_USE_COLOR,
|
OPT_STYLE_USE_COLOR,
|
||||||
@@ -58,19 +57,26 @@ import EditIcon from "@mui/icons-material/Edit";
|
|||||||
import CancelIcon from "@mui/icons-material/Cancel";
|
import CancelIcon from "@mui/icons-material/Cancel";
|
||||||
import SaveIcon from "@mui/icons-material/Save";
|
import SaveIcon from "@mui/icons-material/Save";
|
||||||
import { kissLog } from "../../libs/log";
|
import { kissLog } from "../../libs/log";
|
||||||
|
import { useApiList } from "../../hooks/Api";
|
||||||
|
|
||||||
function RuleFields({ rule, rules, setShow, setKeyword }) {
|
function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||||
const initFormValues = {
|
const initFormValues = useMemo(
|
||||||
|
() => ({
|
||||||
...(rule?.pattern === "*" ? GLOBLA_RULE : DEFAULT_RULE),
|
...(rule?.pattern === "*" ? GLOBLA_RULE : DEFAULT_RULE),
|
||||||
...(rule || {}),
|
...(rule || {}),
|
||||||
};
|
}),
|
||||||
const editMode = !!rule;
|
[rule]
|
||||||
|
);
|
||||||
|
const editMode = useMemo(() => !!rule, [rule]);
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const [disabled, setDisabled] = useState(editMode);
|
const [disabled, setDisabled] = useState(editMode);
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [formValues, setFormValues] = useState(initFormValues);
|
const [formValues, setFormValues] = useState(initFormValues);
|
||||||
const [showMore, setShowMore] = useState(!rules);
|
const [showMore, setShowMore] = useState(!rules);
|
||||||
|
const [isModified, setIsModified] = useState(false);
|
||||||
|
const { enabledApis } = useApiList();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
pattern,
|
pattern,
|
||||||
selector,
|
selector,
|
||||||
@@ -82,7 +88,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
parentStyle = "",
|
parentStyle = "",
|
||||||
injectJs = "",
|
injectJs = "",
|
||||||
injectCss = "",
|
injectCss = "",
|
||||||
translator,
|
apiSlug,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
textStyle,
|
textStyle,
|
||||||
@@ -106,6 +112,13 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
// transRemoveHook = "",
|
// transRemoveHook = "",
|
||||||
} = formValues;
|
} = formValues;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initFormValues) return;
|
||||||
|
const hasChanged =
|
||||||
|
JSON.stringify(initFormValues) !== JSON.stringify(formValues);
|
||||||
|
setIsModified(hasChanged);
|
||||||
|
}, [initFormValues, formValues]);
|
||||||
|
|
||||||
const hasSamePattern = (str) => {
|
const hasSamePattern = (str) => {
|
||||||
for (const item of rules.list) {
|
for (const item of rules.list) {
|
||||||
if (item.pattern === str && rule?.pattern !== str) {
|
if (item.pattern === str && rule?.pattern !== str) {
|
||||||
@@ -417,16 +430,16 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
select
|
select
|
||||||
size="small"
|
size="small"
|
||||||
fullWidth
|
fullWidth
|
||||||
name="translator"
|
name="apiSlug"
|
||||||
value={translator}
|
value={apiSlug}
|
||||||
label={i18n("translate_service")}
|
label={i18n("translate_service")}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{GlobalItem}
|
{GlobalItem}
|
||||||
{OPT_TRANS_ALL.map((item) => (
|
{enabledApis.map((api) => (
|
||||||
<MenuItem key={item} value={item}>
|
<MenuItem key={api.apiSlug} value={api.apiSlug}>
|
||||||
{item}
|
{api.apiName}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
@@ -738,6 +751,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
type="submit"
|
type="submit"
|
||||||
startIcon={<SaveIcon />}
|
startIcon={<SaveIcon />}
|
||||||
|
disabled={!isModified}
|
||||||
>
|
>
|
||||||
{i18n("save")}
|
{i18n("save")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -842,7 +856,7 @@ function ShareButton({ rules, injectRules, selectedUrl }) {
|
|||||||
window.open(url, "_blank");
|
window.open(url, "_blank");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert.warning(i18n("error_got_some_wrong"));
|
alert.warning(i18n("error_got_some_wrong"));
|
||||||
kissLog(err, "share rules");
|
kissLog("share rules", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -871,7 +885,7 @@ function UserRules({ subRules, rules }) {
|
|||||||
try {
|
try {
|
||||||
await rules.merge(JSON.parse(data));
|
await rules.merge(JSON.parse(data));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "import rules");
|
kissLog("import rules", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1004,7 +1018,7 @@ function SubRulesItem({
|
|||||||
await delSubRules(url);
|
await delSubRules(url);
|
||||||
await deleteDataCache(url);
|
await deleteDataCache(url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "del subrules");
|
kissLog("del subrules", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1017,7 +1031,7 @@ function SubRulesItem({
|
|||||||
}
|
}
|
||||||
await updateDataCache(url);
|
await updateDataCache(url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "sync sub rules");
|
kissLog("sync sub rules", err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -1096,7 +1110,7 @@ function SubRulesEdit({ subList, addSub, updateDataCache }) {
|
|||||||
setShowInput(false);
|
setShowInput(false);
|
||||||
setInputText("");
|
setInputText("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "fetch rules");
|
kissLog("fetch rules", err);
|
||||||
setInputError(i18n("error_fetch_url"));
|
setInputError(i18n("error_fetch_url"));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export default function Settings() {
|
|||||||
caches.delete(CACHE_NAME);
|
caches.delete(CACHE_NAME);
|
||||||
alert.success(i18n("clear_success"));
|
alert.success(i18n("clear_success"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "clear cache");
|
kissLog("clear cache", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ export default function Settings() {
|
|||||||
try {
|
try {
|
||||||
await updateSetting(JSON.parse(data));
|
await updateSetting(JSON.parse(data));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "import setting");
|
kissLog("import setting", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,7 +119,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"),
|
||||||
transInterval = 200,
|
transInterval = 100,
|
||||||
langDetector = OPT_TRANS_MICROSOFT,
|
langDetector = OPT_TRANS_MICROSOFT,
|
||||||
} = setting;
|
} = setting;
|
||||||
const { isHide = false, fabClickAction = 0 } = fab || {};
|
const { isHide = false, fabClickAction = 0 } = fab || {};
|
||||||
@@ -163,7 +163,7 @@ export default function Settings() {
|
|||||||
label={i18n("min_translate_length")}
|
label={i18n("min_translate_length")}
|
||||||
type="number"
|
type="number"
|
||||||
name="minLength"
|
name="minLength"
|
||||||
defaultValue={minLength}
|
value={minLength}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ export default function Settings() {
|
|||||||
label={i18n("max_translate_length")}
|
label={i18n("max_translate_length")}
|
||||||
type="number"
|
type="number"
|
||||||
name="maxLength"
|
name="maxLength"
|
||||||
defaultValue={maxLength}
|
value={maxLength}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ export default function Settings() {
|
|||||||
label={i18n("num_of_newline_characters")}
|
label={i18n("num_of_newline_characters")}
|
||||||
type="number"
|
type="number"
|
||||||
name="newlineLength"
|
name="newlineLength"
|
||||||
defaultValue={newlineLength}
|
value={newlineLength}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ export default function Settings() {
|
|||||||
label={i18n("translate_interval")}
|
label={i18n("translate_interval")}
|
||||||
type="number"
|
type="number"
|
||||||
name="transInterval"
|
name="transInterval"
|
||||||
defaultValue={transInterval}
|
value={transInterval}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -198,7 +198,7 @@ export default function Settings() {
|
|||||||
label={i18n("http_timeout")}
|
label={i18n("http_timeout")}
|
||||||
type="number"
|
type="number"
|
||||||
name="httpTimeout"
|
name="httpTimeout"
|
||||||
defaultValue={httpTimeout}
|
value={httpTimeout}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<FormControl size="small">
|
<FormControl size="small">
|
||||||
@@ -238,7 +238,7 @@ export default function Settings() {
|
|||||||
name="fabClickAction"
|
name="fabClickAction"
|
||||||
value={fabClickAction}
|
value={fabClickAction}
|
||||||
label={i18n("fab_click_action")}
|
label={i18n("fab_click_action")}
|
||||||
onChange= {(e) => updateFab({ fabClickAction: e.target.value })}
|
onChange={(e) => updateFab({ fabClickAction: e.target.value })}
|
||||||
>
|
>
|
||||||
<MenuItem value={0}>{i18n("fab_click_menu")}</MenuItem>
|
<MenuItem value={0}>{i18n("fab_click_menu")}</MenuItem>
|
||||||
<MenuItem value={1}>{i18n("fab_click_translate")}</MenuItem>
|
<MenuItem value={1}>{i18n("fab_click_translate")}</MenuItem>
|
||||||
@@ -302,7 +302,7 @@ export default function Settings() {
|
|||||||
i18n("pattern_helper") + " " + i18n("disabled_csplist_helper")
|
i18n("pattern_helper") + " " + i18n("disabled_csplist_helper")
|
||||||
}
|
}
|
||||||
name="csplist"
|
name="csplist"
|
||||||
defaultValue={csplist}
|
value={csplist}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
multiline
|
multiline
|
||||||
/>
|
/>
|
||||||
@@ -345,7 +345,7 @@ export default function Settings() {
|
|||||||
label={i18n("translate_blacklist")}
|
label={i18n("translate_blacklist")}
|
||||||
helperText={i18n("pattern_helper")}
|
helperText={i18n("pattern_helper")}
|
||||||
name="blacklist"
|
name="blacklist"
|
||||||
defaultValue={blacklist}
|
value={blacklist}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
maxRows={10}
|
maxRows={10}
|
||||||
multiline
|
multiline
|
||||||
|
|||||||
@@ -2,32 +2,61 @@ import Stack from "@mui/material/Stack";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { shortcutListener } from "../../libs/shortcut";
|
import { shortcutListener } from "../../libs/shortcut";
|
||||||
|
import { useI18n } from "../../hooks/I18n";
|
||||||
|
|
||||||
export default function ShortcutInput({ value, onChange, label, helperText }) {
|
export default function ShortcutInput({
|
||||||
const [disabled, setDisabled] = useState(true);
|
value: keys,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
helperText,
|
||||||
|
}) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editingKeys, setEditingKeys] = useState([]);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const commitChanges = () => {
|
||||||
|
if (editingKeys.length > 0) {
|
||||||
|
onChange(editingKeys);
|
||||||
|
}
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
commitChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditClick = () => {
|
||||||
|
setEditingKeys([]);
|
||||||
|
setIsEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled) {
|
if (!isEditing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const inputElement = inputRef.current;
|
||||||
inputRef.current.focus();
|
if (inputElement) {
|
||||||
onChange([]);
|
inputElement.focus();
|
||||||
|
|
||||||
const clearShortcut = shortcutListener((curkeys, allkeys) => {
|
|
||||||
onChange(allkeys);
|
|
||||||
if (curkeys.length === 0) {
|
|
||||||
setDisabled(true);
|
|
||||||
}
|
}
|
||||||
}, inputRef.current);
|
const clearShortcut = shortcutListener((pressedKeys, event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setEditingKeys([...pressedKeys]);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearShortcut();
|
clearShortcut();
|
||||||
};
|
};
|
||||||
}, [disabled, onChange]);
|
}, [isEditing]);
|
||||||
|
|
||||||
|
const displayValue = isEditing ? editingKeys : keys;
|
||||||
|
const formattedValue = displayValue
|
||||||
|
.map((item) => (item === " " ? "Space" : item))
|
||||||
|
.join(" + ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" alignItems="flex-start">
|
<Stack direction="row" alignItems="flex-start">
|
||||||
@@ -35,22 +64,22 @@ export default function ShortcutInput({ value, onChange, label, helperText }) {
|
|||||||
size="small"
|
size="small"
|
||||||
label={label}
|
label={label}
|
||||||
name={label}
|
name={label}
|
||||||
value={value.map((item) => (item === " " ? "Space" : item)).join(" + ")}
|
value={formattedValue}
|
||||||
fullWidth
|
fullWidth
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
disabled={disabled}
|
disabled={!isEditing}
|
||||||
onBlur={() => {
|
onBlur={handleBlur}
|
||||||
setDisabled(true);
|
helperText={isEditing ? i18n("pls_press_shortcut") : helperText}
|
||||||
}}
|
|
||||||
helperText={helperText}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
{isEditing ? (
|
||||||
onClick={() => {
|
<IconButton onClick={commitChanges} color="primary">
|
||||||
setDisabled(false);
|
<CheckIcon />
|
||||||
}}
|
|
||||||
>
|
|
||||||
{<EditIcon />}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<IconButton onClick={handleEditClick}>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import { useAlert } from "../../hooks/Alert";
|
|||||||
import { useSetting } from "../../hooks/Setting";
|
import { useSetting } from "../../hooks/Setting";
|
||||||
import { kissLog } from "../../libs/log";
|
import { kissLog } from "../../libs/log";
|
||||||
import SyncIcon from "@mui/icons-material/Sync";
|
import SyncIcon from "@mui/icons-material/Sync";
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||||
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
import ContentPasteIcon from "@mui/icons-material/ContentPaste";
|
||||||
|
|
||||||
export default function SyncSetting() {
|
export default function SyncSetting() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -44,10 +44,10 @@ export default function SyncSetting() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await syncSettingAndRules();
|
await syncSettingAndRules();
|
||||||
await reloadSetting();
|
reloadSetting();
|
||||||
alert.success(i18n("sync_success"));
|
alert.success(i18n("sync_success"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "sync all");
|
kissLog("sync all", err);
|
||||||
alert.error(i18n("sync_failed"));
|
alert.error(i18n("sync_failed"));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -56,37 +56,37 @@ export default function SyncSetting() {
|
|||||||
|
|
||||||
const handleGenerateShareString = async () => {
|
const handleGenerateShareString = async () => {
|
||||||
try {
|
try {
|
||||||
const base64Config = btoa(JSON.stringify({
|
const base64Config = btoa(
|
||||||
|
JSON.stringify({
|
||||||
syncType: syncType,
|
syncType: syncType,
|
||||||
syncUrl: syncUrl,
|
syncUrl: syncUrl,
|
||||||
syncUser: syncUser,
|
syncUser: syncUser,
|
||||||
syncKey: syncKey,
|
syncKey: syncKey,
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
const shareString = `${OPT_SYNCTOKEN_PERFIX}${base64Config}`;
|
const shareString = `${OPT_SYNCTOKEN_PERFIX}${base64Config}`;
|
||||||
await navigator.clipboard.writeText(shareString);
|
await navigator.clipboard.writeText(shareString);
|
||||||
console.debug("Share string copied to clipboard", shareString);
|
kissLog("Share string copied to clipboard", shareString);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to copy share string to clipboard", error);
|
kissLog("Failed to copy share string to clipboard", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImportFromClipboard = async () => {
|
const handleImportFromClipboard = async () => {
|
||||||
try {
|
try {
|
||||||
const text = await navigator.clipboard.readText();
|
const text = await navigator.clipboard.readText();
|
||||||
console.debug('read_clipboard', text)
|
kissLog("read_clipboard", text);
|
||||||
if (text.startsWith(OPT_SYNCTOKEN_PERFIX)) {
|
if (text.startsWith(OPT_SYNCTOKEN_PERFIX)) {
|
||||||
const base64Config = text.slice(OPT_SYNCTOKEN_PERFIX.length);
|
const base64Config = text.slice(OPT_SYNCTOKEN_PERFIX.length);
|
||||||
const jsonString = atob(base64Config);
|
const jsonString = atob(base64Config);
|
||||||
const updatedConfig = JSON.parse(jsonString);
|
const updatedConfig = JSON.parse(jsonString);
|
||||||
|
|
||||||
if (!OPT_SYNCTYPE_ALL.includes(updatedConfig.syncType)) {
|
if (!OPT_SYNCTYPE_ALL.includes(updatedConfig.syncType)) {
|
||||||
console.error('error syncType', updatedConfig.syncType)
|
kissLog("error syncType", updatedConfig.syncType);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (updatedConfig.syncUrl) {
|
||||||
updatedConfig.syncUrl
|
|
||||||
) {
|
|
||||||
updateSync({
|
updateSync({
|
||||||
syncType: updatedConfig.syncType,
|
syncType: updatedConfig.syncType,
|
||||||
syncUrl: updatedConfig.syncUrl,
|
syncUrl: updatedConfig.syncUrl,
|
||||||
@@ -94,17 +94,16 @@ export default function SyncSetting() {
|
|||||||
syncKey: updatedConfig.syncKey,
|
syncKey: updatedConfig.syncKey,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error("Invalid config structure");
|
kissLog("Invalid config structure");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error("Invalid share string", text);
|
kissLog("Invalid share string", text);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to read from clipboard or parse JSON", error);
|
kissLog("Failed to read from clipboard or parse JSON", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (!sync) {
|
if (!sync) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import TextField from "@mui/material/TextField";
|
|||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import {
|
import {
|
||||||
OPT_TRANS_ALL,
|
|
||||||
OPT_LANGS_FROM,
|
OPT_LANGS_FROM,
|
||||||
OPT_LANGS_TO,
|
OPT_LANGS_TO,
|
||||||
OPT_TRANBOX_TRIGGER_CLICK,
|
OPT_TRANBOX_TRIGGER_CLICK,
|
||||||
@@ -16,11 +15,13 @@ import { useCallback } from "react";
|
|||||||
import { limitNumber } from "../../libs/utils";
|
import { limitNumber } from "../../libs/utils";
|
||||||
import { useTranbox } from "../../hooks/Tranbox";
|
import { useTranbox } from "../../hooks/Tranbox";
|
||||||
import { isExt } from "../../libs/client";
|
import { isExt } from "../../libs/client";
|
||||||
|
import { useApiList } from "../../hooks/Api";
|
||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
|
|
||||||
export default function Tranbox() {
|
export default function Tranbox() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { tranboxSetting, updateTranbox } = useTranbox();
|
const { tranboxSetting, updateTranbox } = useTranbox();
|
||||||
|
const { enabledApis } = useApiList();
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -47,7 +48,7 @@ export default function Tranbox() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
translator,
|
apiSlug,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
toLang2 = "en",
|
toLang2 = "en",
|
||||||
@@ -72,14 +73,14 @@ export default function Tranbox() {
|
|||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
size="small"
|
size="small"
|
||||||
name="translator"
|
name="apiSlug"
|
||||||
value={translator}
|
value={apiSlug}
|
||||||
label={i18n("translate_service")}
|
label={i18n("translate_service")}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{OPT_TRANS_ALL.map((item) => (
|
{enabledApis.map((api) => (
|
||||||
<MenuItem key={item} value={item}>
|
<MenuItem key={api.apiSlug} value={api.apiSlug}>
|
||||||
{item}
|
{api.apiName}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
@@ -147,7 +148,7 @@ export default function Tranbox() {
|
|||||||
label={i18n("tranbtn_offset_x")}
|
label={i18n("tranbtn_offset_x")}
|
||||||
type="number"
|
type="number"
|
||||||
name="btnOffsetX"
|
name="btnOffsetX"
|
||||||
defaultValue={btnOffsetX}
|
value={btnOffsetX}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -156,7 +157,7 @@ export default function Tranbox() {
|
|||||||
label={i18n("tranbtn_offset_y")}
|
label={i18n("tranbtn_offset_y")}
|
||||||
type="number"
|
type="number"
|
||||||
name="btnOffsetY"
|
name="btnOffsetY"
|
||||||
defaultValue={btnOffsetY}
|
value={btnOffsetY}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -165,7 +166,7 @@ export default function Tranbox() {
|
|||||||
label={i18n("tranbox_offset_x")}
|
label={i18n("tranbox_offset_x")}
|
||||||
type="number"
|
type="number"
|
||||||
name="boxOffsetX"
|
name="boxOffsetX"
|
||||||
defaultValue={boxOffsetX}
|
value={boxOffsetX}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -174,7 +175,7 @@ export default function Tranbox() {
|
|||||||
label={i18n("tranbox_offset_y")}
|
label={i18n("tranbox_offset_y")}
|
||||||
type="number"
|
type="number"
|
||||||
name="boxOffsetY"
|
name="boxOffsetY"
|
||||||
defaultValue={boxOffsetY}
|
value={boxOffsetY}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -245,7 +246,7 @@ export default function Tranbox() {
|
|||||||
size="small"
|
size="small"
|
||||||
label={i18n("extend_styles")}
|
label={i18n("extend_styles")}
|
||||||
name="extStyles"
|
name="extStyles"
|
||||||
defaultValue={extStyles}
|
value={extStyles}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
maxRows={10}
|
maxRows={10}
|
||||||
multiline
|
multiline
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import ThemeProvider from "../../hooks/Theme";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { isGm } from "../../libs/client";
|
import { isGm } from "../../libs/client";
|
||||||
import { sleep } from "../../libs/utils";
|
import { sleep } from "../../libs/utils";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
|
||||||
import { trySyncSettingAndRules } from "../../libs/sync";
|
import { trySyncSettingAndRules } from "../../libs/sync";
|
||||||
import { AlertProvider } from "../../hooks/Alert";
|
import { AlertProvider } from "../../hooks/Alert";
|
||||||
|
import { ConfirmProvider } from "../../hooks/Confirm";
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
@@ -22,6 +22,7 @@ import InputSetting from "./InputSetting";
|
|||||||
import Tranbox from "./Tranbox";
|
import Tranbox from "./Tranbox";
|
||||||
import FavWords from "./FavWords";
|
import FavWords from "./FavWords";
|
||||||
import MouseHoverSetting from "./MouseHover";
|
import MouseHoverSetting from "./MouseHover";
|
||||||
|
import Loading from "../../hooks/Loading";
|
||||||
|
|
||||||
export default function Options() {
|
export default function Options() {
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
@@ -91,22 +92,14 @@ export default function Options() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
return (
|
return <Loading />;
|
||||||
<center>
|
|
||||||
<Divider>
|
|
||||||
<Link
|
|
||||||
href={process.env.REACT_APP_HOMEPAGE}
|
|
||||||
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
|
||||||
</Divider>
|
|
||||||
<CircularProgress />
|
|
||||||
</center>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingProvider>
|
<SettingProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AlertProvider>
|
<AlertProvider>
|
||||||
|
<ConfirmProvider>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Layout />}>
|
<Route path="/" element={<Layout />}>
|
||||||
@@ -122,6 +115,7 @@ export default function Options() {
|
|||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
</ConfirmProvider>
|
||||||
</AlertProvider>
|
</AlertProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</SettingProvider>
|
</SettingProvider>
|
||||||
|
|||||||
@@ -20,11 +20,9 @@ import {
|
|||||||
MSG_OPEN_OPTIONS,
|
MSG_OPEN_OPTIONS,
|
||||||
MSG_SAVE_RULE,
|
MSG_SAVE_RULE,
|
||||||
MSG_COMMAND_SHORTCUTS,
|
MSG_COMMAND_SHORTCUTS,
|
||||||
OPT_TRANS_ALL,
|
|
||||||
OPT_LANGS_FROM,
|
OPT_LANGS_FROM,
|
||||||
OPT_LANGS_TO,
|
OPT_LANGS_TO,
|
||||||
OPT_STYLE_ALL,
|
OPT_STYLE_ALL,
|
||||||
DEFAULT_TRANS_APIS,
|
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { sendIframeMsg } from "../../libs/iframe";
|
import { sendIframeMsg } from "../../libs/iframe";
|
||||||
import { saveRule } from "../../libs/rules";
|
import { saveRule } from "../../libs/rules";
|
||||||
@@ -33,14 +31,14 @@ import { kissLog } from "../../libs/log";
|
|||||||
|
|
||||||
// 插件popup没有参数
|
// 插件popup没有参数
|
||||||
// 网页弹框有
|
// 网页弹框有
|
||||||
export default function Popup({ setShowPopup, translator: tran }) {
|
export default function Popup({ setShowPopup, translator }) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const [rule, setRule] = useState(tran?.rule);
|
const [rule, setRule] = useState(translator?.rule);
|
||||||
const [transApis, setTransApis] = useState(tran?.setting?.transApis || []);
|
const [transApis, setTransApis] = useState(translator?.setting?.transApis || []);
|
||||||
const [commands, setCommands] = useState({});
|
const [commands, setCommands] = useState({});
|
||||||
|
|
||||||
const handleOpenSetting = () => {
|
const handleOpenSetting = () => {
|
||||||
if (!tran) {
|
if (!translator) {
|
||||||
browser?.runtime.openOptionsPage();
|
browser?.runtime.openOptionsPage();
|
||||||
} else if (isExt) {
|
} else if (isExt) {
|
||||||
sendBgMsg(MSG_OPEN_OPTIONS);
|
sendBgMsg(MSG_OPEN_OPTIONS);
|
||||||
@@ -54,14 +52,14 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
try {
|
try {
|
||||||
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
|
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
|
||||||
|
|
||||||
if (!tran) {
|
if (!translator) {
|
||||||
await sendTabMsg(MSG_TRANS_TOGGLE);
|
await sendTabMsg(MSG_TRANS_TOGGLE);
|
||||||
} else {
|
} else {
|
||||||
tran.toggle();
|
translator.toggle();
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "toggle trans");
|
kissLog("toggle trans", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,14 +68,14 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setRule((pre) => ({ ...pre, [name]: value }));
|
setRule((pre) => ({ ...pre, [name]: value }));
|
||||||
|
|
||||||
if (!tran) {
|
if (!translator) {
|
||||||
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||||
} else {
|
} else {
|
||||||
tran.updateRule({ [name]: value });
|
translator.updateRule({ [name]: value });
|
||||||
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "update rule");
|
kissLog("update rule", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,23 +86,23 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
const handleSaveRule = async () => {
|
const handleSaveRule = async () => {
|
||||||
try {
|
try {
|
||||||
let href = window.location.href;
|
let href = window.location.href;
|
||||||
if (!tran) {
|
if (!translator) {
|
||||||
const tab = await getCurTab();
|
const tab = await getCurTab();
|
||||||
href = tab.url;
|
href = tab.url;
|
||||||
}
|
}
|
||||||
const newRule = { ...rule, pattern: href.split("/")[2] };
|
const newRule = { ...rule, pattern: href.split("/")[2] };
|
||||||
if (isExt && tran) {
|
if (isExt && translator) {
|
||||||
sendBgMsg(MSG_SAVE_RULE, newRule);
|
sendBgMsg(MSG_SAVE_RULE, newRule);
|
||||||
} else {
|
} else {
|
||||||
saveRule(newRule);
|
saveRule(newRule);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "save rule");
|
kissLog("save rule", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tran) {
|
if (translator) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -115,10 +113,10 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
setTransApis(res.setting.transApis);
|
setTransApis(res.setting.transApis);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "query rule");
|
kissLog("query rule", err);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [tran]);
|
}, [translator]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -130,7 +128,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
commands[name] = shortcut;
|
commands[name] = shortcut;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const shortcuts = tran.setting.shortcuts;
|
const shortcuts = translator.setting.shortcuts;
|
||||||
if (shortcuts) {
|
if (shortcuts) {
|
||||||
Object.entries(shortcuts).forEach(([key, val]) => {
|
Object.entries(shortcuts).forEach(([key, val]) => {
|
||||||
commands[key] = val.join("+");
|
commands[key] = val.join("+");
|
||||||
@@ -139,21 +137,18 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
}
|
}
|
||||||
setCommands(commands);
|
setCommands(commands);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "query cmds");
|
kissLog("query cmds", err);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [tran]);
|
}, [translator]);
|
||||||
|
|
||||||
const optApis = useMemo(
|
const optApis = useMemo(
|
||||||
() =>
|
() =>
|
||||||
OPT_TRANS_ALL.map((key) => ({
|
transApis
|
||||||
...(transApis[key] || DEFAULT_TRANS_APIS[key]),
|
.filter((api) => !api.isDisabled)
|
||||||
apiKey: key,
|
.map((api) => ({
|
||||||
}))
|
key: api.apiSlug,
|
||||||
.filter((item) => !item.isDisabled)
|
name: api.apiName || api.apiSlug,
|
||||||
.map(({ apiKey, apiName }) => ({
|
|
||||||
key: apiKey,
|
|
||||||
name: apiName?.trim() || apiKey,
|
|
||||||
})),
|
})),
|
||||||
[transApis]
|
[transApis]
|
||||||
);
|
);
|
||||||
@@ -161,7 +156,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
if (!rule) {
|
if (!rule) {
|
||||||
return (
|
return (
|
||||||
<Box minWidth={300}>
|
<Box minWidth={300}>
|
||||||
{!tran && (
|
{!translator && (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
<Divider />
|
<Divider />
|
||||||
@@ -178,7 +173,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
transOpen,
|
transOpen,
|
||||||
translator,
|
apiSlug,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
textStyle,
|
textStyle,
|
||||||
@@ -190,7 +185,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box width={320}>
|
<Box width={320}>
|
||||||
{!tran && (
|
{!translator && (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
<Divider />
|
<Divider />
|
||||||
@@ -275,8 +270,8 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
select
|
select
|
||||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||||
size="small"
|
size="small"
|
||||||
value={translator}
|
value={apiSlug}
|
||||||
name="translator"
|
name="apiSlug"
|
||||||
label={i18n("translate_service")}
|
label={i18n("translate_service")}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } 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";
|
||||||
@@ -26,10 +26,10 @@ export default function DictCont({ text }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo
|
// todo: 修复
|
||||||
const dictRes = await apiTranslate({
|
const dictRes = await apiTranslate({
|
||||||
text,
|
text,
|
||||||
translator: OPT_TRANS_BAIDU,
|
apiSlug: OPT_TRANS_BAIDU,
|
||||||
fromLang: "en",
|
fromLang: "en",
|
||||||
toLang: "zh-CN",
|
toLang: "zh-CN",
|
||||||
});
|
});
|
||||||
@@ -45,19 +45,12 @@ export default function DictCont({ text }) {
|
|||||||
})();
|
})();
|
||||||
}, [text]);
|
}, [text]);
|
||||||
|
|
||||||
if (error) {
|
const copyText = useMemo(() => {
|
||||||
return <Alert severity="error">{error}</Alert>;
|
if (!dictResult) {
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
return [
|
||||||
return <CircularProgress size={16} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!text || !dictResult) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyText = [
|
|
||||||
dictResult.src,
|
dictResult.src,
|
||||||
dictResult.voice
|
dictResult.voice
|
||||||
?.map(Object.entries)
|
?.map(Object.entries)
|
||||||
@@ -70,19 +63,29 @@ export default function DictCont({ text }) {
|
|||||||
})
|
})
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
}, [text, dictResult]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <CircularProgress size={16} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="KT-transbox-dict" spacing={1}>
|
<Stack className="KT-transbox-dict" spacing={1}>
|
||||||
|
{text && (
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
|
<Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
|
||||||
{dictResult.src}
|
{dictResult?.src || text}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<CopyBtn text={copyText} />
|
<CopyBtn text={copyText} />
|
||||||
<FavBtn word={dictResult.src} />
|
<FavBtn word={dictResult?.src || text} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && <Alert severity="error">{error}</Alert>}
|
||||||
|
|
||||||
|
{dictResult && (
|
||||||
<Typography component="div">
|
<Typography component="div">
|
||||||
<Typography component="div">
|
<Typography component="div">
|
||||||
{dictResult.voice
|
{dictResult.voice
|
||||||
@@ -109,6 +112,7 @@ export default function DictCont({ text }) {
|
|||||||
))}
|
))}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ export default function FavBtn({ word }) {
|
|||||||
const { favWords, toggleFav } = useFavWords();
|
const { favWords, toggleFav } = useFavWords();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await toggleFav(word);
|
toggleFav(word);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "set fav");
|
kissLog("set fav", err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ import LockIcon from "@mui/icons-material/Lock";
|
|||||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import {
|
import { OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
|
||||||
OPT_TRANS_ALL,
|
|
||||||
OPT_LANGS_FROM,
|
|
||||||
OPT_LANGS_TO,
|
|
||||||
DEFAULT_TRANS_APIS,
|
|
||||||
} from "../../config";
|
|
||||||
import { useState, useRef, useMemo } from "react";
|
import { useState, useRef, useMemo } from "react";
|
||||||
import TranCont from "./TranCont";
|
import TranCont from "./TranCont";
|
||||||
import DictCont from "./DictCont";
|
import DictCont from "./DictCont";
|
||||||
@@ -119,21 +114,18 @@ function TranForm({
|
|||||||
|
|
||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
const [editText, setEditText] = useState("");
|
const [editText, setEditText] = useState("");
|
||||||
const [translator, setTranslator] = useState(tranboxSetting.translator);
|
const [apiSlug, setApiSlug] = useState(tranboxSetting.apiSlug);
|
||||||
const [fromLang, setFromLang] = useState(tranboxSetting.fromLang);
|
const [fromLang, setFromLang] = useState(tranboxSetting.fromLang);
|
||||||
const [toLang, setToLang] = useState(tranboxSetting.toLang);
|
const [toLang, setToLang] = useState(tranboxSetting.toLang);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
const optApis = useMemo(
|
const optApis = useMemo(
|
||||||
() =>
|
() =>
|
||||||
OPT_TRANS_ALL.map((key) => ({
|
transApis
|
||||||
...(transApis[key] || DEFAULT_TRANS_APIS[key]),
|
.filter((api) => !api.isDisabled)
|
||||||
apiKey: key,
|
.map((api) => ({
|
||||||
}))
|
key: api.apiSlug,
|
||||||
.filter((item) => !item.isDisabled)
|
name: api.apiName || api.apiSlug,
|
||||||
.map(({ apiKey, apiName }) => ({
|
|
||||||
key: apiKey,
|
|
||||||
name: apiName?.trim() || apiKey,
|
|
||||||
})),
|
})),
|
||||||
[transApis]
|
[transApis]
|
||||||
);
|
);
|
||||||
@@ -194,11 +186,11 @@ function TranForm({
|
|||||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
value={translator}
|
value={apiSlug}
|
||||||
name="translator"
|
name="apiSlug"
|
||||||
label={i18n("translate_service")}
|
label={i18n("translate_service")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setTranslator(e.target.value);
|
setApiSlug(e.target.value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{optApis.map(({ key, name }) => (
|
{optApis.map(({ key, name }) => (
|
||||||
@@ -266,7 +258,7 @@ function TranForm({
|
|||||||
enDict === "-") && (
|
enDict === "-") && (
|
||||||
<TranCont
|
<TranCont
|
||||||
text={text}
|
text={text}
|
||||||
translator={translator}
|
apiSlug={apiSlug}
|
||||||
fromLang={fromLang}
|
fromLang={fromLang}
|
||||||
toLang={toLang}
|
toLang={toLang}
|
||||||
toLang2={tranboxSetting.toLang2}
|
toLang2={tranboxSetting.toLang2}
|
||||||
@@ -307,6 +299,7 @@ export default function TranBox({
|
|||||||
enDict,
|
enDict,
|
||||||
}) {
|
}) {
|
||||||
const [mouseHover, setMouseHover] = useState(false);
|
const [mouseHover, setMouseHover] = useState(false);
|
||||||
|
// todo: 这里的 SettingProvider 不应和 background 的共用
|
||||||
return (
|
return (
|
||||||
<SettingProvider>
|
<SettingProvider>
|
||||||
<ThemeProvider styles={extStyles}>
|
<ThemeProvider styles={extStyles}>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Box from "@mui/material/Box";
|
|||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import { DEFAULT_TRANS_APIS } from "../../config";
|
import { DEFAULT_API_SETTING } from "../../config";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { apiTranslate } from "../../apis";
|
import { apiTranslate } from "../../apis";
|
||||||
import CopyBtn from "./CopyBtn";
|
import CopyBtn from "./CopyBtn";
|
||||||
@@ -13,7 +13,7 @@ import { tryDetectLang } from "../../libs";
|
|||||||
|
|
||||||
export default function TranCont({
|
export default function TranCont({
|
||||||
text,
|
text,
|
||||||
translator,
|
apiSlug,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
toLang2 = "en",
|
toLang2 = "en",
|
||||||
@@ -42,10 +42,11 @@ export default function TranCont({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const apiSetting =
|
const apiSetting =
|
||||||
transApis[translator] || DEFAULT_TRANS_APIS[translator];
|
transApis.find((api) => api.apiSlug === apiSlug) ||
|
||||||
|
DEFAULT_API_SETTING;
|
||||||
const [trText] = await apiTranslate({
|
const [trText] = await apiTranslate({
|
||||||
text,
|
text,
|
||||||
translator,
|
apiSlug,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang: to,
|
toLang: to,
|
||||||
apiSetting,
|
apiSetting,
|
||||||
@@ -57,7 +58,7 @@ export default function TranCont({
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [text, translator, fromLang, toLang, toLang2, transApis, langDetector]);
|
}, [text, apiSlug, fromLang, toLang, toLang2, transApis, langDetector]);
|
||||||
|
|
||||||
if (simpleStyle) {
|
if (simpleStyle) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ export default function Slection({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
kissLog(err, "registerMenuCommand");
|
kissLog("registerMenuCommand", err);
|
||||||
}
|
}
|
||||||
}, [handleTranbox, contextMenuType, langMap]);
|
}, [handleTranbox, contextMenuType, langMap]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user