Compare commits

...

11 Commits

Author SHA1 Message Date
Gabe Yuan
beca31f55d v1.7.15 2023-12-21 14:24:08 +08:00
Gabe Yuan
c7df103950 feat: add gemini translator 2023-12-21 14:15:14 +08:00
Gabe Yuan
4bf7972ad5 fix: fab button fontsize 2023-12-19 15:34:40 +08:00
Gabe Yuan
534eaed1ed refactor: input translate 2023-12-18 11:46:37 +08:00
Gabe Yuan
7e014e7385 fix: contextMenus setting 2023-12-15 11:43:01 +08:00
Gabe Yuan
34adb2660b fix: grant GM.unregisterMenuCommand 2023-12-15 10:58:49 +08:00
Gabe Yuan
b6bc165cf0 fix: context munus 2023-12-11 17:26:49 +08:00
Gabe Yuan
bdd5ed7fc7 feat: mutual translation effect with the target language 2023-12-11 15:54:54 +08:00
Gabe Yuan
95d19417c3 fix: touch tap limit 2023-12-11 11:40:20 +08:00
Gabe Yuan
30ebebdd71 fix: tranbtn position: absolute 2023-12-11 11:38:47 +08:00
Gabe Yuan
e9c557776d feat: context menus setting 2023-12-11 11:25:02 +08:00
23 changed files with 483 additions and 297 deletions

2
.env
View File

@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=1.7.14
REACT_APP_VERSION=1.7.15
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator

View File

@@ -12,7 +12,7 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
- [x] Chrome/Edge/Firefox/Kiwi
- [ ] Safari
- [x] Supports multiple translation services
- [x] Google/Microsoft/DeepL/OpenAI/CloudflareAI/Baidu/Tencent
- [x] Google/Microsoft/DeepL/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
- [x] Custom translation interface
- [x] Covers common translation scenarios
- [x] Web bilingual translation

View File

@@ -12,7 +12,7 @@
- [x] Chrome/Edge/Firefox/Kiwi
- [ ] Safari
- [x] 支持多种翻译服务
- [x] Google/Microsoft/DeepL/OpenAI/CloudflareAI/Baidu/Tencent
- [x] Google/Microsoft/DeepL/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
- [x] 自定义翻译接口
- [x] 覆盖常见翻译场景
- [x] 网页双语对照翻译

View File

