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=KISS Translator
REACT_APP_NAME_CN=简约翻译 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 REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator

View File

@@ -1,40 +1,5 @@
# KISS Translator # 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) English | [简体中文](README.md)
A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator). 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] Tencent/Volcengine
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter - [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
- [x] DeepL/DeepLX/NiuTrans - [x] DeepL/DeepLX/NiuTrans
- [x] BuiltinAI/AzureAI/CloudflareAI - [x] AzureAI / CloudflareAI
- [x] Custom translation interface - [x] Chrome built-in AI translation (BuiltinAI)
- [x] Covers common translation scenarios - [x] Covers common translation scenarios
- [x] Web bilingual translation - [x] Webpage bilingual translation
- [x] Input box translation - [x] Input-box translation
- [x] Seletction translation - Instantly translate text in input fields into other languages via shortcut keys
- [x] Open the translation box on any page - [x] Text selection translation
- [x] Favorite Words - [x] Open translation popup on any page, support multiple translation services for comparison
- [x] Mouseover translation - [x] English dictionary lookup
- [x] Save vocabulary
- [x] Hover translation
- [x] YouTube subtitle translation - [x] YouTube subtitle translation
- [x] Support for various translation effects - Support translating video subtitles with any translation service and display bilingually
- [x] Customizable text recognition and full-text translation - Built-in basic subtitle merging and sentence-splitting algorithm to improve translation quality
- [x] Customizable translation styles - Supports AI-powered sentence segmentation for even better translation
- [x] Support for rich text translation and display - Custom subtitle style
- [x] Support for displaying only the translated text (hiding the original text) - [x] Supports diverse translation modes
- [x] Advanced translation API features - [x] Supports both automatic text recognition and manual rule modes
- [x] Aggregate and send translated texts in batches - Automatic text recognition mode allows most sites to be translated fully without writing rules
- [x] AI contextual conversation memory - Manual rule mode enables extreme optimization for specific sites
- [x] Customizable AI terminology dictionary - [x] Custom translation styling
- [x] AI-powered subtitle segmentation and translation - [x] Supports rich-text translation and rendering, preserving links and other text styles where possible
- [x] Customizable hooks and parameters - [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] Cross-client data synchronization
- [x] KISS-Workercloudflare/docker - [x] KISS-Workercloudflare/docker
- [x] WebDAV - [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. 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 ### 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) 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 ## Future Plans
This is a side project with no strict timeline. Community contributions are welcome. The following are preliminary feature directions: 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) | 简体中文 [English](README.en.md) | 简体中文
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。 一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
@@ -53,27 +22,35 @@
- [x] Tencent/Volcengine - [x] Tencent/Volcengine
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter - [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
- [x] DeepL/DeepLX/NiuTrans - [x] DeepL/DeepLX/NiuTrans
- [x] BuiltinAI/AzureAI/CloudflareAI - [x] AzureAI/CloudflareAI
- [x] 自定义翻译接口 - [x] Chrome浏览器内置AI翻译(BuiltinAI)
- [x] 覆盖常见翻译场景 - [x] 覆盖常见翻译场景
- [x] 网页双语对照翻译 - [x] 网页双语对照翻译
- [x] 输入框翻译 - [x] 输入框翻译
- 通过快捷键立即将输入框内文本翻译成其他语言
- [x] 划词翻译 - [x] 划词翻译
- [x] 任意页面打开翻译框 - [x] 任意页面打开翻译框,可用多种翻译服务对比翻译
- [x] 英文词典翻译
- [x] 收藏词汇 - [x] 收藏词汇
- [x] 鼠标悬停翻译 - [x] 鼠标悬停翻译
- [x] YouTube 字幕翻译 - [x] YouTube 字幕翻译
- 支持任意翻译服务对视频字幕进行翻译并双语显示
- 内置基础的字幕合并与断句算法,提升翻译效果
- 支持AI断句功能可进一步提升翻译质量
- 自定义字幕样式
- [x] 支持多样翻译效果 - [x] 支持多样翻译效果
- [x] 自定识别文本,全文翻译 - [x] 支持自动识别文本与手动规则两种模式
- 自动识别文本模式使得绝大部分网站无需编写规则也能翻译完整
- 手动规则模式,可以针对特定网站极致优化
- [x] 自定义译文样式 - [x] 自定义译文样式
- [x] 支持富文本翻译及显示 - [x] 支持富文本翻译及显示,能够尽量保留原文中的链接及其他文本样式
- [x] 支持仅显示译文(隐藏原文) - [x] 支持仅显示译文(隐藏原文)
- [x] 翻译接口高级功能 - [x] 翻译接口高级功能
- [x] 通过自定义接口,理论上支持任何翻译接口
- [x] 聚合批量发送翻译文本 - [x] 聚合批量发送翻译文本
- [x] AI上下文会话记忆 - [x] 支持AI上下文会话记忆功能,提升翻译效果
- [x] 自定义AI术语词典 - [x] 自定义AI术语词典
- [x] 字幕文本AI智能断句及翻译 - [x] 所有接口均支持Hook和自定义参数等高级功能
- [x] 自定义Hook自定义参数
- [x] 跨客户端数据同步 - [x] 跨客户端数据同步
- [x] KISS-Workercloudflare/docker - [x] KISS-Workercloudflare/docker
- [x] WebDAV - [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) 示例参考: [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", options: paths.appSrc + "/options.js",
background: paths.appSrc + "/background.js", background: paths.appSrc + "/background.js",
content: paths.appSrc + "/content.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"; 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", "name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script", "description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "2.0.3", "version": "2.0.5",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"private": true, "private": true,
"dependencies": { "dependencies": {

View File

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

View File

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

View File

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

View File

@@ -14,9 +14,9 @@ import {
MSG_BUILTINAI_TRANSLATE, MSG_BUILTINAI_TRANSLATE,
OPT_TRANS_BUILTINAI, OPT_TRANS_BUILTINAI,
URL_CACHE_SUBTITLE, URL_CACHE_SUBTITLE,
OPT_LANGS_TO_CODE,
} from "../config"; } from "../config";
import { sha256, withTimeout } from "../libs/utils"; import { sha256, withTimeout } from "../libs/utils";
import { kissLog } from "../libs/log";
import { import {
handleTranslate, handleTranslate,
handleSubtitle, handleSubtitle,
@@ -424,7 +424,7 @@ export const apiTranslate = async ({
usePool = true, usePool = true,
}) => { }) => {
if (!text) { if (!text) {
return ["", false]; throw new Error("The text cannot be empty.");
} }
const { apiType, apiSlug, useBatchFetch } = apiSetting; const { apiType, apiSlug, useBatchFetch } = apiSetting;
@@ -432,8 +432,7 @@ export const apiTranslate = async ({
const from = langMap.get(fromLang); const from = langMap.get(fromLang);
const to = langMap.get(toLang); const to = langMap.get(toLang);
if (!to) { if (!to) {
kissLog(`target lang: ${toLang} not support`); throw new Error(`The target lang: ${toLang} not support`);
return ["", false];
} }
// todo: 优化缓存失效因素 // todo: 优化缓存失效因素
@@ -451,7 +450,7 @@ export const apiTranslate = async ({
if (useCache) { if (useCache) {
const cache = await getHttpCachePolyfill(cacheInput); const cache = await getHttpCachePolyfill(cacheInput);
if (cache?.trText) { if (cache?.trText) {
return [cache.trText, cache.isSame]; return cache;
} }
} }
@@ -499,8 +498,12 @@ export const apiTranslate = async ({
let trText = ""; let trText = "";
let srLang = ""; let srLang = "";
let srCode = "";
if (Array.isArray(tranlation)) { if (Array.isArray(tranlation)) {
[trText, srLang = ""] = tranlation; [trText, srLang = ""] = tranlation;
if (srLang) {
srCode = OPT_LANGS_TO_CODE[apiType].get(srLang) || "";
}
} else if (typeof tranlation === "string") { } else if (typeof tranlation === "string") {
trText = tranlation; trText = tranlation;
} }
@@ -513,10 +516,10 @@ export const apiTranslate = async ({
// 插入缓存 // 插入缓存
if (useCache) { 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, API_SPE_TYPES,
INPUT_PLACE_FROM, INPUT_PLACE_FROM,
INPUT_PLACE_TO, INPUT_PLACE_TO,
// INPUT_PLACE_TEXT, INPUT_PLACE_TEXT,
INPUT_PLACE_KEY, INPUT_PLACE_KEY,
INPUT_PLACE_MODEL, INPUT_PLACE_MODEL,
DEFAULT_USER_AGENT, DEFAULT_USER_AGENT,
defaultSystemPrompt, defaultSystemPrompt,
defaultSubtitlePrompt, defaultSubtitlePrompt,
defaultNobatchPrompt,
defaultNobatchUserPrompt,
} from "../config"; } from "../config";
import { msAuth } from "../libs/auth"; import { msAuth } from "../libs/auth";
import { genDeeplFree } from "./deepl"; import { genDeeplFree } from "./deepl";
import { genBaidu } from "./baidu"; import { genBaidu } from "./baidu";
import interpreter from "../libs/interpreter"; import { interpreter } from "../libs/interpreter";
import { parseJsonObj, extractJson } from "../libs/utils"; import { parseJsonObj, extractJson } from "../libs/utils";
import { kissLog } from "../libs/log"; import { kissLog } from "../libs/log";
import { fetchData } from "../libs/fetch"; import { fetchData } from "../libs/fetch";
@@ -66,15 +68,17 @@ const genSystemPrompt = ({ systemPrompt, from, to }) =>
.replaceAll(INPUT_PLACE_TO, to); .replaceAll(INPUT_PLACE_TO, to);
const genUserPrompt = ({ const genUserPrompt = ({
// userPrompt, nobatchUserPrompt,
useBatchFetch,
tone, tone,
glossary = {}, glossary = {},
// from, from,
to, to,
texts, texts,
docInfo, docInfo,
}) => { }) => {
const prompt = JSON.stringify({ if (useBatchFetch) {
return JSON.stringify({
targetLanguage: to, targetLanguage: to,
title: docInfo.title, title: docInfo.title,
description: docInfo.description, description: docInfo.description,
@@ -82,22 +86,23 @@ const genUserPrompt = ({
glossary, glossary,
tone, tone,
}); });
}
// if (userPrompt.includes(INPUT_PLACE_TEXT)) { return nobatchUserPrompt
// return userPrompt .replaceAll(INPUT_PLACE_FROM, from)
// .replaceAll(INPUT_PLACE_FROM, from) .replaceAll(INPUT_PLACE_TO, to)
// .replaceAll(INPUT_PLACE_TO, to) .replaceAll(INPUT_PLACE_TEXT, texts[0]);
// .replaceAll(INPUT_PLACE_TEXT, prompt);
// }
return prompt;
}; };
const parseAIRes = (raw) => { const parseAIRes = (raw, useBatchFetch = true) => {
if (!raw) { if (!raw) {
return []; return [];
} }
if (!useBatchFetch) {
return [[raw]];
}
try { try {
const jsonString = extractJson(raw); const jsonString = extractJson(raw);
if (!jsonString) return []; if (!jsonString) return [];
@@ -497,7 +502,7 @@ const genOpenRouter = ({
}; };
const genOllama = ({ const genOllama = ({
think, // think,
url, url,
key, key,
systemPrompt, systemPrompt,
@@ -523,7 +528,7 @@ const genOllama = ({
], ],
temperature, temperature,
max_tokens: maxTokens, max_tokens: maxTokens,
think, // think,
stream: false, stream: false,
}; };
@@ -627,7 +632,10 @@ export const genTransReq = async ({ reqHook, ...args }) => {
apiSlug, apiSlug,
key, key,
systemPrompt, systemPrompt,
userPrompt, // userPrompt,
nobatchPrompt = defaultNobatchPrompt,
nobatchUserPrompt = defaultNobatchUserPrompt,
useBatchFetch,
from, from,
to, to,
texts, texts,
@@ -647,11 +655,16 @@ export const genTransReq = async ({ reqHook, ...args }) => {
} }
if (API_SPE_TYPES.ai.has(apiType)) { 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 args.userPrompt = !!events
? JSON.stringify(events) ? JSON.stringify(events)
: genUserPrompt({ : genUserPrompt({
userPrompt, nobatchUserPrompt,
useBatchFetch,
from, from,
to, to,
texts, texts,
@@ -717,10 +730,11 @@ export const parseTransRes = async (
toLang, toLang,
langMap, langMap,
resHook, resHook,
thinkIgnore, // thinkIgnore,
history, history,
userMsg, userMsg,
apiType, apiType,
useBatchFetch,
} }
) => { ) => {
// 执行 response hook // 执行 response hook
@@ -811,13 +825,13 @@ export const parseTransRes = async (
content: modelMsg.content, content: modelMsg.content,
}); });
} }
return parseAIRes(res?.choices?.[0]?.message?.content ?? ""); return parseAIRes(modelMsg?.content, useBatchFetch);
case OPT_TRANS_GEMINI: case OPT_TRANS_GEMINI:
modelMsg = res?.candidates?.[0]?.content; modelMsg = res?.candidates?.[0]?.content;
if (history && userMsg && modelMsg) { if (history && userMsg && modelMsg) {
history.add(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: case OPT_TRANS_CLAUDE:
modelMsg = { role: res?.role, content: res?.content?.text }; modelMsg = { role: res?.role, content: res?.content?.text };
if (history && userMsg && modelMsg) { if (history && userMsg && modelMsg) {
@@ -826,18 +840,18 @@ export const parseTransRes = async (
content: modelMsg.content, content: modelMsg.content,
}); });
} }
return parseAIRes(res?.content?.[0]?.text ?? ""); return parseAIRes(res?.content?.[0]?.text ?? "", useBatchFetch);
case OPT_TRANS_CLOUDFLAREAI: case OPT_TRANS_CLOUDFLAREAI:
return [[res?.result?.translated_text]]; return [[res?.result?.translated_text]];
case OPT_TRANS_OLLAMA: case OPT_TRANS_OLLAMA:
modelMsg = res?.choices?.[0]?.message; modelMsg = res?.choices?.[0]?.message;
const deepModels = thinkIgnore // const deepModels = thinkIgnore
.split(",") // .split(",")
.filter((model) => model?.trim()); // .filter((model) => model?.trim());
if (deepModels.some((model) => res?.model?.startsWith(model))) { // if (deepModels.some((model) => res?.model?.startsWith(model))) {
modelMsg?.content.replace(/<think>[\s\S]*<\/think>/i, ""); // modelMsg?.content.replace(/<think>[\s\S]*<\/think>/i, "");
} // }
if (history && userMsg && modelMsg) { if (history && userMsg && modelMsg) {
history.add(userMsg, { history.add(userMsg, {
@@ -845,9 +859,9 @@ export const parseTransRes = async (
content: modelMsg.content, content: modelMsg.content,
}); });
} }
return parseAIRes(modelMsg?.content); return parseAIRes(modelMsg?.content, useBatchFetch);
case OPT_TRANS_CUSTOMIZE: case OPT_TRANS_CUSTOMIZE:
return res?.map((item) => [item.text, item.src]); return (res?.translations ?? res)?.map((item) => [item.text, item.src]);
default: default:
} }

View File

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

View File

@@ -46,7 +46,7 @@ export const OPT_TRANS_OPENROUTER = "OpenRouter";
export const OPT_TRANS_CUSTOMIZE = "Custom"; export const OPT_TRANS_CUSTOMIZE = "Custom";
// 内置支持的翻译引擎 // 内置支持的翻译引擎
export const OPT_ALL_TYPES = [ export const OPT_ALL_TRANS_TYPES = [
OPT_TRANS_BUILTINAI, OPT_TRANS_BUILTINAI,
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_GOOGLE_2, OPT_TRANS_GOOGLE_2,
@@ -82,7 +82,7 @@ export const OPT_LANGDETECTOR_MAP = new Set(OPT_LANGDETECTOR_ALL);
// 翻译引擎特殊集合 // 翻译引擎特殊集合
export const API_SPE_TYPES = { export const API_SPE_TYPES = {
// 内置翻译 // 内置翻译
builtin: new Set(OPT_ALL_TYPES), builtin: new Set(OPT_ALL_TRANS_TYPES),
// 机器翻译 // 机器翻译
machine: new Set([ machine: new Set([
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
@@ -340,6 +340,9 @@ Object.entries(OPT_LANGS_TO_SPEC).forEach(([t, m]) => {
OPT_LANGS_TO_CODE[t] = specToCode(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. export const defaultSystemPrompt = `Act as a translation API. Output a single raw JSON object only. No extra text or fences.
Input: Input:
@@ -430,6 +433,8 @@ const defaultApi = {
model: "", // 模型名称 model: "", // 模型名称
systemPrompt: defaultSystemPrompt, systemPrompt: defaultSystemPrompt,
subtitlePrompt: defaultSubtitlePrompt, subtitlePrompt: defaultSubtitlePrompt,
nobatchPrompt: defaultNobatchPrompt,
nobatchUserPrompt: defaultNobatchUserPrompt,
userPrompt: "", userPrompt: "",
tone: BUILTIN_STONES[0], // 翻译风格 tone: BUILTIN_STONES[0], // 翻译风格
placeholder: BUILTIN_PLACEHOLDERS[0], // 占位符 placeholder: BUILTIN_PLACEHOLDERS[0], // 占位符
@@ -450,8 +455,8 @@ const defaultApi = {
contextSize: DEFAULT_CONTEXT_SIZE, // 智能上下文保留会话数 contextSize: DEFAULT_CONTEXT_SIZE, // 智能上下文保留会话数
temperature: 0.0, temperature: 0.0,
maxTokens: 20480, maxTokens: 20480,
think: false, // think: false, // (OpenAI 兼容接口未支持,暂时移除)
thinkIgnore: "qwen3,deepseek-r1", // thinkIgnore: "qwen3,deepseek-r1", // (OpenAI 兼容接口未支持,暂时移除)
isDisabled: false, // 是否不显示, isDisabled: false, // 是否不显示,
region: "", // Azure 专用 region: "", // Azure 专用
}; };
@@ -499,7 +504,6 @@ const defaultApiOpts = {
[OPT_TRANS_DEEPLX]: { [OPT_TRANS_DEEPLX]: {
...defaultApi, ...defaultApi,
url: "http://localhost:1188/translate", url: "http://localhost:1188/translate",
fetchLimit: 1,
}, },
[OPT_TRANS_NIUTRANS]: { [OPT_TRANS_NIUTRANS]: {
...defaultApi, ...defaultApi,
@@ -512,7 +516,6 @@ const defaultApiOpts = {
url: "https://api.openai.com/v1/chat/completions", url: "https://api.openai.com/v1/chat/completions",
model: "gpt-4", model: "gpt-4",
useBatchFetch: true, useBatchFetch: true,
fetchLimit: 1,
}, },
[OPT_TRANS_GEMINI]: { [OPT_TRANS_GEMINI]: {
...defaultApi, ...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], ...defaultApiOpts[apiType],
apiSlug: apiType, apiSlug: apiType,
apiName: 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_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`, zh_TW: `注入 JS`,
}, },
inject_js_helper: { inject_js_helper: {
zh: `初始化时注入运行,一个页面仅运行一次。`, zh: `预加载时注入,一个页面仅运行一次。内置全局对象 KT: {
en: `Injected and run at initialization, and only run once per page.`, apiTranslate,
zh_TW: `初始化時注入運行,一個頁面僅運行一次。`, 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: { inject_css: {
zh: `注入CSS`, zh: `注入CSS`,
@@ -1160,9 +1184,9 @@ export const I18N = {
zh_TW: `觸控設定`, zh_TW: `觸控設定`,
}, },
touch_translate_shortcut: { touch_translate_shortcut: {
zh: `触屏翻译快捷方式`, zh: `触屏翻译快捷方式 (支持多选)`,
en: `Touch Translate Shortcut`, en: `Touch Translate Shortcut (multiple supported)`,
zh_TW: `觸控翻譯捷徑`, zh_TW: `觸控翻譯捷徑 (支援多選)`,
}, },
touch_tap_0: { touch_tap_0: {
zh: `禁用`, zh: `禁用`,
@@ -1349,15 +1373,35 @@ export const I18N = {
en: `Transbox Follow Selection`, en: `Transbox Follow Selection`,
zh_TW: `翻譯框跟隨選取文字`, zh_TW: `翻譯框跟隨選取文字`,
}, },
tranbox_auto_height: {
zh: `翻译框自适应高度`,
en: `Translation box adaptive height`,
zh_TW: `翻譯框自適應高度`,
},
translate_start_hook: { translate_start_hook: {
zh: `翻译开始钩子函数`, zh: `翻译开始钩子函数`,
en: `Translate Start Hook`, en: `Translate Start Hook`,
zh_TW: `翻譯開始 Hook`, zh_TW: `翻譯開始 Hook`,
}, },
translate_start_hook_helper: { translate_start_hook_helper: {
zh: `翻译前时运行,入参为: ({hostNode, parentNode, nodes})`, zh: `翻译前时运行,入参为: {text,
en: `Run before translation, input parameters are: ({hostNode, parentNode, nodes})`, fromLang,
zh_TW: `翻譯前時運行,入參為: ({hostNode, parentNode, nodes})`, 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: { translate_end_hook: {
zh: `翻译完成钩子函数`, zh: `翻译完成钩子函数`,
@@ -1604,6 +1648,11 @@ export const I18N = {
en: `Enable bilingual display`, en: `Enable bilingual display`,
zh_TW: `雙語顯示`, zh_TW: `雙語顯示`,
}, },
is_skip_ad: {
zh: `是否快进广告`,
en: `Should I fast forward to the ad?`,
zh_TW: `是否快轉廣告`,
},
background_styles: { background_styles: {
zh: `背景样式`, zh: `背景样式`,
en: `DBackground Style`, en: `DBackground Style`,
@@ -1735,6 +1784,11 @@ export const I18N = {
en: `Highlight after translation`, en: `Highlight after translation`,
zh_TW: `翻譯後高亮`, 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] || ""; export const newI18n = (lang) => (key) => I18N[key]?.[lang] || "";

View File

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

View File

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

View File

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

View File

@@ -57,9 +57,9 @@ export function useRules() {
const put = useCallback( const put = useCallback(
(pattern, obj) => { (pattern, obj) => {
save((prev) => { save((prev) => {
if (pattern !== obj.pattern) { // if (pattern !== obj.pattern) {
return prev; // return prev;
} // }
return prev.map((item) => return prev.map((item) =>
item.pattern === pattern ? { ...item, ...obj } : 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"; import ContentFab from "../views/Action/ContentFab";
export class FabManager extends ShadowDomManager { export class FabManager extends ShadowDomManager {
constructor({ translator, processActions, fabConfig }) { constructor({ processActions, fabConfig }) {
super({ super({
id: APP_CONSTS.fabID, id: APP_CONSTS.fabID,
className: "notranslate", className: "notranslate",
reactComponent: ContentFab, reactComponent: ContentFab,
props: { translator, processActions, fabConfig }, props: { processActions, fabConfig },
}); });
if (!fabConfig?.isHide) { if (!fabConfig?.isHide) {

View File

@@ -41,6 +41,12 @@ export const fetchGM = async (
}); });
}, },
onerror: reject, 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); (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 // Function to inject external JavaScript file
export const injectExternalJs = (src, id = "kiss-translator-external-js") => { export const injectExternalJs = (src, id = "kiss-translator-external-js") => {
if (document.getElementById(id)) { if (document.getElementById(id)) {

View File

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

View File

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

View File

@@ -60,7 +60,7 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
"parentStyle", "parentStyle",
"grandStyle", "grandStyle",
"injectJs", "injectJs",
"injectCss", // "injectCss",
// "fixerSelector", // "fixerSelector",
"transStartHook", "transStartHook",
"transEndHook", "transEndHook",
@@ -154,7 +154,7 @@ export const checkRules = (rules) => {
parentStyle, parentStyle,
grandStyle, grandStyle,
injectJs, injectJs,
injectCss, // injectCss,
apiSlug, apiSlug,
fromLang, fromLang,
toLang, toLang,
@@ -193,7 +193,7 @@ export const checkRules = (rules) => {
parentStyle: type(parentStyle) === "string" ? parentStyle : "", parentStyle: type(parentStyle) === "string" ? parentStyle : "",
grandStyle: type(grandStyle) === "string" ? grandStyle : "", grandStyle: type(grandStyle) === "string" ? grandStyle : "",
injectJs: type(injectJs) === "string" ? injectJs : "", injectJs: type(injectJs) === "string" ? injectJs : "",
injectCss: type(injectCss) === "string" ? injectCss : "", // injectCss: type(injectCss) === "string" ? injectCss : "",
bgColor: type(bgColor) === "string" ? bgColor : "", bgColor: type(bgColor) === "string" ? bgColor : "",
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "", textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
apiSlug: 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 targetKeySet = new Set(targetKeys);
const onKeyDown = (pressedKeys, event) => { const onKeyDown = (pressedKeys, event) => {
if (isSameSet(targetKeySet, pressedKeys)) { if (isSameSet(targetKeySet, pressedKeys)) {
// event.preventDefault(); // event.preventDefault(); // 阻止浏览器的默认行为
event.stopPropagation(); // event.stopPropagation(); // 阻止事件继续(向父元素)冒泡
fn(); fn();
} }
}; };

View File

@@ -5,6 +5,7 @@ import {
STOKEY_RULES_OLD, STOKEY_RULES_OLD,
STOKEY_WORDS, STOKEY_WORDS,
STOKEY_FAB, STOKEY_FAB,
STOKEY_TRANBOX,
STOKEY_SYNC, STOKEY_SYNC,
STOKEY_MSAUTH, STOKEY_MSAUTH,
STOKEY_BDAUTH, STOKEY_BDAUTH,
@@ -135,6 +136,13 @@ export const getFabWithDefault = async () => (await getFab()) || {};
export const setFab = (obj) => setObj(STOKEY_FAB, obj); export const setFab = (obj) => setObj(STOKEY_FAB, obj);
export const putFab = (obj) => putObj(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]: ` [OPT_STYLE_DASHBOX]: `
border: 2px dashed ${bgColor || DEFAULT_COLOR}; border: 2px dashed ${bgColor || DEFAULT_COLOR};
display: inline-block; display: block;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
box-sizing: border-box; box-sizing: border-box;
`, `,

View File

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

View File

@@ -28,7 +28,7 @@ import { logger } from "./log";
export default class TranslatorManager { export default class TranslatorManager {
#clearShortcuts = []; #clearShortcuts = [];
#menuCommandIds = []; #menuCommandIds = [];
#clearTouchListener = null; #clearTouchListeners = [];
#isActive = false; #isActive = false;
#isUserscript; #isUserscript;
#isIframe; #isIframe;
@@ -54,15 +54,15 @@ export default class TranslatorManager {
isIframe, isIframe,
}); });
if (!isIframe) {
this._transboxManager = new TransboxManager(setting); this._transboxManager = new TransboxManager(setting);
if (!isIframe) {
this._inputTranslator = new InputTranslator(setting); this._inputTranslator = new InputTranslator(setting);
this._popupManager = new PopupManager({ this._popupManager = new PopupManager({
translator: this._translator, translator: this._translator,
processActions: this.#processActions.bind(this), processActions: this.#processActions.bind(this),
}); });
this._fabManager = new FabManager({ this._fabManager = new FabManager({
translator: this._translator,
processActions: this.#processActions.bind(this), processActions: this.#processActions.bind(this),
fabConfig, fabConfig,
}); });
@@ -110,10 +110,8 @@ export default class TranslatorManager {
this.#clearShortcuts = []; this.#clearShortcuts = [];
// 触屏 // 触屏
if (this.#clearTouchListener) { this.#clearTouchListeners.forEach((clear) => clear());
this.#clearTouchListener(); this.#clearTouchListeners = [];
this.#clearTouchListener = null;
}
// 油猴菜单 // 油猴菜单
if (globalThis.GM && this.#menuCommandIds.length > 0) { if (globalThis.GM && this.#menuCommandIds.length > 0) {
@@ -139,14 +137,17 @@ export default class TranslatorManager {
window.addEventListener("message", this.#windowMessageHandler); window.addEventListener("message", this.#windowMessageHandler);
} else { } else {
browser.runtime.onMessage.addListener(this.#browserMessageHandler); browser.runtime.onMessage.addListener(this.#browserMessageHandler);
if (this.#isIframe) {
window.addEventListener("message", this.#windowMessageHandler);
}
} }
} }
#setupTouchOperations() { #setupTouchOperations() {
if (this.#isIframe) return; if (this.#isIframe) return;
const { touchTranslate = 2 } = this._translator.setting; const { touchModes = [2] } = this._translator.setting;
if (touchTranslate === 0) { if (touchModes.length === 0) {
return; return;
} }
@@ -154,35 +155,31 @@ export default class TranslatorManager {
this.#processActions({ action: MSG_TRANS_TOGGLE }); this.#processActions({ action: MSG_TRANS_TOGGLE });
}; };
switch (touchTranslate) { const handleListener = (mode) => {
let options = null;
switch (mode) {
case 2: case 2:
case 3: case 3:
case 4: case 4:
this.#clearTouchListener = touchTapListener(handleTap, { options = { taps: 1, fingers: mode };
taps: 1,
fingers: touchTranslate,
});
break; break;
case 5: case 5:
this.#clearTouchListener = touchTapListener(handleTap, { options = { taps: 2, fingers: 1 };
taps: 2,
fingers: 1,
});
break; break;
case 6: case 6:
this.#clearTouchListener = touchTapListener(handleTap, { options = { taps: 3, fingers: 1 };
taps: 3,
fingers: 1,
});
break; break;
case 7: case 7:
this.#clearTouchListener = touchTapListener(handleTap, { options = { taps: 2, fingers: 2 };
taps: 2,
fingers: 2,
});
break; break;
default: default:
} }
if (options) {
this.#clearTouchListeners.push(touchTapListener(handleTap, options));
}
};
touchModes.forEach((mode) => handleListener(mode));
} }
#handleWindowMessage(event) { #handleWindowMessage(event) {
@@ -190,7 +187,7 @@ export default class TranslatorManager {
} }
#handleBrowserMessage(message, sender, sendResponse) { #handleBrowserMessage(message, sender, sendResponse) {
const result = this.#processActions(message); const result = this.#processActions(message, true);
const response = result || { const response = result || {
rule: this._translator.rule, rule: this._translator.rule,
setting: this._translator.setting, setting: this._translator.setting,
@@ -248,9 +245,9 @@ export default class TranslatorManager {
]; ];
} }
#processActions({ action, args } = {}) { #processActions({ action, args } = {}, fromExt = false) {
if (this.#isUserscript) { if (!fromExt) {
sendIframeMsg(action); sendIframeMsg(action, args);
} }
switch (action) { 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 { logger } from "../libs/log.js";
import { truncateWords } from "../libs/utils.js"; import { truncateWords } from "../libs/utils.js";
import { apiTranslate } from "../apis/index.js";
/** /**
* @class BilingualSubtitleManager * @class BilingualSubtitleManager
@@ -8,7 +9,6 @@ import { truncateWords } from "../libs/utils.js";
export class BilingualSubtitleManager { export class BilingualSubtitleManager {
#videoEl; #videoEl;
#formattedSubtitles = []; #formattedSubtitles = [];
#translationService;
#captionWindowEl = null; #captionWindowEl = null;
#paperEl = null; #paperEl = null;
#currentSubtitleIndex = -1; #currentSubtitleIndex = -1;
@@ -20,14 +20,12 @@ export class BilingualSubtitleManager {
* @param {object} options * @param {object} options
* @param {HTMLVideoElement} options.videoEl - 页面上的 video 元素。 * @param {HTMLVideoElement} options.videoEl - 页面上的 video 元素。
* @param {Array<object>} options.formattedSubtitles - 已格式化好的字幕数组。 * @param {Array<object>} options.formattedSubtitles - 已格式化好的字幕数组。
* @param {(text: string, toLang: string) => Promise<string>} options.translationService - 外部翻译函数。
* @param {object} options.setting - 配置对象,如目标翻译语言。 * @param {object} options.setting - 配置对象,如目标翻译语言。
*/ */
constructor({ videoEl, formattedSubtitles, translationService, setting }) { constructor({ videoEl, formattedSubtitles, setting }) {
this.#setting = setting; this.#setting = setting;
this.#videoEl = videoEl; this.#videoEl = videoEl;
this.#formattedSubtitles = formattedSubtitles; this.#formattedSubtitles = formattedSubtitles;
this.#translationService = translationService;
this.onTimeUpdate = this.onTimeUpdate.bind(this); this.onTimeUpdate = this.onTimeUpdate.bind(this);
this.onSeek = this.onSeek.bind(this); this.onSeek = this.onSeek.bind(this);
@@ -128,15 +126,14 @@ export class BilingualSubtitleManager {
let initialBottom; let initialBottom;
let dragElementHeight; let dragElementHeight;
const onMouseDown = (e) => { const onDragStart = (e) => {
e.stopPropagation(); if (e.type === "mousedown" && e.button !== 0) return;
e.preventDefault();
if (e.button !== 0) return; e.preventDefault();
isDragging = true; isDragging = true;
handleElement.style.cursor = "grabbing"; handleElement.style.cursor = "grabbing";
startY = e.clientY; startY = e.type === "touchstart" ? e.touches[0].clientY : e.clientY;
initialBottom = initialBottom =
boundaryContainer.getBoundingClientRect().bottom - boundaryContainer.getBoundingClientRect().bottom -
@@ -144,17 +141,23 @@ export class BilingualSubtitleManager {
dragElementHeight = dragElement.offsetHeight; dragElementHeight = dragElement.offsetHeight;
document.addEventListener("mousemove", onMouseMove, { capture: true }); document.addEventListener("mousemove", onDragMove, { capture: true });
document.addEventListener("mouseup", onMouseUp, { 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; if (!isDragging) return;
e.preventDefault(); 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; let newBottom = initialBottom - deltaY;
const containerHeight = boundaryContainer.clientHeight; const containerHeight = boundaryContainer.clientHeight;
@@ -167,17 +170,18 @@ export class BilingualSubtitleManager {
dragElement.style.bottom = `${newBottom}px`; dragElement.style.bottom = `${newBottom}px`;
}; };
const onMouseUp = (e) => { const onDragEnd = (e) => {
if (!isDragging) return; if (!isDragging) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation();
isDragging = false; isDragging = false;
handleElement.style.cursor = "grab"; handleElement.style.cursor = "grab";
document.removeEventListener("mousemove", onMouseMove, { capture: true }); document.removeEventListener("mousemove", onDragMove, { capture: true });
document.removeEventListener("mouseup", onMouseUp, { 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; const finalBottomPx = dragElement.style.bottom;
setTimeout(() => { setTimeout(() => {
@@ -185,7 +189,10 @@ export class BilingualSubtitleManager {
}, 50); }, 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; subtitle.isTranslating = true;
try { try {
const { fromLang, toLang, apiSetting } = this.#setting; const { fromLang, toLang, apiSetting } = this.#setting;
const [translatedText] = await this.#translationService({ const { trText } = await apiTranslate({
text: subtitle.text, text: subtitle.text,
fromLang, fromLang,
toLang, toLang,
apiSetting, apiSetting,
}); });
subtitle.translation = translatedText; subtitle.translation = trText;
} catch (error) { } catch (error) {
logger.info("Translation failed for:", subtitle.text, error); logger.info("Translation failed for:", subtitle.text, error);
subtitle.translation = "[Translation failed]"; 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 { logger } from "../libs/log.js";
import { apiSubtitle, apiTranslate } from "../apis/index.js"; import { apiSubtitle } from "../apis/index.js";
import { BilingualSubtitleManager } from "./BilingualSubtitleManager.js"; import { BilingualSubtitleManager } from "./BilingualSubtitleManager.js";
import { import {
MSG_XHR_DATA_YOUTUBE, MSG_XHR_DATA_YOUTUBE,
@@ -70,6 +70,8 @@ class YouTubeCaptionProvider {
} }
#moAds(adContainer) { #moAds(adContainer) {
const { skipAd = false } = this.#setting;
const adLayoutSelector = ".ytp-ad-player-overlay-layout"; const adLayoutSelector = ".ytp-ad-player-overlay-layout";
const skipBtnSelector = const skipBtnSelector =
".ytp-skip-ad-button, .ytp-ad-skip-button, .ytp-ad-skip-button-modern"; ".ytp-skip-ad-button, .ytp-ad-skip-button, .ytp-ad-skip-button-modern";
@@ -83,30 +85,32 @@ class YouTubeCaptionProvider {
if (node.matches(adLayoutSelector)) { if (node.matches(adLayoutSelector)) {
logger.debug("Youtube Provider: AD start playing!", node); logger.debug("Youtube Provider: AD start playing!", node);
// todo: 顺带把广告快速跳过 // todo: 顺带把广告快速跳过
if (videoEl) { if (videoEl && skipAd) {
videoEl.playbackRate = 16; videoEl.playbackRate = 16;
videoEl.currentTime = videoEl.duration; videoEl.currentTime = videoEl.duration;
} }
if (this.#managerInstance) { if (this.#managerInstance) {
this.#managerInstance.setIsAdPlaying(true); this.#managerInstance.setIsAdPlaying(true);
} }
} else if (node.matches(skipBtnSelector)) { } else if (node.matches(skipBtnSelector) && skipAd) {
logger.debug("Youtube Provider: AD skip button!", node); logger.debug("Youtube Provider: AD skip button!", node);
node.click(); node.click();
} }
if (skipAd) {
const skipBtn = node?.querySelector(skipBtnSelector); const skipBtn = node?.querySelector(skipBtnSelector);
if (skipBtn) { if (skipBtn) {
logger.debug("Youtube Provider: AD skip button!!", skipBtn); logger.debug("Youtube Provider: AD skip button!!", skipBtn);
skipBtn.click(); skipBtn.click();
} }
}
}); });
mutation.removedNodes.forEach((node) => { mutation.removedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return; if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches(adLayoutSelector)) { if (node.matches(adLayoutSelector)) {
logger.debug("Youtube Provider: Ad ends!"); logger.debug("Youtube Provider: Ad ends!");
if (videoEl) { if (videoEl && skipAd) {
videoEl.playbackRate = 1; videoEl.playbackRate = 1;
} }
if (this.#managerInstance) { if (this.#managerInstance) {
@@ -161,14 +165,13 @@ class YouTubeCaptionProvider {
this.#ytControls = ytControls; this.#ytControls = ytControls;
const kissControls = document.createElement("div"); const kissControls = document.createElement("div");
kissControls.className = "kiss-bilingual-subtitle-controls"; kissControls.className = "notranslate kiss-subtitle-controls";
Object.assign(kissControls.style, { Object.assign(kissControls.style, {
height: "100%", height: "100%",
}); });
const toggleButton = document.createElement("button"); const toggleButton = document.createElement("button");
toggleButton.className = toggleButton.className = "ytp-button kiss-subtitle-button";
"ytp-button notranslate kiss-bilingual-subtitle-button";
toggleButton.title = APP_NAME; toggleButton.title = APP_NAME;
Object.assign(toggleButton.style, { Object.assign(toggleButton.style, {
color: "white", color: "white",
@@ -505,7 +508,6 @@ class YouTubeCaptionProvider {
this.#managerInstance = new BilingualSubtitleManager({ this.#managerInstance = new BilingualSubtitleManager({
videoEl, videoEl,
formattedSubtitles: this.#subtitles, formattedSubtitles: this.#subtitles,
translationService: apiTranslate,
setting: { ...this.#setting, fromLang: this.#fromLang }, setting: { ...this.#setting, fromLang: this.#fromLang },
}); });
this.#managerInstance.start(); this.#managerInstance.start();
@@ -590,7 +592,7 @@ class YouTubeCaptionProvider {
return subtitles; return subtitles;
} }
#isQualityPoor(lines, lengthThreshold = 250, percentageThreshold = 0.1) { #isQualityPoor(lines, lengthThreshold = 250, percentageThreshold = 0.2) {
if (lines.length === 0) return false; if (lines.length === 0) return false;
const longLinesCount = lines.filter( const longLinesCount = lines.filter(
(line) => line.text.length > lengthThreshold (line) => line.text.length > lengthThreshold
@@ -913,7 +915,7 @@ class YouTubeCaptionProvider {
} }
} }
#showNotification(message, duration = 3000) { #showNotification(message, duration = 2000) {
if (!this.#notificationEl) this.#createNotificationElement(); if (!this.#notificationEl) this.#createNotificationElement();
this.#notificationEl.textContent = message; this.#notificationEl.textContent = message;
this.#notificationEl.style.opacity = "1"; this.#notificationEl.style.opacity = "1";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,7 @@ export default function SubtitleSetting() {
chunkLength, chunkLength,
toLang, toLang,
isBilingual, isBilingual,
skipAd = false,
windowStyle, windowStyle,
originStyle, originStyle,
translationStyle, translationStyle,
@@ -145,6 +146,20 @@ export default function SubtitleSetting() {
<MenuItem value={false}>{i18n("disable")}</MenuItem> <MenuItem value={false}>{i18n("disable")}</MenuItem>
</TextField> </TextField>
</Grid> </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> </Grid>
</Box> </Box>

View File

@@ -68,6 +68,7 @@ export default function Tranbox() {
hideClickAway = false, hideClickAway = false,
simpleStyle = false, simpleStyle = false,
followSelection = false, followSelection = false,
autoHeight = false,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK, triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
// extStyles = "", // extStyles = "",
enDict = OPT_DICT_BING, enDict = OPT_DICT_BING,
@@ -330,6 +331,20 @@ export default function Tranbox() {
max={200} max={200}
/> />
</Grid> </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 && ( {!isExt && (
<Grid item xs={12} sm={12} md={6} lg={3}> <Grid item xs={12} sm={12} md={6} lg={3}>
<ShortcutInput <ShortcutInput

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ import {
import { isMobile } from "../../libs/mobile"; import { isMobile } from "../../libs/mobile";
import { kissLog } from "../../libs/log"; import { kissLog } from "../../libs/log";
import { useLangMap } from "../../hooks/I18n"; import { useLangMap } from "../../hooks/I18n";
import { debouncePutTranBox, getTranBox } from "../../libs/storage";
export default function Slection({ export default function Slection({
contextMenuType, contextMenuType,
@@ -107,6 +108,29 @@ export default function Slection({
return "onMouseUp"; return "onMouseUp";
}, [triggerMode]); }, [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(() => { useEffect(() => {
async function handleMouseup(e) { async function handleMouseup(e) {
e.stopPropagation(); e.stopPropagation();