Compare commits

..

47 Commits

Author SHA1 Message Date
Gabe
30c2cca2e1 Update version number: 2.0.5 2025-10-31 23:14:01 +08:00
Gabe
af3241b773 doc: readme 2025-10-31 23:12:27 +08:00
Gabe
eca0a63273 fix: subtitle 2025-10-31 20:23:58 +08:00
Gabe
f15cdb38d6 fix: put rule can write different pattern (#367) 2025-10-31 15:33:56 +08:00
Gabe
5b8577aaa7 doc: readme 2025-10-31 14:42:55 +08:00
Gabe
0a4a2b46c1 feat: support ai model with no batch 2025-10-31 14:36:33 +08:00
Gabe
53d441b3f5 doc: readme 2025-10-31 11:25:43 +08:00
Gabe
d4c346e40a fix: Increase the Max Tokens limit 2025-10-31 10:42:08 +08:00
Gabe
2dcacf71e4 fix: apitranslate bug 2025-10-31 10:25:53 +08:00
Gabe
9ded6446a7 fix: input translate 2025-10-31 10:05:01 +08:00
Gabe
0a6f4a9f02 fix: input translate (#342) 2025-10-31 01:39:27 +08:00
Gabe
635e588bcc fix: skip builtin ignore selector when autoscan disabled 2025-10-31 00:22:26 +08:00
Gabe
7343db78a5 fix: iframe bugs 2025-10-30 22:02:43 +08:00
Gabe
ccd457c992 fix: iframe bugs 2025-10-30 22:01:08 +08:00
Gabe
97676f114e fix: remove tink from ollama 2025-10-30 20:07:01 +08:00
Gabe
e83c1eb017 fix: throw error msg 2025-10-30 19:42:00 +08:00
Gabe
e417c0106a fix: change default fetchLimit 2025-10-30 19:10:07 +08:00
Gabe
3c09840d35 feat: can set rootMargin for IntersectionObserver. 2025-10-30 01:05:13 +08:00
Gabe
7361a94f8c feat: can set rootMargin for IntersectionObserver. 2025-10-30 01:03:46 +08:00
Gabe
a9bffe3913 feat: can set rootMargin for IntersectionObserver. 2025-10-30 00:55:17 +08:00
Gabe
5322555eba feat: can set whether skip ads. 2025-10-30 00:31:17 +08:00
Gabe
172dce2867 fix: hooks & injectjs 2025-10-30 00:19:13 +08:00
Gabe
5735fee36e fix: set main width 100% 2025-10-28 00:45:10 +08:00
Gabe
91642d8784 feat: support subtitle dragable on mobile 2025-10-28 00:36:38 +08:00
Gabe
9d8f3f4211 feat: add shadowroot injector 2025-10-28 00:07:44 +08:00
Gabe
66d39da80a fix: check io.observe must be element 2025-10-27 20:45:22 +08:00
Gabe
fbd4a31a9c fix: ignore script ellement 2025-10-27 20:00:05 +08:00
Gabe
3d7e03ddaf Update version number: 2.0.4 2025-10-26 20:26:52 +08:00
Gabe
1f213bf257 fix: styles 2025-10-26 20:10:13 +08:00
Gabe
b38f079611 fix: change showNotification duration 2025-10-26 19:59:51 +08:00
Gabe
21e639cacd fix: createMutationObserver 2025-10-26 18:42:59 +08:00
Gabe
bdaf665b7c feat: The translation box can be set to adaptive height 2025-10-26 16:18:56 +08:00
Gabe
61a515c1d2 feat: Support multi-touch selection 2025-10-26 00:06:52 +08:00
Gabe
1b646df908 feat: Remember the tranbox position and size 2025-10-25 23:18:39 +08:00
Gabe
5550f939b2 doc: readme 2025-10-25 18:41:20 +08:00
Gabe
b34fb5a600 doc: readme 2025-10-25 18:38:55 +08:00
Gabe
c0dce5c0b1 fix: Optimized text scanning logic 2025-10-25 17:46:29 +08:00
Gabe
d56bd2920f fix: isQualityPoor 2025-10-24 21:44:54 +08:00
Gabe
48ad100a64 fix: Optimized the scan node logic 2025-10-24 21:37:26 +08:00
Gabe
ef07a172a9 doc: custom api 2025-10-24 20:58:08 +08:00
Gabe
f492d47719 fix: disable field of rule 2025-10-24 20:57:10 +08:00
Gabe
ac8c07deb4 fix: keepselector for twitter 2025-10-24 01:46:36 +08:00
Gabe
ca48ab639e fix: remove stopPropagation for shortcut 2025-10-23 19:52:18 +08:00
Gabe
7c5232c1a1 fix: Make keepSelector effective even if richText is disabled 2025-10-23 19:32:59 +08:00
Gabe
4fac7fdfe1 fix: update custom api 2025-10-23 14:35:21 +08:00
Gabe
f7fc9560d5 fix: update custom api 2025-10-23 14:33:12 +08:00
Gabe
f7ba744e7f fix: ignore selector 2025-10-23 11:07:25 +08:00
53 changed files with 982 additions and 744 deletions

2
.env
View File

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

View File

@@ -1,40 +1,5 @@
# KISS Translator
**New Version Preview:**
After a period of intermittent development, the planned features for the new version are essentially complete. The main new features are as follows:
* **Core Translation Logic Refactoring:**
* Supports both automatic text detection and manual selection modes.
* The automatic text detection mode enables complete translation for the vast majority of websites without the need to write specific rules.
* The previous manual rule mode has been retained for meticulous optimization on specific websites.
* Supports rich text translation, preserving links and other text styles from the original content as much as possible.
* Optimize the display effect of showing only translated text (hiding original text).
* **API Refactoring:**
* Supports adding and deleting an arbitrary number of APIs.
* Supports aggregating text for sending, reducing the number of calls to the translation API and improving performance.
* Supports the built-in Chrome AI translation API, enabling AI-powered translation without an internet connection.
* Supports AI contextual conversation memory to enhance translation quality.
* All APIs support advanced features such as hooks and custom parameters.
* Added support for Azure AI translation interface.
* **Optimized YouTube Subtitle Support:**
* Supports translating video subtitles with any translation service and displaying them bilingually.
* Includes a built-in basic algorithm for subtitle merging and sentence splitting to improve translation results.
* Supports an AI-powered sentence splitting function to further enhance translation quality.
* **English Dictionary Redundancy:**
* Added Bing and Youdao dictionaries.
* Fixed the vocabulary collection feature.
* **User Experience Optimization:**
* The pop-up translation box for selected text now supports simultaneous translation by multiple services.
* The translation control panel has been updated with many new quick-toggle functions.
* Added a Playground page for convenient API debugging.
**Note:** Due to extensive refactoring, the configuration file for the new version is not backward compatible with the old version. Therefore, please back up your data manually before upgrading. Furthermore, **do not import old configuration files after upgrading to the new version.**
English | [简体中文](README.md)
A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
@@ -57,27 +22,35 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
- [x] Tencent/Volcengine
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
- [x] DeepL/DeepLX/NiuTrans
- [x] BuiltinAI/AzureAI/CloudflareAI
- [x] Custom translation interface
- [x] AzureAI / CloudflareAI
- [x] Chrome built-in AI translation (BuiltinAI)
- [x] Covers common translation scenarios
- [x] Web bilingual translation
- [x] Input box translation
- [x] Seletction translation
- [x] Open the translation box on any page
- [x] Favorite Words
- [x] Mouseover translation
- [x] Webpage bilingual translation
- [x] Input-box translation
- Instantly translate text in input fields into other languages via shortcut keys
- [x] Text selection translation
- [x] Open translation popup on any page, support multiple translation services for comparison
- [x] English dictionary lookup
- [x] Save vocabulary
- [x] Hover translation
- [x] YouTube subtitle translation
- [x] Support for various translation effects
- [x] Customizable text recognition and full-text translation
- [x] Customizable translation styles
- [x] Support for rich text translation and display
- [x] Support for displaying only the translated text (hiding the original text)
- [x] Advanced translation API features
- [x] Aggregate and send translated texts in batches
- [x] AI contextual conversation memory
- [x] Customizable AI terminology dictionary
- [x] AI-powered subtitle segmentation and translation
- [x] Customizable hooks and parameters
- Support translating video subtitles with any translation service and display bilingually
- Built-in basic subtitle merging and sentence-splitting algorithm to improve translation quality
- Supports AI-powered sentence segmentation for even better translation
- Custom subtitle style
- [x] Supports diverse translation modes
- [x] Supports both automatic text recognition and manual rule modes
- Automatic text recognition mode allows most sites to be translated fully without writing rules
- Manual rule mode enables extreme optimization for specific sites
- [x] Custom translation styling
- [x] Supports rich-text translation and rendering, preserving links and other text styles where possible
- [x] Option to show only translation (hide original text)
- [x] Advanced translation API features
- [x] With custom API support, theoretically works with any translation service
- [x] Batch aggregation of translation requests
- [x] Supports AI conversation context memory to improve translation quality
- [x] Custom AI terminology dictionary
- [x] All APIs support hooks and custom parameters for advanced usage
- [x] Cross-client data synchronization
- [x] KISS-Workercloudflare/docker
- [x] WebDAV
@@ -139,9 +112,20 @@ Personal Rules > Subscription Rules > Global Rules
Among these, Global Rules have the lowest priority but are very important as they serve as the default rules.
### Local Ollama interface cannot be used
### API (Ollama, etc.) Test Failure
If encountering a 403 error, refer to: https://github.com/fishjar/kiss-translator/issues/174
Common reasons for API test failures include:
- Incorrect address:
- For example, `Ollama` has a native API address and an `Openai`-compatible address. This plugin currently supports the `Openai`-compatible address and does not support the `Ollama` native API address.
- Some AI models do not support batch translation:
- In this case, you can choose to disable batch translation or use a custom API.
- Alternatively, you can use a custom API. For details, please refer to: [Custom API Example Documentation](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
- Some AI models have inconsistent parameters:
- For example, the parameters of the `Gemini` native API are highly inconsistent. Some model versions do not support certain parameters, leading to errors.
- In this case, you can modify the request body using a `Hook`, or replace it with `Gemini2` (an OpenAI-compatible address).
- The server restricts cross-origin access, returning a 403 error:
- For example, `Ollama` requires adding the environment variable `OLLAMA_ORIGINS=*` when starting. See: https://github.com/fishjar/kiss-translator/issues/174
### Custom API doesn't work in Tampermonkey scripts
@@ -153,6 +137,10 @@ Custom APIs are very powerful and flexible, and can theoretically connect to any
Example reference: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
### How to directly access the Tampermonkey script settings page
Settings page address: https://fishjar.github.io/kiss-translator/options.html
## Future Plans
This is a side project with no strict timeline. Community contributions are welcome. The following are preliminary feature directions:

View File

@@ -1,36 +1,5 @@
# 简约翻译
> **新版预告**
>
> 经过一段时间断续开发,新版的预期功能已基本完成,主要引入的新特性如下:
>
> - 核心翻译逻辑重构:
> - 支持自动识别文本与手动选择两种模式。
> - 自动识别文本模式使得绝大部分网站无需编写规则也能翻译完整。
> - 保留之前的手动规则模式,可以针对特定网站极致优化。
> - 支持富文本翻译,能够尽量保留原文中的链接及其他文本样式。
> - 优化仅显示译文(隐藏原文)显示效果。
> - 接口重构:
> - 支持添加、删除任意数量的接口。
> - 支持聚合发送文本,减少翻译接口调用次数,提升性能。
> - 支持chrome内置AI翻译接口无需通过网络即可实现AI翻译。
> - 支持AI上下文会话记忆功能提升翻译效果。
> - 所有接口均支持Hook和自定义参数等高级功能。
> - 新增Azure AI翻译接口支持
> - 优化 YouTube 字幕支持:
> - 支持任意翻译服务对视频字幕进行翻译并双语显示。
> - 内置基础的字幕合并与断句算法,提升翻译效果。
> - 支持AI断句功能可进一步提升翻译质量。
> - 英文词典备灾:
> - 新增bing、有道词典。
> - 修复词汇收藏功能。
> - 用户操作优化:
> - 划词翻译框支持多种翻译服务同时翻译。
> - 翻译控制面板新增许多快捷切换功能。
> - 新增Playground页面方便调试接口。
>
> 注意:由于经过大量重构,使得新版配置文件很难与旧版兼容,因此在升级前请手动备份相关数据。并且,**升级新版后,勿再导入旧版配置**。
[English](README.en.md) | 简体中文
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
@@ -53,27 +22,35 @@
- [x] Tencent/Volcengine
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
- [x] DeepL/DeepLX/NiuTrans
- [x] BuiltinAI/AzureAI/CloudflareAI
- [x] 自定义翻译接口
- [x] AzureAI/CloudflareAI
- [x] Chrome浏览器内置AI翻译(BuiltinAI)
- [x] 覆盖常见翻译场景
- [x] 网页双语对照翻译
- [x] 输入框翻译
- 通过快捷键立即将输入框内文本翻译成其他语言
- [x] 划词翻译
- [x] 任意页面打开翻译框
- [x] 任意页面打开翻译框,可用多种翻译服务对比翻译
- [x] 英文词典翻译
- [x] 收藏词汇
- [x] 鼠标悬停翻译
- [x] YouTube 字幕翻译
- 支持任意翻译服务对视频字幕进行翻译并双语显示
- 内置基础的字幕合并与断句算法,提升翻译效果
- 支持AI断句功能可进一步提升翻译质量
- 自定义字幕样式
- [x] 支持多样翻译效果
- [x] 自定识别文本,全文翻译
- [x] 支持自动识别文本与手动规则两种模式
- 自动识别文本模式使得绝大部分网站无需编写规则也能翻译完整
- 手动规则模式,可以针对特定网站极致优化
- [x] 自定义译文样式
- [x] 支持富文本翻译及显示
- [x] 支持富文本翻译及显示,能够尽量保留原文中的链接及其他文本样式
- [x] 支持仅显示译文(隐藏原文)
- [x] 翻译接口高级功能
- [x] 通过自定义接口,理论上支持任何翻译接口
- [x] 聚合批量发送翻译文本
- [x] AI上下文会话记忆
- [x] 支持AI上下文会话记忆功能,提升翻译效果
- [x] 自定义AI术语词典
- [x] 字幕文本AI智能断句及翻译
- [x] 自定义Hook自定义参数
- [x] 所有接口均支持Hook和自定义参数等高级功能
- [x] 跨客户端数据同步
- [x] KISS-Workercloudflare/docker
- [x] WebDAV
@@ -135,9 +112,20 @@
其中全局规则优先级最低,但非常重要,相当于兜底规则。
### 本地的Ollama接口不能使用
### 接口(Ollama等)测试失败
如果出现403的情况参考https://github.com/fishjar/kiss-translator/issues/174
一般接口测试失败常见有以下几种原因:
- 地址填错了:
- 比如 `Ollama` 有原生接口地址和 `Openai` 兼容的地址,本插件目前统一支持 `Openai` 兼容的地址,不支持 `Ollama` 原生接口地址
- 某些AI模型不支持聚合翻译
- 此种情况可以选择禁用聚合翻译或通过自定义接口的方式来使用。
- 或通过自定义接口的方式来使用,详情参考: [自定义接口示例文档](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
- 某些AI模型的参数不一致
- 比如 `Gemini` 原生接口参数非常不一致,部分版本的模型不支持某些参数会导致返回错误。
- 此种情况可以通过 `Hook` 修改请求 `body` ,或者更换为 `Gemini2` (`Openai` 兼容的地址)
- 服务器跨域限制访问返回403错误
- 比如 `Ollama` 启动时须添加环境变量 `OLLAMA_ORIGINS=*`, 参考https://github.com/fishjar/kiss-translator/issues/174
### 填写的接口在油猴脚本不能使用
@@ -149,6 +137,10 @@
示例参考: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
### 如何直接进入油猴脚本设置页面
设置页面地址: https://fishjar.github.io/kiss-translator/options.html
## 未来规划
本项目为业余开发,无严格时间表,欢迎社区共建。以下为初步设想的功能方向:

View File

@@ -32,7 +32,8 @@ const extWebpack = (config, env) => {
options: paths.appSrc + "/options.js",
background: paths.appSrc + "/background.js",
content: paths.appSrc + "/content.js",
injector: paths.appSrc + "/injector.js",
"injector-subtitle": paths.appSrc + "/injector-subtitle.js",
"injector-shadowroot": paths.appSrc + "/injector-shadowroot.js",
};
config.output.filename = "[name].js";

View File

@@ -1,5 +1,44 @@
# 自定义接口示例
## 默认接口规范
如果接口的请求数据和返回数据符合以下规范,
则无需填写 `Request Hook``Response Hook`
Request body
```json
{
"texts": ["hello"], // 需要翻译的文本列表
"from":"auto", // 原文语言
"to": "zh-CN" // 目标语言
}
```
Response
```json
[
{
"text": "你好", // 译文
"src": "en" // 原文语言
}
]
```
v2.0.4版后亦支持以下 Response 格式
```json
{
"translations": [ // 译文列表
{
"text": "你好", // 译文
"src": "en" // 原文语言
}
]
}
```
## 谷歌翻译接口
> 此接口不支持聚合

View File

@@ -1,7 +1,7 @@
{
"name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "2.0.3",
"version": "2.0.5",
"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": "2.0.3",
"version": "2.0.5",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -17,7 +17,8 @@
}
],
"web_accessible_resources": [
"injector.js"
"injector-subtitle.js",
"injector-shadowroot.js"
],
"commands": {
"_execute_browser_action": {

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "2.0.3",
"version": "2.0.5",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -19,8 +19,12 @@
],
"web_accessible_resources": [
{
"resources": ["injector.js"],
"resources": ["injector-subtitle.js"],
"matches": ["https://www.youtube.com/*"]
},
{
"resources": ["injector-shadowroot.js"],
"matches": ["<all_urls>"]
}
],
"commands": {

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "2.0.3",
"version": "2.0.5",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -23,7 +23,8 @@
}
],
"web_accessible_resources": [
"injector.js"
"injector-subtitle.js",
"injector-shadowroot.js"
],
"commands": {
"_execute_browser_action": {

View File

@@ -14,9 +14,9 @@ import {
MSG_BUILTINAI_TRANSLATE,
OPT_TRANS_BUILTINAI,
URL_CACHE_SUBTITLE,
OPT_LANGS_TO_CODE,
} from "../config";
import { sha256, withTimeout } from "../libs/utils";
import { kissLog } from "../libs/log";
import {
handleTranslate,
handleSubtitle,
@@ -424,7 +424,7 @@ export const apiTranslate = async ({
usePool = true,
}) => {
if (!text) {
return ["", false];
throw new Error("The text cannot be empty.");
}
const { apiType, apiSlug, useBatchFetch } = apiSetting;
@@ -432,8 +432,7 @@ export const apiTranslate = async ({
const from = langMap.get(fromLang);
const to = langMap.get(toLang);
if (!to) {
kissLog(`target lang: ${toLang} not support`);
return ["", false];
throw new Error(`The target lang: ${toLang} not support`);
}
// todo: 优化缓存失效因素
@@ -451,7 +450,7 @@ export const apiTranslate = async ({
if (useCache) {
const cache = await getHttpCachePolyfill(cacheInput);
if (cache?.trText) {
return [cache.trText, cache.isSame];
return cache;
}
}
@@ -499,8 +498,12 @@ export const apiTranslate = async ({
let trText = "";
let srLang = "";
let srCode = "";
if (Array.isArray(tranlation)) {
[trText, srLang = ""] = tranlation;
if (srLang) {
srCode = OPT_LANGS_TO_CODE[apiType].get(srLang) || "";
}
} else if (typeof tranlation === "string") {
trText = tranlation;
}
@@ -513,10 +516,10 @@ export const apiTranslate = async ({
// 插入缓存
if (useCache) {
putHttpCachePolyfill(cacheInput, null, { trText, isSame, srLang });
putHttpCachePolyfill(cacheInput, null, { trText, isSame, srLang, srCode });
}
return [trText, isSame];
return { trText, srLang, srCode, isSame };
};
// 字幕处理/翻译

View File

@@ -22,17 +22,19 @@ import {
API_SPE_TYPES,
INPUT_PLACE_FROM,
INPUT_PLACE_TO,
// INPUT_PLACE_TEXT,
INPUT_PLACE_TEXT,
INPUT_PLACE_KEY,
INPUT_PLACE_MODEL,
DEFAULT_USER_AGENT,
defaultSystemPrompt,
defaultSubtitlePrompt,
defaultNobatchPrompt,
defaultNobatchUserPrompt,
} from "../config";
import { msAuth } from "../libs/auth";
import { genDeeplFree } from "./deepl";
import { genBaidu } from "./baidu";
import interpreter from "../libs/interpreter";
import { interpreter } from "../libs/interpreter";
import { parseJsonObj, extractJson } from "../libs/utils";
import { kissLog } from "../libs/log";
import { fetchData } from "../libs/fetch";
@@ -66,38 +68,41 @@ const genSystemPrompt = ({ systemPrompt, from, to }) =>
.replaceAll(INPUT_PLACE_TO, to);
const genUserPrompt = ({
// userPrompt,
nobatchUserPrompt,
useBatchFetch,
tone,
glossary = {},
// from,
from,
to,
texts,
docInfo,
}) => {
const prompt = JSON.stringify({
targetLanguage: to,
title: docInfo.title,
description: docInfo.description,
segments: texts.map((text, i) => ({ id: i, text })),
glossary,
tone,
});
if (useBatchFetch) {
return JSON.stringify({
targetLanguage: to,
title: docInfo.title,
description: docInfo.description,
segments: texts.map((text, i) => ({ id: i, text })),
glossary,
tone,
});
}
// if (userPrompt.includes(INPUT_PLACE_TEXT)) {
// return userPrompt
// .replaceAll(INPUT_PLACE_FROM, from)
// .replaceAll(INPUT_PLACE_TO, to)
// .replaceAll(INPUT_PLACE_TEXT, prompt);
// }
return prompt;
return nobatchUserPrompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, texts[0]);
};
const parseAIRes = (raw) => {
const parseAIRes = (raw, useBatchFetch = true) => {
if (!raw) {
return [];
}
if (!useBatchFetch) {
return [[raw]];
}
try {
const jsonString = extractJson(raw);
if (!jsonString) return [];
@@ -497,7 +502,7 @@ const genOpenRouter = ({
};
const genOllama = ({
think,
// think,
url,
key,
systemPrompt,
@@ -523,7 +528,7 @@ const genOllama = ({
],
temperature,
max_tokens: maxTokens,
think,
// think,
stream: false,
};
@@ -627,7 +632,10 @@ export const genTransReq = async ({ reqHook, ...args }) => {
apiSlug,
key,
systemPrompt,
userPrompt,
// userPrompt,
nobatchPrompt = defaultNobatchPrompt,
nobatchUserPrompt = defaultNobatchUserPrompt,
useBatchFetch,
from,
to,
texts,
@@ -647,11 +655,16 @@ export const genTransReq = async ({ reqHook, ...args }) => {
}
if (API_SPE_TYPES.ai.has(apiType)) {
args.systemPrompt = genSystemPrompt({ systemPrompt, from, to });
args.systemPrompt = genSystemPrompt({
systemPrompt: useBatchFetch ? systemPrompt : nobatchPrompt,
from,
to,
});
args.userPrompt = !!events
? JSON.stringify(events)
: genUserPrompt({
userPrompt,
nobatchUserPrompt,
useBatchFetch,
from,
to,
texts,
@@ -717,10 +730,11 @@ export const parseTransRes = async (
toLang,
langMap,
resHook,
thinkIgnore,
// thinkIgnore,
history,
userMsg,
apiType,
useBatchFetch,
}
) => {
// 执行 response hook
@@ -811,13 +825,13 @@ export const parseTransRes = async (
content: modelMsg.content,
});
}
return parseAIRes(res?.choices?.[0]?.message?.content ?? "");
return parseAIRes(modelMsg?.content, useBatchFetch);
case OPT_TRANS_GEMINI:
modelMsg = res?.candidates?.[0]?.content;
if (history && userMsg && modelMsg) {
history.add(userMsg, modelMsg);
}
return parseAIRes(res?.candidates?.[0]?.content?.parts?.[0]?.text ?? "");
return parseAIRes(modelMsg?.parts?.[0]?.text ?? "", useBatchFetch);
case OPT_TRANS_CLAUDE:
modelMsg = { role: res?.role, content: res?.content?.text };
if (history && userMsg && modelMsg) {
@@ -826,18 +840,18 @@ export const parseTransRes = async (
content: modelMsg.content,
});
}
return parseAIRes(res?.content?.[0]?.text ?? "");
return parseAIRes(res?.content?.[0]?.text ?? "", useBatchFetch);
case OPT_TRANS_CLOUDFLAREAI:
return [[res?.result?.translated_text]];
case OPT_TRANS_OLLAMA:
modelMsg = res?.choices?.[0]?.message;
const deepModels = thinkIgnore
.split(",")
.filter((model) => model?.trim());
if (deepModels.some((model) => res?.model?.startsWith(model))) {
modelMsg?.content.replace(/<think>[\s\S]*<\/think>/i, "");
}
// const deepModels = thinkIgnore
// .split(",")
// .filter((model) => model?.trim());
// if (deepModels.some((model) => res?.model?.startsWith(model))) {
// modelMsg?.content.replace(/<think>[\s\S]*<\/think>/i, "");
// }
if (history && userMsg && modelMsg) {
history.add(userMsg, {
@@ -845,9 +859,9 @@ export const parseTransRes = async (
content: modelMsg.content,
});
}
return parseAIRes(modelMsg?.content);
return parseAIRes(modelMsg?.content, useBatchFetch);
case OPT_TRANS_CUSTOMIZE:
return res?.map((item) => [item.text, item.src]);
return (res?.translations ?? res)?.map((item) => [item.text, item.src]);
default:
}

View File

@@ -33,7 +33,7 @@ import { sendTabMsg } from "./libs/msg";
import { trySyncAllSubRules } from "./libs/subRules";
import { saveRule } from "./libs/rules";
import { getCurTabId } from "./libs/msg";
import { injectInlineJs, injectInternalCss } from "./libs/injector";
import { injectInlineJsBg, injectInternalCss } from "./libs/injector";
import { kissLog, logger } from "./libs/log";
import { chromeDetect, chromeTranslate } from "./libs/builtinAI";
@@ -268,7 +268,7 @@ const messageHandlers = {
[MSG_PUT_HTTPCACHE]: (args) => putHttpCache(args),
[MSG_OPEN_OPTIONS]: () => browser.runtime.openOptionsPage(),
[MSG_SAVE_RULE]: (args) => saveRule(args),
[MSG_INJECT_JS]: (args) => injectToCurrentTab(injectInlineJs, args),
[MSG_INJECT_JS]: (args) => injectToCurrentTab(injectInlineJsBg, args),
[MSG_INJECT_CSS]: (args) => injectToCurrentTab(injectInternalCss, args),
[MSG_UPDATE_CSP]: (args) => updateCspRules(args),
[MSG_CONTEXT_MENUS]: (args) => addContextMenus(args),
@@ -285,20 +285,11 @@ const messageHandlers = {
*/
browser.runtime.onMessage.addListener(async ({ action, args }) => {
const handler = messageHandlers[action];
if (!handler) {
const errorMessage = `Message action is unavailable: ${action}`;
kissLog("runtime onMessage", action, new Error(errorMessage));
return null;
throw new Error(`Message action is unavailable: ${action}`);
}
try {
const result = await handler(args);
return result;
} catch (err) {
kissLog("runtime onMessage", action, err);
return null;
}
return handler(args);
});
/**

View File

@@ -46,7 +46,7 @@ export const OPT_TRANS_OPENROUTER = "OpenRouter";
export const OPT_TRANS_CUSTOMIZE = "Custom";
// 内置支持的翻译引擎
export const OPT_ALL_TYPES = [
export const OPT_ALL_TRANS_TYPES = [
OPT_TRANS_BUILTINAI,
OPT_TRANS_GOOGLE,
OPT_TRANS_GOOGLE_2,
@@ -82,7 +82,7 @@ export const OPT_LANGDETECTOR_MAP = new Set(OPT_LANGDETECTOR_ALL);
// 翻译引擎特殊集合
export const API_SPE_TYPES = {
// 内置翻译
builtin: new Set(OPT_ALL_TYPES),
builtin: new Set(OPT_ALL_TRANS_TYPES),
// 机器翻译
machine: new Set([
OPT_TRANS_MICROSOFT,
@@ -340,6 +340,9 @@ Object.entries(OPT_LANGS_TO_SPEC).forEach(([t, m]) => {
OPT_LANGS_TO_CODE[t] = specToCode(m);
});
export const defaultNobatchPrompt = `You are a professional, authentic machine translation engine.`;
export const defaultNobatchUserPrompt = `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`;
export const defaultSystemPrompt = `Act as a translation API. Output a single raw JSON object only. No extra text or fences.
Input:
@@ -430,6 +433,8 @@ const defaultApi = {
model: "", // 模型名称
systemPrompt: defaultSystemPrompt,
subtitlePrompt: defaultSubtitlePrompt,
nobatchPrompt: defaultNobatchPrompt,
nobatchUserPrompt: defaultNobatchUserPrompt,
userPrompt: "",
tone: BUILTIN_STONES[0], // 翻译风格
placeholder: BUILTIN_PLACEHOLDERS[0], // 占位符
@@ -450,8 +455,8 @@ const defaultApi = {
contextSize: DEFAULT_CONTEXT_SIZE, // 智能上下文保留会话数
temperature: 0.0,
maxTokens: 20480,
think: false,
thinkIgnore: "qwen3,deepseek-r1",
// think: false, // (OpenAI 兼容接口未支持,暂时移除)
// thinkIgnore: "qwen3,deepseek-r1", // (OpenAI 兼容接口未支持,暂时移除)
isDisabled: false, // 是否不显示,
region: "", // Azure 专用
};
@@ -499,7 +504,6 @@ const defaultApiOpts = {
[OPT_TRANS_DEEPLX]: {
...defaultApi,
url: "http://localhost:1188/translate",
fetchLimit: 1,
},
[OPT_TRANS_NIUTRANS]: {
...defaultApi,
@@ -512,7 +516,6 @@ const defaultApiOpts = {
url: "https://api.openai.com/v1/chat/completions",
model: "gpt-4",
useBatchFetch: true,
fetchLimit: 1,
},
[OPT_TRANS_GEMINI]: {
...defaultApi,
@@ -557,7 +560,7 @@ const defaultApiOpts = {
};
// 内置翻译接口列表(带参数)
export const DEFAULT_API_LIST = OPT_ALL_TYPES.map((apiType) => ({
export const DEFAULT_API_LIST = OPT_ALL_TRANS_TYPES.map((apiType) => ({
...defaultApiOpts[apiType],
apiSlug: apiType,
apiName: apiType,
@@ -565,4 +568,6 @@ export const DEFAULT_API_LIST = OPT_ALL_TYPES.map((apiType) => ({
}));
export const DEFAULT_API_TYPE = OPT_TRANS_MICROSOFT;
export const DEFAULT_API_SETTING = DEFAULT_API_LIST[DEFAULT_API_TYPE];
export const DEFAULT_API_SETTING = DEFAULT_API_LIST.find(
(a) => a.apiType === DEFAULT_API_TYPE
);

View File

@@ -745,9 +745,33 @@ export const I18N = {
zh_TW: `注入 JS`,
},
inject_js_helper: {
zh: `初始化时注入运行,一个页面仅运行一次。`,
en: `Injected and run at initialization, and only run once per page.`,
zh_TW: `初始化時注入運行,一個頁面僅運行一次。`,
zh: `预加载时注入,一个页面仅运行一次。内置全局对象 KT: {
apiTranslate,
apiDectect,
apiSetting,
apisMap,
toLang,
docInfo,
glossary,
}`,
en: `Injected during preload, runs only once per page. Built-in global object KT: {
apiTranslate,
apiDectect,
apiSetting,
apisMap,
toLang,
docInfo,
glossary,
}`,
zh_TW: `預先載入時注入,一個頁面僅運行一次。內建全域物件 KT: {
apiTranslate,
apiDectect,
apiSetting,
apisMap,
toLang,
docInfo,
glossary,
}`,
},
inject_css: {
zh: `注入CSS`,
@@ -1160,9 +1184,9 @@ export const I18N = {
zh_TW: `觸控設定`,
},
touch_translate_shortcut: {
zh: `触屏翻译快捷方式`,
en: `Touch Translate Shortcut`,
zh_TW: `觸控翻譯捷徑`,
zh: `触屏翻译快捷方式 (支持多选)`,
en: `Touch Translate Shortcut (multiple supported)`,
zh_TW: `觸控翻譯捷徑 (支援多選)`,
},
touch_tap_0: {
zh: `禁用`,
@@ -1349,15 +1373,35 @@ export const I18N = {
en: `Transbox Follow Selection`,
zh_TW: `翻譯框跟隨選取文字`,
},
tranbox_auto_height: {
zh: `翻译框自适应高度`,
en: `Translation box adaptive height`,
zh_TW: `翻譯框自適應高度`,
},
translate_start_hook: {
zh: `翻译开始钩子函数`,
en: `Translate Start Hook`,
zh_TW: `翻譯開始 Hook`,
},
translate_start_hook_helper: {
zh: `翻译前时运行,入参为: ({hostNode, parentNode, nodes})`,
en: `Run before translation, input parameters are: ({hostNode, parentNode, nodes})`,
zh_TW: `翻譯前時運行,入參為: ({hostNode, parentNode, nodes})`,
zh: `翻译前时运行,入参为: {text,
fromLang,
toLang,
apiSetting,
docInfo,
glossary,}`,
en: `Run before translation, input parameters are: {text,
fromLang,
toLang,
apiSetting,
docInfo,
glossary,}`,
zh_TW: `翻譯前時運行,入參為: {text,
fromLang,
toLang,
apiSetting,
docInfo,
glossary,}`,
},
translate_end_hook: {
zh: `翻译完成钩子函数`,
@@ -1604,6 +1648,11 @@ export const I18N = {
en: `Enable bilingual display`,
zh_TW: `雙語顯示`,
},
is_skip_ad: {
zh: `是否快进广告`,
en: `Should I fast forward to the ad?`,
zh_TW: `是否快轉廣告`,
},
background_styles: {
zh: `背景样式`,
en: `DBackground Style`,
@@ -1735,6 +1784,11 @@ export const I18N = {
en: `Highlight after translation`,
zh_TW: `翻譯後高亮`,
},
pagescroll_root_margin: {
zh: `滚动加载提前触发 (0-10000px)`,
en: `Early triggering of scroll loading (0-10000px)`,
zh_TW: `滾動載入提前觸發 (0-10000px)`,
},
};
export const newI18n = (lang) => (key) => I18N[key]?.[lang] || "";

View File

@@ -97,7 +97,7 @@ background: linear-gradient(
export const DEFAULT_SELECTOR =
"h1, h2, h3, h4, h5, h6, li, p, dd, blockquote, figcaption, label, legend";
export const DEFAULT_IGNORE_SELECTOR = "button, footer, pre, mark, nav";
export const DEFAULT_KEEP_SELECTOR = `a:has(code)`;
export const DEFAULT_KEEP_SELECTOR = `code, cite, math, .math, a:has(code)`;
export const DEFAULT_RULE = {
pattern: "", // 匹配网址
selector: "", // 选择器
@@ -117,7 +117,7 @@ export const DEFAULT_RULE = {
parentStyle: "", // 选择器父节点样式
grandStyle: "", // 选择器父节点样式
injectJs: "", // 注入JS
injectCss: "", // 注入CSS
// injectCss: "", // 注入CSS (作废)
transOnly: GLOBAL_KEY, // 是否仅显示译文
// transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译 (暂时作废)
transTag: GLOBAL_KEY, // 译文元素标签
@@ -160,7 +160,7 @@ export const GLOBLA_RULE = {
parentStyle: DEFAULT_SELECT_STYLE, // 选择器父节点样式
grandStyle: DEFAULT_SELECT_STYLE, // 选择器祖节点样式
injectJs: "", // 注入JS
injectCss: "", // 注入CSS
// injectCss: "", // 注入CSS(作废)
transOnly: "false", // 是否仅显示译文
// transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译 (暂时作废)
transTag: DEFAULT_TRANS_TAG, // 译文元素标签
@@ -211,7 +211,8 @@ const RULES_MAP = {
},
"twitter.com, https://x.com": {
selector: `[data-testid='tweetText']`,
keepSelector: `img, svg, span:has(a), div:has(a)`,
keepSelector: `img, svg, a, span:has(a), div:has(a)`,
ignoreSelector: `button, [data-testid='videoPlayer'], [role='group']`,
autoScan: `false`,
},
"www.youtube.com/live_chat": {
@@ -223,6 +224,11 @@ const RULES_MAP = {
rootsSelector: `ytd-page-manager`,
ignoreSelector: `aside, button, footer, form, header, pre, mark, nav, #player, #container, .caption-window, .ytp-settings-menu`,
},
"web.telegram.org": {
autoScan: `false`,
selector: ".text-content, .embedded-text-wrapper",
rootsSelector: ".Transition",
},
};
export const BUILTIN_RULES = Object.entries(RULES_MAP).map(

View File

@@ -88,6 +88,7 @@ export const DEFAULT_TRANBOX_SETTING = {
hideClickAway: false, // 是否点击外部关闭弹窗
simpleStyle: false, // 是否简洁界面
followSelection: false, // 翻译框是否跟随选中文本
autoHeight: false, // 自适应高度
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
// extStyles: "", // 附加样式
enDict: OPT_DICT_BING, // 英文词典
@@ -113,6 +114,7 @@ export const DEFAULT_SUBTITLE_SETTING = {
// fromLang: "en",
toLang: "zh-CN",
isBilingual: true, // 是否双语显示
skipAd: false, // 是否快进广告
windowStyle: SUBTITLE_WINDOW_STYLE, // 背景样式
originStyle: SUBTITLE_ORIGIN_STYLE, // 原文样式
translationStyle: SUBTITLE_TRANSLATION_STYLE, // 译文样式
@@ -166,7 +168,8 @@ export const DEFAULT_SETTING = {
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
touchTranslate: 2, // 触屏翻译 {5:单指双击6:单指三击7:双指双击}
// touchTranslate: 2, // 触屏翻译 {5:单指双击6:单指三击7:双指双击} (作废)
touchModes: [2], // 触屏翻译 {5:单指双击6:单指三击7:双指双击} (多选)
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
orilist: DEFAULT_ORILIST.join(",\n"), // 禁用CSP名单
@@ -179,4 +182,5 @@ export const DEFAULT_SETTING = {
transAllnow: false, // 是否立即全部翻译
subtitleSetting: DEFAULT_SUBTITLE_SETTING, // 字幕设置
logLevel: LogLevel.INFO.value, // 日志级别
rootMargin: 500, // 提前触发翻译
};

View File

@@ -16,6 +16,7 @@ export const STOKEY_RULES = `${APP_NAME}_rules_v${APP_VERSION[0]}`;
export const STOKEY_WORDS = `${APP_NAME}_words`;
export const STOKEY_SYNC = `${APP_NAME}_sync`;
export const STOKEY_FAB = `${APP_NAME}_fab`;
export const STOKEY_TRANBOX = `${APP_NAME}_tranbox`;
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
export const CACHE_NAME = `${APP_NAME}_cache`;

View File

@@ -59,7 +59,11 @@ export function AlertProvider({ children }) {
onClose={handleClose}
anchorOrigin={{ vertical, horizontal }}
>
<Alert onClose={handleClose} severity={severity} sx={{ width: "100%" }}>
<Alert
onClose={handleClose}
severity={severity}
sx={{ minWidth: "300px", maxWidth: "80%" }}
>
{message}
</Alert>
</Snackbar>

View File

@@ -57,9 +57,9 @@ export function useRules() {
const put = useCallback(
(pattern, obj) => {
save((prev) => {
if (pattern !== obj.pattern) {
return prev;
}
// if (pattern !== obj.pattern) {
// return prev;
// }
return prev.map((item) =>
item.pattern === pattern ? { ...item, ...obj } : item
);

View File

@@ -0,0 +1,3 @@
import { shadowRootInjector } from "./injectors/shadowroot";
shadowRootInjector();

3
src/injector-subtitle.js Normal file
View File

@@ -0,0 +1,3 @@
import { XMLHttpRequestInjector } from "./injectors/xmlhttp";
XMLHttpRequestInjector();

View File

@@ -1,3 +0,0 @@
import { XMLHttpRequestInjector } from "./subtitle/XMLHttpRequestInjector";
XMLHttpRequestInjector();

27
src/injectors/index.js Normal file
View File

@@ -0,0 +1,27 @@
import { browser } from "../libs/browser";
import { isExt } from "../libs/client";
import { injectExternalJs, injectInlineJs } from "../libs/injector";
import { shadowRootInjector } from "./shadowroot";
import { XMLHttpRequestInjector } from "./xmlhttp";
export const INJECTOR = {
subtitle: "injector-subtitle.js",
shadowroot: "injector-shadowroot.js",
};
const injectorMap = {
[INJECTOR.subtitle]: XMLHttpRequestInjector,
[INJECTOR.shadowroot]: shadowRootInjector,
};
export function injectJs(name, id = "kiss-translator-inject-js") {
const injector = injectorMap[name];
if (!injector) return;
if (isExt) {
const src = browser.runtime.getURL(name);
injectExternalJs(src, id);
} else {
injectInlineJs(`(${injector})()`, id);
}
}

View File

@@ -0,0 +1,12 @@
export const shadowRootInjector = () => {
try {
const orig = Element.prototype.attachShadow;
Element.prototype.attachShadow = function (...args) {
const root = orig.apply(this, args);
window.postMessage({ type: "KISS_SHADOW_ROOT_CREATED" }, "*");
return root;
};
} catch (err) {
console.log("shadowRootInjector", err);
}
};

23
src/injectors/xmlhttp.js Normal file
View File

@@ -0,0 +1,23 @@
export const XMLHttpRequestInjector = () => {
try {
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (...args) {
const url = args[1];
if (typeof url === "string" && url.includes("timedtext")) {
this.addEventListener("load", function () {
window.postMessage(
{
type: "KISS_XHR_DATA_YOUTUBE",
url: this.responseURL,
response: this.responseText,
},
window.location.origin
);
});
}
return originalOpen.apply(this, args);
};
} catch (err) {
console.log("XMLHttpRequestInjector", err);
}
};

View File

@@ -3,12 +3,12 @@ import { APP_CONSTS } from "../config";
import ContentFab from "../views/Action/ContentFab";
export class FabManager extends ShadowDomManager {
constructor({ translator, processActions, fabConfig }) {
constructor({ processActions, fabConfig }) {
super({
id: APP_CONSTS.fabID,
className: "notranslate",
reactComponent: ContentFab,
props: { translator, processActions, fabConfig },
props: { processActions, fabConfig },
});
if (!fabConfig?.isHide) {

View File

@@ -41,6 +41,12 @@ export const fetchGM = async (
});
},
onerror: reject,
onabort: () => {
reject(new Error("GM request onabort."));
},
ontimeout: () => {
reject(new Error("GM request timeout."));
},
});
});

View File

@@ -13,6 +13,18 @@ export const injectInlineJs = (code, id = "kiss-translator-inline-js") => {
(document.head || document.documentElement).appendChild(el);
};
export const injectInlineJsBg = (code, id = "kiss-translator-inline-js") => {
if (document.getElementById(id)) {
return;
}
const el = document.createElement("script");
el.type = "text/javascript";
el.id = id;
el.textContent = code;
(document.head || document.documentElement).appendChild(el);
};
// Function to inject external JavaScript file
export const injectExternalJs = (src, id = "kiss-translator-external-js") => {
if (document.getElementById(id)) {

View File

@@ -4,11 +4,11 @@ import {
OPT_LANGS_LIST,
DEFAULT_API_SETTING,
} from "../config";
import { genEventName, removeEndchar, matchInputStr } from "./utils";
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
import { stepShortcutRegister } from "./shortcut";
import { apiTranslate } from "../apis";
import { createLoadingSVG } from "./svg";
import { kissLog } from "./log";
import { logger } from "./log";
function isInputNode(node) {
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
@@ -18,21 +18,94 @@ function isEditAbleNode(node) {
return node.hasAttribute("contenteditable");
}
function replaceContentEditableText(node, newText) {
node.focus();
const selection = window.getSelection();
if (!selection) return;
async function replaceContentEditableText(node, newText) {
try {
logger.debug("try replace editable 1: pasteEvent");
const range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
node.focus();
range.deleteContents();
const textNode = document.createTextNode(newText);
range.insertNode(textNode);
const selection = window.getSelection();
if (!selection) throw new Error("window.getSelection() is not available.");
selection.collapseToEnd();
const targetNode = node.querySelector("p") || node;
const range = document.createRange();
range.selectNodeContents(targetNode);
selection.removeAllRanges();
selection.addRange(range);
const dataTransfer = new DataTransfer();
dataTransfer.setData("text/plain", newText);
const pasteEvent = new ClipboardEvent("paste", {
clipboardData: dataTransfer,
bubbles: true,
cancelable: true,
});
node.dispatchEvent(pasteEvent);
await sleep(50);
if (node.innerText.trim() === newText) {
return true;
}
throw new Error("Strategy 1 failed to replace text correctly.");
} catch (error) {
logger.debug("Strategy 1 Failed:", error.message);
}
try {
logger.debug("try replace editable 2: execCommand");
node.focus();
const selection = window.getSelection();
if (!selection) throw new Error("window.getSelection() is not available.");
const targetNode = node.querySelector("p") || node;
const range = document.createRange();
range.selectNodeContents(targetNode);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand("insertText", false, newText);
await sleep(50);
if (node.innerText.trim() === newText) {
return true;
}
throw new Error("Strategy 2 failed to replace text correctly.");
} catch (error) {
logger.debug("Strategy 2 Failed:", error.message);
}
try {
logger.debug("try replace editable 3: textContent");
node.focus();
const targetNode = node.querySelector("p") || node;
const textSpan = targetNode.querySelector('span[data-lexical-text="true"]');
if (textSpan) {
textSpan.textContent = newText;
} else {
targetNode.textContent = newText;
}
node.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
await sleep(50);
if (node.innerText.trim() === newText) {
return true;
}
throw new Error("Strategy 3 failed to replace text correctly.");
} catch (error) {
logger.debug("Strategy 3 Failed:", error.message);
}
return false;
}
function getNodeText(node) {
@@ -107,7 +180,7 @@ export class InputTranslator {
);
this.#isEnabled = true;
kissLog("Input Translator enabled.");
logger.info("Input Translator enabled.");
}
/**
@@ -122,7 +195,7 @@ export class InputTranslator {
this.#unregisterShortcut = null;
}
this.#isEnabled = false;
kissLog("Input Translator disabled.");
logger.info("Input Translator disabled.");
}
/**
@@ -193,25 +266,30 @@ export class InputTranslator {
try {
addLoading(node, loadingId);
const [trText, isSame] = await apiTranslate({
const { trText, isSame } = await apiTranslate({
text,
fromLang,
toLang,
apiSetting,
});
if (!trText || isSame) return;
const newText = trText?.trim() || "";
if (!newText || isSame) return;
if (isInputNode(node)) {
node.value = trText;
node.value = newText;
node.dispatchEvent(
new Event("input", { bubbles: true, cancelable: true })
);
} else {
replaceContentEditableText(node, trText);
const success = await replaceContentEditableText(node, newText);
if (!success) {
// todo: 提示可以黏贴
logger.info("Replace editable text failed");
}
}
} catch (err) {
kissLog("Translate input error:", err);
logger.info("Translate input error:", err);
} finally {
removeLoading(loadingId);
}

View File

@@ -1,6 +1,6 @@
import Sval from "sval";
const interpreter = new Sval({
export const interpreter = new Sval({
// ECMA Version of the code
// 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15
// or 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024
@@ -12,5 +12,3 @@ const interpreter = new Sval({
// Whether the code runs in a sandbox
sandBox: true,
});
export default interpreter;

View File

@@ -60,7 +60,7 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
"parentStyle",
"grandStyle",
"injectJs",
"injectCss",
// "injectCss",
// "fixerSelector",
"transStartHook",
"transEndHook",
@@ -154,7 +154,7 @@ export const checkRules = (rules) => {
parentStyle,
grandStyle,
injectJs,
injectCss,
// injectCss,
apiSlug,
fromLang,
toLang,
@@ -193,7 +193,7 @@ export const checkRules = (rules) => {
parentStyle: type(parentStyle) === "string" ? parentStyle : "",
grandStyle: type(grandStyle) === "string" ? grandStyle : "",
injectJs: type(injectJs) === "string" ? injectJs : "",
injectCss: type(injectCss) === "string" ? injectCss : "",
// injectCss: type(injectCss) === "string" ? injectCss : "",
bgColor: type(bgColor) === "string" ? bgColor : "",
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
apiSlug:

View File

@@ -1,56 +0,0 @@
import { kissLog } from "./log";
/**
* @class ShadowRootMonitor
* @description 通过覆写 Element.prototype.attachShadow 来监控页面上所有新创建的 Shadow DOM
*/
export default class ShadowRootMonitor {
/**
* @param {function(ShadowRoot): void} callback - 当一个新的 shadowRoot 被创建时调用的回调函数。
*/
constructor(callback) {
if (typeof callback !== "function") {
throw new Error("Callback must be a function.");
}
this.callback = callback;
this.isMonitoring = false;
this.originalAttachShadow = Element.prototype.attachShadow;
}
/**
* 开始监控 shadowRoot 的创建。
*/
start() {
if (this.isMonitoring) {
return;
}
const monitorInstance = this;
Element.prototype.attachShadow = function (...args) {
const shadowRoot = monitorInstance.originalAttachShadow.apply(this, args);
if (shadowRoot) {
try {
monitorInstance.callback(shadowRoot);
} catch (error) {
kissLog("Error in ShadowRootMonitor callback", error);
}
}
return shadowRoot;
};
this.isMonitoring = true;
}
/**
* 停止监控,并恢复原始的 attachShadow 方法。
*/
stop() {
if (!this.isMonitoring) {
return;
}
Element.prototype.attachShadow = this.originalAttachShadow;
this.isMonitoring = false;
}
}

View File

@@ -63,8 +63,8 @@ export const shortcutRegister = (targetKeys = [], fn, target = document) => {
const targetKeySet = new Set(targetKeys);
const onKeyDown = (pressedKeys, event) => {
if (isSameSet(targetKeySet, pressedKeys)) {
// event.preventDefault();
event.stopPropagation();
// event.preventDefault(); // 阻止浏览器的默认行为
// event.stopPropagation(); // 阻止事件继续(向父元素)冒泡
fn();
}
};

View File

@@ -5,6 +5,7 @@ import {
STOKEY_RULES_OLD,
STOKEY_WORDS,
STOKEY_FAB,
STOKEY_TRANBOX,
STOKEY_SYNC,
STOKEY_MSAUTH,
STOKEY_BDAUTH,
@@ -135,6 +136,13 @@ export const getFabWithDefault = async () => (await getFab()) || {};
export const setFab = (obj) => setObj(STOKEY_FAB, obj);
export const putFab = (obj) => putObj(STOKEY_FAB, obj);
/**
* tranbox位置大小
*/
export const getTranBox = () => getObj(STOKEY_TRANBOX);
export const putTranBox = (obj) => putObj(STOKEY_TRANBOX, obj);
export const debouncePutTranBox = debounce(putTranBox, 300);
/**
* 数据同步
*/

View File

@@ -84,7 +84,7 @@ const genStyles = ({
// 虚线框
[OPT_STYLE_DASHBOX]: `
border: 2px dashed ${bgColor || DEFAULT_COLOR};
display: inline-block;
display: block;
padding: 0.2em 0.4em;
box-sizing: border-box;
`,

View File

@@ -2,8 +2,6 @@ import {
APP_UPNAME,
APP_LCNAME,
APP_CONSTS,
MSG_INJECT_JS,
MSG_INJECT_CSS,
OPT_STYLE_FUZZY,
GLOBLA_RULE,
DEFAULT_SETTING,
@@ -16,14 +14,10 @@ import {
OPT_SPLIT_PARAGRAPH_DISABLE,
OPT_SPLIT_PARAGRAPH_TEXTLENGTH,
} from "../config";
import interpreter from "./interpreter";
import ShadowRootMonitor from "./shadowRootMonitor";
import { interpreter } from "./interpreter";
import { clearFetchPool } from "./pool";
import { debounce, scheduleIdle, genEventName, truncateWords } from "./utils";
import { apiTranslate } from "../apis";
import { sendBgMsg } from "./msg";
import { isExt } from "./client";
import { injectInlineJs, injectInternalCss } from "./injector";
import { kissLog } from "./log";
import { clearAllBatchQueue } from "./batchQueue";
import { genTextClass } from "./style";
@@ -31,6 +25,7 @@ import { createLoadingSVG } from "./svg";
import { shortcutRegister } from "./shortcut";
import { tryDetectLang } from "./detect";
import { trustedTypesHelper } from "./trustedTypes";
import { injectJs, INJECTOR } from "../injectors";
/**
* @class Translator
@@ -77,7 +72,7 @@ export class Translator {
"VIDEO",
]),
INLINE: new Set([
"A",
// "A",
"ABBR",
"ACRONYM",
"B",
@@ -106,7 +101,7 @@ export class Translator {
"SCRIPT",
"SELECT",
"SMALL",
"SPAN",
// "SPAN",
"STRONG",
"SUB",
"SUP",
@@ -206,11 +201,17 @@ export class Translator {
// 14. 包含常见扩展名的文件名 (例如: document.pdf, image.jpeg)
/^[^\s\\/:]+?\.[a-zA-Z0-9]{2,5}$/,
// todo: 数字和特殊字符组成的字符串
];
static DEFAULT_OPTIONS = DEFAULT_SETTING; // 默认配置
static DEFAULT_RULE = GLOBLA_RULE; // 默认规则
static isElement(el) {
return el instanceof Element;
}
static isElementOrFragment(el) {
return el instanceof Element || el instanceof DocumentFragment;
}
@@ -221,6 +222,7 @@ export class Translator {
if (Translator.TAGS.INLINE.has(el.nodeName)) return false;
if (Translator.TAGS.BLOCK.has(el.nodeName)) return true;
if (el.attributes?.display?.value?.includes("inline")) return false;
if (Translator.displayCache.has(el)) {
return Translator.displayCache.get(el);
@@ -231,11 +233,22 @@ export class Translator {
return isBlock;
}
// 判断是否包含块级子元素
static hasBlockNode(el) {
if (!Translator.isElementOrFragment(el)) return false;
for (const child of el.childNodes) {
if (Translator.isBlockNode(child)) {
return true;
}
}
return false;
}
// 判断是否直接包含非空文本节点
static hasTextNode(el) {
if (!Translator.isElementOrFragment(el)) return false;
for (const node of el.childNodes) {
if (node.nodeType === Node.TEXT_NODE && /\S/.test(node.nodeValue)) {
for (const child of el.childNodes) {
if (child.nodeType === Node.TEXT_NODE && /\S/.test(child.nodeValue)) {
return true;
}
}
@@ -248,18 +261,23 @@ export class Translator {
}
// 内置忽略元素
static BUILTIN_IGNORE_SELECTOR = `abbr, address, area, audio, br, canvas, code,
data, datalist, dfn, embed, head, iframe, img, input, kbd, noscript, map,
object, option, output, param, picture, progress,
samp, select, script, style, sub, sup, svg, track, time, textarea, template,
var, video, wbr, .notranslate, [contenteditable], [translate='no'],
${APP_LCNAME}, #${APP_CONSTS.fabID}, #${APP_CONSTS.boxID},
.${APP_CONSTS.fabID}_warpper, .${APP_CONSTS.boxID}_warpper`;
static KISS_IGNORE_SELECTOR = `${APP_LCNAME}, .kiss-caption-container, .kiss-subtitle-controls
#${APP_CONSTS.fabID}, .${APP_CONSTS.fabID}_warpper,
#${APP_CONSTS.boxID}, .${APP_CONSTS.boxID}_warpper,
#${APP_CONSTS.popupID}, .${APP_CONSTS.popupID}_warpper`;
static BUILTIN_IGNORE_SELECTOR = `address, area, audio, br, canvas,
data, datalist, embed, head, iframe, input, noscript, map,
object, option, param, picture, progress,
select, script, style, track, textarea, template,
video, wbr, .notranslate, [contenteditable], [translate='no'],
${Translator.KISS_IGNORE_SELECTOR}`;
#setting; // 设置选项
#rule; // 规则
#isInitialized = false; // 初始化状态
#isJsInjected = false; // 注入用户JS
#isShadowRootJsInjected = false; //
#mouseHoverEnabled = false; // 鼠标悬停翻译
#enabled = false; // 全局默认状态
#runId = 0; // 用于中止过期的异步请求
@@ -287,17 +305,23 @@ export class Translator {
#hoveredNode = null; // 存储当前悬停的可翻译节点
#boundMouseMoveHandler; // 鼠标事件
#boundKeyDownHandler; // 键盘事件
#windowMessageHandler = null;
#debouncedFindShadowRoot = null;
#io; // IntersectionObserver
#mo; // MutationObserver
#dmm; // DebounceMouseMover
#srm; // ShadowRootMonitor
#rescanQueue = new Set(); // “脏容器”队列
#isQueueProcessing = false; // 队列处理状态标志
// 忽略元素
get #ignoreSelector() {
if (this.#rule.autoScan === "false") {
return `${Translator.KISS_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
}
return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
}
@@ -350,12 +374,12 @@ export class Translator {
this.#io = this.#createIntersectionObserver();
this.#mo = this.#createMutationObserver();
this.#dmm = this.#createDebounceMouseMover();
this.#srm = this.#createShadowRootMonitor();
// 监控shadowroot
if (this.#rule.hasShadowroot === "true") {
this.#srm.start();
}
this.#windowMessageHandler = this.#handleWindowMessage.bind(this);
this.#debouncedFindShadowRoot = debounce(
this.#findAndObserveShadowRoot.bind(this),
300
);
// 鼠标悬停翻译
if (this.#setting.mouseHoverSetting.useMouseHover) {
@@ -392,15 +416,41 @@ export class Translator {
this.#startObserveRoot(root);
});
// 查找现有的所有shadowroot
if (this.#rule.hasShadowroot === "true") {
try {
this.#findAllShadowRoots().forEach((shadowRoot) => {
this.#startObserveShadowRoot(shadowRoot);
});
} catch (err) {
kissLog("findAllShadowRoots", err);
}
this.#attachShadowRootListener();
this.#findAndObserveShadowRoot();
}
}
#handleWindowMessage(event) {
if (event.data?.type === "KISS_SHADOW_ROOT_CREATED") {
this.#debouncedFindShadowRoot();
}
}
#attachShadowRootListener() {
if (!this.#isShadowRootJsInjected) {
const id = "kiss-translator-inject-shadowroot-js";
injectJs(INJECTOR.shadowroot, id);
this.#isShadowRootJsInjected = true;
}
window.addEventListener("message", this.#windowMessageHandler);
}
#removeShadowRootListener() {
window.removeEventListener("message", this.#windowMessageHandler);
}
// 查找现有的所有shadowroot
#findAndObserveShadowRoot() {
try {
this.#findAllShadowRoots().forEach((shadowRoot) => {
this.#startObserveShadowRoot(shadowRoot);
});
} catch (err) {
kissLog("findAllShadowRoots", err);
}
}
@@ -502,11 +552,13 @@ export class Translator {
// 监控翻译单元的可见性
#createIntersectionObserver() {
const { transInterval, rootMargin = 500 } = this.#setting;
const pending = new Set();
const flush = debounce(() => {
pending.forEach((node) => this.#performSyncNode(node));
pending.clear();
}, this.#setting.transInterval);
}, transInterval);
return new IntersectionObserver(
(entries) => {
@@ -520,7 +572,7 @@ export class Translator {
}
});
},
{ threshold: 0.01 }
{ threshold: 0.01, rootMargin: `${rootMargin}px 0px ${rootMargin}px 0px` }
);
}
@@ -528,33 +580,35 @@ export class Translator {
#createMutationObserver() {
return new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (this.#skipMoNodes.has(mutation.target)) return;
if (
mutation.type === "characterData" &&
mutation.oldValue !== mutation.target.nodeValue
this.#skipMoNodes.has(mutation.target) ||
mutation.nextSibling?.tagName === this.#translationTagName
) {
this.#queueForRescan(mutation.target.parentElement);
} else if (mutation.type === "childList") {
if (mutation.nextSibling?.tagName === this.#translationTagName) {
// 恢复原文时插入元素,忽略
continue;
}
continue;
}
if (mutation.type === "characterData") {
if (
mutation.oldValue !== mutation.target.nodeValue &&
!this.#combinedSkipsRegex.test(mutation.target.nodeValue)
) {
this.#queueForRescan(mutation.target.parentElement);
}
} else if (mutation.type === "childList") {
let nodes = new Set();
let hasText = false;
mutation.addedNodes.forEach((node) => {
if (this.#skipMoNodes.has(node)) return;
if (
this.#skipMoNodes.has(node) ||
node.nodeName === this.#translationTagName
) {
return;
}
if (/\S/.test(node.nodeValue)) {
if (node.nodeType === Node.TEXT_NODE) {
hasText = true;
} else if (
Translator.isElementOrFragment(node) &&
node.nodeName !== this.#translationTagName
) {
nodes.add(node);
}
if (node.nodeType === Node.TEXT_NODE) {
hasText = true;
} else if (Translator.isElementOrFragment(node)) {
nodes.add(node);
}
});
if (hasText) {
@@ -591,13 +645,6 @@ export class Translator {
}, 100);
}
// 创建shadowroot的回调
#createShadowRootMonitor() {
return new ShadowRootMonitor((shadowRoot) => {
this.#startObserveShadowRoot(shadowRoot);
});
}
// 跟踪鼠标下的可翻译节点
#handleMouseMove(event) {
let targetNode = event.composedPath()[0];
@@ -727,6 +774,9 @@ export class Translator {
// 开始/重新监控节点
#startObserveNode(node) {
// todo: DocumentFragment 无法被 this.#io.observe
if (!Translator.isElement(node)) return;
if (this.#rule.highlightWords === OPT_HIGHLIGHT_WORDS_BEFORETRANS) {
this.#highlightWordsDeeply(node);
}
@@ -772,6 +822,7 @@ export class Translator {
#scanNode(rootNode) {
if (
!Translator.isElementOrFragment(rootNode) ||
// rootNode.matches?.(this.#rule.keepSelector) ||
rootNode.matches?.(this.#ignoreSelector)
) {
return;
@@ -783,13 +834,24 @@ export class Translator {
}
const hasText = Translator.hasTextNode(rootNode);
if (hasText) {
if (!hasText && rootNode.children.length === 1) {
this.#scanNode(rootNode.children[0]);
return;
}
const hasBlock = Translator.hasBlockNode(rootNode);
if (hasText || !hasBlock) {
this.#startObserveNode(rootNode);
}
for (const child of rootNode.children) {
if (!hasText || Translator.isBlockNode(child)) {
this.#scanNode(child);
if (hasBlock) {
for (const child of rootNode.children) {
const isBlock = Translator.isBlockNode(child);
if (!hasText || isBlock) {
this.#scanNode(child);
}
}
}
}
@@ -1028,6 +1090,7 @@ export class Translator {
if (
Translator.TAGS.BREAK_LINE.has(node.nodeName) ||
node.matches?.(this.#ignoreSelector) ||
node.nodeName === this.#translationTagName
) {
return true;
@@ -1087,7 +1150,6 @@ export class Translator {
const {
transTag,
textStyle,
transStartHook,
transEndHook,
transOnly,
termsStyle,
@@ -1106,20 +1168,6 @@ export class Translator {
const parentNode = hostNode.parentElement;
const hideOrigin = transOnly === "true";
// 翻译开始钩子函数
if (transStartHook?.trim()) {
try {
interpreter.run(`exports.transStartHook = ${transStartHook}`);
interpreter.exports.transStartHook({
hostNode,
parentNode,
nodes,
});
} catch (err) {
kissLog("transStartHook", err);
}
}
try {
const [processedString, placeholderMap] = this.#serializeForTranslation(
nodes,
@@ -1144,10 +1192,8 @@ export class Translator {
nodes[nodes.length - 1].after(wrapper);
const currentRunId = this.#runId;
const [translatedText, isSameLang] = await this.#translateFetch(
processedString,
deLang
);
const { trText: translatedText, isSame: isSameLang } =
await this.#translateFetch(processedString, deLang);
if (this.#runId !== currentRunId) {
throw new Error("Request terminated");
}
@@ -1240,10 +1286,7 @@ export class Translator {
}
// 文本节点
if (
this.#rule.hasRichText === "false" ||
node.nodeType === Node.TEXT_NODE
) {
if (node.nodeType === Node.TEXT_NODE) {
let text = node.textContent;
// 专业术语替换
@@ -1269,8 +1312,10 @@ export class Translator {
// 元素节点
if (node.nodeType === Node.ELEMENT_NODE) {
if (
Translator.TAGS.REPLACE.has(node.tagName) ||
(this.#rule.hasRichText === "true" &&
Translator.TAGS.REPLACE.has(node.tagName)) ||
node.matches(this.#rule.keepSelector) ||
// node.matches(this.#ignoreSelector) ||
!node.textContent.trim()
) {
if (node.tagName === "IMG" || node.tagName === "SVG") {
@@ -1285,7 +1330,10 @@ export class Translator {
innerContent += traverse(child);
});
if (Translator.TAGS.WARP.has(node.tagName)) {
if (
this.#rule.hasRichText === "true" &&
Translator.TAGS.WARP.has(node.tagName)
) {
wrapCounter++;
const startPlaceholder = `<${this.#placeholder.tagName}${wrapCounter}>`;
const endPlaceholder = `</${this.#placeholder.tagName}${wrapCounter}>`;
@@ -1331,16 +1379,39 @@ export class Translator {
// 发起翻译请求
#translateFetch(text, deLang = "") {
const { fromLang, toLang } = this.#rule;
const { toLang, transStartHook } = this.#rule;
const fromLang = deLang || this.#rule.fromLang;
const apiSetting = { ...this.#apiSetting };
const docInfo = { ...this.#docInfo };
const glossary = { ...this.#glossary };
const apisMap = this.#apisMap;
return apiTranslate({
const args = {
text,
fromLang: deLang || fromLang,
fromLang,
toLang,
apiSetting: this.#apiSetting,
docInfo: this.#docInfo,
glossary: this.#glossary,
});
apiSetting,
docInfo,
glossary,
};
// 翻译开始钩子函数
if (transStartHook?.trim()) {
try {
interpreter.run(`exports.transStartHook = ${transStartHook}`);
const hookResult = interpreter.exports.transStartHook({
...args,
apisMap,
});
if (hookResult) {
Object.assign(args, ...hookResult);
}
} catch (err) {
kissLog("transStartHook", err);
}
}
return apiTranslate(args);
}
// 查找指定节点下所有译文节点
@@ -1489,6 +1560,8 @@ export class Translator {
// 停止监听,重置参数
#resetOptions() {
this.#removeShadowRootListener();
this.#io.disconnect();
this.#mo.disconnect();
this.#viewNodes.clear();
@@ -1534,14 +1607,35 @@ export class Translator {
this.#isJsInjected = true;
try {
const { injectJs, injectCss } = this.#rule;
if (isExt) {
injectJs && sendBgMsg(MSG_INJECT_JS, injectJs);
injectCss && sendBgMsg(MSG_INJECT_CSS, injectCss);
} else {
injectJs &&
injectInlineJs(injectJs, "kiss-translator-userinit-injector");
injectCss && injectInternalCss(injectCss);
// const { injectJs, injectCss } = this.#rule;
// if (isExt) {
// injectJs && sendBgMsg(MSG_INJECT_JS, injectJs);
// injectCss && sendBgMsg(MSG_INJECT_CSS, injectCss);
// } else {
// injectJs &&
// injectInlineJs(injectJs, "kiss-translator-userinit-injector");
// injectCss && injectInternalCss(injectCss);
// }
const { injectJs, toLang } = this.#rule;
if (injectJs?.trim()) {
const apiSetting = { ...this.#apiSetting };
const docInfo = { ...this.#docInfo };
const glossary = { ...this.#glossary };
const apisMap = this.#apisMap;
const apiDectect = tryDetectLang;
interpreter.import({
KT: {
apiTranslate,
apiDectect,
apiSetting,
apisMap,
toLang,
docInfo,
glossary,
},
});
interpreter.run(injectJs);
}
} catch (err) {
kissLog("inject js", err);
@@ -1592,8 +1686,8 @@ export class Translator {
try {
const deLang = await tryDetectLang(title);
const [translatedTitle] = await this.#translateFetch(title, deLang);
document.title = translatedTitle || title;
const { trText } = await this.#translateFetch(title, deLang);
document.title = trText || title;
} catch (err) {
kissLog("tanslate title", err);
}
@@ -1659,7 +1753,6 @@ export class Translator {
stop() {
this.disable();
this.#resetOptions();
this.#srm.stop();
this.#disableMouseHover();
this.#removeInjector();
this.#isInitialized = false;

View File

@@ -28,7 +28,7 @@ import { logger } from "./log";
export default class TranslatorManager {
#clearShortcuts = [];
#menuCommandIds = [];
#clearTouchListener = null;
#clearTouchListeners = [];
#isActive = false;
#isUserscript;
#isIframe;
@@ -54,15 +54,15 @@ export default class TranslatorManager {
isIframe,
});
this._transboxManager = new TransboxManager(setting);
if (!isIframe) {
this._transboxManager = new TransboxManager(setting);
this._inputTranslator = new InputTranslator(setting);
this._popupManager = new PopupManager({
translator: this._translator,
processActions: this.#processActions.bind(this),
});
this._fabManager = new FabManager({
translator: this._translator,
processActions: this.#processActions.bind(this),
fabConfig,
});
@@ -110,10 +110,8 @@ export default class TranslatorManager {
this.#clearShortcuts = [];
// 触屏
if (this.#clearTouchListener) {
this.#clearTouchListener();
this.#clearTouchListener = null;
}
this.#clearTouchListeners.forEach((clear) => clear());
this.#clearTouchListeners = [];
// 油猴菜单
if (globalThis.GM && this.#menuCommandIds.length > 0) {
@@ -139,14 +137,17 @@ export default class TranslatorManager {
window.addEventListener("message", this.#windowMessageHandler);
} else {
browser.runtime.onMessage.addListener(this.#browserMessageHandler);
if (this.#isIframe) {
window.addEventListener("message", this.#windowMessageHandler);
}
}
}
#setupTouchOperations() {
if (this.#isIframe) return;
const { touchTranslate = 2 } = this._translator.setting;
if (touchTranslate === 0) {
const { touchModes = [2] } = this._translator.setting;
if (touchModes.length === 0) {
return;
}
@@ -154,35 +155,31 @@ export default class TranslatorManager {
this.#processActions({ action: MSG_TRANS_TOGGLE });
};
switch (touchTranslate) {
case 2:
case 3:
case 4:
this.#clearTouchListener = touchTapListener(handleTap, {
taps: 1,
fingers: touchTranslate,
});
break;
case 5:
this.#clearTouchListener = touchTapListener(handleTap, {
taps: 2,
fingers: 1,
});
break;
case 6:
this.#clearTouchListener = touchTapListener(handleTap, {
taps: 3,
fingers: 1,
});
break;
case 7:
this.#clearTouchListener = touchTapListener(handleTap, {
taps: 2,
fingers: 2,
});
break;
default:
}
const handleListener = (mode) => {
let options = null;
switch (mode) {
case 2:
case 3:
case 4:
options = { taps: 1, fingers: mode };
break;
case 5:
options = { taps: 2, fingers: 1 };
break;
case 6:
options = { taps: 3, fingers: 1 };
break;
case 7:
options = { taps: 2, fingers: 2 };
break;
default:
}
if (options) {
this.#clearTouchListeners.push(touchTapListener(handleTap, options));
}
};
touchModes.forEach((mode) => handleListener(mode));
}
#handleWindowMessage(event) {
@@ -190,7 +187,7 @@ export default class TranslatorManager {
}
#handleBrowserMessage(message, sender, sendResponse) {
const result = this.#processActions(message);
const result = this.#processActions(message, true);
const response = result || {
rule: this._translator.rule,
setting: this._translator.setting,
@@ -248,9 +245,9 @@ export default class TranslatorManager {
];
}
#processActions({ action, args } = {}) {
if (this.#isUserscript) {
sendIframeMsg(action);
#processActions({ action, args } = {}, fromExt = false) {
if (!fromExt) {
sendIframeMsg(action, args);
}
switch (action) {

View File

@@ -1,158 +0,0 @@
/**
* 修复程序类型
*/
export const FIXER_NONE = "-";
export const FIXER_BR = "br";
export const FIXER_BN = "bn";
export const FIXER_BR_DIV = "brToDiv";
export const FIXER_BN_DIV = "bnToDiv";
export const FIXER_ALL = [
FIXER_NONE,
FIXER_BR,
FIXER_BN,
FIXER_BR_DIV,
FIXER_BN_DIV,
];
/**
* 修复过的标记
*/
const fixedSign = "kiss-fixed";
/**
* 采用 `br` 换行网站的修复函数
* 目标是将 `br` 替换成 `p`
* @param {*} node
* @returns
*/
function brFixer(node, tag = "p") {
if (node.hasAttribute(fixedSign)) {
return;
}
node.setAttribute(fixedSign, "true");
const gapTags = ["BR", "WBR"];
const newlineTags = [
"DIV",
"UL",
"OL",
"LI",
"H1",
"H2",
"H3",
"H4",
"H5",
"H6",
"P",
"HR",
"PRE",
"TABLE",
"BLOCKQUOTE",
];
let html = "";
node.childNodes.forEach(function (child, index) {
if (index === 0) {
html += `<${tag} class="kiss-p">`;
}
if (gapTags.indexOf(child.nodeName) !== -1) {
html += `</${tag}><${tag} class="kiss-p">`;
} else if (newlineTags.indexOf(child.nodeName) !== -1) {
html += `</${tag}>${child.outerHTML}<${tag} class="kiss-p">`;
} else if (child.outerHTML) {
html += child.outerHTML;
} else if (child.textContent) {
html += child.textContent;
}
if (index === node.childNodes.length - 1) {
html += `</${tag}>`;
}
});
node.innerHTML = html;
}
function brDivFixer(node) {
return brFixer(node, "div");
}
/**
* 目标是将 `\n` 替换成 `p`
* @param {*} node
* @returns
*/
function bnFixer(node, tag = "p") {
if (node.hasAttribute(fixedSign)) {
return;
}
node.setAttribute(fixedSign, "true");
node.innerHTML = node.innerHTML
.split("\n")
.map((item) => `<${tag} class="kiss-p">${item || "&nbsp;"}</${tag}>`)
.join("");
}
function bnDivFixer(node) {
return bnFixer(node, "div");
}
/**
* 查找、监听节点,并执行修复函数
* @param {*} selector
* @param {*} fixer
* @param {*} rootSelector
*/
function run(selector, fixer, rootSelector) {
const mutaObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addNode) {
if (addNode && addNode.querySelectorAll) {
addNode.querySelectorAll(selector).forEach(function (node) {
fixer(node);
});
}
});
});
});
let rootNodes = [document];
if (rootSelector) {
rootNodes = document.querySelectorAll(rootSelector);
}
rootNodes.forEach(function (rootNode) {
rootNode.querySelectorAll(selector).forEach(function (node) {
fixer(node);
});
mutaObserver.observe(rootNode, {
childList: true,
subtree: true,
});
});
}
/**
* 修复程序映射
*/
const fixerMap = {
[FIXER_BR]: brFixer,
[FIXER_BN]: bnFixer,
[FIXER_BR_DIV]: brDivFixer,
[FIXER_BN_DIV]: bnDivFixer,
};
/**
* 执行fixer
* @param {*} param0
*/
export function runFixer(selector, fixer = "-", rootSelector) {
try {
if (Object.keys(fixerMap).includes(fixer)) {
run(selector, fixerMap[fixer], rootSelector);
}
} catch (err) {
console.error(`[kiss-webfix run]: ${err.message}`);
}
}

View File

@@ -1,5 +1,6 @@
import { logger } from "../libs/log.js";
import { truncateWords } from "../libs/utils.js";
import { apiTranslate } from "../apis/index.js";
/**
* @class BilingualSubtitleManager
@@ -8,7 +9,6 @@ import { truncateWords } from "../libs/utils.js";
export class BilingualSubtitleManager {
#videoEl;
#formattedSubtitles = [];
#translationService;
#captionWindowEl = null;
#paperEl = null;
#currentSubtitleIndex = -1;
@@ -20,14 +20,12 @@ export class BilingualSubtitleManager {
* @param {object} options
* @param {HTMLVideoElement} options.videoEl - 页面上的 video 元素。
* @param {Array<object>} options.formattedSubtitles - 已格式化好的字幕数组。
* @param {(text: string, toLang: string) => Promise<string>} options.translationService - 外部翻译函数。
* @param {object} options.setting - 配置对象,如目标翻译语言。
*/
constructor({ videoEl, formattedSubtitles, translationService, setting }) {
constructor({ videoEl, formattedSubtitles, setting }) {
this.#setting = setting;
this.#videoEl = videoEl;
this.#formattedSubtitles = formattedSubtitles;
this.#translationService = translationService;
this.onTimeUpdate = this.onTimeUpdate.bind(this);
this.onSeek = this.onSeek.bind(this);
@@ -128,15 +126,14 @@ export class BilingualSubtitleManager {
let initialBottom;
let dragElementHeight;
const onMouseDown = (e) => {
e.stopPropagation();
e.preventDefault();
const onDragStart = (e) => {
if (e.type === "mousedown" && e.button !== 0) return;
if (e.button !== 0) return;
e.preventDefault();
isDragging = true;
handleElement.style.cursor = "grabbing";
startY = e.clientY;
startY = e.type === "touchstart" ? e.touches[0].clientY : e.clientY;
initialBottom =
boundaryContainer.getBoundingClientRect().bottom -
@@ -144,17 +141,23 @@ export class BilingualSubtitleManager {
dragElementHeight = dragElement.offsetHeight;
document.addEventListener("mousemove", onMouseMove, { capture: true });
document.addEventListener("mouseup", onMouseUp, { capture: true });
document.addEventListener("mousemove", onDragMove, { capture: true });
document.addEventListener("touchmove", onDragMove, {
capture: true,
passive: false,
});
document.addEventListener("mouseup", onDragEnd, { capture: true });
document.addEventListener("touchend", onDragEnd, { capture: true });
};
const onMouseMove = (e) => {
const onDragMove = (e) => {
if (!isDragging) return;
e.preventDefault();
e.stopPropagation();
const deltaY = e.clientY - startY;
const currentY =
e.type === "touchmove" ? e.touches[0].clientY : e.clientY;
const deltaY = currentY - startY;
let newBottom = initialBottom - deltaY;
const containerHeight = boundaryContainer.clientHeight;
@@ -167,17 +170,18 @@ export class BilingualSubtitleManager {
dragElement.style.bottom = `${newBottom}px`;
};
const onMouseUp = (e) => {
const onDragEnd = (e) => {
if (!isDragging) return;
e.preventDefault();
e.stopPropagation();
isDragging = false;
handleElement.style.cursor = "grab";
document.removeEventListener("mousemove", onMouseMove, { capture: true });
document.removeEventListener("mouseup", onMouseUp, { capture: true });
document.removeEventListener("mousemove", onDragMove, { capture: true });
document.removeEventListener("touchmove", onDragMove, { capture: true });
document.removeEventListener("mouseup", onDragEnd, { capture: true });
document.removeEventListener("touchend", onDragEnd, { capture: true });
const finalBottomPx = dragElement.style.bottom;
setTimeout(() => {
@@ -185,7 +189,10 @@ export class BilingualSubtitleManager {
}, 50);
};
handleElement.addEventListener("mousedown", onMouseDown);
handleElement.addEventListener("mousedown", onDragStart);
handleElement.addEventListener("touchstart", onDragStart, {
passive: false,
});
}
/**
@@ -300,13 +307,13 @@ export class BilingualSubtitleManager {
subtitle.isTranslating = true;
try {
const { fromLang, toLang, apiSetting } = this.#setting;
const [translatedText] = await this.#translationService({
const { trText } = await apiTranslate({
text: subtitle.text,
fromLang,
toLang,
apiSetting,
});
subtitle.translation = translatedText;
subtitle.translation = trText;
} catch (error) {
logger.info("Translation failed for:", subtitle.text, error);
subtitle.translation = "[Translation failed]";

View File

@@ -1,19 +0,0 @@
export const XMLHttpRequestInjector = () => {
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (...args) {
const url = args[1];
if (typeof url === "string" && url.includes("timedtext")) {
this.addEventListener("load", function () {
window.postMessage(
{
type: "KISS_XHR_DATA_YOUTUBE",
url: this.responseURL,
response: this.responseText,
},
window.location.origin
);
});
}
return originalOpen.apply(this, args);
};
};

View File

@@ -1,5 +1,5 @@
import { logger } from "../libs/log.js";
import { apiSubtitle, apiTranslate } from "../apis/index.js";
import { apiSubtitle } from "../apis/index.js";
import { BilingualSubtitleManager } from "./BilingualSubtitleManager.js";
import {
MSG_XHR_DATA_YOUTUBE,
@@ -70,6 +70,8 @@ class YouTubeCaptionProvider {
}
#moAds(adContainer) {
const { skipAd = false } = this.#setting;
const adLayoutSelector = ".ytp-ad-player-overlay-layout";
const skipBtnSelector =
".ytp-skip-ad-button, .ytp-ad-skip-button, .ytp-ad-skip-button-modern";
@@ -83,22 +85,24 @@ class YouTubeCaptionProvider {
if (node.matches(adLayoutSelector)) {
logger.debug("Youtube Provider: AD start playing!", node);
// todo: 顺带把广告快速跳过
if (videoEl) {
if (videoEl && skipAd) {
videoEl.playbackRate = 16;
videoEl.currentTime = videoEl.duration;
}
if (this.#managerInstance) {
this.#managerInstance.setIsAdPlaying(true);
}
} else if (node.matches(skipBtnSelector)) {
} else if (node.matches(skipBtnSelector) && skipAd) {
logger.debug("Youtube Provider: AD skip button!", node);
node.click();
}
const skipBtn = node?.querySelector(skipBtnSelector);
if (skipBtn) {
logger.debug("Youtube Provider: AD skip button!!", skipBtn);
skipBtn.click();
if (skipAd) {
const skipBtn = node?.querySelector(skipBtnSelector);
if (skipBtn) {
logger.debug("Youtube Provider: AD skip button!!", skipBtn);
skipBtn.click();
}
}
});
mutation.removedNodes.forEach((node) => {
@@ -106,7 +110,7 @@ class YouTubeCaptionProvider {
if (node.matches(adLayoutSelector)) {
logger.debug("Youtube Provider: Ad ends!");
if (videoEl) {
if (videoEl && skipAd) {
videoEl.playbackRate = 1;
}
if (this.#managerInstance) {
@@ -161,14 +165,13 @@ class YouTubeCaptionProvider {
this.#ytControls = ytControls;
const kissControls = document.createElement("div");
kissControls.className = "kiss-bilingual-subtitle-controls";
kissControls.className = "notranslate kiss-subtitle-controls";
Object.assign(kissControls.style, {
height: "100%",
});
const toggleButton = document.createElement("button");
toggleButton.className =
"ytp-button notranslate kiss-bilingual-subtitle-button";
toggleButton.className = "ytp-button kiss-subtitle-button";
toggleButton.title = APP_NAME;
Object.assign(toggleButton.style, {
color: "white",
@@ -505,7 +508,6 @@ class YouTubeCaptionProvider {
this.#managerInstance = new BilingualSubtitleManager({
videoEl,
formattedSubtitles: this.#subtitles,
translationService: apiTranslate,
setting: { ...this.#setting, fromLang: this.#fromLang },
});
this.#managerInstance.start();
@@ -590,7 +592,7 @@ class YouTubeCaptionProvider {
return subtitles;
}
#isQualityPoor(lines, lengthThreshold = 250, percentageThreshold = 0.1) {
#isQualityPoor(lines, lengthThreshold = 250, percentageThreshold = 0.2) {
if (lines.length === 0) return false;
const longLinesCount = lines.filter(
(line) => line.text.length > lengthThreshold
@@ -913,7 +915,7 @@ class YouTubeCaptionProvider {
}
}
#showNotification(message, duration = 3000) {
#showNotification(message, duration = 2000) {
if (!this.#notificationEl) this.#createNotificationElement();
this.#notificationEl.textContent = message;
this.#notificationEl.style.opacity = "1";

View File

@@ -1,18 +1,15 @@
import { YouTubeInitializer } from "./YouTubeCaptionProvider.js";
import { browser } from "../libs/browser.js";
import { isMatch } from "../libs/utils.js";
import { DEFAULT_API_SETTING } from "../config/api.js";
import { DEFAULT_SUBTITLE_SETTING } from "../config/setting.js";
import { injectExternalJs } from "../libs/injector.js";
import { logger } from "../libs/log.js";
import { XMLHttpRequestInjector } from "./XMLHttpRequestInjector.js";
import { injectInlineJs } from "../libs/injector.js";
import { injectJs, INJECTOR } from "../injectors/index.js";
const providers = [
{ pattern: "https://www.youtube.com", start: YouTubeInitializer },
];
export function runSubtitle({ href, setting, isUserscript }) {
export function runSubtitle({ href, setting }) {
try {
const subtitleSetting = setting.subtitleSetting || DEFAULT_SUBTITLE_SETTING;
if (!subtitleSetting.enabled) {
@@ -21,13 +18,8 @@ export function runSubtitle({ href, setting, isUserscript }) {
const provider = providers.find((item) => isMatch(href, item.pattern));
if (provider) {
const id = "kiss-translator-xmlHttp-injector";
if (isUserscript) {
injectInlineJs(`(${XMLHttpRequestInjector})()`, id);
} else {
const src = browser.runtime.getURL("injector.js");
injectExternalJs(src, id);
}
const id = "kiss-translator-inject-subtitle-js";
injectJs(INJECTOR.subtitle, id);
const apiSetting =
setting.transApis.find(

View File

@@ -5,11 +5,9 @@ import Draggable from "./Draggable";
import { useState, useMemo, useCallback } from "react";
import { SettingProvider } from "../../hooks/Setting";
import { MSG_TRANS_TOGGLE, MSG_POPUP_TOGGLE } from "../../config";
import { sendIframeMsg } from "../../libs/iframe";
import useWindowSize from "../../hooks/WindowSize";
export default function ContentFab({
translator,
fabConfig: { x: fabX, y: fabY, fabClickAction = 0 } = {},
processActions,
}) {
@@ -28,13 +26,12 @@ export default function ContentFab({
const handleClick = useCallback(() => {
if (!moved) {
if (fabClickAction === 1) {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
processActions({ action: MSG_TRANS_TOGGLE });
} else {
processActions({ action: MSG_POPUP_TOGGLE });
}
}
}, [moved, translator, fabClickAction, processActions]);
}, [moved, fabClickAction, processActions]);
const fabProps = useMemo(
() => ({

View File

@@ -27,7 +27,7 @@ import ReusableAutocomplete from "./ReusableAutocomplete";
import ShowMoreButton from "./ShowMoreButton";
import {
OPT_TRANS_DEEPLX,
OPT_TRANS_OLLAMA,
// OPT_TRANS_OLLAMA,
OPT_TRANS_CUSTOMIZE,
OPT_TRANS_NIUTRANS,
OPT_TRANS_BUILTINAI,
@@ -38,12 +38,14 @@ import {
DEFAULT_BATCH_SIZE,
DEFAULT_BATCH_LENGTH,
DEFAULT_CONTEXT_SIZE,
OPT_ALL_TYPES,
OPT_ALL_TRANS_TYPES,
API_SPE_TYPES,
BUILTIN_STONES,
BUILTIN_PLACEHOLDERS,
BUILTIN_PLACETAGS,
OPT_TRANS_AZUREAI,
defaultNobatchPrompt,
defaultNobatchUserPrompt,
} from "../../config";
import ValidationInput from "../../hooks/ValidationInput";
@@ -54,18 +56,25 @@ function TestButton({ api }) {
const handleApiTest = async () => {
try {
setLoading(true);
const [text] = await apiTranslate({
text: "hello world",
const text = "hello world";
const { trText } = await apiTranslate({
text,
fromLang: "en",
toLang: "zh-CN",
apiSetting: { ...api },
useCache: false,
usePool: false,
});
if (!text) {
if (!trText) {
throw new Error("empty result");
}
alert.success(i18n("test_success"));
alert.success(
<>
<div>{i18n("test_success")}</div>
<div>{text}</div>
<div>{trText}</div>
</>
);
} catch (err) {
// alert.error(`${i18n("test_failed")}: ${err.message}`);
let msg = err.message;
@@ -77,24 +86,7 @@ function TestButton({ api }) {
alert.error(
<>
<div>{i18n("test_failed")}</div>
{msg === err.message ? (
<div
style={{
maxWidth: 400,
}}
>
{msg}
</div>
) : (
<pre
style={{
maxWidth: 400,
overflow: "auto",
}}
>
{msg}
</pre>
)}
{msg === err.message ? <div>{msg}</div> : <pre>{msg}</pre>}
</>
);
} finally {
@@ -181,12 +173,14 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
model = "",
apiType,
systemPrompt = "",
nobatchPrompt = defaultNobatchPrompt,
nobatchUserPrompt = defaultNobatchUserPrompt,
subtitlePrompt = "",
// userPrompt = "",
customHeader = "",
customBody = "",
think = false,
thinkIgnore = "",
// think = false,
// thinkIgnore = "",
fetchLimit = DEFAULT_FETCH_LIMIT,
fetchInterval = DEFAULT_FETCH_INTERVAL,
httpTimeout = DEFAULT_HTTP_TIMEOUT,
@@ -195,7 +189,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
reqHook = "",
resHook = "",
temperature = 0,
maxTokens = 256,
maxTokens = 20480,
apiName = "",
isDisabled = false,
useBatchFetch = false,
@@ -309,29 +303,53 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
<ValidationInput
size="small"
fullWidth
label={"Max Tokens"}
label={"Max Tokens (0-1000000)"}
type="number"
name="maxTokens"
value={maxTokens}
onChange={handleChange}
min={0}
max={2 ** 15}
max={1000000}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}></Grid>
</Grid>
</Box>
<TextField
size="small"
label={"SYSTEM PROMPT"}
name="systemPrompt"
value={systemPrompt}
onChange={handleChange}
multiline
maxRows={10}
helperText={i18n("system_prompt_helper")}
/>
{useBatchFetch ? (
<TextField
size="small"
label={"BATCH SYSTEM PROMPT"}
name="systemPrompt"
value={systemPrompt}
onChange={handleChange}
multiline
maxRows={10}
helperText={i18n("system_prompt_helper")}
/>
) : (
<>
<TextField
size="small"
label={"SYSTEM PROMPT"}
name="nobatchPrompt"
value={nobatchPrompt}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
size="small"
label={"USER PROMPT"}
name="nobatchUserPrompt"
value={nobatchUserPrompt}
onChange={handleChange}
multiline
maxRows={10}
/>
</>
)}
<TextField
size="small"
label={"SUBTITLE PROMPT"}
@@ -354,7 +372,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
</>
)}
{apiType === OPT_TRANS_OLLAMA && (
{/* {apiType === OPT_TRANS_OLLAMA && (
<>
<TextField
select
@@ -375,7 +393,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange}
/>
</>
)}
)} */}
{apiType === OPT_TRANS_NIUTRANS && (
<>
@@ -775,7 +793,7 @@ export default function Apis() {
const apiTypes = useMemo(
() =>
OPT_ALL_TYPES.map((type) => ({
OPT_ALL_TRANS_TYPES.map((type) => ({
type,
label: type,
})),

View File

@@ -40,7 +40,7 @@ export default function Layout() {
/>
</Box>
<Box component="main" sx={{ flex: 1, p: 2 }}>
<Box component="main" sx={{ flex: 1, p: 2, width: "100%" }}>
<Outlet />
</Box>
</Box>

View File

@@ -108,7 +108,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
parentStyle = "",
grandStyle = "",
injectJs = "",
injectCss = "",
// injectCss = "",
apiSlug,
fromLang,
toLang,
@@ -459,6 +459,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
type="number"
name="splitLength"
value={splitLength}
disabled={disabled}
onChange={handleChange}
min={0}
max={1000}
@@ -694,7 +695,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
maxRows={10}
/> */}
<TextField
{/* <TextField
size="small"
label={i18n("inject_css")}
helperText={i18n("inject_css_helper")}
@@ -704,7 +705,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
onChange={handleChange}
maxRows={10}
multiline
/>
/> */}
<TextField
size="small"
label={i18n("inject_js")}

View File

@@ -94,7 +94,7 @@ export default function Settings() {
newlineLength = TRANS_NEWLINE_LENGTH,
httpTimeout = DEFAULT_HTTP_TIMEOUT,
contextMenuType = 1,
touchTranslate = 2,
touchModes = [2],
blacklist = DEFAULT_BLACKLIST.join(",\n"),
csplist = DEFAULT_CSPLIST.join(",\n"),
orilist = DEFAULT_ORILIST.join(",\n"),
@@ -105,6 +105,7 @@ export default function Settings() {
skipLangs = [],
// detectRemote = true,
transAllnow = false,
rootMargin = 500,
} = setting;
const { isHide = false, fabClickAction = 0 } = fab || {};
@@ -268,10 +269,13 @@ export default function Settings() {
select
fullWidth
size="small"
name="touchTranslate"
value={touchTranslate}
name="touchModes"
value={touchModes}
label={i18n("touch_translate_shortcut")}
onChange={handleChange}
SelectProps={{
multiple: true,
}}
>
{[0, 2, 3, 4, 5, 6, 7].map((item) => (
<MenuItem key={item} value={item}>
@@ -295,34 +299,6 @@ export default function Settings() {
<MenuItem value={2}>{i18n("secondary_context_menus")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="transAllnow"
value={transAllnow}
label={i18n("trigger_mode")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("mk_pagescroll")}</MenuItem>
<MenuItem value={true}>{i18n("mk_pageopen")}</MenuItem>
</TextField>
</Grid>
{/* <Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="detectRemote"
value={detectRemote}
label={i18n("detect_lang_remote")}
onChange={handleChange}
>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
</TextField>
</Grid> */}
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
@@ -341,6 +317,47 @@ export default function Settings() {
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="transAllnow"
value={transAllnow}
label={i18n("trigger_mode")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("mk_pagescroll")}</MenuItem>
<MenuItem value={true}>{i18n("mk_pageopen")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<ValidationInput
fullWidth
size="small"
label={i18n("pagescroll_root_margin")}
type="number"
name="rootMargin"
value={rootMargin}
onChange={handleChange}
min={0}
max={10000}
/>
</Grid>
{/* <Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="detectRemote"
value={detectRemote}
label={i18n("detect_lang_remote")}
onChange={handleChange}
>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
</TextField>
</Grid> */}
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select

View File

@@ -32,6 +32,7 @@ export default function SubtitleSetting() {
chunkLength,
toLang,
isBilingual,
skipAd = false,
windowStyle,
originStyle,
translationStyle,
@@ -145,6 +146,20 @@ export default function SubtitleSetting() {
<MenuItem value={false}>{i18n("disable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
fullWidth
select
size="small"
name="skipAd"
value={skipAd}
label={i18n("is_skip_ad")}
onChange={handleChange}
>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
</TextField>
</Grid>
</Grid>
</Box>

View File

@@ -68,6 +68,7 @@ export default function Tranbox() {
hideClickAway = false,
simpleStyle = false,
followSelection = false,
autoHeight = false,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
// extStyles = "",
enDict = OPT_DICT_BING,
@@ -330,6 +331,20 @@ export default function Tranbox() {
max={200}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
fullWidth
select
size="small"
name="autoHeight"
value={autoHeight}
label={i18n("tranbox_auto_height")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
{!isExt && (
<Grid item xs={12} sm={12} md={6} lg={3}>
<ShortcutInput

View File

@@ -150,6 +150,7 @@ export default function DraggableResizable({
setPosition,
onChangeSize,
onChangePosition,
autoHeight,
...props
}) {
const lineWidth = 4;
@@ -222,11 +223,19 @@ export default function DraggableResizable({
</Pointer>
<Box
className="KT-draggable-container"
style={{
width: size.w,
height: size.h,
overflow: "hidden auto",
}}
style={
autoHeight
? {
width: size.w,
maxHeight: size.h,
overflow: "hidden auto",
}
: {
width: size.w,
height: size.h,
overflow: "hidden auto",
}
}
>
{children}
</Box>

View File

@@ -115,7 +115,15 @@ export default function TranBox({
text,
setText,
setShowBox,
tranboxSetting: { enDict, enSug, apiSlugs, fromLang, toLang, toLang2 },
tranboxSetting: {
enDict,
enSug,
apiSlugs,
fromLang,
toLang,
toLang2,
autoHeight,
},
transApis,
boxSize,
setBoxSize,
@@ -141,6 +149,7 @@ export default function TranBox({
size={boxSize}
setSize={setBoxSize}
setPosition={setBoxPosition}
autoHeight={autoHeight}
header={
<Header
setShowBox={setShowBox}

View File

@@ -38,7 +38,7 @@ export default function TranCont({
setTrText("");
setError("");
const [trText] = await apiTranslate({
const { trText } = await apiTranslate({
text,
fromLang,
toLang,

View File

@@ -15,6 +15,7 @@ import {
import { isMobile } from "../../libs/mobile";
import { kissLog } from "../../libs/log";
import { useLangMap } from "../../hooks/I18n";
import { debouncePutTranBox, getTranBox } from "../../libs/storage";
export default function Slection({
contextMenuType,
@@ -107,6 +108,29 @@ export default function Slection({
return "onMouseUp";
}, [triggerMode]);
useEffect(() => {
(async () => {
try {
const { w, h, x, y } = (await getTranBox()) || {};
if (w !== undefined && h !== undefined) {
setBoxSize({ w, h });
}
if (x !== undefined && y !== undefined) {
setBoxPosition({
x: limitNumber(x, 0, window.innerWidth),
y: limitNumber(y, 0, window.innerHeight),
});
}
} catch (err) {
//
}
})();
}, []);
useEffect(() => {
debouncePutTranBox({ ...boxSize, ...boxPosition });
}, [boxSize, boxPosition]);
useEffect(() => {
async function handleMouseup(e) {
e.stopPropagation();