@@ -85,6 +85,7 @@ const userscriptWebpack = (config, env) => {
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
// @grant GM.xmlHttpRequest
// @grant GM.registerMenuCommand
// @grant GM.unregisterMenuCommand
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.deleteValue
@@ -97,6 +98,7 @@ const userscriptWebpack = (config, env) => {
// @connect api.deepl.com
// @connect www2.deepl.com
// @connect api.openai.com
// @connect generativelanguage.googleapis.com
// @connect openai.azure.com
// @connect workers.dev
// @connect github.io

View File

@@ -1,7 +1,7 @@
{
"name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.7.14",
"version": "1.7.15",
"author": "Gabe<yugang2002@gmail.com>",
"private": true,
"dependencies": {

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.7.14",
"version": "1.7.15",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.7.14",
"version": "1.7.15",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -9,6 +9,7 @@ import {
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_CUSTOMIZE,
URL_CACHE_TRAN,
@@ -181,6 +182,10 @@ export const apiTranslate = async ({
trText = res?.choices?.[0].message.content;
isSame = text === trText;
break;
case OPT_TRANS_GEMINI:
trText = res?.candidates?.[0].content.parts[0].text;
isSame = text === trText;
break;
case OPT_TRANS_CLOUDFLAREAI:
trText = res?.result?.translated_text;
isSame = text === trText;

View File

@@ -8,6 +8,7 @@ import {
MSG_SAVE_RULE,
MSG_TRANS_TOGGLE_STYLE,
MSG_OPEN_TRANBOX,
MSG_CONTEXT_MENUS,
CMD_TOGGLE_TRANSLATE,
CMD_TOGGLE_STYLE,
CMD_OPEN_OPTIONS,
@@ -24,12 +25,9 @@ import { saveRule } from "./libs/rules";
globalThis.ContextType = "BACKGROUND";
/**
* 插件安装
* 添加右键菜单
*/
browser.runtime.onInstalled.addListener(() => {
tryInitDefaultData();
// 右键菜单
function addContextMenus() {
browser.contextMenus.create({
id: CMD_TOGGLE_TRANSLATE,
title: browser.i18n.getMessage("toggle_translate"),
@@ -55,6 +53,23 @@ browser.runtime.onInstalled.addListener(() => {
title: browser.i18n.getMessage("open_options"),
contexts: ["all"],
});
}
/**
* 清除右键菜单
*/
function removeContextMenus() {
browser.contextMenus.removeAll();
}
/**
* 插件安装
*/
browser.runtime.onInstalled.addListener(() => {
tryInitDefaultData();
// 右键菜单
addContextMenus();
});
/**
@@ -64,14 +79,24 @@ browser.runtime.onStartup.addListener(async () => {
// 同步数据
await trySyncSettingAndRules();
const {
clearCache,
contextMenus = true,
subrulesList,
} = await getSettingWithDefault();
// 清除缓存
const setting = await getSettingWithDefault();
if (setting.clearCache) {
if (clearCache) {
tryClearCaches();
}
// 右键菜单
if (!contextMenus) {
removeContextMenus();
}
// 同步订阅规则
trySyncAllSubRules(setting);
trySyncAllSubRules({ subrulesList });
});
/**
@@ -105,6 +130,14 @@ browser.runtime.onMessage.addListener(
case MSG_SAVE_RULE:
saveRule(args);
break;
case MSG_CONTEXT_MENUS:
const { contextMenus } = args;
if (contextMenus) {
addContextMenus();
} else {
removeContextMenus();
}
break;
default:
sendResponse({ error: `message action is unavailable: ${action}` });
}
@@ -131,6 +164,9 @@ browser.commands.onCommand.addListener((command) => {
}
});
/**
* 监听右键菜单
*/
browser.contextMenus.onClicked.addListener(({ menuItemId }) => {
switch (menuItemId) {
case CMD_TOGGLE_TRANSLATE:

View File

@@ -24,6 +24,7 @@ import { runWebfix } from "./libs/webfix";
import { matchRule } from "./libs/rules";
import { trySyncAllSubRules } from "./libs/subRules";
import { isInBlacklist } from "./libs/blacklist";
import inputTranslate from "./libs/inputTranslate";
/**
* 油猴脚本设置页面
@@ -142,7 +143,11 @@ async function showFab(translator) {
* @param {*} param0
* @returns
*/
function showTransbox({ tranboxSetting = DEFAULT_TRANBOX_SETTING, transApis }) {
function showTransbox({
contextMenus = true,
tranboxSetting = DEFAULT_TRANBOX_SETTING,
transApis,
}) {
if (!tranboxSetting?.transOpen) {
return;
}
@@ -163,7 +168,11 @@ function showTransbox({ tranboxSetting = DEFAULT_TRANBOX_SETTING, transApis }) {
ReactDOM.createRoot(shadowRootElement).render(
<React.StrictMode>
<CacheProvider value={cache}>
<Slection tranboxSetting={tranboxSetting} transApis={transApis} />
<Slection
contextMenus={contextMenus}
tranboxSetting={tranboxSetting}
transApis={transApis}
/>
</CacheProvider>
</React.StrictMode>
);
@@ -257,6 +266,9 @@ export async function run(isUserscript = false) {
windowListener(rule);
!isUserscript && runtimeListener(translator);
// 输入框翻译
inputTranslate(setting);
// 划词翻译
showTransbox(setting);

View File

@@ -215,6 +215,14 @@ export const I18N = {
zh: `目标语言`,
en: `Target Language`,
},
to_lang2: {
zh: `第二目标语言`,
en: `Target Language 2`,
},
to_lang2_helper: {
zh: `设定后,与目标语言产生互译效果,但依赖远程语言识别。`,
en: `After setting, it will produce mutual translation effect with the target language, but it relies on remote language recognition.`,
},
text_style: {
zh: `文字样式`,
en: `Text Style`,
@@ -694,5 +702,9 @@ export const I18N = {
disable_langs_helper: {
zh: `此功能依赖准确的语言检测,建议启用远程语言检测。`,
en: `This feature relies on accurate language detection. It is recommended to enable remote language detection.`,
}
},
add_context_menus: {
zh: `添加右键菜单`,
en: `Add Context Menus`,
},
};

View File

@@ -63,6 +63,7 @@ export const MSG_OPEN_TRANBOX = "open_tranbox";
export const MSG_TRANS_GETRULE = "trans_getrule";
export const MSG_TRANS_PUTRULE = "trans_putrule";
export const MSG_TRANS_CURRULE = "trans_currule";
export const MSG_CONTEXT_MENUS = "context_menus";
export const THEME_LIGHT = "light";
export const THEME_DARK = "dark";
@@ -93,6 +94,7 @@ export const OPT_TRANS_DEEPLFREE = "DeepLFree";
export const OPT_TRANS_BAIDU = "Baidu";
export const OPT_TRANS_TENCENT = "Tencent";
export const OPT_TRANS_OPENAI = "OpenAI";
export const OPT_TRANS_GEMINI = "Gemini";
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
export const OPT_TRANS_CUSTOMIZE = "Custom";
export const OPT_TRANS_ALL = [
@@ -104,6 +106,7 @@ export const OPT_TRANS_ALL = [
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_CUSTOMIZE,
];
@@ -226,6 +229,9 @@ export const OPT_LANGS_SPECIAL = {
[OPT_TRANS_OPENAI]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_GEMINI]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_CLOUDFLAREAI]: new Map([
["auto", ""],
["zh-CN", "chinese"],
@@ -307,6 +313,7 @@ export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
export const PROMPT_PLACE_FROM = "{{from}}"; // 占位符
export const PROMPT_PLACE_TO = "{{to}}"; // 占位符
export const PROMPT_PLACE_TEXT = "{{text}}"; // 占位符
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
@@ -344,6 +351,7 @@ export const DEFAULT_TRANBOX_SETTING = {
translator: OPT_TRANS_MICROSOFT,
fromLang: "auto",
toLang: "zh-CN",
toLang2: "en",
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
btnOffsetX: 10,
btnOffsetY: 10,
@@ -386,6 +394,12 @@ export const DEFAULT_TRANS_APIS = {
model: "gpt-4",
prompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
},
[OPT_TRANS_GEMINI]: {
url: "https://generativelanguage.googleapis.com/v1/models",
key: "",
model: "gemini-pro",
prompt: `Translate the following text from ${PROMPT_PLACE_FROM} to ${PROMPT_PLACE_TO}:\n\n${PROMPT_PLACE_TEXT}`,
},
[OPT_TRANS_CLOUDFLAREAI]: {
url: "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/@cf/meta/m2m100-1.2b",
key: "",
@@ -431,6 +445,7 @@ export const DEFAULT_SETTING = {
injectRules: true, // 是否注入订阅规则
injectWebfix: true, // 是否注入修复补丁
detectRemote: false, // 是否使用远程语言检测
contextMenus: true, // 是否添加右键菜单
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
transApis: DEFAULT_TRANS_APIS, // 翻译接口

205
src/libs/inputTranslate.js Normal file
View File

@@ -0,0 +1,205 @@
import {
DEFAULT_INPUT_RULE,
DEFAULT_TRANS_APIS,
DEFAULT_INPUT_SHORTCUT,
OPT_LANGS_LIST,
} from "../config";
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
import { stepShortcutRegister } from "./shortcut";
import { apiTranslate } from "../apis";
import { tryDetectLang } from ".";
import { loadingSvg } from "./svg";
function isInputNode(node) {
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
}
function isEditAbleNode(node) {
return node.hasAttribute("contenteditable");
}
function selectContent(node) {
node.focus();
const range = document.createRange();
range.selectNodeContents(node);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
function pasteContentEvent(node, text) {
node.focus();
const data = new DataTransfer();
data.setData("text/plain", text);
const event = new ClipboardEvent("paste", { clipboardData: data });
document.dispatchEvent(event);
data.clearData();
}
function pasteContentCommand(node, text) {
node.focus();
document.execCommand("insertText", false, text);
}
function collapseToEnd(node) {
node.focus();
const selection = window.getSelection();
selection.collapseToEnd();
}
function getNodeText(node) {
if (isInputNode(node)) {
return node.value;
}
return node.innerText || node.textContent || "";
}
function addLoading(node, loadingId) {
const div = document.createElement("div");
div.id = loadingId;
div.innerHTML = loadingSvg;
div.style.cssText = `
width: ${node.offsetWidth}px;
height: ${node.offsetHeight}px;
line-height: ${node.offsetHeight}px;
position: absolute;
text-align: center;
left: ${node.offsetLeft}px;
top: ${node.offsetTop}px;
z-index: 2147483647;
`;
node.offsetParent?.appendChild(div);
}
function removeLoading(node, loadingId) {
const div = node.offsetParent.querySelector(`#${loadingId}`);
if (div) {
div.remove();
}
}
/**
* 输入框翻译
*/
export default function inputTranslate ({
inputRule: {
transOpen,
triggerShortcut,
translator,
fromLang,
toLang,
triggerCount,
triggerTime,
transSign,
} = DEFAULT_INPUT_RULE,
transApis,
detectRemote,
}) {
if (!transOpen) {
return;
}
const apiSetting = transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
if (triggerShortcut.length === 0) {
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
triggerCount = 1;
}
stepShortcutRegister(
triggerShortcut,
async () => {
let node = document.activeElement;
if (!node) {
return;
}
while (node.shadowRoot) {
node = node.shadowRoot.activeElement;
}
if (!isInputNode(node) && !isEditAbleNode(node)) {
return;
}
let initText = getNodeText(node);
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
// todo: remove multiple char
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
}
if (!initText.trim()) {
return;
}
let text = initText;
if (transSign) {
const res = matchInputStr(text, transSign);
if (res) {
let lang = res[1];
if (lang === "zh" || lang === "cn") {
lang = "zh-CN";
} else if (lang === "tw" || lang === "hk") {
lang = "zh-TW";
}
if (lang && OPT_LANGS_LIST.includes(lang)) {
toLang = lang;
}
text = res[2];
}
}
// console.log("input -->", text);
const loadingId = "kiss-" + genEventName();
try {
addLoading(node, loadingId);
const deLang = await tryDetectLang(text, detectRemote);
if (deLang && toLang.includes(deLang)) {
return;
}
const [trText, isSame] = await apiTranslate({
translator,
text,
fromLang,
toLang,
apiSetting,
});
if (!trText || isSame) {
return;
}
if (isInputNode(node)) {
node.value = trText;
node.dispatchEvent(
new Event("input", { bubbles: true, cancelable: true })
);
return;
}
selectContent(node);
await sleep(200);
pasteContentEvent(node, trText);
await sleep(200);
// todo: use includes?
if (getNodeText(node).startsWith(initText)) {
pasteContentCommand(node, trText);
await sleep(100);
} else {
collapseToEnd(node);
}
} catch (err) {
console.log("[translate input]", err.message);
} finally {
removeLoading(node, loadingId);
}
},
triggerCount,
triggerTime
);
}

View File

@@ -8,12 +8,14 @@ import {
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_CUSTOMIZE,
URL_MICROSOFT_TRAN,
URL_TENCENT_TRANSMART,
PROMPT_PLACE_FROM,
PROMPT_PLACE_TO,
PROMPT_PLACE_TEXT,
} from "../config";
import { msAuth } from "./auth";
import { genDeeplFree } from "../apis/deepl";
@@ -178,6 +180,37 @@ const genOpenAI = ({ text, from, to, url, key, prompt, model }) => {
return [url, init];
};
const genGemini = ({ text, from, to, url, key, prompt, model }) => {
prompt = prompt
.replaceAll(PROMPT_PLACE_FROM, from)
.replaceAll(PROMPT_PLACE_TO, to)
.replaceAll(PROMPT_PLACE_TEXT, text);
const data = {
contents: [
{
// role: "user",
parts: [
{
text: prompt,
},
],
},
],
};
const input = `${url}/${model}:generateContent?key=${key}`;
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
return [input, init];
};
const genCloudflareAI = ({ text, from, to, url, key }) => {
const data = {
text,
@@ -241,6 +274,8 @@ export const newTransReq = ({ translator, text, from, to }, apiSetting) => {
return genTencent(args);
case OPT_TRANS_OPENAI:
return genOpenAI(args);
case OPT_TRANS_GEMINI:
return genGemini(args);
case OPT_TRANS_CLOUDFLAREAI:
return genCloudflareAI(args);
case OPT_TRANS_CUSTOMIZE:

View File

@@ -9,101 +9,16 @@ import {
SHADOW_KEY,
OPT_MOUSEKEY_DISABLE,
OPT_MOUSEKEY_MOUSEOVER,
DEFAULT_INPUT_RULE,
DEFAULT_TRANS_APIS,
DEFAULT_INPUT_SHORTCUT,
OPT_LANGS_LIST,
} from "../config";
import Content from "../views/Content";
import { updateFetchPool, clearFetchPool } from "./fetch";
import {
debounce,
genEventName,
removeEndchar,
matchInputStr,
sleep,
} from "./utils";
import { stepShortcutRegister } from "./shortcut";
import { apiTranslate } from "../apis";
import { tryDetectLang } from ".";
import { loadingSvg } from "./svg";
function isInputNode(node) {
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
}
function isEditAbleNode(node) {
return node.hasAttribute("contenteditable");
}
function selectContent(node) {
node.focus();
const range = document.createRange();
range.selectNodeContents(node);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
function pasteContentEvent(node, text) {
node.focus();
const data = new DataTransfer();
data.setData("text/plain", text);
const event = new ClipboardEvent("paste", { clipboardData: data });
document.dispatchEvent(event);
data.clearData();
}
function pasteContentCommand(node, text) {
node.focus();
document.execCommand("insertText", false, text);
}
function collapseToEnd(node) {
node.focus();
const selection = window.getSelection();
selection.collapseToEnd();
}
function getNodeText(node) {
if (isInputNode(node)) {
return node.value;
}
return node.innerText || node.textContent || "";
}
function addLoading(node, loadingId) {
const div = document.createElement("div");
div.id = loadingId;
div.innerHTML = loadingSvg;
div.style.cssText = `
width: ${node.offsetWidth}px;
height: ${node.offsetHeight}px;
line-height: ${node.offsetHeight}px;
position: absolute;
text-align: center;
left: ${node.offsetLeft}px;
top: ${node.offsetTop}px;
z-index: 2147483647;
`;
node.offsetParent?.appendChild(div);
}
function removeLoading(node, loadingId) {
const div = node.offsetParent.querySelector(`#${loadingId}`);
if (div) {
div.remove();
}
}
import { debounce, genEventName } from "./utils";
/**
* 翻译类
*/
export class Translator {
_rule = {};
_inputRule = {};
_setting = {};
_rootNodes = new Set();
_tranNodes = new Map();
@@ -187,11 +102,6 @@ export class Translator {
if (rule.transOpen === "true") {
this._register();
}
this._inputRule = setting.inputRule || DEFAULT_INPUT_RULE;
if (this._inputRule.transOpen) {
this._registerInput();
}
}
get setting() {
@@ -355,125 +265,6 @@ export class Translator {
}
};
_registerInput = () => {
const {
triggerShortcut: initTriggerShortcut,
translator,
fromLang,
toLang: initToLang,
triggerCount: initTriggerCount,
triggerTime,
transSign,
} = this._inputRule;
const apiSetting =
this._setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
const { detectRemote } = this._setting;
let triggerShortcut = initTriggerShortcut;
let triggerCount = initTriggerCount;
if (triggerShortcut.length === 0) {
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
triggerCount = 1;
}
stepShortcutRegister(
triggerShortcut,
async () => {
let node = document.activeElement;
if (!node) {
return;
}
while (node.shadowRoot) {
node = node.shadowRoot.activeElement;
}
if (!isInputNode(node) && !isEditAbleNode(node)) {
return;
}
let initText = getNodeText(node);
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
// todo: remove multiple char
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
}
if (!initText.trim()) {
return;
}
let text = initText;
let toLang = initToLang;
if (transSign) {
const res = matchInputStr(text, transSign);
if (res) {
let lang = res[1];
if (lang === "zh" || lang === "cn") {
lang = "zh-CN";
} else if (lang === "tw" || lang === "hk") {
lang = "zh-TW";
}
if (lang && OPT_LANGS_LIST.includes(lang)) {
toLang = lang;
}
text = res[2];
}
}
// console.log("input -->", text);
const loadingId = "kiss-" + genEventName();
try {
addLoading(node, loadingId);
const deLang = await tryDetectLang(text, detectRemote);
if (deLang && toLang.includes(deLang)) {
return;
}
const [trText, isSame] = await apiTranslate({
translator,
text,
fromLang,
toLang,
apiSetting,
});
if (!trText || isSame) {
return;
}
if (isInputNode(node)) {
node.value = trText;
node.dispatchEvent(
new Event("input", { bubbles: true, cancelable: true })
);
return;
}
selectContent(node);
await sleep(200);
pasteContentEvent(node, trText);
await sleep(200);
// todo: use includes?
if (getNodeText(node).startsWith(initText)) {
pasteContentCommand(node, trText);
await sleep(100);
} else {
collapseToEnd(node);
}
} catch (err) {
console.log("[translate input]", err.message);
} finally {
removeLoading(node, loadingId);
}
},
triggerCount,
triggerTime
);
};
_handleMouseover = (e) => {
// console.log("mouseenter", e);
if (!this._tranNodes.has(e.target)) {

View File

@@ -95,40 +95,42 @@ export default function Action({ translator, fab }) {
// 注册菜单
try {
const menuCommandIds = [];
menuCommandIds.push(
GM.registerMenuCommand(
"Toggle Translate (Alt+q)",
(event) => {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
setShowPopup(false);
},
"Q"
),
GM.registerMenuCommand(
"Toggle Style (Alt+c)",
(event) => {
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
setShowPopup(false);
},
"C"
),
GM.registerMenuCommand(
"Open Menu (Alt+k)",
(event) => {
setShowPopup((pre) => !pre);
},
"K"
),
GM.registerMenuCommand(
"Open Setting (Alt+o)",
(event) => {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
},
"O"
)
);
const { contextMenus = true } = translator.setting;
contextMenus &&
menuCommandIds.push(
GM.registerMenuCommand(
"Toggle Translate (Alt+q)",
(event) => {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
setShowPopup(false);
},
"Q"
),
GM.registerMenuCommand(
"Toggle Style (Alt+c)",
(event) => {
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
setShowPopup(false);
},
"C"
),
GM.registerMenuCommand(
"Open Menu (Alt+k)",
(event) => {
setShowPopup((pre) => !pre);
},
"K"
),
GM.registerMenuCommand(
"Open Setting (Alt+o)",
(event) => {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
},
"O"
)
);
return () => {
menuCommandIds.forEach((id) => {
@@ -215,7 +217,12 @@ export default function Action({ translator, fab }) {
}
}}
>
<TranslateIcon />
<TranslateIcon
sx={{
width: 24,
height: 24,
}}
/>
</Fab>
}
/>

View File

@@ -9,6 +9,7 @@ import {
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI,
OPT_TRANS_GEMINI,
OPT_TRANS_CUSTOMIZE,
URL_KISS_PROXY,
} from "../../config";
@@ -115,7 +116,7 @@ function ApiFields({ translator }) {
/>
</>
)}
{translator === OPT_TRANS_OPENAI && (
{(translator === OPT_TRANS_OPENAI || translator === OPT_TRANS_GEMINI) && (
<>
<TextField
size="small"

View File

@@ -25,10 +25,12 @@ import {
OPT_SHORTCUT_SETTING,
OPT_LANGS_TO,
DEFAULT_BLACKLIST,
MSG_CONTEXT_MENUS,
} from "../../config";
import { useShortcut } from "../../hooks/Shortcut";
import ShortcutInput from "./ShortcutInput";
import { useFab } from "../../hooks/Fab";
import { sendBgMsg } from "../../libs/msg";
function ShortcutItem({ action, label }) {
const { shortcut, setShortcut } = useShortcut(action);
@@ -44,7 +46,6 @@ export default function Settings() {
const { fab, updateFab } = useFab();
const handleChange = (e) => {
console.log("e", e);
e.preventDefault();
let { name, value } = e.target;
switch (name) {
@@ -64,7 +65,10 @@ export default function Settings() {
value = limitNumber(value, 1, 1000);
break;
case "touchTranslate":
value = limitNumber(value, 0, 3);
value = limitNumber(value, 0, 4);
break;
case "contextMenus":
isExt && sendBgMsg(MSG_CONTEXT_MENUS, { contextMenus: value });
break;
default:
}
@@ -92,6 +96,7 @@ export default function Settings() {
newlineLength = TRANS_NEWLINE_LENGTH,
mouseKey = OPT_MOUSEKEY_DISABLE,
detectRemote = false,
contextMenus = true,
touchTranslate = 2,
blacklist = DEFAULT_BLACKLIST.join(",\n"),
disableLangs = [],
@@ -209,6 +214,19 @@ export default function Settings() {
</Select>
</FormControl>
<FormControl size="small">
<InputLabel>{i18n("add_context_menus")}</InputLabel>
<Select
name="contextMenus"
value={contextMenus}
label={i18n("add_context_menus")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</Select>
</FormControl>
<FormControl size="small">
<InputLabel>{i18n("detect_lang_remote")}</InputLabel>
<Select
@@ -242,23 +260,25 @@ export default function Settings() {
</FormControl>
{isExt ? (
<FormControl size="small">
<InputLabel>{i18n("if_clear_cache")}</InputLabel>
<Select
name="clearCache"
value={clearCache}
label={i18n("if_clear_cache")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
</Select>
<FormHelperText>
<Link component="button" onClick={handleClearCache}>
{i18n("clear_all_cache_now")}
</Link>
</FormHelperText>
</FormControl>
<>
<FormControl size="small">
<InputLabel>{i18n("if_clear_cache")}</InputLabel>
<Select
name="clearCache"
value={clearCache}
label={i18n("if_clear_cache")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
</Select>
<FormHelperText>
<Link component="button" onClick={handleClearCache}>
{i18n("clear_all_cache_now")}
</Link>
</FormHelperText>
</FormControl>
</>
) : (
<>
<Box>

View File

@@ -44,6 +44,7 @@ export default function Tranbox() {
translator,
fromLang,
toLang,
toLang2 = "en",
tranboxShortcut,
btnOffsetX,
btnOffsetY,
@@ -112,6 +113,22 @@ export default function Tranbox() {
))}
</TextField>
<TextField
select
size="small"
name="toLang2"
value={toLang2}
label={i18n("to_lang2")}
helperText={i18n("to_lang2_helper")}
onChange={handleChange}
>
{[["none", "None"], ...OPT_LANGS_TO].map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField
size="small"
label={i18n("tranbtn_offset_x")}

View File

@@ -24,6 +24,7 @@ function TranForm({ text, setText, tranboxSetting, transApis }) {
const [translator, setTranslator] = useState(tranboxSetting.translator);
const [fromLang, setFromLang] = useState(tranboxSetting.fromLang);
const [toLang, setToLang] = useState(tranboxSetting.toLang);
const [toLang2, setToLang2] = useState(tranboxSetting.toLang2);
const inputRef = useRef(null);
return (
@@ -150,6 +151,9 @@ function TranForm({ text, setText, tranboxSetting, transApis }) {
translator={translator}
fromLang={fromLang}
toLang={toLang}
toLang2={toLang2}
setToLang={setToLang}
setToLang2={setToLang2}
transApis={transApis}
/>
</Stack>

View File

@@ -3,7 +3,7 @@ export default function TranBtn({ onClick, position, tranboxSetting }) {
<div
style={{
cursor: "pointer",
position: "fixed",
position: "absolute",
left: position.x + tranboxSetting.btnOffsetX,
top: position.y + tranboxSetting.btnOffsetY,
zIndex: 2147483647,

View File

@@ -6,7 +6,7 @@ import Stack from "@mui/material/Stack";
import { useI18n } from "../../hooks/I18n";
import { DEFAULT_TRANS_APIS, OPT_TRANS_BAIDU } from "../../config";
import { useEffect, useState } from "react";
import { apiTranslate } from "../../apis";
import { apiTranslate, apiBaiduLangdetect } from "../../apis";
import { isValidWord } from "../../libs/utils";
import CopyBtn from "./CopyBtn";
import DictCont from "./DictCont";
@@ -16,6 +16,9 @@ export default function TranCont({
translator,
fromLang,
toLang,
toLang2 = "en",
setToLang,
setToLang2,
transApis,
}) {
const i18n = useI18n();
@@ -32,6 +35,16 @@ export default function TranCont({
setError("");
setDictResult(null);
// 互译
if (toLang !== toLang2 && toLang2 !== "none") {
const detectLang = await apiBaiduLangdetect(text);
if (detectLang === toLang) {
setToLang(toLang2);
setToLang2(toLang);
return;
}
}
const apiSetting =
transApis[translator] || DEFAULT_TRANS_APIS[translator];
const tranRes = await apiTranslate({
@@ -63,7 +76,16 @@ export default function TranCont({
setLoading(false);
}
})();
}, [text, translator, fromLang, toLang, transApis]);
}, [
text,
translator,
fromLang,
toLang,
toLang2,
setToLang,
setToLang2,
transApis,
]);
return (
<>

View File

@@ -6,7 +6,7 @@ import { sleep } from "../../libs/utils";
import { isGm } from "../../libs/client";
import { MSG_OPEN_TRANBOX, DEFAULT_TRANBOX_SHORTCUT } from "../../config";
export default function Slection({ tranboxSetting, transApis }) {
export default function Slection({ contextMenus, tranboxSetting, transApis }) {
const [showBox, setShowBox] = useState(false);
const [showBtn, setShowBtn] = useState(false);
const [selectedText, setSelText] = useState("");
@@ -52,7 +52,8 @@ export default function Slection({ tranboxSetting, transApis }) {
}
!tranboxSetting.hideTranBtn && setShowBtn(true);
setPosition({ x: e.clientX, y: e.clientY });
// setPosition({ x: e.clientX, y: e.clientY });
setPosition({ x: e.pageX, y: e.pageY });
}
window.addEventListener("mouseup", handleMouseup);
@@ -87,15 +88,16 @@ export default function Slection({ tranboxSetting, transApis }) {
// 注册菜单
try {
const menuCommandIds = [];
menuCommandIds.push(
GM.registerMenuCommand(
"Translate Selected Text (Alt+S)",
(event) => {
handleTranbox();
},
"S"
)
);
contextMenus &&
menuCommandIds.push(
GM.registerMenuCommand(
"Translate Selected Text (Alt+S)",
(event) => {
handleTranbox();
},
"S"
)
);
return () => {
menuCommandIds.forEach((id) => {
@@ -105,7 +107,7 @@ export default function Slection({ tranboxSetting, transApis }) {
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}, [handleTranbox]);
}, [handleTranbox, contextMenus]);
return (
<>