Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30c2cca2e1 | ||
|
|
af3241b773 | ||
|
|
eca0a63273 | ||
|
|
f15cdb38d6 | ||
|
|
5b8577aaa7 | ||
|
|
0a4a2b46c1 | ||
|
|
53d441b3f5 | ||
|
|
d4c346e40a | ||
|
|
2dcacf71e4 | ||
|
|
9ded6446a7 | ||
|
|
0a6f4a9f02 | ||
|
|
635e588bcc | ||
|
|
7343db78a5 | ||
|
|
ccd457c992 | ||
|
|
97676f114e | ||
|
|
e83c1eb017 | ||
|
|
e417c0106a | ||
|
|
3c09840d35 | ||
|
|
7361a94f8c | ||
|
|
a9bffe3913 | ||
|
|
5322555eba | ||
|
|
172dce2867 | ||
|
|
5735fee36e | ||
|
|
91642d8784 | ||
|
|
9d8f3f4211 | ||
|
|
66d39da80a | ||
|
|
fbd4a31a9c | ||
|
|
3d7e03ddaf | ||
|
|
1f213bf257 | ||
|
|
b38f079611 | ||
|
|
21e639cacd | ||
|
|
bdaf665b7c | ||
|
|
61a515c1d2 | ||
|
|
1b646df908 | ||
|
|
5550f939b2 | ||
|
|
b34fb5a600 | ||
|
|
c0dce5c0b1 | ||
|
|
d56bd2920f | ||
|
|
48ad100a64 | ||
|
|
ef07a172a9 | ||
|
|
f492d47719 | ||
|
|
ac8c07deb4 | ||
|
|
ca48ab639e | ||
|
|
7c5232c1a1 | ||
|
|
4fac7fdfe1 | ||
|
|
f7fc9560d5 | ||
|
|
f7ba744e7f | ||
|
|
315164f142 | ||
|
|
429cab5223 | ||
|
|
deecbc874b | ||
|
|
504f4cafa0 | ||
|
|
6d327d17af | ||
|
|
74290eb52b | ||
|
|
60b9653fd3 | ||
|
|
53e32d3031 | ||
|
|
ed279cf8a1 | ||
|
|
296b898bda | ||
|
|
2325155b1e | ||
|
|
b6ff4aae6a | ||
|
|
f04002fdb8 | ||
|
|
b8cb254f56 | ||
|
|
3983477904 | ||
|
|
f011f5ae38 | ||
|
|
18ecab18df | ||
|
|
793c481221 | ||
|
|
4fee3688ea | ||
|
|
b9693436bb | ||
|
|
6b18d8f934 | ||
|
|
65c325de9a | ||
|
|
8da5aaf259 | ||
|
|
00e8fdd3e6 | ||
|
|
b2eea5d0d7 | ||
|
|
9a8e24f590 | ||
|
|
32c6d45cb0 | ||
|
|
74ce6f2f1f | ||
|
|
573865cf10 | ||
|
|
56d4733e2a | ||
|
|
a8965a01e3 | ||
|
|
beef51ef38 |
2
.env
2
.env
@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
|
||||
|
||||
REACT_APP_NAME=KISS Translator
|
||||
REACT_APP_NAME_CN=简约翻译
|
||||
REACT_APP_VERSION=2.0.1
|
||||
REACT_APP_VERSION=2.0.5
|
||||
|
||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||
|
||||
|
||||
106
README.en.md
106
README.en.md
@@ -1,40 +1,5 @@
|
||||
# KISS Translator
|
||||
|
||||
**New Version Preview:**
|
||||
|
||||
After a period of intermittent development, the planned features for the new version are essentially complete. The main new features are as follows:
|
||||
|
||||
* **Core Translation Logic Refactoring:**
|
||||
* Supports both automatic text detection and manual selection modes.
|
||||
* The automatic text detection mode enables complete translation for the vast majority of websites without the need to write specific rules.
|
||||
* The previous manual rule mode has been retained for meticulous optimization on specific websites.
|
||||
* Supports rich text translation, preserving links and other text styles from the original content as much as possible.
|
||||
* Optimize the display effect of showing only translated text (hiding original text).
|
||||
|
||||
* **API Refactoring:**
|
||||
* Supports adding and deleting an arbitrary number of APIs.
|
||||
* Supports aggregating text for sending, reducing the number of calls to the translation API and improving performance.
|
||||
* Supports the built-in Chrome AI translation API, enabling AI-powered translation without an internet connection.
|
||||
* Supports AI contextual conversation memory to enhance translation quality.
|
||||
* All APIs support advanced features such as hooks and custom parameters.
|
||||
* Added support for Azure AI translation interface.
|
||||
|
||||
* **Optimized YouTube Subtitle Support:**
|
||||
* Supports translating video subtitles with any translation service and displaying them bilingually.
|
||||
* Includes a built-in basic algorithm for subtitle merging and sentence splitting to improve translation results.
|
||||
* Supports an AI-powered sentence splitting function to further enhance translation quality.
|
||||
|
||||
* **English Dictionary Redundancy:**
|
||||
* Added Bing and Youdao dictionaries.
|
||||
* Fixed the vocabulary collection feature.
|
||||
|
||||
* **User Experience Optimization:**
|
||||
* The pop-up translation box for selected text now supports simultaneous translation by multiple services.
|
||||
* The translation control panel has been updated with many new quick-toggle functions.
|
||||
* Added a Playground page for convenient API debugging.
|
||||
|
||||
**Note:** Due to extensive refactoring, the configuration file for the new version is not backward compatible with the old version. Therefore, please back up your data manually before upgrading. Furthermore, **do not import old configuration files after upgrading to the new version.**
|
||||
|
||||
English | [简体中文](README.md)
|
||||
|
||||
A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
|
||||
@@ -57,27 +22,35 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
|
||||
- [x] Tencent/Volcengine
|
||||
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
|
||||
- [x] DeepL/DeepLX/NiuTrans
|
||||
- [x] BuiltinAI/AzureAI/CloudflareAI
|
||||
- [x] Custom translation interface
|
||||
- [x] AzureAI / CloudflareAI
|
||||
- [x] Chrome built-in AI translation (BuiltinAI)
|
||||
- [x] Covers common translation scenarios
|
||||
- [x] Web bilingual translation
|
||||
- [x] Input box translation
|
||||
- [x] Seletction translation
|
||||
- [x] Open the translation box on any page
|
||||
- [x] Favorite Words
|
||||
- [x] Mouseover translation
|
||||
- [x] Webpage bilingual translation
|
||||
- [x] Input-box translation
|
||||
- Instantly translate text in input fields into other languages via shortcut keys
|
||||
- [x] Text selection translation
|
||||
- [x] Open translation popup on any page, support multiple translation services for comparison
|
||||
- [x] English dictionary lookup
|
||||
- [x] Save vocabulary
|
||||
- [x] Hover translation
|
||||
- [x] YouTube subtitle translation
|
||||
- [x] Support for various translation effects
|
||||
- [x] Customizable text recognition and full-text translation
|
||||
- [x] Customizable translation styles
|
||||
- [x] Support for rich text translation and display
|
||||
- [x] Support for displaying only the translated text (hiding the original text)
|
||||
- [x] Advanced translation API features
|
||||
- [x] Aggregate and send translated texts in batches
|
||||
- [x] AI contextual conversation memory
|
||||
- [x] Customizable AI terminology dictionary
|
||||
- [x] AI-powered subtitle segmentation and translation
|
||||
- [x] Customizable hooks and parameters
|
||||
- Support translating video subtitles with any translation service and display bilingually
|
||||
- Built-in basic subtitle merging and sentence-splitting algorithm to improve translation quality
|
||||
- Supports AI-powered sentence segmentation for even better translation
|
||||
- Custom subtitle style
|
||||
- [x] Supports diverse translation modes
|
||||
- [x] Supports both automatic text recognition and manual rule modes
|
||||
- Automatic text recognition mode allows most sites to be translated fully without writing rules
|
||||
- Manual rule mode enables extreme optimization for specific sites
|
||||
- [x] Custom translation styling
|
||||
- [x] Supports rich-text translation and rendering, preserving links and other text styles where possible
|
||||
- [x] Option to show only translation (hide original text)
|
||||
- [x] Advanced translation API features
|
||||
- [x] With custom API support, theoretically works with any translation service
|
||||
- [x] Batch aggregation of translation requests
|
||||
- [x] Supports AI conversation context memory to improve translation quality
|
||||
- [x] Custom AI terminology dictionary
|
||||
- [x] All APIs support hooks and custom parameters for advanced usage
|
||||
- [x] Cross-client data synchronization
|
||||
- [x] KISS-Worker(cloudflare/docker)
|
||||
- [x] WebDAV
|
||||
@@ -139,14 +112,35 @@ Personal Rules > Subscription Rules > Global Rules
|
||||
|
||||
Among these, Global Rules have the lowest priority but are very important as they serve as the default rules.
|
||||
|
||||
### Local Ollama interface cannot be used
|
||||
### API (Ollama, etc.) Test Failure
|
||||
|
||||
If encountering a 403 error, refer to: https://github.com/fishjar/kiss-translator/issues/174
|
||||
Common reasons for API test failures include:
|
||||
|
||||
- Incorrect address:
|
||||
- For example, `Ollama` has a native API address and an `Openai`-compatible address. This plugin currently supports the `Openai`-compatible address and does not support the `Ollama` native API address.
|
||||
- Some AI models do not support batch translation:
|
||||
- In this case, you can choose to disable batch translation or use a custom API.
|
||||
- Alternatively, you can use a custom API. For details, please refer to: [Custom API Example Documentation](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
- Some AI models have inconsistent parameters:
|
||||
- For example, the parameters of the `Gemini` native API are highly inconsistent. Some model versions do not support certain parameters, leading to errors.
|
||||
- In this case, you can modify the request body using a `Hook`, or replace it with `Gemini2` (an OpenAI-compatible address).
|
||||
- The server restricts cross-origin access, returning a 403 error:
|
||||
- For example, `Ollama` requires adding the environment variable `OLLAMA_ORIGINS=*` when starting. See: https://github.com/fishjar/kiss-translator/issues/174
|
||||
|
||||
### Custom API doesn't work in Tampermonkey scripts
|
||||
|
||||
Tampermonkey scripts require adding domains to the whitelist; otherwise, requests cannot be sent.
|
||||
|
||||
### How to set up a hook function for a custom API
|
||||
|
||||
Custom APIs are very powerful and flexible, and can theoretically connect to any translation API.
|
||||
|
||||
Example reference: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
|
||||
### How to directly access the Tampermonkey script settings page
|
||||
|
||||
Settings page address: https://fishjar.github.io/kiss-translator/options.html
|
||||
|
||||
## Future Plans
|
||||
|
||||
This is a side project with no strict timeline. Community contributions are welcome. The following are preliminary feature directions:
|
||||
|
||||
80
README.md
80
README.md
@@ -1,36 +1,5 @@
|
||||
# 简约翻译
|
||||
|
||||
> **新版预告**:
|
||||
>
|
||||
> 经过一段时间断续开发,新版的预期功能已基本完成,主要引入的新特性如下:
|
||||
>
|
||||
> - 核心翻译逻辑重构:
|
||||
> - 支持自动识别文本与手动选择两种模式。
|
||||
> - 自动识别文本模式使得绝大部分网站无需编写规则也能翻译完整。
|
||||
> - 保留之前的手动规则模式,可以针对特定网站极致优化。
|
||||
> - 支持富文本翻译,能够尽量保留原文中的链接及其他文本样式。
|
||||
> - 优化仅显示译文(隐藏原文)显示效果。
|
||||
> - 接口重构:
|
||||
> - 支持添加、删除任意数量的接口。
|
||||
> - 支持聚合发送文本,减少翻译接口调用次数,提升性能。
|
||||
> - 支持chrome内置AI翻译接口,无需通过网络即可实现AI翻译。
|
||||
> - 支持AI上下文会话记忆功能,提升翻译效果。
|
||||
> - 所有接口均支持Hook和自定义参数等高级功能。
|
||||
> - 新增Azure AI翻译接口支持
|
||||
> - 优化 YouTube 字幕支持:
|
||||
> - 支持任意翻译服务对视频字幕进行翻译并双语显示。
|
||||
> - 内置基础的字幕合并与断句算法,提升翻译效果。
|
||||
> - 支持AI断句功能,可进一步提升翻译质量。
|
||||
> - 英文词典备灾:
|
||||
> - 新增bing、有道词典。
|
||||
> - 修复词汇收藏功能。
|
||||
> - 用户操作优化:
|
||||
> - 划词翻译框支持多种翻译服务同时翻译。
|
||||
> - 翻译控制面板新增许多快捷切换功能。
|
||||
> - 新增Playground页面,方便调试接口。
|
||||
>
|
||||
> 注意:由于经过大量重构,使得新版配置文件很难与旧版兼容,因此在升级前请手动备份相关数据。并且,**升级新版后,勿再导入旧版配置**。
|
||||
|
||||
[English](README.en.md) | 简体中文
|
||||
|
||||
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
||||
@@ -53,27 +22,35 @@
|
||||
- [x] Tencent/Volcengine
|
||||
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
|
||||
- [x] DeepL/DeepLX/NiuTrans
|
||||
- [x] BuiltinAI/AzureAI/CloudflareAI
|
||||
- [x] 自定义翻译接口
|
||||
- [x] AzureAI/CloudflareAI
|
||||
- [x] Chrome浏览器内置AI翻译(BuiltinAI)
|
||||
- [x] 覆盖常见翻译场景
|
||||
- [x] 网页双语对照翻译
|
||||
- [x] 输入框翻译
|
||||
- 通过快捷键立即将输入框内文本翻译成其他语言
|
||||
- [x] 划词翻译
|
||||
- [x] 任意页面打开翻译框
|
||||
- [x] 任意页面打开翻译框,可用多种翻译服务对比翻译
|
||||
- [x] 英文词典翻译
|
||||
- [x] 收藏词汇
|
||||
- [x] 鼠标悬停翻译
|
||||
- [x] YouTube 字幕翻译
|
||||
- 支持任意翻译服务对视频字幕进行翻译并双语显示
|
||||
- 内置基础的字幕合并与断句算法,提升翻译效果
|
||||
- 支持AI断句功能,可进一步提升翻译质量
|
||||
- 自定义字幕样式
|
||||
- [x] 支持多样翻译效果
|
||||
- [x] 自定识别文本,全文翻译
|
||||
- [x] 支持自动识别文本与手动规则两种模式
|
||||
- 自动识别文本模式使得绝大部分网站无需编写规则也能翻译完整
|
||||
- 手动规则模式,可以针对特定网站极致优化
|
||||
- [x] 自定义译文样式
|
||||
- [x] 支持富文本翻译及显示
|
||||
- [x] 支持富文本翻译及显示,能够尽量保留原文中的链接及其他文本样式
|
||||
- [x] 支持仅显示译文(隐藏原文)
|
||||
- [x] 翻译接口高级功能
|
||||
- [x] 通过自定义接口,理论上支持任何翻译接口
|
||||
- [x] 聚合批量发送翻译文本
|
||||
- [x] AI上下文会话记忆
|
||||
- [x] 支持AI上下文会话记忆功能,提升翻译效果
|
||||
- [x] 自定义AI术语词典
|
||||
- [x] 字幕文本AI智能断句及翻译
|
||||
- [x] 自定义Hook,自定义参数
|
||||
- [x] 所有接口均支持Hook和自定义参数等高级功能
|
||||
- [x] 跨客户端数据同步
|
||||
- [x] KISS-Worker(cloudflare/docker)
|
||||
- [x] WebDAV
|
||||
@@ -135,14 +112,35 @@
|
||||
|
||||
其中全局规则优先级最低,但非常重要,相当于兜底规则。
|
||||
|
||||
### 本地的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
|
||||
|
||||
### 填写的接口在油猴脚本不能使用
|
||||
|
||||
油猴脚本需要增加域名白名单,否则不能发出请求。
|
||||
|
||||
### 如何设置自定义接口的hook函数
|
||||
|
||||
自定义接口功能非常强大、灵活,理论可以接入任何翻译接口。
|
||||
|
||||
示例参考: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
|
||||
### 如何直接进入油猴脚本设置页面
|
||||
|
||||
设置页面地址: https://fishjar.github.io/kiss-translator/options.html
|
||||
|
||||
## 未来规划
|
||||
|
||||
本项目为业余开发,无严格时间表,欢迎社区共建。以下为初步设想的功能方向:
|
||||
|
||||
@@ -32,7 +32,8 @@ const extWebpack = (config, env) => {
|
||||
options: paths.appSrc + "/options.js",
|
||||
background: paths.appSrc + "/background.js",
|
||||
content: paths.appSrc + "/content.js",
|
||||
injector: paths.appSrc + "/injector.js",
|
||||
"injector-subtitle": paths.appSrc + "/injector-subtitle.js",
|
||||
"injector-shadowroot": paths.appSrc + "/injector-shadowroot.js",
|
||||
};
|
||||
|
||||
config.output.filename = "[name].js";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 自定义接口示例(本文档已过期,新版不再适用)
|
||||
|
||||
V2版的示例请查看这里:[custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
|
||||
以下示例为网友提供,仅供学习参考。
|
||||
|
||||
## 本地运行 Seed-X-PPO-7B 量化模型
|
||||
|
||||
295
custom-api_v2.md
Normal file
295
custom-api_v2.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# 自定义接口示例
|
||||
|
||||
## 默认接口规范
|
||||
|
||||
如果接口的请求数据和返回数据符合以下规范,
|
||||
则无需填写 `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" // 原文语言
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 谷歌翻译接口
|
||||
|
||||
> 此接口不支持聚合
|
||||
|
||||
URL
|
||||
|
||||
```
|
||||
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
async (args) => {
|
||||
const url = args.url.replace("{{text}}", args.texts[0]);
|
||||
const method = "GET";
|
||||
return { url, method };
|
||||
};
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
async ({ res }) => {
|
||||
return { translations: [[res?.sentences?.[0]?.trans || "", res?.src]] };
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## Ollama
|
||||
|
||||
> 此示例为支持聚合的模型类(要支持上下文,需进一步改动)
|
||||
|
||||
* 注意 ollama 启动参数需要添加环境变量 `OLLAMA_ORIGINS=*`
|
||||
* 检查环境变量生效命令:`systemctl show ollama | grep OLLAMA_ORIGINS`
|
||||
|
||||
URL
|
||||
|
||||
```
|
||||
http://localhost:11434/v1/chat/completions
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
async (args) => {
|
||||
const url = args.url;
|
||||
const method = "POST";
|
||||
const headers = { "Content-type": "application/json" };
|
||||
const body = {
|
||||
model: "gemma3",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
'Act as a translation API. Output a single raw JSON object only. No extra text or fences.\n\nInput:\n{"targetLanguage":"<lang>","title":"<context>","description":"<context>","segments":[{"id":1,"text":"..."}],"glossary":{"sourceTerm":"targetTerm"},"tone":"<formal|casual>"}\n\nOutput:\n{"translations":[{"id":1,"text":"...","sourceLanguage":"<detected>"}]}\n\nRules:\n1. Use title/description for context only; do not output them.\n2. Keep id, order, and count of segments.\n3. Preserve whitespace, HTML entities, and all HTML-like tags (e.g., <i1>, <a1>). Translate inner text only.\n4. Highest priority: Follow \'glossary\'. Use value for translation; if value is "", keep the key.\n5. Do not translate: content in <code>, <pre>, text enclosed in backticks, or placeholders like {1}, {{1}}, [1], [[1]].\n6. Apply the specified tone to the translation.\n7. Detect sourceLanguage for each segment.\n8. Return empty or unchanged inputs as is.\n\nExample:\nInput: {"targetLanguage":"zh-CN","segments":[{"id":1,"text":"A <b>React</b> component."}],"glossary":{"component":"组件","React":""}}\nOutput: {"translations":[{"id":1,"text":"一个<b>React</b>组件","sourceLanguage":"en"}]}\n\nFail-safe: On any error, return {"translations":[]}.',
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: JSON.stringify({
|
||||
targetLanguage: args.to,
|
||||
segments: args.texts.map((text, id) => ({ id, text })),
|
||||
glossary: {},
|
||||
}),
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 20480,
|
||||
think: false,
|
||||
stream: false,
|
||||
};
|
||||
|
||||
return { url, body, headers, method };
|
||||
};
|
||||
```
|
||||
|
||||
v2.0.2 Request Hook 可以简化为:
|
||||
|
||||
```js
|
||||
async (args) => {
|
||||
const url = args.url;
|
||||
const method = "POST";
|
||||
const headers = { "Content-type": "application/json" };
|
||||
const body = {
|
||||
model: "gemma3", // v2.0.2 版后此处可填 args.model
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: args.defaultSystemPrompt, // 或者 args.systemPrompt
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: JSON.stringify({
|
||||
targetLanguage: args.to,
|
||||
segments: args.texts.map((text, id) => ({ id, text })),
|
||||
glossary: {},
|
||||
}),
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 20480,
|
||||
think: false,
|
||||
stream: false,
|
||||
};
|
||||
|
||||
return { url, body, headers, method };
|
||||
};
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
async ({ res }) => {
|
||||
const extractJson = (raw) => {
|
||||
const jsonRegex = /({.*}|\[.*\])/s;
|
||||
const match = raw.match(jsonRegex);
|
||||
return match ? match[0] : null;
|
||||
};
|
||||
|
||||
const parseAIRes = (raw) => {
|
||||
if (!raw) return [];
|
||||
|
||||
try {
|
||||
const jsonString = extractJson(raw);
|
||||
if (!jsonString) return [];
|
||||
|
||||
const data = JSON.parse(jsonString);
|
||||
if (Array.isArray(data.translations)) {
|
||||
return data.translations.map((item) => [
|
||||
item?.text ?? "",
|
||||
item?.sourceLanguage ?? "",
|
||||
]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("parseAIRes", err);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const translations = parseAIRes(res?.choices?.[0]?.message?.content);
|
||||
|
||||
return { translations };
|
||||
};
|
||||
```
|
||||
|
||||
v2.0.2 版后内置`parseAIRes`函数,Response Hook 可以简化为:
|
||||
|
||||
```js
|
||||
async ({ res, parseAIRes }) => {
|
||||
const translations = parseAIRes(res?.choices?.[0]?.message?.content);
|
||||
return { translations };
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## 硅基流动
|
||||
|
||||
> 此示例为不支持聚合模型类,支持聚合的模型类参考上面 Ollama 的写法
|
||||
|
||||
URL
|
||||
|
||||
```
|
||||
https://api.siliconflow.cn/v1/chat/completions
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
async (args) => {
|
||||
const url = args.url;
|
||||
const method = "POST";
|
||||
const headers = {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${args.key}`,
|
||||
};
|
||||
const body = {
|
||||
model: "tencent/Hunyuan-MT-7B", // v2.0.2 版后此处可填 args.model
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"You are a professional, authentic machine translation engine.",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Translate the following source text to ${args.to}. Output translation directly without any additional text.\n\nSource Text: ${args.texts[0]}\n\nTranslated Text:`,
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 20480,
|
||||
};
|
||||
|
||||
return { url, body, headers, method };
|
||||
};
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
async ({ res }) => {
|
||||
return { translations: [[res?.choices?.[0]?.message?.content || ""]] };
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## 语言代码表及说明
|
||||
|
||||
Hook参数里面的语言含义说明:
|
||||
|
||||
- `toLang`, `fromLang` 是本插件支持的标准语言代码
|
||||
- `to`, `from` 是转换后的适用于特定接口的语言代码
|
||||
|
||||
如果你的自定义接口与下面的标准语言代码不匹配,需要自行映射转换。
|
||||
|
||||
```
|
||||
["en", "English - English"],
|
||||
["zh-CN", "Simplified Chinese - 简体中文"],
|
||||
["zh-TW", "Traditional Chinese - 繁體中文"],
|
||||
["ar", "Arabic - العربية"],
|
||||
["bg", "Bulgarian - Български"],
|
||||
["ca", "Catalan - Català"],
|
||||
["hr", "Croatian - Hrvatski"],
|
||||
["cs", "Czech - Čeština"],
|
||||
["da", "Danish - Dansk"],
|
||||
["nl", "Dutch - Nederlands"],
|
||||
["fi", "Finnish - Suomi"],
|
||||
["fr", "French - Français"],
|
||||
["de", "German - Deutsch"],
|
||||
["el", "Greek - Ελληνικά"],
|
||||
["hi", "Hindi - हिन्दी"],
|
||||
["hu", "Hungarian - Magyar"],
|
||||
["id", "Indonesian - Indonesia"],
|
||||
["it", "Italian - Italiano"],
|
||||
["ja", "Japanese - 日本語"],
|
||||
["ko", "Korean - 한국어"],
|
||||
["ms", "Malay - Melayu"],
|
||||
["mt", "Maltese - Malti"],
|
||||
["nb", "Norwegian - Norsk Bokmål"],
|
||||
["pl", "Polish - Polski"],
|
||||
["pt", "Portuguese - Português"],
|
||||
["ro", "Romanian - Română"],
|
||||
["ru", "Russian - Русский"],
|
||||
["sk", "Slovak - Slovenčina"],
|
||||
["sl", "Slovenian - Slovenščina"],
|
||||
["es", "Spanish - Español"],
|
||||
["sv", "Swedish - Svenska"],
|
||||
["ta", "Tamil - தமிழ்"],
|
||||
["te", "Telugu - తెలుగు"],
|
||||
["th", "Thai - ไทย"],
|
||||
["tr", "Turkish - Türkçe"],
|
||||
["uk", "Ukrainian - Українська"],
|
||||
["vi", "Vietnamese - Tiếng Việt"],
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kiss-translator",
|
||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.5",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.5",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -17,7 +17,8 @@
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"injector.js"
|
||||
"injector-subtitle.js",
|
||||
"injector-shadowroot.js"
|
||||
],
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.5",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -19,8 +19,12 @@
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["injector.js"],
|
||||
"resources": ["injector-subtitle.js"],
|
||||
"matches": ["https://www.youtube.com/*"]
|
||||
},
|
||||
{
|
||||
"resources": ["injector-shadowroot.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.5",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -23,7 +23,8 @@
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"injector.js"
|
||||
"injector-subtitle.js",
|
||||
"injector-shadowroot.js"
|
||||
],
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
MSG_BUILTINAI_TRANSLATE,
|
||||
OPT_TRANS_BUILTINAI,
|
||||
URL_CACHE_SUBTITLE,
|
||||
OPT_LANGS_TO_CODE,
|
||||
} from "../config";
|
||||
import { sha256, withTimeout } from "../libs/utils";
|
||||
import { kissLog } from "../libs/log";
|
||||
import {
|
||||
handleTranslate,
|
||||
handleSubtitle,
|
||||
@@ -106,7 +106,7 @@ export const apiMicrosoftLangdetect = async (text) => {
|
||||
|
||||
const key = `${URL_CACHE_DELANG}_${OPT_TRANS_MICROSOFT}`;
|
||||
const queue = getBatchQueue(key, handleMicrosoftLangdetect, {
|
||||
batchInterval: 500,
|
||||
batchInterval: 200,
|
||||
batchSize: 20,
|
||||
batchLength: 100000,
|
||||
});
|
||||
@@ -424,7 +424,7 @@ export const apiTranslate = async ({
|
||||
usePool = true,
|
||||
}) => {
|
||||
if (!text) {
|
||||
return ["", false];
|
||||
throw new Error("The text cannot be empty.");
|
||||
}
|
||||
|
||||
const { apiType, apiSlug, useBatchFetch } = apiSetting;
|
||||
@@ -432,8 +432,7 @@ export const apiTranslate = async ({
|
||||
const from = langMap.get(fromLang);
|
||||
const to = langMap.get(toLang);
|
||||
if (!to) {
|
||||
kissLog(`target lang: ${toLang} not support`);
|
||||
return ["", false];
|
||||
throw new Error(`The target lang: ${toLang} not support`);
|
||||
}
|
||||
|
||||
// todo: 优化缓存失效因素
|
||||
@@ -451,7 +450,7 @@ export const apiTranslate = async ({
|
||||
if (useCache) {
|
||||
const cache = await getHttpCachePolyfill(cacheInput);
|
||||
if (cache?.trText) {
|
||||
return [cache.trText, cache.isSame];
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,8 +498,12 @@ export const apiTranslate = async ({
|
||||
|
||||
let trText = "";
|
||||
let srLang = "";
|
||||
let srCode = "";
|
||||
if (Array.isArray(tranlation)) {
|
||||
[trText, srLang = ""] = tranlation;
|
||||
if (srLang) {
|
||||
srCode = OPT_LANGS_TO_CODE[apiType].get(srLang) || "";
|
||||
}
|
||||
} else if (typeof tranlation === "string") {
|
||||
trText = tranlation;
|
||||
}
|
||||
@@ -513,10 +516,10 @@ export const apiTranslate = async ({
|
||||
|
||||
// 插入缓存
|
||||
if (useCache) {
|
||||
putHttpCachePolyfill(cacheInput, null, { trText, isSame, srLang });
|
||||
putHttpCachePolyfill(cacheInput, null, { trText, isSame, srLang, srCode });
|
||||
}
|
||||
|
||||
return [trText, isSame];
|
||||
return { trText, srLang, srCode, isSame };
|
||||
};
|
||||
|
||||
// 字幕处理/翻译
|
||||
|
||||
@@ -22,15 +22,19 @@ import {
|
||||
API_SPE_TYPES,
|
||||
INPUT_PLACE_FROM,
|
||||
INPUT_PLACE_TO,
|
||||
// INPUT_PLACE_TEXT,
|
||||
INPUT_PLACE_TEXT,
|
||||
INPUT_PLACE_KEY,
|
||||
INPUT_PLACE_MODEL,
|
||||
DEFAULT_USER_AGENT,
|
||||
defaultSystemPrompt,
|
||||
defaultSubtitlePrompt,
|
||||
defaultNobatchPrompt,
|
||||
defaultNobatchUserPrompt,
|
||||
} from "../config";
|
||||
import { msAuth } from "../libs/auth";
|
||||
import { genDeeplFree } from "./deepl";
|
||||
import { genBaidu } from "./baidu";
|
||||
import interpreter from "../libs/interpreter";
|
||||
import { interpreter } from "../libs/interpreter";
|
||||
import { parseJsonObj, extractJson } from "../libs/utils";
|
||||
import { kissLog } from "../libs/log";
|
||||
import { fetchData } from "../libs/fetch";
|
||||
@@ -64,42 +68,46 @@ const genSystemPrompt = ({ systemPrompt, from, to }) =>
|
||||
.replaceAll(INPUT_PLACE_TO, to);
|
||||
|
||||
const genUserPrompt = ({
|
||||
// userPrompt,
|
||||
nobatchUserPrompt,
|
||||
useBatchFetch,
|
||||
tone,
|
||||
glossary = {},
|
||||
// from,
|
||||
from,
|
||||
to,
|
||||
texts,
|
||||
docInfo,
|
||||
}) => {
|
||||
const prompt = JSON.stringify({
|
||||
targetLanguage: to,
|
||||
title: docInfo.title,
|
||||
description: docInfo.description,
|
||||
segments: texts.map((text, i) => ({ id: i, text })),
|
||||
glossary,
|
||||
tone,
|
||||
});
|
||||
if (useBatchFetch) {
|
||||
return JSON.stringify({
|
||||
targetLanguage: to,
|
||||
title: docInfo.title,
|
||||
description: docInfo.description,
|
||||
segments: texts.map((text, i) => ({ id: i, text })),
|
||||
glossary,
|
||||
tone,
|
||||
});
|
||||
}
|
||||
|
||||
// if (userPrompt.includes(INPUT_PLACE_TEXT)) {
|
||||
// return userPrompt
|
||||
// .replaceAll(INPUT_PLACE_FROM, from)
|
||||
// .replaceAll(INPUT_PLACE_TO, to)
|
||||
// .replaceAll(INPUT_PLACE_TEXT, prompt);
|
||||
// }
|
||||
|
||||
return prompt;
|
||||
return nobatchUserPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, texts[0]);
|
||||
};
|
||||
|
||||
const parseAIRes = (raw) => {
|
||||
const parseAIRes = (raw, useBatchFetch = true) => {
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!useBatchFetch) {
|
||||
return [[raw]];
|
||||
}
|
||||
|
||||
try {
|
||||
const jsonString = extractJson(raw);
|
||||
const data = JSON.parse(jsonString);
|
||||
if (!jsonString) return [];
|
||||
|
||||
const data = JSON.parse(jsonString);
|
||||
if (Array.isArray(data.translations)) {
|
||||
// todo: 考虑序号id可能会打乱
|
||||
return data.translations.map((item) => [
|
||||
@@ -494,7 +502,7 @@ const genOpenRouter = ({
|
||||
};
|
||||
|
||||
const genOllama = ({
|
||||
think,
|
||||
// think,
|
||||
url,
|
||||
key,
|
||||
systemPrompt,
|
||||
@@ -520,7 +528,7 @@ const genOllama = ({
|
||||
],
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
think,
|
||||
// think,
|
||||
stream: false,
|
||||
};
|
||||
|
||||
@@ -624,7 +632,10 @@ export const genTransReq = async ({ reqHook, ...args }) => {
|
||||
apiSlug,
|
||||
key,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
// userPrompt,
|
||||
nobatchPrompt = defaultNobatchPrompt,
|
||||
nobatchUserPrompt = defaultNobatchUserPrompt,
|
||||
useBatchFetch,
|
||||
from,
|
||||
to,
|
||||
texts,
|
||||
@@ -644,11 +655,16 @@ export const genTransReq = async ({ reqHook, ...args }) => {
|
||||
}
|
||||
|
||||
if (API_SPE_TYPES.ai.has(apiType)) {
|
||||
args.systemPrompt = genSystemPrompt({ systemPrompt, from, to });
|
||||
args.systemPrompt = genSystemPrompt({
|
||||
systemPrompt: useBatchFetch ? systemPrompt : nobatchPrompt,
|
||||
from,
|
||||
to,
|
||||
});
|
||||
args.userPrompt = !!events
|
||||
? JSON.stringify(events)
|
||||
: genUserPrompt({
|
||||
userPrompt,
|
||||
nobatchUserPrompt,
|
||||
useBatchFetch,
|
||||
from,
|
||||
to,
|
||||
texts,
|
||||
@@ -677,13 +693,16 @@ export const genTransReq = async ({ reqHook, ...args }) => {
|
||||
if (reqHook?.trim() && !events) {
|
||||
try {
|
||||
interpreter.run(`exports.reqHook = ${reqHook}`);
|
||||
const hookResult = await interpreter.exports.reqHook(args, {
|
||||
url,
|
||||
body,
|
||||
headers,
|
||||
userMsg,
|
||||
method,
|
||||
});
|
||||
const hookResult = await interpreter.exports.reqHook(
|
||||
{ ...args, defaultSystemPrompt, defaultSubtitlePrompt },
|
||||
{
|
||||
url,
|
||||
body,
|
||||
headers,
|
||||
userMsg,
|
||||
method,
|
||||
}
|
||||
);
|
||||
if (hookResult && hookResult.url) {
|
||||
return genInit(hookResult);
|
||||
}
|
||||
@@ -711,10 +730,11 @@ export const parseTransRes = async (
|
||||
toLang,
|
||||
langMap,
|
||||
resHook,
|
||||
thinkIgnore,
|
||||
// thinkIgnore,
|
||||
history,
|
||||
userMsg,
|
||||
apiType,
|
||||
useBatchFetch,
|
||||
}
|
||||
) => {
|
||||
// 执行 response hook
|
||||
@@ -731,6 +751,8 @@ export const parseTransRes = async (
|
||||
fromLang,
|
||||
toLang,
|
||||
langMap,
|
||||
extractJson,
|
||||
parseAIRes,
|
||||
});
|
||||
if (hookResult && Array.isArray(hookResult.translations)) {
|
||||
if (history && userMsg && hookResult.modelMsg) {
|
||||
@@ -803,13 +825,13 @@ export const parseTransRes = async (
|
||||
content: modelMsg.content,
|
||||
});
|
||||
}
|
||||
return parseAIRes(res?.choices?.[0]?.message?.content ?? "");
|
||||
return parseAIRes(modelMsg?.content, useBatchFetch);
|
||||
case OPT_TRANS_GEMINI:
|
||||
modelMsg = res?.candidates?.[0]?.content;
|
||||
if (history && userMsg && modelMsg) {
|
||||
history.add(userMsg, modelMsg);
|
||||
}
|
||||
return parseAIRes(res?.candidates?.[0]?.content?.parts?.[0]?.text ?? "");
|
||||
return parseAIRes(modelMsg?.parts?.[0]?.text ?? "", useBatchFetch);
|
||||
case OPT_TRANS_CLAUDE:
|
||||
modelMsg = { role: res?.role, content: res?.content?.text };
|
||||
if (history && userMsg && modelMsg) {
|
||||
@@ -818,18 +840,18 @@ export const parseTransRes = async (
|
||||
content: modelMsg.content,
|
||||
});
|
||||
}
|
||||
return parseAIRes(res?.content?.[0]?.text ?? "");
|
||||
return parseAIRes(res?.content?.[0]?.text ?? "", useBatchFetch);
|
||||
case OPT_TRANS_CLOUDFLAREAI:
|
||||
return [[res?.result?.translated_text]];
|
||||
case OPT_TRANS_OLLAMA:
|
||||
modelMsg = res?.choices?.[0]?.message;
|
||||
|
||||
const deepModels = thinkIgnore
|
||||
.split(",")
|
||||
.filter((model) => model?.trim());
|
||||
if (deepModels.some((model) => res?.model?.startsWith(model))) {
|
||||
modelMsg?.content.replace(/<think>[\s\S]*<\/think>/i, "");
|
||||
}
|
||||
// const deepModels = thinkIgnore
|
||||
// .split(",")
|
||||
// .filter((model) => model?.trim());
|
||||
// if (deepModels.some((model) => res?.model?.startsWith(model))) {
|
||||
// modelMsg?.content.replace(/<think>[\s\S]*<\/think>/i, "");
|
||||
// }
|
||||
|
||||
if (history && userMsg && modelMsg) {
|
||||
history.add(userMsg, {
|
||||
@@ -837,9 +859,9 @@ export const parseTransRes = async (
|
||||
content: modelMsg.content,
|
||||
});
|
||||
}
|
||||
return parseAIRes(modelMsg?.content);
|
||||
return parseAIRes(modelMsg?.content, useBatchFetch);
|
||||
case OPT_TRANS_CUSTOMIZE:
|
||||
return res?.map((item) => [item.text, item.src]);
|
||||
return (res?.translations ?? res)?.map((item) => [item.text, item.src]);
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -911,7 +933,7 @@ export const handleTranslate = async (
|
||||
httpTimeout,
|
||||
});
|
||||
if (!response) {
|
||||
throw new Error("tranlate got empty response");
|
||||
throw new Error("translate got empty response");
|
||||
}
|
||||
|
||||
const result = await parseTransRes(response, {
|
||||
@@ -925,8 +947,8 @@ export const handleTranslate = async (
|
||||
userMsg,
|
||||
...apiSetting,
|
||||
});
|
||||
if (!Array.isArray(result)) {
|
||||
throw new Error("tranlate got an unexpected result");
|
||||
if (!result?.length) {
|
||||
throw new Error("translate got an unexpected result");
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -33,7 +33,7 @@ import { sendTabMsg } from "./libs/msg";
|
||||
import { trySyncAllSubRules } from "./libs/subRules";
|
||||
import { saveRule } from "./libs/rules";
|
||||
import { getCurTabId } from "./libs/msg";
|
||||
import { injectInlineJs, injectInternalCss } from "./libs/injector";
|
||||
import { injectInlineJsBg, injectInternalCss } from "./libs/injector";
|
||||
import { kissLog, logger } from "./libs/log";
|
||||
import { chromeDetect, chromeTranslate } from "./libs/builtinAI";
|
||||
|
||||
@@ -268,7 +268,7 @@ const messageHandlers = {
|
||||
[MSG_PUT_HTTPCACHE]: (args) => putHttpCache(args),
|
||||
[MSG_OPEN_OPTIONS]: () => browser.runtime.openOptionsPage(),
|
||||
[MSG_SAVE_RULE]: (args) => saveRule(args),
|
||||
[MSG_INJECT_JS]: (args) => injectToCurrentTab(injectInlineJs, args),
|
||||
[MSG_INJECT_JS]: (args) => injectToCurrentTab(injectInlineJsBg, args),
|
||||
[MSG_INJECT_CSS]: (args) => injectToCurrentTab(injectInternalCss, args),
|
||||
[MSG_UPDATE_CSP]: (args) => updateCspRules(args),
|
||||
[MSG_CONTEXT_MENUS]: (args) => addContextMenus(args),
|
||||
@@ -285,20 +285,11 @@ const messageHandlers = {
|
||||
*/
|
||||
browser.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||
const handler = messageHandlers[action];
|
||||
|
||||
if (!handler) {
|
||||
const errorMessage = `Message action is unavailable: ${action}`;
|
||||
kissLog("runtime onMessage", action, new Error(errorMessage));
|
||||
return null;
|
||||
throw new Error(`Message action is unavailable: ${action}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handler(args);
|
||||
return result;
|
||||
} catch (err) {
|
||||
kissLog("runtime onMessage", action, err);
|
||||
return null;
|
||||
}
|
||||
return handler(args);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
138
src/common.js
138
src/common.js
@@ -1,19 +1,11 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import Action from "./views/Action";
|
||||
import createCache from "@emotion/cache";
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import { OPT_HIGHLIGHT_WORDS_DISABLE } from "./config";
|
||||
import {
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
APP_CONSTS,
|
||||
} from "./config";
|
||||
import { getFabWithDefault, getSettingWithDefault } from "./libs/storage";
|
||||
import { Translator } from "./libs/translator";
|
||||
import { isIframe, sendIframeMsg } from "./libs/iframe";
|
||||
import { touchTapListener } from "./libs/touch";
|
||||
import { debounce, genEventName } from "./libs/utils";
|
||||
getFabWithDefault,
|
||||
getSettingWithDefault,
|
||||
getWordsWithDefault,
|
||||
} from "./libs/storage";
|
||||
import { isIframe } from "./libs/iframe";
|
||||
import { genEventName } from "./libs/utils";
|
||||
import { handlePing, injectScript } from "./libs/gm";
|
||||
import { matchRule } from "./libs/rules";
|
||||
import { trySyncAllSubRules } from "./libs/subRules";
|
||||
@@ -21,6 +13,7 @@ import { isInBlacklist } from "./libs/blacklist";
|
||||
import { runSubtitle } from "./subtitle/subtitle";
|
||||
import { logger } from "./libs/log";
|
||||
import { injectInlineJs } from "./libs/injector";
|
||||
import TranslatorManager from "./libs/translatorManager";
|
||||
|
||||
/**
|
||||
* 油猴脚本设置页面
|
||||
@@ -43,62 +36,6 @@ function runSettingPage() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* iframe 页面执行
|
||||
* @param {*} translator
|
||||
*/
|
||||
function runIframe(translator) {
|
||||
window.addEventListener("message", (e) => {
|
||||
const { action, args } = e.data || {};
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
translator?.toggle();
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
translator?.toggleStyle();
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
translator.updateRule(args || {});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 悬浮按钮
|
||||
* @param {*} translator
|
||||
* @returns
|
||||
*/
|
||||
async function showFab(translator) {
|
||||
const fab = await getFabWithDefault();
|
||||
const $action = document.createElement("div");
|
||||
$action.id = APP_CONSTS.fabID;
|
||||
$action.className = "notranslate";
|
||||
$action.style.fontSize = "0";
|
||||
$action.style.width = "0";
|
||||
$action.style.height = "0";
|
||||
document.body.parentElement.appendChild($action);
|
||||
const shadowContainer = $action.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
const shadowRootElement = document.createElement("div");
|
||||
shadowRootElement.className = `${APP_CONSTS.fabID}_warpper notranslate`;
|
||||
shadowContainer.appendChild(emotionRoot);
|
||||
shadowContainer.appendChild(shadowRootElement);
|
||||
const cache = createCache({
|
||||
key: APP_CONSTS.fabID,
|
||||
prepend: true,
|
||||
container: emotionRoot,
|
||||
});
|
||||
ReactDOM.createRoot(shadowRootElement).render(
|
||||
<React.StrictMode>
|
||||
<CacheProvider value={cache}>
|
||||
<Action translator={translator} fab={fab} />
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误信息到页面顶部
|
||||
* @param {*} message
|
||||
@@ -161,22 +98,19 @@ function showErr(message) {
|
||||
setTimeout(removeBanner, 10000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听触屏操作
|
||||
* @param {*} translator
|
||||
* @returns
|
||||
*/
|
||||
function touchOperation(translator) {
|
||||
const { touchTranslate = 2 } = translator.setting;
|
||||
if (touchTranslate === 0) {
|
||||
return;
|
||||
async function getFavWords(rule) {
|
||||
if (
|
||||
rule.highlightWords &&
|
||||
rule.highlightWords !== OPT_HIGHLIGHT_WORDS_DISABLE
|
||||
) {
|
||||
try {
|
||||
return Object.keys(await getWordsWithDefault());
|
||||
} catch (err) {
|
||||
logger.info("get fav words", err);
|
||||
}
|
||||
}
|
||||
|
||||
const handleTap = debounce(() => {
|
||||
translator.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
});
|
||||
touchTapListener(handleTap, touchTranslate);
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,34 +143,28 @@ export async function run(isUserscript = false) {
|
||||
|
||||
// 翻译网页
|
||||
const rule = await matchRule(href, setting);
|
||||
const translator = new Translator(rule, setting, isUserscript);
|
||||
const favWords = await getFavWords(rule);
|
||||
const fabConfig = await getFabWithDefault();
|
||||
const translatorManager = new TranslatorManager({
|
||||
setting,
|
||||
rule,
|
||||
fabConfig,
|
||||
favWords,
|
||||
isIframe,
|
||||
isUserscript,
|
||||
});
|
||||
translatorManager.start();
|
||||
|
||||
// 适配iframe
|
||||
if (isIframe) {
|
||||
runIframe(translator);
|
||||
return;
|
||||
}
|
||||
|
||||
// 字幕翻译
|
||||
runSubtitle({ href, setting, rule, isUserscript });
|
||||
|
||||
// 监听消息
|
||||
// !isUserscript && runtimeListener(translator);
|
||||
|
||||
// 输入框翻译
|
||||
// inputTranslate(setting);
|
||||
|
||||
// 划词翻译
|
||||
// showTransbox(setting, rule);
|
||||
|
||||
// 浮球按钮
|
||||
await showFab(translator);
|
||||
|
||||
// 触屏操作
|
||||
touchOperation(translator);
|
||||
|
||||
// 同步订阅规则
|
||||
isUserscript && (await trySyncAllSubRules(setting));
|
||||
if (isUserscript) {
|
||||
trySyncAllSubRules(setting);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[KISS-Translator]", err);
|
||||
showErr(err.message);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const DEFAULT_HTTP_TIMEOUT = 10000; // 调用超时时间
|
||||
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
|
||||
export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
|
||||
export const DEFAULT_BATCH_INTERVAL = 1000; // 批处理请求间隔时间
|
||||
export const DEFAULT_BATCH_INTERVAL = 400; // 批处理请求间隔时间
|
||||
export const DEFAULT_BATCH_SIZE = 10; // 每次最多发送段落数量
|
||||
export const DEFAULT_BATCH_LENGTH = 10000; // 每次发送最大文字数量
|
||||
export const DEFAULT_CONTEXT_SIZE = 3; // 上下文会话数量
|
||||
@@ -46,7 +46,7 @@ export const OPT_TRANS_OPENROUTER = "OpenRouter";
|
||||
export const OPT_TRANS_CUSTOMIZE = "Custom";
|
||||
|
||||
// 内置支持的翻译引擎
|
||||
export const OPT_ALL_TYPES = [
|
||||
export const OPT_ALL_TRANS_TYPES = [
|
||||
OPT_TRANS_BUILTINAI,
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_GOOGLE_2,
|
||||
@@ -82,7 +82,7 @@ export const OPT_LANGDETECTOR_MAP = new Set(OPT_LANGDETECTOR_ALL);
|
||||
// 翻译引擎特殊集合
|
||||
export const API_SPE_TYPES = {
|
||||
// 内置翻译
|
||||
builtin: new Set(OPT_ALL_TYPES),
|
||||
builtin: new Set(OPT_ALL_TRANS_TYPES),
|
||||
// 机器翻译
|
||||
machine: new Set([
|
||||
OPT_TRANS_MICROSOFT,
|
||||
@@ -340,7 +340,10 @@ Object.entries(OPT_LANGS_TO_SPEC).forEach(([t, m]) => {
|
||||
OPT_LANGS_TO_CODE[t] = specToCode(m);
|
||||
});
|
||||
|
||||
const defaultSystemPrompt = `Act as a translation API. Output a single raw JSON object only. No extra text or fences.
|
||||
export const defaultNobatchPrompt = `You are a professional, authentic machine translation engine.`;
|
||||
export const defaultNobatchUserPrompt = `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`;
|
||||
|
||||
export const defaultSystemPrompt = `Act as a translation API. Output a single raw JSON object only. No extra text or fences.
|
||||
|
||||
Input:
|
||||
{"targetLanguage":"<lang>","title":"<context>","description":"<context>","segments":[{"id":1,"text":"..."}],"glossary":{"sourceTerm":"targetTerm"},"tone":"<formal|casual>"}
|
||||
@@ -381,7 +384,7 @@ Fail-safe: On any error, return {"translations":[]}.`;
|
||||
// 4. **Special Cases**: '[Music]' (and similar cues) are standalone entries. Translate appropriately (e.g., '[音乐]', '[Musique]').
|
||||
// `;
|
||||
|
||||
const defaultSubtitlePrompt = `You are an expert AI for subtitle generation. Convert a JSON array of word-level timestamps into a bilingual VTT file.
|
||||
export const defaultSubtitlePrompt = `You are an expert AI for subtitle generation. Convert a JSON array of word-level timestamps into a bilingual VTT file.
|
||||
|
||||
**Workflow:**
|
||||
1. Merge \`text\` fields into complete sentences; ignore empty text.
|
||||
@@ -409,16 +412,16 @@ Good morning.
|
||||
\`\`\``;
|
||||
|
||||
const defaultRequestHook = `async (args, { url, body, headers, userMsg, method } = {}) => {
|
||||
console.log("request hook args:", args);
|
||||
console.log("request hook args:", { args, url, body, headers, userMsg, method });
|
||||
// return { url, body, headers, userMsg, method };
|
||||
}`;
|
||||
};`;
|
||||
|
||||
const defaultResponseHook = `async ({ res, ...args }) => {
|
||||
console.log("reaponse hook args:", res, args);
|
||||
console.log("reaponse hook args:", { res, args });
|
||||
// const translations = [["你好", "zh"]];
|
||||
// const modelMsg = "";
|
||||
// return { translations, modelMsg };
|
||||
}`;
|
||||
};`;
|
||||
|
||||
// 翻译接口默认参数
|
||||
const defaultApi = {
|
||||
@@ -430,6 +433,8 @@ const defaultApi = {
|
||||
model: "", // 模型名称
|
||||
systemPrompt: defaultSystemPrompt,
|
||||
subtitlePrompt: defaultSubtitlePrompt,
|
||||
nobatchPrompt: defaultNobatchPrompt,
|
||||
nobatchUserPrompt: defaultNobatchUserPrompt,
|
||||
userPrompt: "",
|
||||
tone: BUILTIN_STONES[0], // 翻译风格
|
||||
placeholder: BUILTIN_PLACEHOLDERS[0], // 占位符
|
||||
@@ -450,8 +455,8 @@ const defaultApi = {
|
||||
contextSize: DEFAULT_CONTEXT_SIZE, // 智能上下文保留会话数
|
||||
temperature: 0.0,
|
||||
maxTokens: 20480,
|
||||
think: false,
|
||||
thinkIgnore: "qwen3,deepseek-r1",
|
||||
// think: false, // (OpenAI 兼容接口未支持,暂时移除)
|
||||
// thinkIgnore: "qwen3,deepseek-r1", // (OpenAI 兼容接口未支持,暂时移除)
|
||||
isDisabled: false, // 是否不显示,
|
||||
region: "", // Azure 专用
|
||||
};
|
||||
@@ -499,7 +504,6 @@ const defaultApiOpts = {
|
||||
[OPT_TRANS_DEEPLX]: {
|
||||
...defaultApi,
|
||||
url: "http://localhost:1188/translate",
|
||||
fetchLimit: 1,
|
||||
},
|
||||
[OPT_TRANS_NIUTRANS]: {
|
||||
...defaultApi,
|
||||
@@ -512,7 +516,6 @@ const defaultApiOpts = {
|
||||
url: "https://api.openai.com/v1/chat/completions",
|
||||
model: "gpt-4",
|
||||
useBatchFetch: true,
|
||||
fetchLimit: 1,
|
||||
},
|
||||
[OPT_TRANS_GEMINI]: {
|
||||
...defaultApi,
|
||||
@@ -557,7 +560,7 @@ const defaultApiOpts = {
|
||||
};
|
||||
|
||||
// 内置翻译接口列表(带参数)
|
||||
export const DEFAULT_API_LIST = OPT_ALL_TYPES.map((apiType) => ({
|
||||
export const DEFAULT_API_LIST = OPT_ALL_TRANS_TYPES.map((apiType) => ({
|
||||
...defaultApiOpts[apiType],
|
||||
apiSlug: apiType,
|
||||
apiName: apiType,
|
||||
@@ -565,4 +568,6 @@ export const DEFAULT_API_LIST = OPT_ALL_TYPES.map((apiType) => ({
|
||||
}));
|
||||
|
||||
export const DEFAULT_API_TYPE = OPT_TRANS_MICROSOFT;
|
||||
export const DEFAULT_API_SETTING = DEFAULT_API_LIST[DEFAULT_API_TYPE];
|
||||
export const DEFAULT_API_SETTING = DEFAULT_API_LIST.find(
|
||||
(a) => a.apiType === DEFAULT_API_TYPE
|
||||
);
|
||||
|
||||
@@ -2,9 +2,11 @@ export const APP_NAME = process.env.REACT_APP_NAME.trim()
|
||||
.split(/\s+/)
|
||||
.join("-");
|
||||
export const APP_LCNAME = APP_NAME.toLowerCase();
|
||||
export const APP_UPNAME = APP_NAME.toUpperCase();
|
||||
export const APP_CONSTS = {
|
||||
fabID: `${APP_LCNAME}-fab`,
|
||||
boxID: `${APP_LCNAME}-box`,
|
||||
popupID: `${APP_LCNAME}-popup`,
|
||||
};
|
||||
|
||||
export const APP_VERSION = process.env.REACT_APP_VERSION.split(".");
|
||||
|
||||
@@ -137,48 +137,44 @@ ${customApiLangs}
|
||||
`;
|
||||
|
||||
const requestHookHelperZH = `1、第一个参数包含如下字段:'texts', 'from', 'to', 'url', 'key', 'model', 'systemPrompt', ...
|
||||
2、返回值必须是包含以下字段的对象: 'url', 'body', 'headers', 'userMsg', 'method'
|
||||
2、返回值必须是包含以下字段的对象: 'url', 'body', 'headers', 'method'
|
||||
3、如返回空值,则hook函数不会产生任何效果。
|
||||
|
||||
// 示例
|
||||
async (args, { url, body, headers, userMsg, method } = {}) => {
|
||||
console.log("request hook args:", args);
|
||||
return { url, body, headers, userMsg, method };
|
||||
}`;
|
||||
|
||||
const requestHookHelperEN = `1. The first parameter contains the following fields: 'texts', 'from', 'to', 'url', 'key', 'model', 'systemPrompt', ...
|
||||
2. The return value must be an object containing the following fields: 'url', 'body', 'headers', 'userMsg', 'method'
|
||||
2. The return value must be an object containing the following fields: 'url', 'body', 'headers', 'method'
|
||||
3. If a null value is returned, the hook function will have no effect.
|
||||
|
||||
// Example
|
||||
async (args, { url, body, headers, userMsg, method } = {}) => {
|
||||
console.log("request hook args:", args);
|
||||
return { url, body, headers, userMsg, method };
|
||||
}`;
|
||||
|
||||
const responsetHookHelperZH = `1、第一个参数包含如下字段:'res', ...
|
||||
2、返回值必须是包含以下字段的对象: 'translations', 'modelMsg'
|
||||
('translations' 应为一个二维数组:[[译文, 源语言]])
|
||||
2、返回值必须是包含以下字段的对象: 'translations'
|
||||
('translations' 应为一个二维数组:[[译文, 原文语言]])
|
||||
3、如返回空值,则hook函数不会产生任何效果。
|
||||
|
||||
// 示例
|
||||
async ({ res, ...args }) => {
|
||||
console.log("reaponse hook args:", res, args);
|
||||
const translations = [["你好", "zh"]];
|
||||
const modelMsg = "";
|
||||
const translations = [["你好", "en"]];
|
||||
const modelMsg = {}; // 用于AI上下文
|
||||
return { translations, modelMsg };
|
||||
}`;
|
||||
|
||||
const responsetHookHelperEN = `1. The first parameter contains the following fields: 'res', ...
|
||||
2. The return value must be an object containing the following fields: 'translations', 'modelMsg'
|
||||
2. The return value must be an object containing the following fields: 'translations'
|
||||
('translations' should be a two-dimensional array: [[translation, source language]]).
|
||||
3. If a null value is returned, the hook function will have no effect.
|
||||
|
||||
// Example
|
||||
async ({ res, ...args }) => {
|
||||
console.log("reaponse hook args:", res, args);
|
||||
const translations = [["你好", "zh"]];
|
||||
const modelMsg = "";
|
||||
const translations = [["你好", "en"]];
|
||||
const modelMsg = {}; // For AI context
|
||||
return { translations, modelMsg };
|
||||
}`;
|
||||
|
||||
@@ -535,9 +531,9 @@ export const I18N = {
|
||||
zh_TW: `2.大部分AI介面都與OpenAI相容,因此選擇新增OpenAI類型即可。`,
|
||||
},
|
||||
about_api_3: {
|
||||
zh: `2、暂未列出的接口,理论上都可以通过自定义接口 (Custom) 的形式支持。`,
|
||||
en: `2. Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`,
|
||||
zh_TW: `2、暫未列出的介面,理論上都可透過自訂介面 (Custom) 的形式支援。`,
|
||||
zh: `3、暂未列出的接口,理论上都可以通过自定义接口 (Custom) 的形式支持。`,
|
||||
en: `3. Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`,
|
||||
zh_TW: `3、暫未列出的介面,理論上都可透過自訂介面 (Custom) 的形式支援。`,
|
||||
},
|
||||
about_api_proxy: {
|
||||
zh: `查看自建一个翻译接口代理`,
|
||||
@@ -718,6 +714,16 @@ export const I18N = {
|
||||
en: `Selector Style`,
|
||||
zh_TW: `選擇器節點樣式`,
|
||||
},
|
||||
terms_style: {
|
||||
zh: `专业术语样式`,
|
||||
en: `Terms Style`,
|
||||
zh_TW: `專業術語樣式`,
|
||||
},
|
||||
highlight_style: {
|
||||
zh: `词汇高亮样式`,
|
||||
en: `Fav Words highlight style`,
|
||||
zh_TW: `詞彙高亮樣式`,
|
||||
},
|
||||
selector_style_helper: {
|
||||
zh: `开启翻译时注入。`,
|
||||
en: `It is injected when translation is turned on.`,
|
||||
@@ -739,9 +745,33 @@ export const I18N = {
|
||||
zh_TW: `注入 JS`,
|
||||
},
|
||||
inject_js_helper: {
|
||||
zh: `初始化时注入运行,一个页面仅运行一次。`,
|
||||
en: `Injected and run at initialization, and only run once per page.`,
|
||||
zh_TW: `初始化時注入運行,一個頁面僅運行一次。`,
|
||||
zh: `预加载时注入,一个页面仅运行一次。内置全局对象 KT: {
|
||||
apiTranslate,
|
||||
apiDectect,
|
||||
apiSetting,
|
||||
apisMap,
|
||||
toLang,
|
||||
docInfo,
|
||||
glossary,
|
||||
}`,
|
||||
en: `Injected during preload, runs only once per page. Built-in global object KT: {
|
||||
apiTranslate,
|
||||
apiDectect,
|
||||
apiSetting,
|
||||
apisMap,
|
||||
toLang,
|
||||
docInfo,
|
||||
glossary,
|
||||
}`,
|
||||
zh_TW: `預先載入時注入,一個頁面僅運行一次。內建全域物件 KT: {
|
||||
apiTranslate,
|
||||
apiDectect,
|
||||
apiSetting,
|
||||
apisMap,
|
||||
toLang,
|
||||
docInfo,
|
||||
glossary,
|
||||
}`,
|
||||
},
|
||||
inject_css: {
|
||||
zh: `注入CSS`,
|
||||
@@ -1154,9 +1184,9 @@ export const I18N = {
|
||||
zh_TW: `觸控設定`,
|
||||
},
|
||||
touch_translate_shortcut: {
|
||||
zh: `触屏翻译快捷方式`,
|
||||
en: `Touch Translate Shortcut`,
|
||||
zh_TW: `觸控翻譯捷徑`,
|
||||
zh: `触屏翻译快捷方式 (支持多选)`,
|
||||
en: `Touch Translate Shortcut (multiple supported)`,
|
||||
zh_TW: `觸控翻譯捷徑 (支援多選)`,
|
||||
},
|
||||
touch_tap_0: {
|
||||
zh: `禁用`,
|
||||
@@ -1178,6 +1208,21 @@ export const I18N = {
|
||||
en: `Four finger tap`,
|
||||
zh_TW: `四指輕觸`,
|
||||
},
|
||||
touch_tap_5: {
|
||||
zh: `单指双击`,
|
||||
en: `Double-click`,
|
||||
zh_TW: `單指雙擊`,
|
||||
},
|
||||
touch_tap_6: {
|
||||
zh: `单指三击`,
|
||||
en: `Triple-click`,
|
||||
zh_TW: `單指三擊`,
|
||||
},
|
||||
touch_tap_7: {
|
||||
zh: `双指双击`,
|
||||
en: `Two-finger double-click`,
|
||||
zh_TW: `雙指雙擊`,
|
||||
},
|
||||
translate_blacklist: {
|
||||
zh: `禁用翻译名单`,
|
||||
en: `Translate Blacklist`,
|
||||
@@ -1328,15 +1373,35 @@ export const I18N = {
|
||||
en: `Transbox Follow Selection`,
|
||||
zh_TW: `翻譯框跟隨選取文字`,
|
||||
},
|
||||
tranbox_auto_height: {
|
||||
zh: `翻译框自适应高度`,
|
||||
en: `Translation box adaptive height`,
|
||||
zh_TW: `翻譯框自適應高度`,
|
||||
},
|
||||
translate_start_hook: {
|
||||
zh: `翻译开始钩子函数`,
|
||||
en: `Translate Start Hook`,
|
||||
zh_TW: `翻譯開始 Hook`,
|
||||
},
|
||||
translate_start_hook_helper: {
|
||||
zh: `翻译前时运行,入参为: ({hostNode, parentNode, nodes})`,
|
||||
en: `Run before translation, input parameters are: ({hostNode, parentNode, nodes})`,
|
||||
zh_TW: `翻譯前時運行,入參為: ({hostNode, parentNode, nodes})`,
|
||||
zh: `翻译前时运行,入参为: {text,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting,
|
||||
docInfo,
|
||||
glossary,}`,
|
||||
en: `Run before translation, input parameters are: {text,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting,
|
||||
docInfo,
|
||||
glossary,}`,
|
||||
zh_TW: `翻譯前時運行,入參為: {text,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting,
|
||||
docInfo,
|
||||
glossary,}`,
|
||||
},
|
||||
translate_end_hook: {
|
||||
zh: `翻译完成钩子函数`,
|
||||
@@ -1583,6 +1648,11 @@ export const I18N = {
|
||||
en: `Enable bilingual display`,
|
||||
zh_TW: `雙語顯示`,
|
||||
},
|
||||
is_skip_ad: {
|
||||
zh: `是否快进广告`,
|
||||
en: `Should I fast forward to the ad?`,
|
||||
zh_TW: `是否快轉廣告`,
|
||||
},
|
||||
background_styles: {
|
||||
zh: `背景样式`,
|
||||
en: `DBackground Style`,
|
||||
@@ -1663,6 +1733,62 @@ export const I18N = {
|
||||
en: `Log Level`,
|
||||
zh_TW: `日誌等級`,
|
||||
},
|
||||
goto_custom_api_example: {
|
||||
zh: `点击查看【自定义接口示例】`,
|
||||
en: `Click to view [Custom Interface Example]`,
|
||||
zh_TW: `點選查看【自訂介面範例】`,
|
||||
},
|
||||
split_paragraph: {
|
||||
zh: `切分长段落`,
|
||||
en: `Split long paragraph`,
|
||||
zh_TW: `切分長段落`,
|
||||
},
|
||||
split_length: {
|
||||
zh: `切分长度 (0-10000)`,
|
||||
en: `Segmentation length(0-10000)`,
|
||||
zh_TW: `切分長度(0-10000)`,
|
||||
},
|
||||
highlight_words: {
|
||||
zh: `高亮收藏词汇`,
|
||||
en: `Highlight favorite words`,
|
||||
zh_TW: `高亮收藏詞彙`,
|
||||
},
|
||||
|
||||
split_disable: {
|
||||
zh: `禁用`,
|
||||
en: `Disable`,
|
||||
zh_TW: `停用`,
|
||||
},
|
||||
split_textlength: {
|
||||
zh: `按照长度切分`,
|
||||
en: `Split by length`,
|
||||
zh_TW: `依長度切分`,
|
||||
},
|
||||
split_punctuation: {
|
||||
zh: `按照句子切分`,
|
||||
en: `Split by sentence`,
|
||||
zh_TW: `按照句子切分`,
|
||||
},
|
||||
highlight_disable: {
|
||||
zh: `禁用`,
|
||||
en: `Disable`,
|
||||
zh_TW: `停用`,
|
||||
},
|
||||
highlight_beforetrans: {
|
||||
zh: `翻译前高亮`,
|
||||
en: `Highlight before translation`,
|
||||
zh_TW: `翻譯前高亮`,
|
||||
},
|
||||
highlight_aftertrans: {
|
||||
zh: `翻译后高亮`,
|
||||
en: `Highlight after translation`,
|
||||
zh_TW: `翻譯後高亮`,
|
||||
},
|
||||
pagescroll_root_margin: {
|
||||
zh: `滚动加载提前触发 (0-10000px)`,
|
||||
en: `Early triggering of scroll loading (0-10000px)`,
|
||||
zh_TW: `滾動載入提前觸發 (0-10000px)`,
|
||||
},
|
||||
};
|
||||
|
||||
export const i18n = (lang) => (key) => I18N[key]?.[lang] || "";
|
||||
export const newI18n = (lang) => (key) => I18N[key]?.[lang] || "";
|
||||
|
||||
@@ -15,6 +15,7 @@ export const MSG_TRANS_GETRULE = "trans_getrule";
|
||||
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
||||
export const MSG_TRANS_CURRULE = "trans_currule";
|
||||
export const MSG_TRANSBOX_TOGGLE = "transbox_toggle";
|
||||
export const MSG_POPUP_TOGGLE = "popup_toggle";
|
||||
export const MSG_MOUSEHOVER_TOGGLE = "mousehover_toggle";
|
||||
export const MSG_TRANSINPUT_TOGGLE = "transinput_toggle";
|
||||
export const MSG_CONTEXT_MENUS = "context_menus";
|
||||
@@ -27,6 +28,8 @@ export const MSG_BUILTINAI_TRANSLATE = "builtinai_translte";
|
||||
export const MSG_SET_LOGLEVEL = "set_loglevel";
|
||||
export const MSG_CLEAR_CACHES = "clear_caches";
|
||||
|
||||
export const EVENT_KISS = "event_kiss_translate";
|
||||
|
||||
export const MSG_XHR_DATA_YOUTUBE = "KISS_XHR_DATA_YOUTUBE";
|
||||
// export const MSG_GLOBAL_VAR_FETCH = "KISS_GLOBAL_VAR_FETCH";
|
||||
// export const MSG_GLOBAL_VAR_BACK = "KISS_GLOBAL_VAR_BACK";
|
||||
|
||||
@@ -63,6 +63,24 @@ export const OPT_TIMING_ALL = [
|
||||
OPT_TIMING_ALT,
|
||||
];
|
||||
|
||||
export const OPT_SPLIT_PARAGRAPH_DISABLE = "split_disable";
|
||||
export const OPT_SPLIT_PARAGRAPH_TEXTLENGTH = "split_textlength";
|
||||
export const OPT_SPLIT_PARAGRAPH_PUNCTUATION = "split_punctuation";
|
||||
export const OPT_SPLIT_PARAGRAPH_ALL = [
|
||||
OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||
OPT_SPLIT_PARAGRAPH_PUNCTUATION,
|
||||
OPT_SPLIT_PARAGRAPH_TEXTLENGTH,
|
||||
];
|
||||
|
||||
export const OPT_HIGHLIGHT_WORDS_DISABLE = "highlight_disable";
|
||||
export const OPT_HIGHLIGHT_WORDS_BEFORETRANS = "highlight_beforetrans";
|
||||
export const OPT_HIGHLIGHT_WORDS_AFTERTRANS = "highlight_aftertrans";
|
||||
export const OPT_HIGHLIGHT_WORDS_ALL = [
|
||||
OPT_HIGHLIGHT_WORDS_DISABLE,
|
||||
OPT_HIGHLIGHT_WORDS_BEFORETRANS,
|
||||
OPT_HIGHLIGHT_WORDS_AFTERTRANS,
|
||||
];
|
||||
|
||||
export const DEFAULT_DIY_STYLE = `color: #333;
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
@@ -78,9 +96,8 @@ background: linear-gradient(
|
||||
|
||||
export const DEFAULT_SELECTOR =
|
||||
"h1, h2, h3, h4, h5, h6, li, p, dd, blockquote, figcaption, label, legend";
|
||||
export const DEFAULT_IGNORE_SELECTOR =
|
||||
"aside, button, footer, form, pre, mark, nav";
|
||||
export const DEFAULT_KEEP_SELECTOR = `a:has(code)`;
|
||||
export const DEFAULT_IGNORE_SELECTOR = "button, footer, pre, mark, nav";
|
||||
export const DEFAULT_KEEP_SELECTOR = `code, cite, math, .math, a:has(code)`;
|
||||
export const DEFAULT_RULE = {
|
||||
pattern: "", // 匹配网址
|
||||
selector: "", // 选择器
|
||||
@@ -94,11 +111,13 @@ export const DEFAULT_RULE = {
|
||||
transOpen: GLOBAL_KEY, // 开启翻译
|
||||
bgColor: "", // 译文颜色
|
||||
textDiyStyle: "", // 自定义译文样式
|
||||
termsStyle: "", // 专业术语样式
|
||||
highlightStyle: "", // 高亮词汇样式
|
||||
selectStyle: "", // 选择器节点样式
|
||||
parentStyle: "", // 选择器父节点样式
|
||||
grandStyle: "", // 选择器父节点样式
|
||||
injectJs: "", // 注入JS
|
||||
injectCss: "", // 注入CSS
|
||||
// injectCss: "", // 注入CSS (作废)
|
||||
transOnly: GLOBAL_KEY, // 是否仅显示译文
|
||||
// transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译 (暂时作废)
|
||||
transTag: GLOBAL_KEY, // 译文元素标签
|
||||
@@ -116,6 +135,9 @@ export const DEFAULT_RULE = {
|
||||
hasShadowroot: GLOBAL_KEY, // 是否包含shadowroot
|
||||
rootsSelector: "", // 翻译范围选择器
|
||||
ignoreSelector: "", // 不翻译的选择器
|
||||
splitParagraph: GLOBAL_KEY, // 切分段落
|
||||
splitLength: 0, // 切分段落长度
|
||||
highlightWords: GLOBAL_KEY, // 高亮词汇
|
||||
};
|
||||
|
||||
// 全局规则
|
||||
@@ -132,11 +154,13 @@ export const GLOBLA_RULE = {
|
||||
transOpen: "false", // 开启翻译
|
||||
bgColor: "", // 译文颜色
|
||||
textDiyStyle: DEFAULT_DIY_STYLE, // 自定义译文样式
|
||||
termsStyle: "font-weight: bold;", // 专业术语样式
|
||||
highlightStyle: "color: red;", // 高亮词汇样式
|
||||
selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
|
||||
parentStyle: DEFAULT_SELECT_STYLE, // 选择器父节点样式
|
||||
grandStyle: DEFAULT_SELECT_STYLE, // 选择器祖节点样式
|
||||
injectJs: "", // 注入JS
|
||||
injectCss: "", // 注入CSS
|
||||
// injectCss: "", // 注入CSS(作废)
|
||||
transOnly: "false", // 是否仅显示译文
|
||||
// transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译 (暂时作废)
|
||||
transTag: DEFAULT_TRANS_TAG, // 译文元素标签
|
||||
@@ -154,6 +178,9 @@ export const GLOBLA_RULE = {
|
||||
hasShadowroot: "false", // 是否包含shadowroot
|
||||
rootsSelector: "body", // 翻译范围选择器
|
||||
ignoreSelector: DEFAULT_IGNORE_SELECTOR, // 不翻译的选择器
|
||||
splitParagraph: OPT_SPLIT_PARAGRAPH_DISABLE, // 切分段落
|
||||
splitLength: 100, // 切分段落长度
|
||||
highlightWords: OPT_HIGHLIGHT_WORDS_DISABLE, // 高亮词汇
|
||||
};
|
||||
|
||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||
@@ -170,34 +197,44 @@ export const DEFAULT_OW_RULE = {
|
||||
|
||||
// todo: 校验几个内置规则
|
||||
const RULES_MAP = {
|
||||
"www.google.com/search": {
|
||||
rootsSelector: `#rcnt`,
|
||||
},
|
||||
// "www.google.com/search": {
|
||||
// rootsSelector: `#rcnt`,
|
||||
// },
|
||||
"en.wikipedia.org": {
|
||||
ignoreSelector: `.button, code, footer, form, mark, pre, .mwe-math-element, .mw-editsection`,
|
||||
},
|
||||
"news.ycombinator.com": {
|
||||
selector: `p, .titleline, .commtext`,
|
||||
rootsSelector: `#bigbox`,
|
||||
selector: `p, .titleline, .commtext, .hn-item-title, .hn-comment-text, .hn-story-title`,
|
||||
keepSelector: `code, img, svg, pre, .sitebit`,
|
||||
ignoreSelector: `button, code, footer, form, header, mark, nav, pre, .reply`,
|
||||
autoScan: `false`,
|
||||
},
|
||||
"twitter.com, https://x.com": {
|
||||
selector: `[data-testid='tweetText']`,
|
||||
keepSelector: `img, svg, span:has(a), div:has(a)`,
|
||||
keepSelector: `img, svg, a, span:has(a), div:has(a)`,
|
||||
ignoreSelector: `button, [data-testid='videoPlayer'], [role='group']`,
|
||||
autoScan: `false`,
|
||||
},
|
||||
"www.youtube.com/live_chat": {
|
||||
rootsSelector: `div#items`,
|
||||
selector: `span.yt-live-chat-text-message-renderer`,
|
||||
autoScan: `false`,
|
||||
},
|
||||
"www.youtube.com": {
|
||||
rootsSelector: `ytd-page-manager`,
|
||||
ignoreSelector: `aside, button, footer, form, header, pre, mark, nav, #player, #container, .caption-window, .ytp-settings-menu`,
|
||||
},
|
||||
"web.telegram.org": {
|
||||
autoScan: `false`,
|
||||
selector: ".text-content, .embedded-text-wrapper",
|
||||
rootsSelector: ".Transition",
|
||||
},
|
||||
};
|
||||
|
||||
export const BUILTIN_RULES = Object.entries(RULES_MAP)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([pattern, rule]) => ({
|
||||
export const BUILTIN_RULES = Object.entries(RULES_MAP).map(
|
||||
([pattern, rule]) => ({
|
||||
// ...DEFAULT_RULE,
|
||||
...rule,
|
||||
pattern,
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
@@ -88,6 +88,7 @@ export const DEFAULT_TRANBOX_SETTING = {
|
||||
hideClickAway: false, // 是否点击外部关闭弹窗
|
||||
simpleStyle: false, // 是否简洁界面
|
||||
followSelection: false, // 翻译框是否跟随选中文本
|
||||
autoHeight: false, // 自适应高度
|
||||
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
|
||||
// extStyles: "", // 附加样式
|
||||
enDict: OPT_DICT_BING, // 英文词典
|
||||
@@ -113,6 +114,7 @@ export const DEFAULT_SUBTITLE_SETTING = {
|
||||
// fromLang: "en",
|
||||
toLang: "zh-CN",
|
||||
isBilingual: true, // 是否双语显示
|
||||
skipAd: false, // 是否快进广告
|
||||
windowStyle: SUBTITLE_WINDOW_STYLE, // 背景样式
|
||||
originStyle: SUBTITLE_ORIGIN_STYLE, // 原文样式
|
||||
translationStyle: SUBTITLE_TRANSLATION_STYLE, // 译文样式
|
||||
@@ -134,9 +136,9 @@ export const DEFAULT_SUBRULES_LIST = [
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_MOUSEHOVER_KEY = ["KeyQ"];
|
||||
export const DEFAULT_MOUSEHOVER_KEY = ["ControlLeft"];
|
||||
export const DEFAULT_MOUSE_HOVER_SETTING = {
|
||||
useMouseHover: true, // 是否启用鼠标悬停翻译
|
||||
useMouseHover: false, // 是否启用鼠标悬停翻译
|
||||
mouseHoverKey: DEFAULT_MOUSEHOVER_KEY, // 鼠标悬停翻译组合键
|
||||
};
|
||||
|
||||
@@ -166,7 +168,8 @@ export const DEFAULT_SETTING = {
|
||||
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
||||
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
||||
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
|
||||
touchTranslate: 2, // 触屏翻译
|
||||
// touchTranslate: 2, // 触屏翻译 {5:单指双击,6:单指三击,7:双指双击} (作废)
|
||||
touchModes: [2], // 触屏翻译 {5:单指双击,6:单指三击,7:双指双击} (多选)
|
||||
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
|
||||
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
|
||||
orilist: DEFAULT_ORILIST.join(",\n"), // 禁用CSP名单
|
||||
@@ -179,4 +182,5 @@ export const DEFAULT_SETTING = {
|
||||
transAllnow: false, // 是否立即全部翻译
|
||||
subtitleSetting: DEFAULT_SUBTITLE_SETTING, // 字幕设置
|
||||
logLevel: LogLevel.INFO.value, // 日志级别
|
||||
rootMargin: 500, // 提前触发翻译
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ export const STOKEY_RULES = `${APP_NAME}_rules_v${APP_VERSION[0]}`;
|
||||
export const STOKEY_WORDS = `${APP_NAME}_words`;
|
||||
export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
||||
export const STOKEY_FAB = `${APP_NAME}_fab`;
|
||||
export const STOKEY_TRANBOX = `${APP_NAME}_tranbox`;
|
||||
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
|
||||
|
||||
export const CACHE_NAME = `${APP_NAME}_cache`;
|
||||
|
||||
@@ -59,7 +59,11 @@ export function AlertProvider({ children }) {
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical, horizontal }}
|
||||
>
|
||||
<Alert onClose={handleClose} severity={severity} sx={{ width: "100%" }}>
|
||||
<Alert
|
||||
onClose={handleClose}
|
||||
severity={severity}
|
||||
sx={{ minWidth: "300px", maxWidth: "80%" }}
|
||||
>
|
||||
{message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { STOKEY_WORDS, KV_WORDS_KEY } from "../config";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useStorage } from "./Storage";
|
||||
import { debounceSyncMeta } from "../libs/storage";
|
||||
|
||||
const DEFAULT_FAVWORDS = {};
|
||||
|
||||
export function useFavWords() {
|
||||
const { data: favWords, save } = useStorage(
|
||||
const { data: favWords, save: saveWords } = useStorage(
|
||||
STOKEY_WORDS,
|
||||
DEFAULT_FAVWORDS,
|
||||
KV_WORDS_KEY
|
||||
);
|
||||
|
||||
const save = useCallback(
|
||||
(objOrFn) => {
|
||||
saveWords(objOrFn);
|
||||
debounceSyncMeta(KV_WORDS_KEY);
|
||||
},
|
||||
[saveWords]
|
||||
);
|
||||
|
||||
const toggleFav = useCallback(
|
||||
(word) => {
|
||||
save((prev) => {
|
||||
|
||||
@@ -2,18 +2,27 @@ import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
import { checkRules } from "../libs/rules";
|
||||
import { useCallback } from "react";
|
||||
import { debounceSyncMeta } from "../libs/storage";
|
||||
|
||||
/**
|
||||
* 规则 hook
|
||||
* @returns
|
||||
*/
|
||||
export function useRules() {
|
||||
const { data: list = [], save } = useStorage(
|
||||
const { data: list = [], save: saveRules } = useStorage(
|
||||
STOKEY_RULES,
|
||||
DEFAULT_RULES,
|
||||
KV_RULES_KEY
|
||||
);
|
||||
|
||||
const save = useCallback(
|
||||
(objOrFn) => {
|
||||
saveRules(objOrFn);
|
||||
debounceSyncMeta(KV_RULES_KEY);
|
||||
},
|
||||
[saveRules]
|
||||
);
|
||||
|
||||
const add = useCallback(
|
||||
(rule) => {
|
||||
save((prev) => {
|
||||
@@ -48,13 +57,9 @@ export function useRules() {
|
||||
const put = useCallback(
|
||||
(pattern, obj) => {
|
||||
save((prev) => {
|
||||
if (
|
||||
prev.some(
|
||||
(item) => item.pattern === obj.pattern && item.pattern !== pattern
|
||||
)
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
// if (pattern !== obj.pattern) {
|
||||
// return prev;
|
||||
// }
|
||||
return prev.map((item) =>
|
||||
item.pattern === pattern ? { ...item, ...obj } : item
|
||||
);
|
||||
@@ -71,15 +76,26 @@ export function useRules() {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const map = new Map();
|
||||
// 不进行深度合并
|
||||
// [...prev, ...adds].forEach((item) => {
|
||||
// const k = item.pattern;
|
||||
// map.set(k, { ...(map.get(k) || {}), ...item });
|
||||
// });
|
||||
prev.forEach((item) => map.set(item.pattern, item));
|
||||
adds.forEach((item) => map.set(item.pattern, item));
|
||||
return [...map.values()];
|
||||
// const map = new Map();
|
||||
// // 不进行深度合并
|
||||
// // [...prev, ...adds].forEach((item) => {
|
||||
// // const k = item.pattern;
|
||||
// // map.set(k, { ...(map.get(k) || {}), ...item });
|
||||
// // });
|
||||
// prev.forEach((item) => map.set(item.pattern, item));
|
||||
// adds.forEach((item) => map.set(item.pattern, item));
|
||||
// return [...map.values()];
|
||||
|
||||
const addsMap = new Map(adds.map((item) => [item.pattern, item]));
|
||||
const prevPatterns = new Set(prev.map((item) => item.pattern));
|
||||
const updatedPrev = prev.map(
|
||||
(prevItem) => addsMap.get(prevItem.pattern) || prevItem
|
||||
);
|
||||
const newItems = adds.filter(
|
||||
(addItem) => !prevPatterns.has(addItem.pattern)
|
||||
);
|
||||
|
||||
return [...newItems, ...updatedPrev];
|
||||
});
|
||||
},
|
||||
[save]
|
||||
|
||||
29
src/hooks/WindowSize.js
Normal file
29
src/hooks/WindowSize.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useDebouncedCallback } from "./DebouncedCallback";
|
||||
|
||||
function useWindowSize() {
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
});
|
||||
|
||||
const debounceWindowResize = useDebouncedCallback(() => {
|
||||
setWindowSize({
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
});
|
||||
}, 200);
|
||||
|
||||
useEffect(() => {
|
||||
debounceWindowResize();
|
||||
|
||||
window.addEventListener("resize", debounceWindowResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", debounceWindowResize);
|
||||
};
|
||||
}, [debounceWindowResize]);
|
||||
|
||||
return windowSize;
|
||||
}
|
||||
|
||||
export default useWindowSize;
|
||||
3
src/injector-shadowroot.js
Normal file
3
src/injector-shadowroot.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { shadowRootInjector } from "./injectors/shadowroot";
|
||||
|
||||
shadowRootInjector();
|
||||
3
src/injector-subtitle.js
Normal file
3
src/injector-subtitle.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { XMLHttpRequestInjector } from "./injectors/xmlhttp";
|
||||
|
||||
XMLHttpRequestInjector();
|
||||
@@ -1,3 +0,0 @@
|
||||
import { XMLHttpRequestInjector } from "./subtitle/XMLHttpRequestInjector";
|
||||
|
||||
XMLHttpRequestInjector();
|
||||
27
src/injectors/index.js
Normal file
27
src/injectors/index.js
Normal 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);
|
||||
}
|
||||
}
|
||||
12
src/injectors/shadowroot.js
Normal file
12
src/injectors/shadowroot.js
Normal 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
23
src/injectors/xmlhttp.js
Normal 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);
|
||||
}
|
||||
};
|
||||
18
src/libs/fabManager.js
Normal file
18
src/libs/fabManager.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import ShadowDomManager from "./shadowDomManager";
|
||||
import { APP_CONSTS } from "../config";
|
||||
import ContentFab from "../views/Action/ContentFab";
|
||||
|
||||
export class FabManager extends ShadowDomManager {
|
||||
constructor({ processActions, fabConfig }) {
|
||||
super({
|
||||
id: APP_CONSTS.fabID,
|
||||
className: "notranslate",
|
||||
reactComponent: ContentFab,
|
||||
props: { processActions, fabConfig },
|
||||
});
|
||||
|
||||
if (!fabConfig?.isHide) {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,12 @@ export const fetchGM = async (
|
||||
});
|
||||
},
|
||||
onerror: reject,
|
||||
onabort: () => {
|
||||
reject(new Error("GM request onabort."));
|
||||
},
|
||||
ontimeout: () => {
|
||||
reject(new Error("GM request timeout."));
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,18 @@ export const injectInlineJs = (code, id = "kiss-translator-inline-js") => {
|
||||
(document.head || document.documentElement).appendChild(el);
|
||||
};
|
||||
|
||||
export const injectInlineJsBg = (code, id = "kiss-translator-inline-js") => {
|
||||
if (document.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = document.createElement("script");
|
||||
el.type = "text/javascript";
|
||||
el.id = id;
|
||||
el.textContent = code;
|
||||
(document.head || document.documentElement).appendChild(el);
|
||||
};
|
||||
|
||||
// Function to inject external JavaScript file
|
||||
export const injectExternalJs = (src, id = "kiss-translator-external-js") => {
|
||||
if (document.getElementById(id)) {
|
||||
|
||||
@@ -4,11 +4,11 @@ import {
|
||||
OPT_LANGS_LIST,
|
||||
DEFAULT_API_SETTING,
|
||||
} from "../config";
|
||||
import { genEventName, removeEndchar, matchInputStr } from "./utils";
|
||||
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
|
||||
import { stepShortcutRegister } from "./shortcut";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { createLoadingSVG } from "./svg";
|
||||
import { kissLog } from "./log";
|
||||
import { logger } from "./log";
|
||||
|
||||
function isInputNode(node) {
|
||||
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
|
||||
@@ -18,21 +18,94 @@ function isEditAbleNode(node) {
|
||||
return node.hasAttribute("contenteditable");
|
||||
}
|
||||
|
||||
function replaceContentEditableText(node, newText) {
|
||||
node.focus();
|
||||
const selection = window.getSelection();
|
||||
if (!selection) return;
|
||||
async function replaceContentEditableText(node, newText) {
|
||||
try {
|
||||
logger.debug("try replace editable 1: pasteEvent");
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
node.focus();
|
||||
|
||||
range.deleteContents();
|
||||
const textNode = document.createTextNode(newText);
|
||||
range.insertNode(textNode);
|
||||
const selection = window.getSelection();
|
||||
if (!selection) throw new Error("window.getSelection() is not available.");
|
||||
|
||||
selection.collapseToEnd();
|
||||
const targetNode = node.querySelector("p") || node;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(targetNode);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.setData("text/plain", newText);
|
||||
|
||||
const pasteEvent = new ClipboardEvent("paste", {
|
||||
clipboardData: dataTransfer,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
node.dispatchEvent(pasteEvent);
|
||||
|
||||
await sleep(50);
|
||||
if (node.innerText.trim() === newText) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error("Strategy 1 failed to replace text correctly.");
|
||||
} catch (error) {
|
||||
logger.debug("Strategy 1 Failed:", error.message);
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug("try replace editable 2: execCommand");
|
||||
|
||||
node.focus();
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection) throw new Error("window.getSelection() is not available.");
|
||||
|
||||
const targetNode = node.querySelector("p") || node;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(targetNode);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
document.execCommand("insertText", false, newText);
|
||||
|
||||
await sleep(50);
|
||||
if (node.innerText.trim() === newText) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error("Strategy 2 failed to replace text correctly.");
|
||||
} catch (error) {
|
||||
logger.debug("Strategy 2 Failed:", error.message);
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug("try replace editable 3: textContent");
|
||||
|
||||
node.focus();
|
||||
|
||||
const targetNode = node.querySelector("p") || node;
|
||||
const textSpan = targetNode.querySelector('span[data-lexical-text="true"]');
|
||||
|
||||
if (textSpan) {
|
||||
textSpan.textContent = newText;
|
||||
} else {
|
||||
targetNode.textContent = newText;
|
||||
}
|
||||
|
||||
node.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
|
||||
|
||||
await sleep(50);
|
||||
if (node.innerText.trim() === newText) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error("Strategy 3 failed to replace text correctly.");
|
||||
} catch (error) {
|
||||
logger.debug("Strategy 3 Failed:", error.message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getNodeText(node) {
|
||||
@@ -107,7 +180,7 @@ export class InputTranslator {
|
||||
);
|
||||
|
||||
this.#isEnabled = true;
|
||||
kissLog("Input Translator enabled.");
|
||||
logger.info("Input Translator enabled.");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,7 +195,7 @@ export class InputTranslator {
|
||||
this.#unregisterShortcut = null;
|
||||
}
|
||||
this.#isEnabled = false;
|
||||
kissLog("Input Translator disabled.");
|
||||
logger.info("Input Translator disabled.");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,25 +266,30 @@ export class InputTranslator {
|
||||
try {
|
||||
addLoading(node, loadingId);
|
||||
|
||||
const [trText, isSame] = await apiTranslate({
|
||||
const { trText, isSame } = await apiTranslate({
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting,
|
||||
});
|
||||
|
||||
if (!trText || isSame) return;
|
||||
const newText = trText?.trim() || "";
|
||||
if (!newText || isSame) return;
|
||||
|
||||
if (isInputNode(node)) {
|
||||
node.value = trText;
|
||||
node.value = newText;
|
||||
node.dispatchEvent(
|
||||
new Event("input", { bubbles: true, cancelable: true })
|
||||
);
|
||||
} else {
|
||||
replaceContentEditableText(node, trText);
|
||||
const success = await replaceContentEditableText(node, newText);
|
||||
if (!success) {
|
||||
// todo: 提示可以黏贴
|
||||
logger.info("Replace editable text failed");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("Translate input error:", err);
|
||||
logger.info("Translate input error:", err);
|
||||
} finally {
|
||||
removeLoading(loadingId);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Sval from "sval";
|
||||
|
||||
const interpreter = new Sval({
|
||||
export const interpreter = new Sval({
|
||||
// ECMA Version of the code
|
||||
// 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15
|
||||
// or 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024
|
||||
@@ -12,5 +12,3 @@ const interpreter = new Sval({
|
||||
// Whether the code runs in a sandbox
|
||||
sandBox: true,
|
||||
});
|
||||
|
||||
export default interpreter;
|
||||
|
||||
26
src/libs/popupManager.js
Normal file
26
src/libs/popupManager.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import ShadowDomManager from "./shadowDomManager";
|
||||
import { APP_CONSTS, EVENT_KISS, MSG_POPUP_TOGGLE } from "../config";
|
||||
import Action from "../views/Action";
|
||||
|
||||
export class PopupManager extends ShadowDomManager {
|
||||
constructor({ translator, processActions }) {
|
||||
super({
|
||||
id: APP_CONSTS.popupID,
|
||||
className: "notranslate",
|
||||
reactComponent: Action,
|
||||
props: { translator, processActions },
|
||||
});
|
||||
}
|
||||
|
||||
toggle(props) {
|
||||
if (this.isVisible) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(EVENT_KISS, {
|
||||
detail: { action: MSG_POPUP_TOGGLE },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.show(props || this._props);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
// OPT_TIMING_ALL,
|
||||
DEFAULT_RULE,
|
||||
GLOBLA_RULE,
|
||||
OPT_SPLIT_PARAGRAPH_ALL,
|
||||
OPT_HIGHLIGHT_WORDS_ALL,
|
||||
} from "../config";
|
||||
import { loadOrFetchSubRules } from "./subRules";
|
||||
import { getRulesWithDefault, setRules } from "./storage";
|
||||
@@ -52,11 +54,13 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
|
||||
"ignoreSelector",
|
||||
"terms",
|
||||
"aiTerms",
|
||||
"termsStyle",
|
||||
"highlightStyle",
|
||||
"selectStyle",
|
||||
"parentStyle",
|
||||
"grandStyle",
|
||||
"injectJs",
|
||||
"injectCss",
|
||||
// "injectCss",
|
||||
// "fixerSelector",
|
||||
"transStartHook",
|
||||
"transEndHook",
|
||||
@@ -81,12 +85,20 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
|
||||
"transTitle",
|
||||
// "detectRemote",
|
||||
// "fixerFunc",
|
||||
"splitParagraph",
|
||||
"highlightWords",
|
||||
].forEach((key) => {
|
||||
if (!rule[key] || rule[key] === GLOBAL_KEY) {
|
||||
rule[key] = globalRule[key];
|
||||
}
|
||||
});
|
||||
|
||||
["splitLength"].forEach((key) => {
|
||||
if (!rule[key]) {
|
||||
rule[key] = globalRule[key];
|
||||
}
|
||||
});
|
||||
|
||||
// if (!rule.skipLangs || rule.skipLangs.length === 0) {
|
||||
// rule.skipLangs = globalRule.skipLangs;
|
||||
// }
|
||||
@@ -136,11 +148,13 @@ export const checkRules = (rules) => {
|
||||
ignoreSelector,
|
||||
terms,
|
||||
aiTerms,
|
||||
termsStyle,
|
||||
highlightStyle,
|
||||
selectStyle,
|
||||
parentStyle,
|
||||
grandStyle,
|
||||
injectJs,
|
||||
injectCss,
|
||||
// injectCss,
|
||||
apiSlug,
|
||||
fromLang,
|
||||
toLang,
|
||||
@@ -162,6 +176,9 @@ export const checkRules = (rules) => {
|
||||
transStartHook,
|
||||
transEndHook,
|
||||
// transRemoveHook,
|
||||
splitParagraph,
|
||||
splitLength,
|
||||
highlightWords,
|
||||
}) => ({
|
||||
pattern: pattern.trim(),
|
||||
selector: type(selector) === "string" ? selector : "",
|
||||
@@ -170,11 +187,13 @@ export const checkRules = (rules) => {
|
||||
ignoreSelector: type(ignoreSelector) === "string" ? ignoreSelector : "",
|
||||
terms: type(terms) === "string" ? terms : "",
|
||||
aiTerms: type(aiTerms) === "string" ? aiTerms : "",
|
||||
termsStyle: type(termsStyle) === "string" ? termsStyle : "",
|
||||
highlightStyle: type(highlightStyle) === "string" ? highlightStyle : "",
|
||||
selectStyle: type(selectStyle) === "string" ? selectStyle : "",
|
||||
parentStyle: type(parentStyle) === "string" ? parentStyle : "",
|
||||
grandStyle: type(grandStyle) === "string" ? grandStyle : "",
|
||||
injectJs: type(injectJs) === "string" ? injectJs : "",
|
||||
injectCss: type(injectCss) === "string" ? injectCss : "",
|
||||
// injectCss: type(injectCss) === "string" ? injectCss : "",
|
||||
bgColor: type(bgColor) === "string" ? bgColor : "",
|
||||
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
|
||||
apiSlug:
|
||||
@@ -200,6 +219,15 @@ export const checkRules = (rules) => {
|
||||
// transRemoveHook:
|
||||
// type(transRemoveHook) === "string" ? transRemoveHook : "",
|
||||
// fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc),
|
||||
splitParagraph: matchValue(
|
||||
[GLOBAL_KEY, ...OPT_SPLIT_PARAGRAPH_ALL],
|
||||
splitParagraph
|
||||
),
|
||||
splitLength: Number.isInteger(splitLength) ? splitLength : 0,
|
||||
highlightWords: matchValue(
|
||||
[GLOBAL_KEY, ...OPT_HIGHLIGHT_WORDS_ALL],
|
||||
highlightWords
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
128
src/libs/shadowDomManager.js
Normal file
128
src/libs/shadowDomManager.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import createCache from "@emotion/cache";
|
||||
import { logger } from "./log";
|
||||
|
||||
export default class ShadowDomManager {
|
||||
#hostElement = null;
|
||||
#reactRoot = null;
|
||||
#isVisible = false;
|
||||
#isProcessing = false;
|
||||
|
||||
_id;
|
||||
_className;
|
||||
_ReactComponent;
|
||||
_props;
|
||||
|
||||
constructor({ id, className = "", reactComponent, props = {} }) {
|
||||
if (!id || !reactComponent) {
|
||||
throw new Error("ID and a React Component must be provided.");
|
||||
}
|
||||
this._id = id;
|
||||
this._className = className;
|
||||
this._ReactComponent = reactComponent;
|
||||
this._props = props;
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
return this.#isVisible;
|
||||
}
|
||||
|
||||
show(props) {
|
||||
if (this.#isVisible || this.#isProcessing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#hostElement) {
|
||||
this.#isProcessing = true;
|
||||
try {
|
||||
this.#mount(props || this._props);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to mount component with id "${this._id}":`, error);
|
||||
this.#isProcessing = false;
|
||||
return;
|
||||
} finally {
|
||||
this.#isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.#hostElement.style.display = "";
|
||||
this.#isVisible = true;
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.#isVisible || !this.#hostElement) {
|
||||
return;
|
||||
}
|
||||
this.#hostElement.style.display = "none";
|
||||
this.#isVisible = false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.#hostElement) {
|
||||
return;
|
||||
}
|
||||
this.#isProcessing = true;
|
||||
|
||||
if (this.#reactRoot) {
|
||||
this.#reactRoot.unmount();
|
||||
}
|
||||
|
||||
this.#hostElement.remove();
|
||||
|
||||
this.#hostElement = null;
|
||||
this.#reactRoot = null;
|
||||
this.#isVisible = false;
|
||||
this.#isProcessing = false;
|
||||
logger.info(`Component with id "${this._id}" has been destroyed.`);
|
||||
}
|
||||
|
||||
toggle(props) {
|
||||
if (this.#isVisible) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show(props || this._props);
|
||||
}
|
||||
}
|
||||
|
||||
#mount(props) {
|
||||
const host = document.createElement("div");
|
||||
host.id = this._id;
|
||||
if (this._className) {
|
||||
host.className = this._className;
|
||||
}
|
||||
host.style.display = "none";
|
||||
document.body.parentElement.appendChild(host);
|
||||
this.#hostElement = host;
|
||||
|
||||
const shadowContainer = host.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
const appRoot = document.createElement("div");
|
||||
appRoot.className = `${this._id}_wrapper`;
|
||||
|
||||
shadowContainer.appendChild(emotionRoot);
|
||||
shadowContainer.appendChild(appRoot);
|
||||
|
||||
const cache = createCache({
|
||||
key: this._id,
|
||||
prepend: true,
|
||||
container: emotionRoot,
|
||||
});
|
||||
|
||||
const enhancedProps = {
|
||||
...props,
|
||||
onClose: this.hide.bind(this),
|
||||
};
|
||||
|
||||
const ComponentToRender = this._ReactComponent;
|
||||
this.#reactRoot = ReactDOM.createRoot(appRoot);
|
||||
this.#reactRoot.render(
|
||||
<React.StrictMode>
|
||||
<CacheProvider value={cache}>
|
||||
<ComponentToRender {...enhancedProps} />
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { kissLog } from "./log";
|
||||
|
||||
/**
|
||||
* @class ShadowRootMonitor
|
||||
* @description 通过覆写 Element.prototype.attachShadow 来监控页面上所有新创建的 Shadow DOM
|
||||
*/
|
||||
export 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;
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@ export const shortcutRegister = (targetKeys = [], fn, target = document) => {
|
||||
const targetKeySet = new Set(targetKeys);
|
||||
const onKeyDown = (pressedKeys, event) => {
|
||||
if (isSameSet(targetKeySet, pressedKeys)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// event.preventDefault(); // 阻止浏览器的默认行为
|
||||
// event.stopPropagation(); // 阻止事件继续(向父元素)冒泡
|
||||
fn();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
STOKEY_RULES_OLD,
|
||||
STOKEY_WORDS,
|
||||
STOKEY_FAB,
|
||||
STOKEY_TRANBOX,
|
||||
STOKEY_SYNC,
|
||||
STOKEY_MSAUTH,
|
||||
STOKEY_BDAUTH,
|
||||
@@ -135,6 +136,13 @@ export const getFabWithDefault = async () => (await getFab()) || {};
|
||||
export const setFab = (obj) => setObj(STOKEY_FAB, obj);
|
||||
export const putFab = (obj) => putObj(STOKEY_FAB, obj);
|
||||
|
||||
/**
|
||||
* tranbox位置大小
|
||||
*/
|
||||
export const getTranBox = () => getObj(STOKEY_TRANBOX);
|
||||
export const putTranBox = (obj) => putObj(STOKEY_TRANBOX, obj);
|
||||
export const debouncePutTranBox = debounce(putTranBox, 300);
|
||||
|
||||
/**
|
||||
* 数据同步
|
||||
*/
|
||||
|
||||
@@ -84,7 +84,7 @@ const genStyles = ({
|
||||
// 虚线框
|
||||
[OPT_STYLE_DASHBOX]: `
|
||||
border: 2px dashed ${bgColor || DEFAULT_COLOR};
|
||||
display: inline-block;
|
||||
display: block;
|
||||
padding: 0.2em 0.4em;
|
||||
box-sizing: border-box;
|
||||
`,
|
||||
|
||||
@@ -67,9 +67,9 @@ export function createLoadingSVG() {
|
||||
* @returns
|
||||
*/
|
||||
export function createLogoSVG({
|
||||
width = "100%",
|
||||
height = "100%",
|
||||
viewBox = "-20 -20 70 70",
|
||||
width = "24",
|
||||
height = "24",
|
||||
viewBox = "-5 -5 40 40",
|
||||
isSelected = false,
|
||||
} = {}) {
|
||||
const svg = createSVGElement("svg", {
|
||||
@@ -80,30 +80,26 @@ export function createLogoSVG({
|
||||
version: "1.1",
|
||||
});
|
||||
|
||||
const primaryColor = "#209CEE";
|
||||
const secondaryColor = "#E9F5FD";
|
||||
|
||||
const path1Fill = isSelected ? primaryColor : secondaryColor;
|
||||
const path2Fill = isSelected ? secondaryColor : primaryColor;
|
||||
|
||||
const path1 = createSVGElement("path", {
|
||||
d: "M0 0 C10.56 0 21.12 0 32 0 C32 10.56 32 21.12 32 32 C21.44 32 10.88 32 0 32 C0 21.44 0 10.88 0 0 Z ",
|
||||
fill: "#209CEE",
|
||||
fill: path1Fill,
|
||||
transform: "translate(0,0)",
|
||||
});
|
||||
|
||||
const path2 = createSVGElement("path", {
|
||||
d: "M0 0 C0.66 0 1.32 0 2 0 C2 2.97 2 5.94 2 9 C2.969375 8.2575 3.93875 7.515 4.9375 6.75 C5.48277344 6.33234375 6.02804688 5.9146875 6.58984375 5.484375 C8.39053593 3.83283924 8.39053593 3.83283924 9 0 C13.95 0 18.9 0 24 0 C24 0.99 24 1.98 24 3 C22.68 3 21.36 3 20 3 C20 9.27 20 15.54 20 22 C19.01 22 18.02 22 17 22 C17 15.73 17 9.46 17 3 C15.35 3 13.7 3 12 3 C11.731875 3.598125 11.46375 4.19625 11.1875 4.8125 C10.01506533 6.97224808 8.80630718 8.35790256 7 10 C8.01790655 12.27071461 8.77442829 13.80784632 10.6875 15.4375 C11.120625 15.953125 11.55375 16.46875 12 17 C11.6875 19.6875 11.6875 19.6875 11 22 C10.34 22 9.68 22 9 22 C8.773125 21.236875 8.54625 20.47375 8.3125 19.6875 C6.73268318 16.45263699 5.16717283 15.58358642 2 14 C2 16.64 2 19.28 2 22 C1.34 22 0.68 22 0 22 C0 14.74 0 7.48 0 0 Z ",
|
||||
fill: "#E9F5FD",
|
||||
fill: path2Fill,
|
||||
transform: "translate(4,5)",
|
||||
});
|
||||
|
||||
svg.appendChild(path1);
|
||||
svg.appendChild(path2);
|
||||
|
||||
if (isSelected) {
|
||||
const redLine = createSVGElement("path", {
|
||||
d: "M0 36 L32 36",
|
||||
stroke: "red",
|
||||
"stroke-width": "3",
|
||||
"stroke-linecap": "round",
|
||||
});
|
||||
svg.appendChild(redLine);
|
||||
}
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,47 @@
|
||||
export function touchTapListener(fn, touchsLength) {
|
||||
export function touchTapListener(fn, options = {}) {
|
||||
const config = {
|
||||
taps: 2,
|
||||
fingers: 1,
|
||||
delay: 300,
|
||||
...options,
|
||||
};
|
||||
|
||||
let maxTouches = 0;
|
||||
let tapCount = 0;
|
||||
let tapTimer = null;
|
||||
|
||||
const handleTouchStart = (e) => {
|
||||
maxTouches = Math.max(maxTouches, e.touches.length);
|
||||
};
|
||||
|
||||
const handleTouchend = (e) => {
|
||||
if (e.touches.length === touchsLength) {
|
||||
fn();
|
||||
if (e.touches.length === 0) {
|
||||
if (maxTouches === config.fingers) {
|
||||
tapCount++;
|
||||
clearTimeout(tapTimer);
|
||||
|
||||
if (tapCount === config.taps) {
|
||||
fn(e);
|
||||
tapCount = 0;
|
||||
} else {
|
||||
tapTimer = setTimeout(() => {
|
||||
tapCount = 0;
|
||||
}, config.delay);
|
||||
}
|
||||
} else {
|
||||
tapCount = 0;
|
||||
clearTimeout(tapTimer);
|
||||
}
|
||||
maxTouches = 0;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("touchstart", handleTouchend);
|
||||
document.addEventListener("touchstart", handleTouchStart, { passive: true });
|
||||
document.addEventListener("touchend", handleTouchend, { passive: true });
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("touchstart", handleTouchend);
|
||||
clearTimeout(tapTimer);
|
||||
document.removeEventListener("touchstart", handleTouchStart);
|
||||
document.removeEventListener("touchend", handleTouchend);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,43 +1,31 @@
|
||||
import {
|
||||
APP_NAME,
|
||||
APP_UPNAME,
|
||||
APP_LCNAME,
|
||||
APP_CONSTS,
|
||||
MSG_INJECT_JS,
|
||||
MSG_INJECT_CSS,
|
||||
OPT_STYLE_FUZZY,
|
||||
GLOBLA_RULE,
|
||||
DEFAULT_SETTING,
|
||||
// DEFAULT_MOUSEHOVER_KEY,
|
||||
OPT_STYLE_NONE,
|
||||
DEFAULT_API_SETTING,
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
MSG_OPEN_TRANBOX,
|
||||
MSG_TRANSBOX_TOGGLE,
|
||||
MSG_MOUSEHOVER_TOGGLE,
|
||||
MSG_TRANSINPUT_TOGGLE,
|
||||
OPT_HIGHLIGHT_WORDS_BEFORETRANS,
|
||||
OPT_HIGHLIGHT_WORDS_AFTERTRANS,
|
||||
OPT_SPLIT_PARAGRAPH_PUNCTUATION,
|
||||
OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||
OPT_SPLIT_PARAGRAPH_TEXTLENGTH,
|
||||
} from "../config";
|
||||
import interpreter from "./interpreter";
|
||||
import { ShadowRootMonitor } from "./shadowroot";
|
||||
import { interpreter } from "./interpreter";
|
||||
import { clearFetchPool } from "./pool";
|
||||
import { debounce, scheduleIdle, genEventName, truncateWords } from "./utils";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { sendBgMsg } from "./msg";
|
||||
import { isExt } from "./client";
|
||||
import { injectInlineJs, injectInternalCss } from "./injector";
|
||||
import { kissLog } from "./log";
|
||||
import { clearAllBatchQueue } from "./batchQueue";
|
||||
import { genTextClass } from "./style";
|
||||
import { createLoadingSVG } from "./svg";
|
||||
import { shortcutRegister } from "./shortcut";
|
||||
import { tryDetectLang } from "./detect";
|
||||
import { browser } from "./browser";
|
||||
import { isIframe, sendIframeMsg } from "./iframe";
|
||||
import { TransboxManager } from "./tranbox";
|
||||
import { InputTranslator } from "./inputTranslate";
|
||||
import { trustedTypesHelper } from "./trustedTypes";
|
||||
import { injectJs, INJECTOR } from "../injectors";
|
||||
|
||||
/**
|
||||
* @class Translator
|
||||
@@ -84,7 +72,7 @@ export class Translator {
|
||||
"VIDEO",
|
||||
]),
|
||||
INLINE: new Set([
|
||||
"A",
|
||||
// "A",
|
||||
"ABBR",
|
||||
"ACRONYM",
|
||||
"B",
|
||||
@@ -113,7 +101,7 @@ export class Translator {
|
||||
"SCRIPT",
|
||||
"SELECT",
|
||||
"SMALL",
|
||||
"SPAN",
|
||||
// "SPAN",
|
||||
"STRONG",
|
||||
"SUB",
|
||||
"SUP",
|
||||
@@ -164,6 +152,8 @@ export class Translator {
|
||||
warpper: `${APP_LCNAME}-wrapper notranslate`,
|
||||
inner: `${APP_LCNAME}-inner`,
|
||||
term: `${APP_LCNAME}-term`,
|
||||
br: `${APP_LCNAME}-br`,
|
||||
highlight: `${APP_LCNAME}-highlight`,
|
||||
};
|
||||
|
||||
// 内置跳过翻译文本
|
||||
@@ -211,11 +201,17 @@ export class Translator {
|
||||
|
||||
// 14. 包含常见扩展名的文件名 (例如: document.pdf, image.jpeg)
|
||||
/^[^\s\\/:]+?\.[a-zA-Z0-9]{2,5}$/,
|
||||
|
||||
// todo: 数字和特殊字符组成的字符串
|
||||
];
|
||||
|
||||
static DEFAULT_OPTIONS = DEFAULT_SETTING; // 默认配置
|
||||
static DEFAULT_RULE = GLOBLA_RULE; // 默认规则
|
||||
|
||||
static isElement(el) {
|
||||
return el instanceof Element;
|
||||
}
|
||||
|
||||
static isElementOrFragment(el) {
|
||||
return el instanceof Element || el instanceof DocumentFragment;
|
||||
}
|
||||
@@ -226,6 +222,7 @@ export class Translator {
|
||||
|
||||
if (Translator.TAGS.INLINE.has(el.nodeName)) return false;
|
||||
if (Translator.TAGS.BLOCK.has(el.nodeName)) return true;
|
||||
if (el.attributes?.display?.value?.includes("inline")) return false;
|
||||
|
||||
if (Translator.displayCache.has(el)) {
|
||||
return Translator.displayCache.get(el);
|
||||
@@ -236,11 +233,22 @@ export class Translator {
|
||||
return isBlock;
|
||||
}
|
||||
|
||||
// 判断是否包含块级子元素
|
||||
static hasBlockNode(el) {
|
||||
if (!Translator.isElementOrFragment(el)) return false;
|
||||
for (const child of el.childNodes) {
|
||||
if (Translator.isBlockNode(child)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 判断是否直接包含非空文本节点
|
||||
static hasTextNode(el) {
|
||||
if (!Translator.isElementOrFragment(el)) return false;
|
||||
for (const node of el.childNodes) {
|
||||
if (node.nodeType === Node.TEXT_NODE && /\S/.test(node.nodeValue)) {
|
||||
for (const child of el.childNodes) {
|
||||
if (child.nodeType === Node.TEXT_NODE && /\S/.test(child.nodeValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -253,18 +261,23 @@ export class Translator {
|
||||
}
|
||||
|
||||
// 内置忽略元素
|
||||
static BUILTIN_IGNORE_SELECTOR = `abbr, address, area, audio, br, canvas, code,
|
||||
data, datalist, dfn, embed, head, iframe, img, input, kbd, noscript, map,
|
||||
object, option, output, param, picture, progress,
|
||||
samp, select, script, style, sub, sup, svg, track, time, textarea, template,
|
||||
var, video, wbr, .notranslate, [contenteditable], [translate='no'],
|
||||
${APP_LCNAME}, #${APP_CONSTS.fabID}, #${APP_CONSTS.boxID},
|
||||
.${APP_CONSTS.fabID}_warpper, .${APP_CONSTS.boxID}_warpper`;
|
||||
static KISS_IGNORE_SELECTOR = `${APP_LCNAME}, .kiss-caption-container, .kiss-subtitle-controls
|
||||
#${APP_CONSTS.fabID}, .${APP_CONSTS.fabID}_warpper,
|
||||
#${APP_CONSTS.boxID}, .${APP_CONSTS.boxID}_warpper,
|
||||
#${APP_CONSTS.popupID}, .${APP_CONSTS.popupID}_warpper`;
|
||||
|
||||
static BUILTIN_IGNORE_SELECTOR = `address, area, audio, br, canvas,
|
||||
data, datalist, embed, head, iframe, input, noscript, map,
|
||||
object, option, param, picture, progress,
|
||||
select, script, style, track, textarea, template,
|
||||
video, wbr, .notranslate, [contenteditable], [translate='no'],
|
||||
${Translator.KISS_IGNORE_SELECTOR}`;
|
||||
|
||||
#setting; // 设置选项
|
||||
#rule; // 规则
|
||||
#isInitialized = false; // 初始化状态
|
||||
#isJsInjected = false; // 注入用户JS
|
||||
#isShadowRootJsInjected = false; //
|
||||
#mouseHoverEnabled = false; // 鼠标悬停翻译
|
||||
#enabled = false; // 全局默认状态
|
||||
#runId = 0; // 用于中止过期的异步请求
|
||||
@@ -272,49 +285,55 @@ export class Translator {
|
||||
#combinedTermsRegex; // 专业术语正则表达式
|
||||
#combinedSkipsRegex; // 跳过文本正则表达式
|
||||
#placeholderRegex; // 恢复htnml正则表达式
|
||||
#translationTagName = APP_NAME; // 翻译容器的标签名
|
||||
#translationTagName = APP_UPNAME; // 翻译容器的标签名
|
||||
#eventName = ""; // 通信事件名称
|
||||
#docInfo = {}; // 网页信息
|
||||
#glossary = {}; // AI词典
|
||||
#textClass = {}; // 译文样式class
|
||||
#textSheet = ""; // 译文样式字典
|
||||
|
||||
#isUserscript = false;
|
||||
#transboxManager = null; // 划词翻译
|
||||
#inputTranslator = null; // 输入框翻译
|
||||
#apisMap = new Map(); // 用于接口快速查找
|
||||
#favWords = []; // 收藏词汇
|
||||
|
||||
#observedNodes = new WeakSet(); // 存储所有被识别出的、可翻译的 DOM 节点单元
|
||||
#translationNodes = new WeakMap(); // 存储所有插入到页面的译文节点
|
||||
#viewNodes = new Set(); // 当前在可视范围内的单元
|
||||
#processedNodes = new WeakMap(); // 已处理(已执行翻译DOM操作)的单元
|
||||
#rootNodes = new Set(); // 已监控的根节点
|
||||
#skipMoNodes = new WeakSet(); // 忽略变化的节点
|
||||
|
||||
#removeKeydownHandler; // 快捷键清理函数
|
||||
#hoveredNode = null; // 存储当前悬停的可翻译节点
|
||||
#boundMouseMoveHandler; // 鼠标事件
|
||||
#boundKeyDownHandler; // 键盘事件
|
||||
#windowMessageHandler = null;
|
||||
|
||||
#debouncedFindShadowRoot = null;
|
||||
|
||||
#io; // IntersectionObserver
|
||||
#mo; // MutationObserver
|
||||
#dmm; // DebounceMouseMover
|
||||
#srm; // ShadowRootMonitor
|
||||
|
||||
#rescanQueue = new Set(); // “脏容器”队列
|
||||
#isQueueProcessing = false; // 队列处理状态标志
|
||||
|
||||
// 忽略元素
|
||||
get #ignoreSelector() {
|
||||
if (this.#rule.autoScan === "false") {
|
||||
return `${Translator.KISS_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
|
||||
}
|
||||
|
||||
return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
|
||||
}
|
||||
|
||||
// 接口参数
|
||||
// todo: 不用频繁查找计算
|
||||
get #apiSetting() {
|
||||
return (
|
||||
this.#setting.transApis.find(
|
||||
(api) => api.apiSlug === this.#rule.apiSlug
|
||||
) || DEFAULT_API_SETTING
|
||||
);
|
||||
// return (
|
||||
// this.#setting.transApis.find(
|
||||
// (api) => api.apiSlug === this.#rule.apiSlug
|
||||
// ) || DEFAULT_API_SETTING
|
||||
// );
|
||||
return this.#apisMap.get(this.#rule.apiSlug) || DEFAULT_API_SETTING;
|
||||
}
|
||||
|
||||
// 占位符
|
||||
@@ -328,11 +347,14 @@ export class Translator {
|
||||
};
|
||||
}
|
||||
|
||||
constructor(rule = {}, setting = {}, isUserscript = false) {
|
||||
constructor({ rule = {}, setting = {}, favWords = [] }) {
|
||||
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
|
||||
this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
|
||||
this.#favWords = favWords;
|
||||
this.#apisMap = new Map(
|
||||
this.#setting.transApis.map((api) => [api.apiSlug, api])
|
||||
);
|
||||
|
||||
this.#isUserscript = isUserscript;
|
||||
this.#eventName = genEventName();
|
||||
this.#docInfo = {
|
||||
title: document.title,
|
||||
@@ -352,31 +374,18 @@ export class Translator {
|
||||
this.#io = this.#createIntersectionObserver();
|
||||
this.#mo = this.#createMutationObserver();
|
||||
this.#dmm = this.#createDebounceMouseMover();
|
||||
this.#srm = this.#createShadowRootMonitor();
|
||||
|
||||
// 监控shadowroot
|
||||
if (this.#rule.hasShadowroot === "true") {
|
||||
this.#srm.start();
|
||||
}
|
||||
this.#windowMessageHandler = this.#handleWindowMessage.bind(this);
|
||||
this.#debouncedFindShadowRoot = debounce(
|
||||
this.#findAndObserveShadowRoot.bind(this),
|
||||
300
|
||||
);
|
||||
|
||||
// 鼠标悬停翻译
|
||||
if (this.#setting.mouseHoverSetting.useMouseHover) {
|
||||
this.#enableMouseHover();
|
||||
}
|
||||
|
||||
if (!isIframe) {
|
||||
// 监听后端事件
|
||||
if (!isUserscript) {
|
||||
this.#runtimeListener();
|
||||
}
|
||||
|
||||
// 划词翻译
|
||||
this.#transboxManager = new TransboxManager(this.setting);
|
||||
|
||||
// 输入框翻译
|
||||
this.#inputTranslator = new InputTranslator(this.setting);
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => this.#run());
|
||||
} else {
|
||||
@@ -407,53 +416,42 @@ export class Translator {
|
||||
this.#startObserveRoot(root);
|
||||
});
|
||||
|
||||
// 查找现有的所有shadowroot
|
||||
if (this.#rule.hasShadowroot === "true") {
|
||||
try {
|
||||
this.#findAllShadowRoots().forEach((shadowRoot) => {
|
||||
this.#startObserveShadowRoot(shadowRoot);
|
||||
});
|
||||
} catch (err) {
|
||||
kissLog("findAllShadowRoots", err);
|
||||
}
|
||||
this.#attachShadowRootListener();
|
||||
this.#findAndObserveShadowRoot();
|
||||
}
|
||||
}
|
||||
|
||||
// 监听后端事件
|
||||
#runtimeListener() {
|
||||
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
this.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
this.toggleStyle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
case MSG_TRANS_GETRULE:
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
this.updateRule(args);
|
||||
sendIframeMsg(MSG_TRANS_PUTRULE, args);
|
||||
break;
|
||||
case MSG_OPEN_TRANBOX:
|
||||
window.dispatchEvent(new CustomEvent(MSG_OPEN_TRANBOX));
|
||||
break;
|
||||
case MSG_TRANSBOX_TOGGLE:
|
||||
this.toggleTransbox();
|
||||
break;
|
||||
case MSG_MOUSEHOVER_TOGGLE:
|
||||
this.toggleMouseHover();
|
||||
break;
|
||||
case MSG_TRANSINPUT_TOGGLE:
|
||||
this.toggleInputTranslate();
|
||||
break;
|
||||
default:
|
||||
return { error: `message action is unavailable: ${action}` };
|
||||
}
|
||||
return { rule: this.rule, setting: this.setting };
|
||||
});
|
||||
#handleWindowMessage(event) {
|
||||
if (event.data?.type === "KISS_SHADOW_ROOT_CREATED") {
|
||||
this.#debouncedFindShadowRoot();
|
||||
}
|
||||
}
|
||||
|
||||
#attachShadowRootListener() {
|
||||
if (!this.#isShadowRootJsInjected) {
|
||||
const id = "kiss-translator-inject-shadowroot-js";
|
||||
injectJs(INJECTOR.shadowroot, id);
|
||||
|
||||
this.#isShadowRootJsInjected = true;
|
||||
}
|
||||
|
||||
window.addEventListener("message", this.#windowMessageHandler);
|
||||
}
|
||||
|
||||
#removeShadowRootListener() {
|
||||
window.removeEventListener("message", this.#windowMessageHandler);
|
||||
}
|
||||
|
||||
// 查找现有的所有shadowroot
|
||||
#findAndObserveShadowRoot() {
|
||||
try {
|
||||
this.#findAllShadowRoots().forEach((shadowRoot) => {
|
||||
this.#startObserveShadowRoot(shadowRoot);
|
||||
});
|
||||
} catch (err) {
|
||||
kissLog("findAllShadowRoots", err);
|
||||
}
|
||||
}
|
||||
|
||||
#createPlaceholderRegex() {
|
||||
@@ -554,11 +552,13 @@ export class Translator {
|
||||
|
||||
// 监控翻译单元的可见性
|
||||
#createIntersectionObserver() {
|
||||
const { transInterval, rootMargin = 500 } = this.#setting;
|
||||
|
||||
const pending = new Set();
|
||||
const flush = debounce(() => {
|
||||
pending.forEach((node) => this.#performSyncNode(node));
|
||||
pending.clear();
|
||||
}, this.#setting.transInterval);
|
||||
}, transInterval);
|
||||
|
||||
return new IntersectionObserver(
|
||||
(entries) => {
|
||||
@@ -572,7 +572,7 @@ export class Translator {
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.01 }
|
||||
{ threshold: 0.01, rootMargin: `${rootMargin}px 0px ${rootMargin}px 0px` }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -581,28 +581,34 @@ export class Translator {
|
||||
return new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (
|
||||
mutation.type === "characterData" &&
|
||||
mutation.oldValue !== mutation.target.nodeValue
|
||||
this.#skipMoNodes.has(mutation.target) ||
|
||||
mutation.nextSibling?.tagName === this.#translationTagName
|
||||
) {
|
||||
this.#queueForRescan(mutation.target.parentElement);
|
||||
} else if (mutation.type === "childList") {
|
||||
if (mutation.nextSibling?.tagName === this.#translationTagName) {
|
||||
// 恢复原文时插入元素,忽略
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mutation.type === "characterData") {
|
||||
if (
|
||||
mutation.oldValue !== mutation.target.nodeValue &&
|
||||
!this.#combinedSkipsRegex.test(mutation.target.nodeValue)
|
||||
) {
|
||||
this.#queueForRescan(mutation.target.parentElement);
|
||||
}
|
||||
} else if (mutation.type === "childList") {
|
||||
let nodes = new Set();
|
||||
let hasText = false;
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (/\S/.test(node.nodeValue)) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
hasText = true;
|
||||
} else if (
|
||||
Translator.isElementOrFragment(node) &&
|
||||
node.nodeName !== this.#translationTagName
|
||||
) {
|
||||
nodes.add(node);
|
||||
}
|
||||
if (
|
||||
this.#skipMoNodes.has(node) ||
|
||||
node.nodeName === this.#translationTagName
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
hasText = true;
|
||||
} else if (Translator.isElementOrFragment(node)) {
|
||||
nodes.add(node);
|
||||
}
|
||||
});
|
||||
if (hasText) {
|
||||
@@ -639,13 +645,6 @@ export class Translator {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 创建shadowroot的回调
|
||||
#createShadowRootMonitor() {
|
||||
return new ShadowRootMonitor((shadowRoot) => {
|
||||
this.#startObserveShadowRoot(shadowRoot);
|
||||
});
|
||||
}
|
||||
|
||||
// 跟踪鼠标下的可翻译节点
|
||||
#handleMouseMove(event) {
|
||||
let targetNode = event.composedPath()[0];
|
||||
@@ -775,6 +774,13 @@ export class Translator {
|
||||
|
||||
// 开始/重新监控节点
|
||||
#startObserveNode(node) {
|
||||
// todo: DocumentFragment 无法被 this.#io.observe
|
||||
if (!Translator.isElement(node)) return;
|
||||
|
||||
if (this.#rule.highlightWords === OPT_HIGHLIGHT_WORDS_BEFORETRANS) {
|
||||
this.#highlightWordsDeeply(node);
|
||||
}
|
||||
|
||||
if (
|
||||
!this.#observedNodes.has(node) &&
|
||||
this.#enabled &&
|
||||
@@ -816,6 +822,7 @@ export class Translator {
|
||||
#scanNode(rootNode) {
|
||||
if (
|
||||
!Translator.isElementOrFragment(rootNode) ||
|
||||
// rootNode.matches?.(this.#rule.keepSelector) ||
|
||||
rootNode.matches?.(this.#ignoreSelector)
|
||||
) {
|
||||
return;
|
||||
@@ -827,13 +834,24 @@ export class Translator {
|
||||
}
|
||||
|
||||
const hasText = Translator.hasTextNode(rootNode);
|
||||
if (hasText) {
|
||||
|
||||
if (!hasText && rootNode.children.length === 1) {
|
||||
this.#scanNode(rootNode.children[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasBlock = Translator.hasBlockNode(rootNode);
|
||||
|
||||
if (hasText || !hasBlock) {
|
||||
this.#startObserveNode(rootNode);
|
||||
}
|
||||
|
||||
for (const child of rootNode.children) {
|
||||
if (!hasText || Translator.isBlockNode(child)) {
|
||||
this.#scanNode(child);
|
||||
if (hasBlock) {
|
||||
for (const child of rootNode.children) {
|
||||
const isBlock = Translator.isBlockNode(child);
|
||||
if (!hasText || isBlock) {
|
||||
this.#scanNode(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -856,7 +874,12 @@ export class Translator {
|
||||
|
||||
// 提前进行语言检测
|
||||
let deLang = "";
|
||||
const { fromLang = "auto", toLang } = this.#rule;
|
||||
const {
|
||||
fromLang = "auto",
|
||||
toLang,
|
||||
splitParagraph = OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||
splitLength = 100,
|
||||
} = this.#rule;
|
||||
const { langDetector, skipLangs = [] } = this.#setting;
|
||||
if (fromLang === "auto") {
|
||||
deLang = await tryDetectLang(node.textContent, langDetector);
|
||||
@@ -871,6 +894,11 @@ export class Translator {
|
||||
}
|
||||
}
|
||||
|
||||
// 切分长段落
|
||||
if (splitParagraph !== OPT_SPLIT_PARAGRAPH_DISABLE) {
|
||||
this.#splitTextNodesBySentence(node, splitParagraph, splitLength);
|
||||
}
|
||||
|
||||
let nodeGroup = [];
|
||||
[...node.childNodes].forEach((child) => {
|
||||
const shouldBreak = this.#shouldBreak(child);
|
||||
@@ -890,6 +918,171 @@ export class Translator {
|
||||
}
|
||||
}
|
||||
|
||||
// 高亮词汇
|
||||
#highlightTextNode(textNode, wordRegex) {
|
||||
if (textNode.parentNode?.nodeName.toLowerCase() === "b") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wordRegex.test(textNode.textContent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
wordRegex.lastIndex = 0;
|
||||
const fragments = textNode.textContent.split(wordRegex);
|
||||
const newNodes = [];
|
||||
|
||||
fragments.forEach((fragment, i) => {
|
||||
if (!fragment) return;
|
||||
|
||||
if (i % 2 === 1) {
|
||||
// 奇数索引是匹配到的关键词
|
||||
const bTag = document.createElement("b");
|
||||
bTag.className = Translator.KISS_CLASS.highlight;
|
||||
bTag.style.cssText = this.#rule.highlightStyle || "";
|
||||
bTag.textContent = fragment;
|
||||
this.#skipMoNodes.add(bTag);
|
||||
newNodes.push(bTag);
|
||||
} else {
|
||||
// 偶数索引是普通文本
|
||||
const newTextNode = document.createTextNode(fragment);
|
||||
this.#skipMoNodes.add(newTextNode);
|
||||
newNodes.push(newTextNode);
|
||||
}
|
||||
});
|
||||
|
||||
if (newNodes.length > 0) {
|
||||
textNode.replaceWith(...newNodes);
|
||||
}
|
||||
}
|
||||
|
||||
// 高亮词汇
|
||||
#highlightWordsDeeply(parentNode) {
|
||||
if (!parentNode || this.#favWords.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const escapedWords = this.#favWords.map(escapeRegex);
|
||||
const wordRegex = new RegExp(`\\b(${escapedWords.join("|")})\\b`, "gi");
|
||||
|
||||
if (parentNode.nodeType === Node.ELEMENT_NODE) {
|
||||
const walker = document.createTreeWalker(
|
||||
parentNode,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
const nodesToProcess = [];
|
||||
let node;
|
||||
while ((node = walker.nextNode())) {
|
||||
nodesToProcess.push(node);
|
||||
}
|
||||
|
||||
nodesToProcess.forEach((textNode) => {
|
||||
this.#highlightTextNode(textNode, wordRegex);
|
||||
});
|
||||
} else if (parentNode.nodeType === Node.TEXT_NODE) {
|
||||
this.#highlightTextNode(parentNode, wordRegex);
|
||||
}
|
||||
}
|
||||
|
||||
// 切分文本段落
|
||||
#splitTextNodesBySentence(parentNode, splitParagraph, splitLength) {
|
||||
const sentenceEndRegexForSplit = /[。!?]+|[.?!]+(?=\s+|$)/g;
|
||||
|
||||
[...parentNode.childNodes].forEach((node) => {
|
||||
if (node.nodeType !== Node.TEXT_NODE || node.textContent.trim() === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = node.textContent;
|
||||
const parts = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = sentenceEndRegexForSplit.exec(text)) !== null) {
|
||||
let realEndIndex = match.index + match[0].length;
|
||||
while (realEndIndex < text.length && /\s/.test(text[realEndIndex])) {
|
||||
realEndIndex++;
|
||||
}
|
||||
parts.push(text.substring(lastIndex, realEndIndex));
|
||||
lastIndex = realEndIndex;
|
||||
sentenceEndRegexForSplit.lastIndex = realEndIndex;
|
||||
}
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(text.substring(lastIndex));
|
||||
}
|
||||
|
||||
const validParts = parts.filter((part) => part.trim().length > 0);
|
||||
if (validParts.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newNodes = validParts.map((part) => {
|
||||
const newNode = document.createTextNode(part);
|
||||
this.#skipMoNodes.add(newNode);
|
||||
return newNode;
|
||||
});
|
||||
|
||||
node.replaceWith(...newNodes);
|
||||
});
|
||||
|
||||
const sentenceEndRegexForTest = /(?:[。!??!]+|(?<!\d)\.)\s*$/;
|
||||
let textLength = 0;
|
||||
|
||||
[...parentNode.childNodes].forEach((node) => {
|
||||
textLength += node.textContent.length;
|
||||
|
||||
const isSentenceEnd = sentenceEndRegexForTest.test(node.textContent);
|
||||
if (!isSentenceEnd || node.nextSibling?.nodeName === "BR") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
splitParagraph === OPT_SPLIT_PARAGRAPH_PUNCTUATION ||
|
||||
(splitParagraph === OPT_SPLIT_PARAGRAPH_TEXTLENGTH &&
|
||||
textLength >= splitLength)
|
||||
) {
|
||||
textLength = 0;
|
||||
|
||||
const br = document.createElement("br");
|
||||
br.className = Translator.KISS_CLASS.br;
|
||||
this.#skipMoNodes.add(br);
|
||||
|
||||
node.after(br);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 清除高亮
|
||||
#removeHighlights(parentNode) {
|
||||
if (!parentNode) return;
|
||||
|
||||
const highlightedElements = parentNode.querySelectorAll(
|
||||
`.${Translator.KISS_CLASS.highlight}`
|
||||
);
|
||||
|
||||
highlightedElements.forEach((element) => {
|
||||
const textNode = document.createTextNode(element.textContent);
|
||||
element.replaceWith(textNode);
|
||||
});
|
||||
|
||||
parentNode.normalize();
|
||||
}
|
||||
|
||||
// 移除br
|
||||
#removeBrTags(parentNode) {
|
||||
if (!parentNode) return;
|
||||
|
||||
parentNode
|
||||
.querySelectorAll(`.${Translator.KISS_CLASS.br}`)
|
||||
.forEach((br) => br.remove());
|
||||
|
||||
parentNode.normalize();
|
||||
}
|
||||
|
||||
// 判断是否需要换行
|
||||
#shouldBreak(node) {
|
||||
if (!Translator.isElementOrFragment(node)) return false;
|
||||
@@ -897,6 +1090,7 @@ export class Translator {
|
||||
|
||||
if (
|
||||
Translator.TAGS.BREAK_LINE.has(node.nodeName) ||
|
||||
node.matches?.(this.#ignoreSelector) ||
|
||||
node.nodeName === this.#translationTagName
|
||||
) {
|
||||
return true;
|
||||
@@ -956,15 +1150,16 @@ export class Translator {
|
||||
const {
|
||||
transTag,
|
||||
textStyle,
|
||||
transStartHook,
|
||||
transEndHook,
|
||||
transOnly,
|
||||
termsStyle,
|
||||
selectStyle,
|
||||
parentStyle,
|
||||
grandStyle,
|
||||
// detectRemote,
|
||||
// toLang,
|
||||
// skipLangs = [],
|
||||
highlightWords,
|
||||
} = this.#rule;
|
||||
const {
|
||||
newlineLength,
|
||||
@@ -973,23 +1168,11 @@ export class Translator {
|
||||
const parentNode = hostNode.parentElement;
|
||||
const hideOrigin = transOnly === "true";
|
||||
|
||||
// 翻译开始钩子函数
|
||||
if (transStartHook?.trim()) {
|
||||
try {
|
||||
interpreter.run(`exports.transStartHook = ${transStartHook}`);
|
||||
interpreter.exports.transStartHook({
|
||||
hostNode,
|
||||
parentNode,
|
||||
nodes,
|
||||
});
|
||||
} catch (err) {
|
||||
kissLog("transStartHook", err);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const [processedString, placeholderMap] =
|
||||
this.#serializeForTranslation(nodes);
|
||||
const [processedString, placeholderMap] = this.#serializeForTranslation(
|
||||
nodes,
|
||||
termsStyle
|
||||
);
|
||||
// console.log("processedString", processedString);
|
||||
if (this.#isInvalidText(processedString)) return;
|
||||
|
||||
@@ -1009,10 +1192,8 @@ export class Translator {
|
||||
nodes[nodes.length - 1].after(wrapper);
|
||||
|
||||
const currentRunId = this.#runId;
|
||||
const [translatedText, isSameLang] = await this.#translateFetch(
|
||||
processedString,
|
||||
deLang
|
||||
);
|
||||
const { trText: translatedText, isSame: isSameLang } =
|
||||
await this.#translateFetch(processedString, deLang);
|
||||
if (this.#runId !== currentRunId) {
|
||||
throw new Error("Request terminated");
|
||||
}
|
||||
@@ -1054,6 +1235,11 @@ export class Translator {
|
||||
parentNode.parentElement.style.cssText += grandStyle;
|
||||
}
|
||||
|
||||
// 高亮词汇
|
||||
if (highlightWords === OPT_HIGHLIGHT_WORDS_AFTERTRANS) {
|
||||
nodes.forEach((node) => this.#highlightWordsDeeply(node));
|
||||
}
|
||||
|
||||
// 翻译完成钩子函数
|
||||
if (transEndHook?.trim()) {
|
||||
try {
|
||||
@@ -1078,7 +1264,7 @@ export class Translator {
|
||||
}
|
||||
|
||||
// 处理节点转为翻译字符串
|
||||
#serializeForTranslation(nodes) {
|
||||
#serializeForTranslation(nodes, termsStyle) {
|
||||
let replaceCounter = 0; // {{n}}
|
||||
let wrapCounter = 0; // <tagn>
|
||||
const placeholderMap = new Map();
|
||||
@@ -1100,10 +1286,7 @@ export class Translator {
|
||||
}
|
||||
|
||||
// 文本节点
|
||||
if (
|
||||
this.#rule.hasRichText === "false" ||
|
||||
node.nodeType === Node.TEXT_NODE
|
||||
) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
let text = node.textContent;
|
||||
|
||||
// 专业术语替换
|
||||
@@ -1118,7 +1301,7 @@ export class Translator {
|
||||
const termValue = this.#termValues[matchedIndex];
|
||||
|
||||
return pushReplace(
|
||||
`<i class="${Translator.KISS_CLASS.term}">${termValue || fullMatch}</i>`
|
||||
`<i class="${Translator.KISS_CLASS.term}" style="${termsStyle}">${termValue || fullMatch}</i>`
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1129,8 +1312,10 @@ export class Translator {
|
||||
// 元素节点
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
if (
|
||||
Translator.TAGS.REPLACE.has(node.tagName) ||
|
||||
(this.#rule.hasRichText === "true" &&
|
||||
Translator.TAGS.REPLACE.has(node.tagName)) ||
|
||||
node.matches(this.#rule.keepSelector) ||
|
||||
// node.matches(this.#ignoreSelector) ||
|
||||
!node.textContent.trim()
|
||||
) {
|
||||
if (node.tagName === "IMG" || node.tagName === "SVG") {
|
||||
@@ -1145,7 +1330,10 @@ export class Translator {
|
||||
innerContent += traverse(child);
|
||||
});
|
||||
|
||||
if (Translator.TAGS.WARP.has(node.tagName)) {
|
||||
if (
|
||||
this.#rule.hasRichText === "true" &&
|
||||
Translator.TAGS.WARP.has(node.tagName)
|
||||
) {
|
||||
wrapCounter++;
|
||||
const startPlaceholder = `<${this.#placeholder.tagName}${wrapCounter}>`;
|
||||
const endPlaceholder = `</${this.#placeholder.tagName}${wrapCounter}>`;
|
||||
@@ -1191,16 +1379,39 @@ export class Translator {
|
||||
|
||||
// 发起翻译请求
|
||||
#translateFetch(text, deLang = "") {
|
||||
const { fromLang, toLang } = this.#rule;
|
||||
const { toLang, transStartHook } = this.#rule;
|
||||
const fromLang = deLang || this.#rule.fromLang;
|
||||
const apiSetting = { ...this.#apiSetting };
|
||||
const docInfo = { ...this.#docInfo };
|
||||
const glossary = { ...this.#glossary };
|
||||
const apisMap = this.#apisMap;
|
||||
|
||||
return apiTranslate({
|
||||
const args = {
|
||||
text,
|
||||
fromLang: deLang || fromLang,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting: this.#apiSetting,
|
||||
docInfo: this.#docInfo,
|
||||
glossary: this.#glossary,
|
||||
});
|
||||
apiSetting,
|
||||
docInfo,
|
||||
glossary,
|
||||
};
|
||||
|
||||
// 翻译开始钩子函数
|
||||
if (transStartHook?.trim()) {
|
||||
try {
|
||||
interpreter.run(`exports.transStartHook = ${transStartHook}`);
|
||||
const hookResult = interpreter.exports.transStartHook({
|
||||
...args,
|
||||
apisMap,
|
||||
});
|
||||
if (hookResult) {
|
||||
Object.assign(args, ...hookResult);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("transStartHook", err);
|
||||
}
|
||||
}
|
||||
|
||||
return apiTranslate(args);
|
||||
}
|
||||
|
||||
// 查找指定节点下所有译文节点
|
||||
@@ -1229,7 +1440,8 @@ export class Translator {
|
||||
|
||||
// 清理译文
|
||||
#removeTranslationElement(el) {
|
||||
this.#processedNodes.delete(el.parentElement);
|
||||
const parentElement = el.parentElement;
|
||||
this.#processedNodes.delete(parentElement);
|
||||
|
||||
// 如果是仅显示译文模式,先恢复原文
|
||||
const { nodes, isHide } = this.#translationNodes.get(el) || {};
|
||||
@@ -1239,6 +1451,12 @@ export class Translator {
|
||||
|
||||
this.#translationNodes.delete(el);
|
||||
el.remove();
|
||||
|
||||
// todo: 可能不应深度清除
|
||||
if (this.#rule.highlightWords === OPT_HIGHLIGHT_WORDS_AFTERTRANS) {
|
||||
this.#removeHighlights(parentElement);
|
||||
}
|
||||
this.#removeBrTags(parentElement);
|
||||
}
|
||||
|
||||
// 恢复原文
|
||||
@@ -1342,6 +1560,8 @@ export class Translator {
|
||||
|
||||
// 停止监听,重置参数
|
||||
#resetOptions() {
|
||||
this.#removeShadowRootListener();
|
||||
|
||||
this.#io.disconnect();
|
||||
this.#mo.disconnect();
|
||||
this.#viewNodes.clear();
|
||||
@@ -1387,14 +1607,35 @@ export class Translator {
|
||||
this.#isJsInjected = true;
|
||||
|
||||
try {
|
||||
const { injectJs, injectCss } = this.#rule;
|
||||
if (isExt) {
|
||||
injectJs && sendBgMsg(MSG_INJECT_JS, injectJs);
|
||||
injectCss && sendBgMsg(MSG_INJECT_CSS, injectCss);
|
||||
} else {
|
||||
injectJs &&
|
||||
injectInlineJs(injectJs, "kiss-translator-userinit-injector");
|
||||
injectCss && injectInternalCss(injectCss);
|
||||
// const { injectJs, injectCss } = this.#rule;
|
||||
// if (isExt) {
|
||||
// injectJs && sendBgMsg(MSG_INJECT_JS, injectJs);
|
||||
// injectCss && sendBgMsg(MSG_INJECT_CSS, injectCss);
|
||||
// } else {
|
||||
// injectJs &&
|
||||
// injectInlineJs(injectJs, "kiss-translator-userinit-injector");
|
||||
// injectCss && injectInternalCss(injectCss);
|
||||
// }
|
||||
|
||||
const { injectJs, toLang } = this.#rule;
|
||||
if (injectJs?.trim()) {
|
||||
const apiSetting = { ...this.#apiSetting };
|
||||
const docInfo = { ...this.#docInfo };
|
||||
const glossary = { ...this.#glossary };
|
||||
const apisMap = this.#apisMap;
|
||||
const apiDectect = tryDetectLang;
|
||||
interpreter.import({
|
||||
KT: {
|
||||
apiTranslate,
|
||||
apiDectect,
|
||||
apiSetting,
|
||||
apisMap,
|
||||
toLang,
|
||||
docInfo,
|
||||
glossary,
|
||||
},
|
||||
});
|
||||
interpreter.run(injectJs);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("inject js", err);
|
||||
@@ -1445,8 +1686,8 @@ export class Translator {
|
||||
|
||||
try {
|
||||
const deLang = await tryDetectLang(title);
|
||||
const [translatedTitle] = await this.#translateFetch(title, deLang);
|
||||
document.title = translatedTitle || title;
|
||||
const { trText } = await this.#translateFetch(title, deLang);
|
||||
document.title = trText || title;
|
||||
} catch (err) {
|
||||
kissLog("tanslate title", err);
|
||||
}
|
||||
@@ -1501,20 +1742,17 @@ export class Translator {
|
||||
toggleTransbox() {
|
||||
this.#setting.tranboxSetting.transOpen =
|
||||
!this.#setting.tranboxSetting.transOpen;
|
||||
this.#transboxManager?.toggle();
|
||||
}
|
||||
|
||||
// 切换输入框翻译
|
||||
toggleInputTranslate() {
|
||||
this.#setting.inputRule.transOpen = !this.#setting.inputRule.transOpen;
|
||||
this.#inputTranslator?.toggle();
|
||||
}
|
||||
|
||||
// 停止运行
|
||||
stop() {
|
||||
this.disable();
|
||||
this.#resetOptions();
|
||||
this.#srm.stop();
|
||||
this.#disableMouseHover();
|
||||
this.#removeInjector();
|
||||
this.#isInitialized = false;
|
||||
|
||||
291
src/libs/translatorManager.js
Normal file
291
src/libs/translatorManager.js
Normal file
@@ -0,0 +1,291 @@
|
||||
import { browser } from "./browser";
|
||||
import { Translator } from "./translator";
|
||||
import { InputTranslator } from "./inputTranslate";
|
||||
import { TransboxManager } from "./tranbox";
|
||||
import { shortcutRegister } from "./shortcut";
|
||||
import { sendIframeMsg } from "./iframe";
|
||||
import { EVENT_KISS, newI18n } from "../config";
|
||||
import { touchTapListener } from "./touch";
|
||||
import { PopupManager } from "./popupManager";
|
||||
import { FabManager } from "./fabManager";
|
||||
import {
|
||||
OPT_SHORTCUT_TRANSLATE,
|
||||
OPT_SHORTCUT_STYLE,
|
||||
OPT_SHORTCUT_POPUP,
|
||||
OPT_SHORTCUT_SETTING,
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
MSG_OPEN_TRANBOX,
|
||||
MSG_TRANSBOX_TOGGLE,
|
||||
MSG_POPUP_TOGGLE,
|
||||
MSG_MOUSEHOVER_TOGGLE,
|
||||
MSG_TRANSINPUT_TOGGLE,
|
||||
} from "../config";
|
||||
import { logger } from "./log";
|
||||
|
||||
export default class TranslatorManager {
|
||||
#clearShortcuts = [];
|
||||
#menuCommandIds = [];
|
||||
#clearTouchListeners = [];
|
||||
#isActive = false;
|
||||
#isUserscript;
|
||||
#isIframe;
|
||||
|
||||
#windowMessageHandler = null;
|
||||
#browserMessageHandler = null;
|
||||
|
||||
_translator;
|
||||
_transboxManager;
|
||||
_inputTranslator;
|
||||
_popupManager;
|
||||
_fabManager;
|
||||
|
||||
constructor({ setting, rule, fabConfig, favWords, isIframe, isUserscript }) {
|
||||
this.#isIframe = isIframe;
|
||||
this.#isUserscript = isUserscript;
|
||||
|
||||
this._translator = new Translator({
|
||||
rule,
|
||||
setting,
|
||||
favWords,
|
||||
isUserscript,
|
||||
isIframe,
|
||||
});
|
||||
|
||||
this._transboxManager = new TransboxManager(setting);
|
||||
|
||||
if (!isIframe) {
|
||||
this._inputTranslator = new InputTranslator(setting);
|
||||
this._popupManager = new PopupManager({
|
||||
translator: this._translator,
|
||||
processActions: this.#processActions.bind(this),
|
||||
});
|
||||
this._fabManager = new FabManager({
|
||||
processActions: this.#processActions.bind(this),
|
||||
fabConfig,
|
||||
});
|
||||
}
|
||||
|
||||
this.#windowMessageHandler = this.#handleWindowMessage.bind(this);
|
||||
this.#browserMessageHandler = this.#handleBrowserMessage.bind(this);
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.#isActive) {
|
||||
logger.info("TranslatorManager is already started.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.#setupMessageListeners();
|
||||
this.#setupTouchOperations();
|
||||
|
||||
if (!this.#isIframe && this.#isUserscript) {
|
||||
this.#registerShortcuts();
|
||||
this.#registerMenus();
|
||||
}
|
||||
|
||||
this.#isActive = true;
|
||||
logger.info("TranslatorManager started.");
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.#isActive) {
|
||||
logger.info("TranslatorManager is not running.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 移除消息监听器
|
||||
if (this.#isUserscript) {
|
||||
window.removeEventListener("message", this.#windowMessageHandler);
|
||||
} else if (
|
||||
browser.runtime.onMessage.hasListener(this.#browserMessageHandler)
|
||||
) {
|
||||
browser.runtime.onMessage.removeListener(this.#browserMessageHandler);
|
||||
}
|
||||
|
||||
// 已注册的快捷键
|
||||
this.#clearShortcuts.forEach((clear) => clear());
|
||||
this.#clearShortcuts = [];
|
||||
|
||||
// 触屏
|
||||
this.#clearTouchListeners.forEach((clear) => clear());
|
||||
this.#clearTouchListeners = [];
|
||||
|
||||
// 油猴菜单
|
||||
if (globalThis.GM && this.#menuCommandIds.length > 0) {
|
||||
this.#menuCommandIds.forEach((id) =>
|
||||
globalThis.GM.unregisterMenuCommand(id)
|
||||
);
|
||||
this.#menuCommandIds = [];
|
||||
}
|
||||
|
||||
// 子模块
|
||||
this._popupManager?.destroy();
|
||||
this._fabManager?.destroy();
|
||||
this._transboxManager?.disable();
|
||||
this._inputTranslator?.disable();
|
||||
this._translator.stop();
|
||||
|
||||
this.#isActive = false;
|
||||
logger.info("TranslatorManager stopped.");
|
||||
}
|
||||
|
||||
#setupMessageListeners() {
|
||||
if (this.#isUserscript) {
|
||||
window.addEventListener("message", this.#windowMessageHandler);
|
||||
} else {
|
||||
browser.runtime.onMessage.addListener(this.#browserMessageHandler);
|
||||
if (this.#isIframe) {
|
||||
window.addEventListener("message", this.#windowMessageHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#setupTouchOperations() {
|
||||
if (this.#isIframe) return;
|
||||
|
||||
const { touchModes = [2] } = this._translator.setting;
|
||||
if (touchModes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleTap = () => {
|
||||
this.#processActions({ action: MSG_TRANS_TOGGLE });
|
||||
};
|
||||
|
||||
const handleListener = (mode) => {
|
||||
let options = null;
|
||||
switch (mode) {
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
options = { taps: 1, fingers: mode };
|
||||
break;
|
||||
case 5:
|
||||
options = { taps: 2, fingers: 1 };
|
||||
break;
|
||||
case 6:
|
||||
options = { taps: 3, fingers: 1 };
|
||||
break;
|
||||
case 7:
|
||||
options = { taps: 2, fingers: 2 };
|
||||
break;
|
||||
default:
|
||||
}
|
||||
if (options) {
|
||||
this.#clearTouchListeners.push(touchTapListener(handleTap, options));
|
||||
}
|
||||
};
|
||||
|
||||
touchModes.forEach((mode) => handleListener(mode));
|
||||
}
|
||||
|
||||
#handleWindowMessage(event) {
|
||||
this.#processActions(event.data);
|
||||
}
|
||||
|
||||
#handleBrowserMessage(message, sender, sendResponse) {
|
||||
const result = this.#processActions(message, true);
|
||||
const response = result || {
|
||||
rule: this._translator.rule,
|
||||
setting: this._translator.setting,
|
||||
};
|
||||
sendResponse(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
#registerShortcuts() {
|
||||
const { shortcuts } = this._translator.setting;
|
||||
this.#clearShortcuts = [
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () =>
|
||||
this.#processActions({ action: MSG_TRANS_TOGGLE })
|
||||
),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () =>
|
||||
this.#processActions({ action: MSG_TRANS_TOGGLE_STYLE })
|
||||
),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () =>
|
||||
this.#processActions({ action: MSG_POPUP_TOGGLE })
|
||||
),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () =>
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank")
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
#registerMenus() {
|
||||
if (!globalThis.GM) return;
|
||||
const { contextMenuType, uiLang } = this._translator.setting;
|
||||
if (contextMenuType === 0) return;
|
||||
|
||||
const i18n = newI18n(uiLang || "zh");
|
||||
const GM = globalThis.GM;
|
||||
this.#menuCommandIds = [
|
||||
GM.registerMenuCommand(
|
||||
i18n("translate_switch"),
|
||||
() => this.#processActions({ action: MSG_TRANS_TOGGLE }),
|
||||
"Q"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
i18n("toggle_style"),
|
||||
() => this.#processActions({ action: MSG_TRANS_TOGGLE_STYLE }),
|
||||
"C"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
i18n("open_menu"),
|
||||
() => this.#processActions({ action: MSG_POPUP_TOGGLE }),
|
||||
"K"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
i18n("open_setting"),
|
||||
() => window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank"),
|
||||
"O"
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
#processActions({ action, args } = {}, fromExt = false) {
|
||||
if (!fromExt) {
|
||||
sendIframeMsg(action, args);
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
this._translator.toggle();
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
this._translator.toggleStyle();
|
||||
break;
|
||||
case MSG_TRANS_GETRULE:
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
this._translator.updateRule(args);
|
||||
break;
|
||||
case MSG_OPEN_TRANBOX:
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(EVENT_KISS, {
|
||||
detail: { action: MSG_OPEN_TRANBOX },
|
||||
})
|
||||
);
|
||||
break;
|
||||
case MSG_POPUP_TOGGLE:
|
||||
this._popupManager?.toggle();
|
||||
break;
|
||||
case MSG_TRANSBOX_TOGGLE:
|
||||
this._transboxManager?.toggle();
|
||||
this._translator.toggleTransbox();
|
||||
break;
|
||||
case MSG_MOUSEHOVER_TOGGLE:
|
||||
this._translator.toggleMouseHover();
|
||||
break;
|
||||
case MSG_TRANSINPUT_TOGGLE:
|
||||
this._inputTranslator?.toggle();
|
||||
this._translator.toggleInputTranslate();
|
||||
break;
|
||||
default:
|
||||
logger.info(`Message action is unavailable: ${action}`);
|
||||
return { error: `Message action is unavailable: ${action}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { logger } from "./log";
|
||||
|
||||
export const trustedTypesHelper = (() => {
|
||||
const POLICY_NAME = "kiss-translator-policy";
|
||||
let policy = null;
|
||||
@@ -13,7 +15,7 @@ export const trustedTypesHelper = (() => {
|
||||
if (err.message.includes("already exists")) {
|
||||
policy = globalThis.trustedTypes.policies.get(POLICY_NAME);
|
||||
} else {
|
||||
console.error("cont create Trusted Types", err);
|
||||
logger.info("cont create Trusted Types", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 || " "}</${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}`);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { logger } from "../libs/log.js";
|
||||
import { truncateWords } from "../libs/utils.js";
|
||||
import { apiTranslate } from "../apis/index.js";
|
||||
|
||||
/**
|
||||
* @class BilingualSubtitleManager
|
||||
@@ -8,7 +9,6 @@ import { truncateWords } from "../libs/utils.js";
|
||||
export class BilingualSubtitleManager {
|
||||
#videoEl;
|
||||
#formattedSubtitles = [];
|
||||
#translationService;
|
||||
#captionWindowEl = null;
|
||||
#paperEl = null;
|
||||
#currentSubtitleIndex = -1;
|
||||
@@ -20,14 +20,12 @@ export class BilingualSubtitleManager {
|
||||
* @param {object} options
|
||||
* @param {HTMLVideoElement} options.videoEl - 页面上的 video 元素。
|
||||
* @param {Array<object>} options.formattedSubtitles - 已格式化好的字幕数组。
|
||||
* @param {(text: string, toLang: string) => Promise<string>} options.translationService - 外部翻译函数。
|
||||
* @param {object} options.setting - 配置对象,如目标翻译语言。
|
||||
*/
|
||||
constructor({ videoEl, formattedSubtitles, translationService, setting }) {
|
||||
constructor({ videoEl, formattedSubtitles, setting }) {
|
||||
this.#setting = setting;
|
||||
this.#videoEl = videoEl;
|
||||
this.#formattedSubtitles = formattedSubtitles;
|
||||
this.#translationService = translationService;
|
||||
|
||||
this.onTimeUpdate = this.onTimeUpdate.bind(this);
|
||||
this.onSeek = this.onSeek.bind(this);
|
||||
@@ -128,15 +126,14 @@ export class BilingualSubtitleManager {
|
||||
let initialBottom;
|
||||
let dragElementHeight;
|
||||
|
||||
const onMouseDown = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const onDragStart = (e) => {
|
||||
if (e.type === "mousedown" && e.button !== 0) return;
|
||||
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
|
||||
isDragging = true;
|
||||
handleElement.style.cursor = "grabbing";
|
||||
startY = e.clientY;
|
||||
startY = e.type === "touchstart" ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
initialBottom =
|
||||
boundaryContainer.getBoundingClientRect().bottom -
|
||||
@@ -144,17 +141,23 @@ export class BilingualSubtitleManager {
|
||||
|
||||
dragElementHeight = dragElement.offsetHeight;
|
||||
|
||||
document.addEventListener("mousemove", onMouseMove, { capture: true });
|
||||
document.addEventListener("mouseup", onMouseUp, { capture: true });
|
||||
document.addEventListener("mousemove", onDragMove, { capture: true });
|
||||
document.addEventListener("touchmove", onDragMove, {
|
||||
capture: true,
|
||||
passive: false,
|
||||
});
|
||||
document.addEventListener("mouseup", onDragEnd, { capture: true });
|
||||
document.addEventListener("touchend", onDragEnd, { capture: true });
|
||||
};
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
const onDragMove = (e) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const deltaY = e.clientY - startY;
|
||||
const currentY =
|
||||
e.type === "touchmove" ? e.touches[0].clientY : e.clientY;
|
||||
const deltaY = currentY - startY;
|
||||
let newBottom = initialBottom - deltaY;
|
||||
|
||||
const containerHeight = boundaryContainer.clientHeight;
|
||||
@@ -167,17 +170,18 @@ export class BilingualSubtitleManager {
|
||||
dragElement.style.bottom = `${newBottom}px`;
|
||||
};
|
||||
|
||||
const onMouseUp = (e) => {
|
||||
const onDragEnd = (e) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
isDragging = false;
|
||||
handleElement.style.cursor = "grab";
|
||||
|
||||
document.removeEventListener("mousemove", onMouseMove, { capture: true });
|
||||
document.removeEventListener("mouseup", onMouseUp, { capture: true });
|
||||
document.removeEventListener("mousemove", onDragMove, { capture: true });
|
||||
document.removeEventListener("touchmove", onDragMove, { capture: true });
|
||||
document.removeEventListener("mouseup", onDragEnd, { capture: true });
|
||||
document.removeEventListener("touchend", onDragEnd, { capture: true });
|
||||
|
||||
const finalBottomPx = dragElement.style.bottom;
|
||||
setTimeout(() => {
|
||||
@@ -185,7 +189,10 @@ export class BilingualSubtitleManager {
|
||||
}, 50);
|
||||
};
|
||||
|
||||
handleElement.addEventListener("mousedown", onMouseDown);
|
||||
handleElement.addEventListener("mousedown", onDragStart);
|
||||
handleElement.addEventListener("touchstart", onDragStart, {
|
||||
passive: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,13 +307,13 @@ export class BilingualSubtitleManager {
|
||||
subtitle.isTranslating = true;
|
||||
try {
|
||||
const { fromLang, toLang, apiSetting } = this.#setting;
|
||||
const [translatedText] = await this.#translationService({
|
||||
const { trText } = await apiTranslate({
|
||||
text: subtitle.text,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting,
|
||||
});
|
||||
subtitle.translation = translatedText;
|
||||
subtitle.translation = trText;
|
||||
} catch (error) {
|
||||
logger.info("Translation failed for:", subtitle.text, error);
|
||||
subtitle.translation = "[Translation failed]";
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { logger } from "../libs/log.js";
|
||||
import { apiSubtitle, apiTranslate } from "../apis/index.js";
|
||||
import { apiSubtitle } from "../apis/index.js";
|
||||
import { BilingualSubtitleManager } from "./BilingualSubtitleManager.js";
|
||||
import {
|
||||
MSG_XHR_DATA_YOUTUBE,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { sleep } from "../libs/utils.js";
|
||||
import { createLogoSVG } from "../libs/svg.js";
|
||||
import { randomBetween } from "../libs/utils.js";
|
||||
import { i18n } from "../config";
|
||||
import { newI18n } from "../config";
|
||||
|
||||
const VIDEO_SELECT = "#container video";
|
||||
const CONTORLS_SELECT = ".ytp-right-controls";
|
||||
@@ -33,7 +33,7 @@ class YouTubeCaptionProvider {
|
||||
|
||||
constructor(setting = {}) {
|
||||
this.#setting = setting;
|
||||
this.#i18n = i18n(setting.uiLang || "zh");
|
||||
this.#i18n = newI18n(setting.uiLang || "zh");
|
||||
}
|
||||
|
||||
initialize() {
|
||||
@@ -70,6 +70,8 @@ class YouTubeCaptionProvider {
|
||||
}
|
||||
|
||||
#moAds(adContainer) {
|
||||
const { skipAd = false } = this.#setting;
|
||||
|
||||
const adLayoutSelector = ".ytp-ad-player-overlay-layout";
|
||||
const skipBtnSelector =
|
||||
".ytp-skip-ad-button, .ytp-ad-skip-button, .ytp-ad-skip-button-modern";
|
||||
@@ -83,22 +85,24 @@ class YouTubeCaptionProvider {
|
||||
if (node.matches(adLayoutSelector)) {
|
||||
logger.debug("Youtube Provider: AD start playing!", node);
|
||||
// todo: 顺带把广告快速跳过
|
||||
if (videoEl) {
|
||||
if (videoEl && skipAd) {
|
||||
videoEl.playbackRate = 16;
|
||||
videoEl.currentTime = videoEl.duration;
|
||||
}
|
||||
if (this.#managerInstance) {
|
||||
this.#managerInstance.setIsAdPlaying(true);
|
||||
}
|
||||
} else if (node.matches(skipBtnSelector)) {
|
||||
} else if (node.matches(skipBtnSelector) && skipAd) {
|
||||
logger.debug("Youtube Provider: AD skip button!", node);
|
||||
node.click();
|
||||
}
|
||||
|
||||
const skipBtn = node?.querySelector(skipBtnSelector);
|
||||
if (skipBtn) {
|
||||
logger.debug("Youtube Provider: AD skip button!!", skipBtn);
|
||||
skipBtn.click();
|
||||
if (skipAd) {
|
||||
const skipBtn = node?.querySelector(skipBtnSelector);
|
||||
if (skipBtn) {
|
||||
logger.debug("Youtube Provider: AD skip button!!", skipBtn);
|
||||
skipBtn.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
mutation.removedNodes.forEach((node) => {
|
||||
@@ -106,7 +110,7 @@ class YouTubeCaptionProvider {
|
||||
|
||||
if (node.matches(adLayoutSelector)) {
|
||||
logger.debug("Youtube Provider: Ad ends!");
|
||||
if (videoEl) {
|
||||
if (videoEl && skipAd) {
|
||||
videoEl.playbackRate = 1;
|
||||
}
|
||||
if (this.#managerInstance) {
|
||||
@@ -161,14 +165,13 @@ class YouTubeCaptionProvider {
|
||||
this.#ytControls = ytControls;
|
||||
|
||||
const kissControls = document.createElement("div");
|
||||
kissControls.className = "kiss-bilingual-subtitle-controls";
|
||||
kissControls.className = "notranslate kiss-subtitle-controls";
|
||||
Object.assign(kissControls.style, {
|
||||
height: "100%",
|
||||
});
|
||||
|
||||
const toggleButton = document.createElement("button");
|
||||
toggleButton.className =
|
||||
"ytp-button notranslate kiss-bilingual-subtitle-button";
|
||||
toggleButton.className = "ytp-button kiss-subtitle-button";
|
||||
toggleButton.title = APP_NAME;
|
||||
Object.assign(toggleButton.style, {
|
||||
color: "white",
|
||||
@@ -199,7 +202,7 @@ class YouTubeCaptionProvider {
|
||||
}
|
||||
};
|
||||
this.#toggleButton = toggleButton;
|
||||
this.#ytControls?.before(kissControls);
|
||||
this.#ytControls?.prepend(kissControls);
|
||||
}
|
||||
|
||||
#isSameLang(lang1, lang2) {
|
||||
@@ -505,7 +508,6 @@ class YouTubeCaptionProvider {
|
||||
this.#managerInstance = new BilingualSubtitleManager({
|
||||
videoEl,
|
||||
formattedSubtitles: this.#subtitles,
|
||||
translationService: apiTranslate,
|
||||
setting: { ...this.#setting, fromLang: this.#fromLang },
|
||||
});
|
||||
this.#managerInstance.start();
|
||||
@@ -590,7 +592,7 @@ class YouTubeCaptionProvider {
|
||||
return subtitles;
|
||||
}
|
||||
|
||||
#isQualityPoor(lines, lengthThreshold = 250, percentageThreshold = 0.1) {
|
||||
#isQualityPoor(lines, lengthThreshold = 250, percentageThreshold = 0.2) {
|
||||
if (lines.length === 0) return false;
|
||||
const longLinesCount = lines.filter(
|
||||
(line) => line.text.length > lengthThreshold
|
||||
@@ -913,7 +915,7 @@ class YouTubeCaptionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
#showNotification(message, duration = 3000) {
|
||||
#showNotification(message, duration = 2000) {
|
||||
if (!this.#notificationEl) this.#createNotificationElement();
|
||||
this.#notificationEl.textContent = message;
|
||||
this.#notificationEl.style.opacity = "1";
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { YouTubeInitializer } from "./YouTubeCaptionProvider.js";
|
||||
import { browser } from "../libs/browser.js";
|
||||
import { isMatch } from "../libs/utils.js";
|
||||
import { DEFAULT_API_SETTING } from "../config/api.js";
|
||||
import { DEFAULT_SUBTITLE_SETTING } from "../config/setting.js";
|
||||
import { injectExternalJs } from "../libs/injector.js";
|
||||
import { logger } from "../libs/log.js";
|
||||
import { XMLHttpRequestInjector } from "./XMLHttpRequestInjector.js";
|
||||
import { injectInlineJs } from "../libs/injector.js";
|
||||
import { injectJs, INJECTOR } from "../injectors/index.js";
|
||||
|
||||
const providers = [
|
||||
{ pattern: "https://www.youtube.com", start: YouTubeInitializer },
|
||||
];
|
||||
|
||||
export function runSubtitle({ href, setting, isUserscript }) {
|
||||
export function runSubtitle({ href, setting }) {
|
||||
try {
|
||||
const subtitleSetting = setting.subtitleSetting || DEFAULT_SUBTITLE_SETTING;
|
||||
if (!subtitleSetting.enabled) {
|
||||
@@ -21,13 +18,8 @@ export function runSubtitle({ href, setting, isUserscript }) {
|
||||
|
||||
const provider = providers.find((item) => isMatch(href, item.pattern));
|
||||
if (provider) {
|
||||
const id = "kiss-translator-xmlHttp-injector";
|
||||
if (isUserscript) {
|
||||
injectInlineJs(`(${XMLHttpRequestInjector})()`, id);
|
||||
} else {
|
||||
const src = browser.runtime.getURL("injector.js");
|
||||
injectExternalJs(src, id);
|
||||
}
|
||||
const id = "kiss-translator-inject-subtitle-js";
|
||||
injectJs(INJECTOR.subtitle, id);
|
||||
|
||||
const apiSetting =
|
||||
setting.transApis.find(
|
||||
|
||||
70
src/views/Action/ContentFab.js
Normal file
70
src/views/Action/ContentFab.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import Fab from "@mui/material/Fab";
|
||||
import TranslateIcon from "@mui/icons-material/Translate";
|
||||
import ThemeProvider from "../../hooks/Theme";
|
||||
import Draggable from "./Draggable";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { SettingProvider } from "../../hooks/Setting";
|
||||
import { MSG_TRANS_TOGGLE, MSG_POPUP_TOGGLE } from "../../config";
|
||||
import useWindowSize from "../../hooks/WindowSize";
|
||||
|
||||
export default function ContentFab({
|
||||
fabConfig: { x: fabX, y: fabY, fabClickAction = 0 } = {},
|
||||
processActions,
|
||||
}) {
|
||||
const fabWidth = 40;
|
||||
const windowSize = useWindowSize();
|
||||
const [moved, setMoved] = useState(false);
|
||||
|
||||
const handleStart = useCallback(() => {
|
||||
setMoved(false);
|
||||
}, []);
|
||||
|
||||
const handleMove = useCallback(() => {
|
||||
setMoved(true);
|
||||
}, []);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!moved) {
|
||||
if (fabClickAction === 1) {
|
||||
processActions({ action: MSG_TRANS_TOGGLE });
|
||||
} else {
|
||||
processActions({ action: MSG_POPUP_TOGGLE });
|
||||
}
|
||||
}
|
||||
}, [moved, fabClickAction, processActions]);
|
||||
|
||||
const fabProps = useMemo(
|
||||
() => ({
|
||||
windowSize,
|
||||
width: fabWidth,
|
||||
height: fabWidth,
|
||||
left: fabX ?? -fabWidth,
|
||||
top: fabY ?? windowSize.h / 2,
|
||||
}),
|
||||
[windowSize, fabWidth, fabX, fabY]
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingProvider>
|
||||
<ThemeProvider>
|
||||
<Draggable
|
||||
key="fab"
|
||||
snapEdge
|
||||
{...fabProps}
|
||||
onStart={handleStart}
|
||||
onMove={handleMove}
|
||||
handler={
|
||||
<Fab size="small" color="primary" onClick={handleClick}>
|
||||
<TranslateIcon
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
}}
|
||||
/>
|
||||
</Fab>
|
||||
}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</SettingProvider>
|
||||
);
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export default function Draggable({
|
||||
height,
|
||||
left,
|
||||
top,
|
||||
show,
|
||||
show = true,
|
||||
snapEdge,
|
||||
onStart,
|
||||
onMove,
|
||||
|
||||
@@ -1,168 +1,62 @@
|
||||
import Fab from "@mui/material/Fab";
|
||||
import TranslateIcon from "@mui/icons-material/Translate";
|
||||
import ThemeProvider from "../../hooks/Theme";
|
||||
import Draggable from "./Draggable";
|
||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { useEffect, useMemo, useCallback, useState } from "react";
|
||||
import { SettingProvider } from "../../hooks/Setting";
|
||||
import Popup from "../Popup";
|
||||
import { debounce } from "../../libs/utils";
|
||||
import { isGm } from "../../libs/client";
|
||||
import Header from "../Popup/Header";
|
||||
import Box from "@mui/material/Box";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import {
|
||||
DEFAULT_SHORTCUTS,
|
||||
OPT_SHORTCUT_TRANSLATE,
|
||||
OPT_SHORTCUT_STYLE,
|
||||
OPT_SHORTCUT_POPUP,
|
||||
OPT_SHORTCUT_SETTING,
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
} from "../../config";
|
||||
import { shortcutRegister } from "../../libs/shortcut";
|
||||
import { sendIframeMsg } from "../../libs/iframe";
|
||||
import { kissLog } from "../../libs/log";
|
||||
import { getI18n } from "../../hooks/I18n";
|
||||
import useWindowSize from "../../hooks/WindowSize";
|
||||
import { EVENT_KISS, MSG_OPEN_OPTIONS, MSG_POPUP_TOGGLE } from "../../config";
|
||||
import PopupCont from "../Popup/PopupCont";
|
||||
import { isExt } from "../../libs/client";
|
||||
import { sendBgMsg } from "../../libs/msg";
|
||||
|
||||
export default function Action({ translator, fab }) {
|
||||
const fabWidth = 40;
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
});
|
||||
const [moved, setMoved] = useState(false);
|
||||
export default function Action({ translator, processActions }) {
|
||||
const [showPopup, setShowPopup] = useState(true);
|
||||
const [rule, setRule] = useState(translator.rule);
|
||||
const [setting, setSetting] = useState(translator.setting);
|
||||
const windowSize = useWindowSize();
|
||||
|
||||
const { fabClickAction = 0 } = fab || {};
|
||||
|
||||
const handleWindowResize = useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
setWindowSize({
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
});
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleWindowClick = (e) => {
|
||||
setShowPopup(false);
|
||||
};
|
||||
|
||||
const handleStart = useCallback(() => {
|
||||
setMoved(false);
|
||||
}, []);
|
||||
|
||||
const handleMove = useCallback(() => {
|
||||
setMoved(true);
|
||||
const handleOpenSetting = useCallback(() => {
|
||||
if (isExt) {
|
||||
sendBgMsg(MSG_OPEN_OPTIONS);
|
||||
} else {
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGm) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册快捷键
|
||||
const shortcuts = translator.setting.shortcuts || DEFAULT_SHORTCUTS;
|
||||
const clearShortcuts = [
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () => {
|
||||
translator.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
setShowPopup(false);
|
||||
}),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () => {
|
||||
translator.toggleStyle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
setShowPopup(false);
|
||||
}),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () => {
|
||||
setShowPopup((pre) => !pre);
|
||||
}),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () => {
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||
}),
|
||||
];
|
||||
|
||||
return () => {
|
||||
clearShortcuts.forEach((fn) => {
|
||||
fn();
|
||||
});
|
||||
const handleWindowClick = () => {
|
||||
setShowPopup(false);
|
||||
};
|
||||
}, [translator]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGm) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册菜单
|
||||
try {
|
||||
const menuCommandIds = [];
|
||||
const { contextMenuType, uiLang } = translator.setting;
|
||||
contextMenuType !== 0 &&
|
||||
menuCommandIds.push(
|
||||
GM.registerMenuCommand(
|
||||
getI18n(uiLang, "translate_switch"),
|
||||
(event) => {
|
||||
translator.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
setShowPopup(false);
|
||||
},
|
||||
"Q"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
getI18n(uiLang, "toggle_style"),
|
||||
(event) => {
|
||||
translator.toggleStyle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
setShowPopup(false);
|
||||
},
|
||||
"C"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
getI18n(uiLang, "open_menu"),
|
||||
(event) => {
|
||||
setShowPopup((pre) => !pre);
|
||||
},
|
||||
"K"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
getI18n(uiLang, "open_setting"),
|
||||
(event) => {
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||
},
|
||||
"O"
|
||||
)
|
||||
);
|
||||
|
||||
return () => {
|
||||
menuCommandIds.forEach((id) => {
|
||||
GM.unregisterMenuCommand(id);
|
||||
});
|
||||
};
|
||||
} catch (err) {
|
||||
kissLog("registerMenuCommand", err);
|
||||
}
|
||||
}, [translator]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
};
|
||||
}, [handleWindowResize]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("click", handleWindowClick);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("click", handleWindowClick);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleStatusUpdate = (event) => {
|
||||
if (event.detail?.action === MSG_POPUP_TOGGLE) {
|
||||
setShowPopup((pre) => !pre);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener(EVENT_KISS, handleStatusUpdate);
|
||||
return () => {
|
||||
document.removeEventListener(EVENT_KISS, handleStatusUpdate);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showPopup) {
|
||||
setRule(translator.rule);
|
||||
setSetting(translator.setting);
|
||||
}
|
||||
}, [showPopup, translator]);
|
||||
|
||||
const popProps = useMemo(() => {
|
||||
const width = Math.min(windowSize.w, 300);
|
||||
const width = Math.min(windowSize.w, 360);
|
||||
const height = Math.min(windowSize.h, 442);
|
||||
const left = (windowSize.w - width) / 2;
|
||||
const top = (windowSize.h - height) / 2;
|
||||
@@ -175,67 +69,38 @@ export default function Action({ translator, fab }) {
|
||||
};
|
||||
}, [windowSize]);
|
||||
|
||||
const fabProps = {
|
||||
windowSize,
|
||||
width: fabWidth,
|
||||
height: fabWidth,
|
||||
left: fab.x ?? -fabWidth,
|
||||
top: fab.y ?? windowSize.h / 2,
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingProvider>
|
||||
<ThemeProvider>
|
||||
<Draggable
|
||||
key="pop"
|
||||
{...popProps}
|
||||
show={showPopup}
|
||||
onStart={handleStart}
|
||||
onMove={handleMove}
|
||||
usePaper
|
||||
handler={
|
||||
<Box style={{ cursor: "move" }}>
|
||||
<Header setShowPopup={setShowPopup} />
|
||||
<Divider />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{showPopup && (
|
||||
<Popup setShowPopup={setShowPopup} translator={translator} />
|
||||
)}
|
||||
</Draggable>
|
||||
<Draggable
|
||||
key="fab"
|
||||
snapEdge
|
||||
{...fabProps}
|
||||
show={fab.isHide ? false : !showPopup}
|
||||
onStart={handleStart}
|
||||
onMove={handleMove}
|
||||
handler={
|
||||
<Fab
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={(e) => {
|
||||
if (!moved) {
|
||||
if (fabClickAction === 1) {
|
||||
translator.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
{showPopup && (
|
||||
<Draggable
|
||||
key="pop"
|
||||
{...popProps}
|
||||
usePaper
|
||||
handler={
|
||||
<Box style={{ cursor: "move" }}>
|
||||
<Header
|
||||
onClose={() => {
|
||||
setShowPopup(false);
|
||||
} else {
|
||||
setShowPopup((pre) => !pre);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TranslateIcon
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box width={360}>
|
||||
<PopupCont
|
||||
rule={rule}
|
||||
setting={setting}
|
||||
setRule={setRule}
|
||||
setSetting={setSetting}
|
||||
handleOpenSetting={handleOpenSetting}
|
||||
processActions={processActions}
|
||||
isContent={true}
|
||||
/>
|
||||
</Fab>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Draggable>
|
||||
)}
|
||||
</ThemeProvider>
|
||||
</SettingProvider>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ import Alert from "@mui/material/Alert";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import Link from "@mui/material/Link";
|
||||
import { useAlert } from "../../hooks/Alert";
|
||||
import { useApiList, useApiItem } from "../../hooks/Api";
|
||||
import { useConfirm } from "../../hooks/Confirm";
|
||||
@@ -26,7 +27,7 @@ import ReusableAutocomplete from "./ReusableAutocomplete";
|
||||
import ShowMoreButton from "./ShowMoreButton";
|
||||
import {
|
||||
OPT_TRANS_DEEPLX,
|
||||
OPT_TRANS_OLLAMA,
|
||||
// OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_TRANS_NIUTRANS,
|
||||
OPT_TRANS_BUILTINAI,
|
||||
@@ -37,12 +38,14 @@ import {
|
||||
DEFAULT_BATCH_SIZE,
|
||||
DEFAULT_BATCH_LENGTH,
|
||||
DEFAULT_CONTEXT_SIZE,
|
||||
OPT_ALL_TYPES,
|
||||
OPT_ALL_TRANS_TYPES,
|
||||
API_SPE_TYPES,
|
||||
BUILTIN_STONES,
|
||||
BUILTIN_PLACEHOLDERS,
|
||||
BUILTIN_PLACETAGS,
|
||||
OPT_TRANS_AZUREAI,
|
||||
defaultNobatchPrompt,
|
||||
defaultNobatchUserPrompt,
|
||||
} from "../../config";
|
||||
import ValidationInput from "../../hooks/ValidationInput";
|
||||
|
||||
@@ -53,18 +56,25 @@ function TestButton({ api }) {
|
||||
const handleApiTest = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [text] = await apiTranslate({
|
||||
text: "hello world",
|
||||
const text = "hello world";
|
||||
const { trText } = await apiTranslate({
|
||||
text,
|
||||
fromLang: "en",
|
||||
toLang: "zh-CN",
|
||||
apiSetting: { ...api },
|
||||
useCache: false,
|
||||
usePool: false,
|
||||
});
|
||||
if (!text) {
|
||||
if (!trText) {
|
||||
throw new Error("empty result");
|
||||
}
|
||||
alert.success(i18n("test_success"));
|
||||
alert.success(
|
||||
<>
|
||||
<div>{i18n("test_success")}</div>
|
||||
<div>{text}</div>
|
||||
<div>{trText}</div>
|
||||
</>
|
||||
);
|
||||
} catch (err) {
|
||||
// alert.error(`${i18n("test_failed")}: ${err.message}`);
|
||||
let msg = err.message;
|
||||
@@ -76,24 +86,7 @@ function TestButton({ api }) {
|
||||
alert.error(
|
||||
<>
|
||||
<div>{i18n("test_failed")}</div>
|
||||
{msg === err.message ? (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
{msg}
|
||||
</div>
|
||||
) : (
|
||||
<pre
|
||||
style={{
|
||||
maxWidth: 400,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{msg}
|
||||
</pre>
|
||||
)}
|
||||
{msg === err.message ? <div>{msg}</div> : <pre>{msg}</pre>}
|
||||
</>
|
||||
);
|
||||
} finally {
|
||||
@@ -180,12 +173,14 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
model = "",
|
||||
apiType,
|
||||
systemPrompt = "",
|
||||
nobatchPrompt = defaultNobatchPrompt,
|
||||
nobatchUserPrompt = defaultNobatchUserPrompt,
|
||||
subtitlePrompt = "",
|
||||
// userPrompt = "",
|
||||
customHeader = "",
|
||||
customBody = "",
|
||||
think = false,
|
||||
thinkIgnore = "",
|
||||
// think = false,
|
||||
// thinkIgnore = "",
|
||||
fetchLimit = DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval = DEFAULT_FETCH_INTERVAL,
|
||||
httpTimeout = DEFAULT_HTTP_TIMEOUT,
|
||||
@@ -194,7 +189,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
reqHook = "",
|
||||
resHook = "",
|
||||
temperature = 0,
|
||||
maxTokens = 256,
|
||||
maxTokens = 20480,
|
||||
apiName = "",
|
||||
isDisabled = false,
|
||||
useBatchFetch = false,
|
||||
@@ -263,7 +258,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{API_SPE_TYPES.ai.has(apiType) && (
|
||||
{(API_SPE_TYPES.ai.has(apiType) || apiType === OPT_TRANS_CUSTOMIZE) && (
|
||||
<>
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={12}>
|
||||
@@ -308,29 +303,53 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
<ValidationInput
|
||||
size="small"
|
||||
fullWidth
|
||||
label={"Max Tokens"}
|
||||
label={"Max Tokens (0-1000000)"}
|
||||
type="number"
|
||||
name="maxTokens"
|
||||
value={maxTokens}
|
||||
onChange={handleChange}
|
||||
min={0}
|
||||
max={2 ** 15}
|
||||
max={1000000}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}></Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={"SYSTEM PROMPT"}
|
||||
name="systemPrompt"
|
||||
value={systemPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
helperText={i18n("system_prompt_helper")}
|
||||
/>
|
||||
{useBatchFetch ? (
|
||||
<TextField
|
||||
size="small"
|
||||
label={"BATCH SYSTEM PROMPT"}
|
||||
name="systemPrompt"
|
||||
value={systemPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
helperText={i18n("system_prompt_helper")}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"SYSTEM PROMPT"}
|
||||
name="nobatchPrompt"
|
||||
value={nobatchPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"USER PROMPT"}
|
||||
name="nobatchUserPrompt"
|
||||
value={nobatchUserPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={"SUBTITLE PROMPT"}
|
||||
@@ -353,7 +372,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{apiType === OPT_TRANS_OLLAMA && (
|
||||
{/* {apiType === OPT_TRANS_OLLAMA && (
|
||||
<>
|
||||
<TextField
|
||||
select
|
||||
@@ -374,7 +393,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{apiType === OPT_TRANS_NIUTRANS && (
|
||||
<>
|
||||
@@ -774,7 +793,7 @@ export default function Apis() {
|
||||
|
||||
const apiTypes = useMemo(
|
||||
() =>
|
||||
OPT_ALL_TYPES.map((type) => ({
|
||||
OPT_ALL_TRANS_TYPES.map((type) => ({
|
||||
type,
|
||||
label: type,
|
||||
})),
|
||||
@@ -806,6 +825,12 @@ export default function Apis() {
|
||||
{i18n("about_api_2")}
|
||||
<br />
|
||||
{i18n("about_api_3")}
|
||||
<Link
|
||||
href="https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md"
|
||||
target="_blank"
|
||||
>
|
||||
{i18n("goto_custom_api_example")}
|
||||
</Link>
|
||||
</Alert>
|
||||
|
||||
<Box>
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function Layout() {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box component="main" sx={{ flex: 1, p: 2 }}>
|
||||
<Box component="main" sx={{ flex: 1, p: 2, width: "100%" }}>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
URL_KISS_RULES_NEW_ISSUE,
|
||||
OPT_SYNCTYPE_WORKER,
|
||||
DEFAULT_TRANS_TAG,
|
||||
OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||
OPT_HIGHLIGHT_WORDS_DISABLE,
|
||||
OPT_SPLIT_PARAGRAPH_ALL,
|
||||
OPT_HIGHLIGHT_WORDS_ALL,
|
||||
} from "../../config";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
@@ -59,6 +63,7 @@ import AddIcon from "@mui/icons-material/Add";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import CancelIcon from "@mui/icons-material/Cancel";
|
||||
import SaveIcon from "@mui/icons-material/Save";
|
||||
import ValidationInput from "../../hooks/ValidationInput";
|
||||
import { kissLog } from "../../libs/log";
|
||||
import { useApiList } from "../../hooks/Api";
|
||||
import ShowMoreButton from "./ShowMoreButton";
|
||||
@@ -97,11 +102,13 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
ignoreSelector = "",
|
||||
terms = "",
|
||||
aiTerms = "",
|
||||
termsStyle = "",
|
||||
highlightStyle = "color: red;",
|
||||
selectStyle = "",
|
||||
parentStyle = "",
|
||||
grandStyle = "",
|
||||
injectJs = "",
|
||||
injectCss = "",
|
||||
// injectCss = "",
|
||||
apiSlug,
|
||||
fromLang,
|
||||
toLang,
|
||||
@@ -123,6 +130,9 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
transStartHook = "",
|
||||
transEndHook = "",
|
||||
// transRemoveHook = "",
|
||||
splitParagraph = OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||
splitLength = 0,
|
||||
highlightWords = OPT_HIGHLIGHT_WORDS_DISABLE,
|
||||
} = formValues;
|
||||
|
||||
const isModified = useMemo(() => {
|
||||
@@ -422,6 +432,59 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
</TextField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="splitParagraph"
|
||||
value={splitParagraph}
|
||||
label={i18n("split_paragraph")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
{OPT_SPLIT_PARAGRAPH_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<ValidationInput
|
||||
fullWidth
|
||||
size="small"
|
||||
label={i18n("split_length")}
|
||||
type="number"
|
||||
name="splitLength"
|
||||
value={splitLength}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
min={0}
|
||||
max={1000}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="highlightWords"
|
||||
value={highlightWords}
|
||||
label={i18n("highlight_words")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
{OPT_HIGHLIGHT_WORDS_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
@@ -547,10 +610,29 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
maxRows={10}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("terms_style")}
|
||||
name="termsStyle"
|
||||
value={termsStyle}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
maxRows={10}
|
||||
multiline
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("highlight_style")}
|
||||
name="highlightStyle"
|
||||
value={highlightStyle}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
maxRows={10}
|
||||
multiline
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("selector_style")}
|
||||
helperText={i18n("selector_style_helper")}
|
||||
name="selectStyle"
|
||||
value={selectStyle}
|
||||
disabled={disabled}
|
||||
@@ -561,7 +643,6 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("selector_parent_style")}
|
||||
helperText={i18n("selector_style_helper")}
|
||||
name="parentStyle"
|
||||
value={parentStyle}
|
||||
disabled={disabled}
|
||||
@@ -572,7 +653,6 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("selector_grand_style")}
|
||||
helperText={i18n("selector_style_helper")}
|
||||
name="grandStyle"
|
||||
value={grandStyle}
|
||||
disabled={disabled}
|
||||
@@ -615,7 +695,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
maxRows={10}
|
||||
/> */}
|
||||
|
||||
<TextField
|
||||
{/* <TextField
|
||||
size="small"
|
||||
label={i18n("inject_css")}
|
||||
helperText={i18n("inject_css_helper")}
|
||||
@@ -625,7 +705,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
onChange={handleChange}
|
||||
maxRows={10}
|
||||
multiline
|
||||
/>
|
||||
/> */}
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("inject_js")}
|
||||
@@ -867,9 +947,9 @@ function UserRules({ subRules, rules }) {
|
||||
|
||||
<UploadButton text={i18n("import")} handleImport={handleImport} />
|
||||
<DownloadButton
|
||||
handleData={() => JSON.stringify([...rules.list].reverse(), null, 2)}
|
||||
handleData={() => JSON.stringify([...rules.list], null, 2)}
|
||||
text={i18n("export")}
|
||||
fileName={`kiss-rules_${Date.now()}.json`}
|
||||
fileName={`kiss-rules_v2_${Date.now()}.json`}
|
||||
/>
|
||||
<DownloadButton
|
||||
handleData={async () => JSON.stringify(await getRulesOld(), null, 2)}
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function Settings() {
|
||||
newlineLength = TRANS_NEWLINE_LENGTH,
|
||||
httpTimeout = DEFAULT_HTTP_TIMEOUT,
|
||||
contextMenuType = 1,
|
||||
touchTranslate = 2,
|
||||
touchModes = [2],
|
||||
blacklist = DEFAULT_BLACKLIST.join(",\n"),
|
||||
csplist = DEFAULT_CSPLIST.join(",\n"),
|
||||
orilist = DEFAULT_ORILIST.join(",\n"),
|
||||
@@ -105,6 +105,7 @@ export default function Settings() {
|
||||
skipLangs = [],
|
||||
// detectRemote = true,
|
||||
transAllnow = false,
|
||||
rootMargin = 500,
|
||||
} = setting;
|
||||
const { isHide = false, fabClickAction = 0 } = fab || {};
|
||||
|
||||
@@ -124,7 +125,7 @@ export default function Settings() {
|
||||
<DownloadButton
|
||||
handleData={() => JSON.stringify(setting, null, 2)}
|
||||
text={i18n("export")}
|
||||
fileName={`kiss-setting_${Date.now()}.json`}
|
||||
fileName={`kiss-setting_v2_${Date.now()}.json`}
|
||||
/>
|
||||
<DownloadButton
|
||||
handleData={async () =>
|
||||
@@ -268,12 +269,15 @@ export default function Settings() {
|
||||
select
|
||||
fullWidth
|
||||
size="small"
|
||||
name="touchTranslate"
|
||||
value={touchTranslate}
|
||||
name="touchModes"
|
||||
value={touchModes}
|
||||
label={i18n("touch_translate_shortcut")}
|
||||
onChange={handleChange}
|
||||
SelectProps={{
|
||||
multiple: true,
|
||||
}}
|
||||
>
|
||||
{[0, 2, 3, 4].map((item) => (
|
||||
{[0, 2, 3, 4, 5, 6, 7].map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(`touch_tap_${item}`)}
|
||||
</MenuItem>
|
||||
@@ -295,34 +299,6 @@ export default function Settings() {
|
||||
<MenuItem value={2}>{i18n("secondary_context_menus")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transAllnow"
|
||||
value={transAllnow}
|
||||
label={i18n("trigger_mode")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("mk_pagescroll")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("mk_pageopen")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
{/* <Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="detectRemote"
|
||||
value={detectRemote}
|
||||
label={i18n("detect_lang_remote")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
||||
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid> */}
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
@@ -341,6 +317,47 @@ export default function Settings() {
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transAllnow"
|
||||
value={transAllnow}
|
||||
label={i18n("trigger_mode")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("mk_pagescroll")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("mk_pageopen")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<ValidationInput
|
||||
fullWidth
|
||||
size="small"
|
||||
label={i18n("pagescroll_root_margin")}
|
||||
type="number"
|
||||
name="rootMargin"
|
||||
value={rootMargin}
|
||||
onChange={handleChange}
|
||||
min={0}
|
||||
max={10000}
|
||||
/>
|
||||
</Grid>
|
||||
{/* <Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="detectRemote"
|
||||
value={detectRemote}
|
||||
label={i18n("detect_lang_remote")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
||||
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid> */}
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
@@ -379,18 +396,19 @@ export default function Settings() {
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("translate_blacklist")}
|
||||
helperText={i18n("pattern_helper")}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
onChange={handleChange}
|
||||
maxRows={10}
|
||||
multiline
|
||||
/>
|
||||
|
||||
{isExt ? (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("disabled_orilist")}
|
||||
helperText={i18n("pattern_helper")}
|
||||
name="orilist"
|
||||
value={orilist}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
@@ -409,6 +427,15 @@ export default function Settings() {
|
||||
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("disabled_orilist")}
|
||||
helperText={i18n("pattern_helper")}
|
||||
name="orilist"
|
||||
value={orilist}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("disabled_csplist")}
|
||||
@@ -453,17 +480,6 @@ export default function Settings() {
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("translate_blacklist")}
|
||||
helperText={i18n("pattern_helper")}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
onChange={handleChange}
|
||||
maxRows={10}
|
||||
multiline
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -32,6 +32,7 @@ export default function SubtitleSetting() {
|
||||
chunkLength,
|
||||
toLang,
|
||||
isBilingual,
|
||||
skipAd = false,
|
||||
windowStyle,
|
||||
originStyle,
|
||||
translationStyle,
|
||||
@@ -145,6 +146,20 @@ export default function SubtitleSetting() {
|
||||
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
size="small"
|
||||
name="skipAd"
|
||||
value={skipAd}
|
||||
label={i18n("is_skip_ad")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
||||
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ export default function Tranbox() {
|
||||
hideClickAway = false,
|
||||
simpleStyle = false,
|
||||
followSelection = false,
|
||||
autoHeight = false,
|
||||
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
|
||||
// extStyles = "",
|
||||
enDict = OPT_DICT_BING,
|
||||
@@ -330,6 +331,20 @@ export default function Tranbox() {
|
||||
max={200}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
size="small"
|
||||
name="autoHeight"
|
||||
value={autoHeight}
|
||||
label={i18n("tranbox_auto_height")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
{!isExt && (
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<ShortcutInput
|
||||
|
||||
@@ -5,7 +5,7 @@ import Stack from "@mui/material/Stack";
|
||||
import DarkModeButton from "../Options/DarkModeButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
export default function Header({ setShowPopup }) {
|
||||
export default function Header({ onClose }) {
|
||||
const handleHomepage = () => {
|
||||
window.open(process.env.REACT_APP_HOMEPAGE, "_blank");
|
||||
};
|
||||
@@ -33,10 +33,10 @@ export default function Header({ setShowPopup }) {
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{setShowPopup ? (
|
||||
{onClose ? (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setShowPopup(false);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
|
||||
422
src/views/Popup/PopupCont.js
Normal file
422
src/views/Popup/PopupCont.js
Normal file
@@ -0,0 +1,422 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import Button from "@mui/material/Button";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { sendBgMsg, sendTabMsg, getCurTab } from "../../libs/msg";
|
||||
import { isExt } from "../../libs/client";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
MSG_SAVE_RULE,
|
||||
MSG_COMMAND_SHORTCUTS,
|
||||
MSG_TRANSBOX_TOGGLE,
|
||||
MSG_MOUSEHOVER_TOGGLE,
|
||||
MSG_TRANSINPUT_TOGGLE,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_STYLE_ALL,
|
||||
} from "../../config";
|
||||
import { saveRule } from "../../libs/rules";
|
||||
import { tryClearCaches } from "../../libs/cache";
|
||||
import { kissLog } from "../../libs/log";
|
||||
import { parseUrlPattern } from "../../libs/utils";
|
||||
|
||||
export default function PopupCont({
|
||||
rule,
|
||||
setting,
|
||||
setRule,
|
||||
setSetting,
|
||||
handleOpenSetting,
|
||||
processActions,
|
||||
isContent = false,
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [commands, setCommands] = useState({});
|
||||
|
||||
const handleTransToggle = async (e) => {
|
||||
try {
|
||||
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
|
||||
|
||||
if (!processActions) {
|
||||
await sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
} else {
|
||||
processActions({ action: MSG_TRANS_TOGGLE });
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("toggle trans", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTransboxToggle = async (e) => {
|
||||
try {
|
||||
setSetting((pre) => ({
|
||||
...pre,
|
||||
tranboxSetting: { ...pre.tranboxSetting, transOpen: e.target.checked },
|
||||
}));
|
||||
|
||||
if (!processActions) {
|
||||
await sendTabMsg(MSG_TRANSBOX_TOGGLE);
|
||||
} else {
|
||||
processActions({ action: MSG_TRANSBOX_TOGGLE });
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("toggle transbox", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMousehoverToggle = async (e) => {
|
||||
try {
|
||||
setSetting((pre) => ({
|
||||
...pre,
|
||||
mouseHoverSetting: {
|
||||
...pre.mouseHoverSetting,
|
||||
useMouseHover: e.target.checked,
|
||||
},
|
||||
}));
|
||||
|
||||
if (!processActions) {
|
||||
await sendTabMsg(MSG_MOUSEHOVER_TOGGLE);
|
||||
} else {
|
||||
processActions({ action: MSG_MOUSEHOVER_TOGGLE });
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("toggle mousehover", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputTransToggle = async (e) => {
|
||||
try {
|
||||
setSetting((pre) => ({
|
||||
...pre,
|
||||
inputRule: {
|
||||
...pre.inputRule,
|
||||
transOpen: e.target.checked,
|
||||
},
|
||||
}));
|
||||
|
||||
if (!processActions) {
|
||||
await sendTabMsg(MSG_TRANSINPUT_TOGGLE);
|
||||
} else {
|
||||
processActions({ action: MSG_TRANSINPUT_TOGGLE });
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("toggle inputtrans", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = async (e) => {
|
||||
try {
|
||||
const { name, value } = e.target;
|
||||
setRule((pre) => ({ ...pre, [name]: value }));
|
||||
|
||||
if (!processActions) {
|
||||
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||
} else {
|
||||
processActions({ action: MSG_TRANS_PUTRULE, args: { [name]: value } });
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("update rule", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearCache = () => {
|
||||
tryClearCaches();
|
||||
};
|
||||
|
||||
const handleSaveRule = async () => {
|
||||
try {
|
||||
let href = "";
|
||||
if (!isContent) {
|
||||
const tab = await getCurTab();
|
||||
href = tab.url;
|
||||
} else {
|
||||
href = window.location?.href;
|
||||
}
|
||||
|
||||
if (!href || typeof href !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = parseUrlPattern(href);
|
||||
const curRule = { ...rule, pattern };
|
||||
if (isExt && isContent) {
|
||||
sendBgMsg(MSG_SAVE_RULE, curRule);
|
||||
} else {
|
||||
saveRule(curRule);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("save rule", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const commands = {};
|
||||
if (isExt) {
|
||||
const res = await sendBgMsg(MSG_COMMAND_SHORTCUTS);
|
||||
res.forEach(({ name, shortcut }) => {
|
||||
commands[name] = shortcut;
|
||||
});
|
||||
} else {
|
||||
const shortcuts = setting.shortcuts;
|
||||
if (shortcuts) {
|
||||
Object.entries(shortcuts).forEach(([key, val]) => {
|
||||
commands[key] = val.join("+");
|
||||
});
|
||||
}
|
||||
}
|
||||
setCommands(commands);
|
||||
} catch (err) {
|
||||
kissLog("query cmds", err);
|
||||
}
|
||||
})();
|
||||
}, [setting.shortcuts]);
|
||||
|
||||
const optApis = useMemo(
|
||||
() =>
|
||||
setting.transApis
|
||||
.filter((api) => !api.isDisabled)
|
||||
.map((api) => ({
|
||||
key: api.apiSlug,
|
||||
name: api.apiName || api.apiSlug,
|
||||
})),
|
||||
[setting.transApis]
|
||||
);
|
||||
|
||||
const tranboxEnabled = setting.tranboxSetting.transOpen;
|
||||
const mouseHoverEnabled = setting.mouseHoverSetting.useMouseHover;
|
||||
const inputTransEnabled = setting.inputRule.transOpen;
|
||||
|
||||
const {
|
||||
transOpen,
|
||||
apiSlug,
|
||||
fromLang,
|
||||
toLang,
|
||||
textStyle,
|
||||
autoScan,
|
||||
transOnly,
|
||||
hasRichText,
|
||||
hasShadowroot,
|
||||
} = rule;
|
||||
|
||||
return (
|
||||
<Stack sx={{ p: 2 }} spacing={2}>
|
||||
<Grid container columns={12} spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={transOpen === "true"}
|
||||
onChange={handleTransToggle}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
commands["toggleTranslate"]
|
||||
? `${i18n("translate_alt")}(${commands["toggleTranslate"]})`
|
||||
: i18n("translate_alt")
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="autoScan"
|
||||
value={autoScan === "true" ? "false" : "true"}
|
||||
checked={autoScan === "true"}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label={i18n("autoscan_alt")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="hasShadowroot"
|
||||
value={hasShadowroot === "true" ? "false" : "true"}
|
||||
checked={hasShadowroot === "true"}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label={i18n("shadowroot_alt")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="hasRichText"
|
||||
value={hasRichText === "true" ? "false" : "true"}
|
||||
checked={hasRichText === "true"}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label={i18n("richtext_alt")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="transOnly"
|
||||
value={transOnly === "true" ? "false" : "true"}
|
||||
checked={transOnly === "true"}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label={i18n("transonly_alt")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="tranboxEnabled"
|
||||
value={!tranboxEnabled}
|
||||
checked={tranboxEnabled}
|
||||
onChange={handleTransboxToggle}
|
||||
/>
|
||||
}
|
||||
label={i18n("selection_translate")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="mouseHoverEnabled"
|
||||
value={!mouseHoverEnabled}
|
||||
checked={mouseHoverEnabled}
|
||||
onChange={handleMousehoverToggle}
|
||||
/>
|
||||
}
|
||||
label={i18n("mousehover_translate")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="inputTransEnabled"
|
||||
value={!inputTransEnabled}
|
||||
checked={inputTransEnabled}
|
||||
onChange={handleInputTransToggle}
|
||||
/>
|
||||
}
|
||||
label={i18n("input_translate")}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={apiSlug}
|
||||
name="apiSlug"
|
||||
label={i18n("translate_service")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{optApis.map(({ key, name }) => (
|
||||
<MenuItem key={key} value={key}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={fromLang}
|
||||
name="fromLang"
|
||||
label={i18n("from_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={toLang}
|
||||
name="toLang"
|
||||
label={i18n("to_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={textStyle}
|
||||
name="textStyle"
|
||||
label={
|
||||
commands["toggleStyle"]
|
||||
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
|
||||
: i18n("text_style_alt")
|
||||
}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{/* {OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||
<TextField
|
||||
size="small"
|
||||
name="bgColor"
|
||||
value={bgColor}
|
||||
label={i18n("bg_color")}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)} */}
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
>
|
||||
<Button variant="text" onClick={handleSaveRule}>
|
||||
{i18n("save_rule")}
|
||||
</Button>
|
||||
<Button variant="text" onClick={handleClearCache}>
|
||||
{i18n("clear_cache")}
|
||||
</Button>
|
||||
<Button variant="text" onClick={handleOpenSetting}>
|
||||
{i18n("setting")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,182 +1,26 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import Button from "@mui/material/Button";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { sendBgMsg, sendTabMsg, getCurTab } from "../../libs/msg";
|
||||
import { sendTabMsg } from "../../libs/msg";
|
||||
import { browser } from "../../libs/browser";
|
||||
import { isExt } from "../../libs/client";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Header from "./Header";
|
||||
import {
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
MSG_OPEN_OPTIONS,
|
||||
MSG_SAVE_RULE,
|
||||
MSG_COMMAND_SHORTCUTS,
|
||||
MSG_TRANSBOX_TOGGLE,
|
||||
MSG_MOUSEHOVER_TOGGLE,
|
||||
MSG_TRANSINPUT_TOGGLE,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_STYLE_ALL,
|
||||
} from "../../config";
|
||||
import { sendIframeMsg } from "../../libs/iframe";
|
||||
import { saveRule } from "../../libs/rules";
|
||||
import { tryClearCaches } from "../../libs/cache";
|
||||
import { MSG_TRANS_GETRULE } from "../../config";
|
||||
import { kissLog } from "../../libs/log";
|
||||
import { parseUrlPattern } from "../../libs/utils";
|
||||
import PopupCont from "./PopupCont";
|
||||
|
||||
// 插件popup没有参数
|
||||
// 网页弹框有
|
||||
export default function Popup({ setShowPopup, translator }) {
|
||||
export default function Popup() {
|
||||
const i18n = useI18n();
|
||||
const [rule, setRule] = useState(translator?.rule);
|
||||
const [setting, setSetting] = useState(translator?.setting);
|
||||
const [commands, setCommands] = useState({});
|
||||
const [rule, setRule] = useState(null);
|
||||
const [setting, setSetting] = useState(null);
|
||||
|
||||
const handleOpenSetting = () => {
|
||||
if (!translator) {
|
||||
browser?.runtime.openOptionsPage();
|
||||
} else if (isExt) {
|
||||
sendBgMsg(MSG_OPEN_OPTIONS);
|
||||
} else {
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||
}
|
||||
setShowPopup && setShowPopup(false);
|
||||
};
|
||||
|
||||
const handleTransToggle = async (e) => {
|
||||
try {
|
||||
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
|
||||
|
||||
if (!translator) {
|
||||
await sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
} else {
|
||||
translator.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("toggle trans", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTransboxToggle = async (e) => {
|
||||
try {
|
||||
setSetting((pre) => ({
|
||||
...pre,
|
||||
tranboxSetting: { ...pre.tranboxSetting, transOpen: e.target.checked },
|
||||
}));
|
||||
|
||||
if (!translator) {
|
||||
await sendTabMsg(MSG_TRANSBOX_TOGGLE);
|
||||
} else {
|
||||
translator.toggleTransbox();
|
||||
sendIframeMsg(MSG_TRANSBOX_TOGGLE);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("toggle transbox", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMousehoverToggle = async (e) => {
|
||||
try {
|
||||
setSetting((pre) => ({
|
||||
...pre,
|
||||
mouseHoverSetting: {
|
||||
...pre.mouseHoverSetting,
|
||||
useMouseHover: e.target.checked,
|
||||
},
|
||||
}));
|
||||
|
||||
if (!translator) {
|
||||
await sendTabMsg(MSG_MOUSEHOVER_TOGGLE);
|
||||
} else {
|
||||
translator.toggleMouseHover();
|
||||
sendIframeMsg(MSG_MOUSEHOVER_TOGGLE);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("toggle mousehover", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputTransToggle = async (e) => {
|
||||
try {
|
||||
setSetting((pre) => ({
|
||||
...pre,
|
||||
inputRule: {
|
||||
...pre.inputRule,
|
||||
transOpen: e.target.checked,
|
||||
},
|
||||
}));
|
||||
|
||||
if (!translator) {
|
||||
await sendTabMsg(MSG_TRANSINPUT_TOGGLE);
|
||||
} else {
|
||||
translator.toggleInputTranslate();
|
||||
sendIframeMsg(MSG_TRANSINPUT_TOGGLE);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("toggle inputtrans", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = async (e) => {
|
||||
try {
|
||||
const { name, value } = e.target;
|
||||
setRule((pre) => ({ ...pre, [name]: value }));
|
||||
|
||||
if (!translator) {
|
||||
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||
} else {
|
||||
translator.updateRule({ [name]: value });
|
||||
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("update rule", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearCache = () => {
|
||||
tryClearCaches();
|
||||
};
|
||||
|
||||
const handleSaveRule = async () => {
|
||||
try {
|
||||
let href = "";
|
||||
if (!translator) {
|
||||
const tab = await getCurTab();
|
||||
href = tab.url;
|
||||
} else {
|
||||
href = window.location?.href;
|
||||
}
|
||||
|
||||
if (!href || typeof href !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = parseUrlPattern(href);
|
||||
const curRule = { ...rule, pattern };
|
||||
if (isExt && translator) {
|
||||
sendBgMsg(MSG_SAVE_RULE, curRule);
|
||||
} else {
|
||||
saveRule(curRule);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("save rule", err);
|
||||
}
|
||||
};
|
||||
const handleOpenSetting = useCallback(() => {
|
||||
browser?.runtime.openOptionsPage();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (translator) {
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
const res = await sendTabMsg(MSG_TRANS_GETRULE);
|
||||
@@ -188,297 +32,27 @@ export default function Popup({ setShowPopup, translator }) {
|
||||
kissLog("query rule", err);
|
||||
}
|
||||
})();
|
||||
}, [translator]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const commands = {};
|
||||
if (isExt) {
|
||||
const res = await sendBgMsg(MSG_COMMAND_SHORTCUTS);
|
||||
res.forEach(({ name, shortcut }) => {
|
||||
commands[name] = shortcut;
|
||||
});
|
||||
} else {
|
||||
const shortcuts = translator.setting.shortcuts;
|
||||
if (shortcuts) {
|
||||
Object.entries(shortcuts).forEach(([key, val]) => {
|
||||
commands[key] = val.join("+");
|
||||
});
|
||||
}
|
||||
}
|
||||
setCommands(commands);
|
||||
} catch (err) {
|
||||
kissLog("query cmds", err);
|
||||
}
|
||||
})();
|
||||
}, [translator]);
|
||||
|
||||
const optApis = useMemo(
|
||||
() =>
|
||||
setting?.transApis
|
||||
.filter((api) => !api.isDisabled)
|
||||
.map((api) => ({
|
||||
key: api.apiSlug,
|
||||
name: api.apiName || api.apiSlug,
|
||||
})),
|
||||
[setting]
|
||||
);
|
||||
|
||||
const tranboxEnabled = setting?.tranboxSetting.transOpen;
|
||||
const mouseHoverEnabled = setting?.mouseHoverSetting.useMouseHover;
|
||||
const inputTransEnabled = setting?.inputRule.transOpen;
|
||||
|
||||
if (!rule) {
|
||||
return (
|
||||
<Box minWidth={300}>
|
||||
{!translator && (
|
||||
<>
|
||||
<Header />
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
return (
|
||||
<Box width={360}>
|
||||
<Header />
|
||||
<Divider />
|
||||
{rule && setting ? (
|
||||
<PopupCont
|
||||
rule={rule}
|
||||
setting={setting}
|
||||
setRule={setRule}
|
||||
setSetting={setSetting}
|
||||
handleOpenSetting={handleOpenSetting}
|
||||
/>
|
||||
) : (
|
||||
<Stack sx={{ p: 2 }} spacing={3}>
|
||||
<Button variant="text" onClick={handleOpenSetting}>
|
||||
{i18n("setting")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
transOpen,
|
||||
apiSlug,
|
||||
fromLang,
|
||||
toLang,
|
||||
textStyle,
|
||||
autoScan,
|
||||
transOnly,
|
||||
hasRichText,
|
||||
hasShadowroot,
|
||||
} = rule;
|
||||
|
||||
return (
|
||||
<Box width={360}>
|
||||
{!translator && (
|
||||
<>
|
||||
<Header />
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<Stack sx={{ p: 2 }} spacing={2}>
|
||||
<Grid container columns={12} spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={transOpen === "true"}
|
||||
onChange={handleTransToggle}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
commands["toggleTranslate"]
|
||||
? `${i18n("translate_alt")}(${commands["toggleTranslate"]})`
|
||||
: i18n("translate_alt")
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="autoScan"
|
||||
value={autoScan === "true" ? "false" : "true"}
|
||||
checked={autoScan === "true"}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label={i18n("autoscan_alt")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="hasShadowroot"
|
||||
value={hasShadowroot === "true" ? "false" : "true"}
|
||||
checked={hasShadowroot === "true"}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label={i18n("shadowroot_alt")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="hasRichText"
|
||||
value={hasRichText === "true" ? "false" : "true"}
|
||||
checked={hasRichText === "true"}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label={i18n("richtext_alt")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="transOnly"
|
||||
value={transOnly === "true" ? "false" : "true"}
|
||||
checked={transOnly === "true"}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label={i18n("transonly_alt")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="tranboxEnabled"
|
||||
value={!tranboxEnabled}
|
||||
checked={tranboxEnabled}
|
||||
onChange={handleTransboxToggle}
|
||||
/>
|
||||
}
|
||||
label={i18n("selection_translate")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="mouseHoverEnabled"
|
||||
value={!mouseHoverEnabled}
|
||||
checked={mouseHoverEnabled}
|
||||
onChange={handleMousehoverToggle}
|
||||
/>
|
||||
}
|
||||
label={i18n("mousehover_translate")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="inputTransEnabled"
|
||||
value={!inputTransEnabled}
|
||||
checked={inputTransEnabled}
|
||||
onChange={handleInputTransToggle}
|
||||
/>
|
||||
}
|
||||
label={i18n("input_translate")}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={apiSlug}
|
||||
name="apiSlug"
|
||||
label={i18n("translate_service")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{optApis.map(({ key, name }) => (
|
||||
<MenuItem key={key} value={key}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={fromLang}
|
||||
name="fromLang"
|
||||
label={i18n("from_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={toLang}
|
||||
name="toLang"
|
||||
label={i18n("to_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={textStyle}
|
||||
name="textStyle"
|
||||
label={
|
||||
commands["toggleStyle"]
|
||||
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
|
||||
: i18n("text_style_alt")
|
||||
}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{/* {OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||
<TextField
|
||||
size="small"
|
||||
name="bgColor"
|
||||
value={bgColor}
|
||||
label={i18n("bg_color")}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)} */}
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
>
|
||||
<Button variant="text" onClick={handleSaveRule}>
|
||||
{i18n("save_rule")}
|
||||
</Button>
|
||||
<Button variant="text" onClick={handleClearCache}>
|
||||
{i18n("clear_cache")}
|
||||
</Button>
|
||||
<Button variant="text" onClick={handleOpenSetting}>
|
||||
{i18n("setting")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ export default function DraggableResizable({
|
||||
setPosition,
|
||||
onChangeSize,
|
||||
onChangePosition,
|
||||
autoHeight,
|
||||
...props
|
||||
}) {
|
||||
const lineWidth = 4;
|
||||
@@ -222,11 +223,19 @@ export default function DraggableResizable({
|
||||
</Pointer>
|
||||
<Box
|
||||
className="KT-draggable-container"
|
||||
style={{
|
||||
width: size.w,
|
||||
height: size.h,
|
||||
overflow: "hidden auto",
|
||||
}}
|
||||
style={
|
||||
autoHeight
|
||||
? {
|
||||
width: size.w,
|
||||
maxHeight: size.h,
|
||||
overflow: "hidden auto",
|
||||
}
|
||||
: {
|
||||
width: size.w,
|
||||
height: size.h,
|
||||
overflow: "hidden auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { isMobile } from "../../libs/mobile";
|
||||
import TranForm from "./TranForm.js";
|
||||
|
||||
function Header({
|
||||
setShowPopup,
|
||||
setShowBox,
|
||||
simpleStyle,
|
||||
setSimpleStyle,
|
||||
hideClickAway,
|
||||
@@ -98,7 +98,7 @@ function Header({
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setShowPopup(false);
|
||||
setShowBox(false);
|
||||
}}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
@@ -111,10 +111,19 @@ function Header({
|
||||
}
|
||||
|
||||
export default function TranBox({
|
||||
showBox,
|
||||
text,
|
||||
setText,
|
||||
setShowBox,
|
||||
tranboxSetting: { enDict, enSug, apiSlugs, fromLang, toLang, toLang2 },
|
||||
tranboxSetting: {
|
||||
enDict,
|
||||
enSug,
|
||||
apiSlugs,
|
||||
fromLang,
|
||||
toLang,
|
||||
toLang2,
|
||||
autoHeight,
|
||||
},
|
||||
transApis,
|
||||
boxSize,
|
||||
setBoxSize,
|
||||
@@ -134,43 +143,46 @@ export default function TranBox({
|
||||
return (
|
||||
<SettingProvider>
|
||||
<ThemeProvider styles={extStyles}>
|
||||
<DraggableResizable
|
||||
position={boxPosition}
|
||||
size={boxSize}
|
||||
setSize={setBoxSize}
|
||||
setPosition={setBoxPosition}
|
||||
header={
|
||||
<Header
|
||||
setShowPopup={setShowBox}
|
||||
simpleStyle={simpleStyle}
|
||||
setSimpleStyle={setSimpleStyle}
|
||||
hideClickAway={hideClickAway}
|
||||
setHideClickAway={setHideClickAway}
|
||||
followSelection={followSelection}
|
||||
setFollowSelection={setFollowSelection}
|
||||
mouseHover={mouseHover}
|
||||
/>
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseEnter={() => setMouseHover(true)}
|
||||
onMouseLeave={() => setMouseHover(false)}
|
||||
>
|
||||
<Box sx={{ p: simpleStyle ? 1 : 2 }}>
|
||||
<TranForm
|
||||
text={text}
|
||||
setText={setText}
|
||||
apiSlugs={apiSlugs}
|
||||
fromLang={fromLang}
|
||||
toLang={toLang}
|
||||
toLang2={toLang2}
|
||||
transApis={transApis}
|
||||
simpleStyle={simpleStyle}
|
||||
langDetector={langDetector}
|
||||
enDict={enDict}
|
||||
enSug={enSug}
|
||||
/>
|
||||
</Box>
|
||||
</DraggableResizable>
|
||||
{showBox && (
|
||||
<DraggableResizable
|
||||
position={boxPosition}
|
||||
size={boxSize}
|
||||
setSize={setBoxSize}
|
||||
setPosition={setBoxPosition}
|
||||
autoHeight={autoHeight}
|
||||
header={
|
||||
<Header
|
||||
setShowBox={setShowBox}
|
||||
simpleStyle={simpleStyle}
|
||||
setSimpleStyle={setSimpleStyle}
|
||||
hideClickAway={hideClickAway}
|
||||
setHideClickAway={setHideClickAway}
|
||||
followSelection={followSelection}
|
||||
setFollowSelection={setFollowSelection}
|
||||
mouseHover={mouseHover}
|
||||
/>
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseEnter={() => setMouseHover(true)}
|
||||
onMouseLeave={() => setMouseHover(false)}
|
||||
>
|
||||
<Box sx={{ p: simpleStyle ? 1 : 2 }}>
|
||||
<TranForm
|
||||
text={text}
|
||||
setText={setText}
|
||||
apiSlugs={apiSlugs}
|
||||
fromLang={fromLang}
|
||||
toLang={toLang}
|
||||
toLang2={toLang2}
|
||||
transApis={transApis}
|
||||
simpleStyle={simpleStyle}
|
||||
langDetector={langDetector}
|
||||
enDict={enDict}
|
||||
enSug={enSug}
|
||||
/>
|
||||
</Box>
|
||||
</DraggableResizable>
|
||||
)}
|
||||
</ThemeProvider>
|
||||
</SettingProvider>
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function TranCont({
|
||||
setTrText("");
|
||||
setError("");
|
||||
|
||||
const [trText] = await apiTranslate({
|
||||
const { trText } = await apiTranslate({
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
OPT_TRANBOX_TRIGGER_CLICK,
|
||||
OPT_TRANBOX_TRIGGER_HOVER,
|
||||
OPT_TRANBOX_TRIGGER_SELECT,
|
||||
EVENT_KISS,
|
||||
} from "../../config";
|
||||
import { isMobile } from "../../libs/mobile";
|
||||
import { kissLog } from "../../libs/log";
|
||||
import { useLangMap } from "../../hooks/I18n";
|
||||
import { debouncePutTranBox, getTranBox } from "../../libs/storage";
|
||||
|
||||
export default function Slection({
|
||||
contextMenuType,
|
||||
@@ -106,6 +108,29 @@ export default function Slection({
|
||||
return "onMouseUp";
|
||||
}, [triggerMode]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const { w, h, x, y } = (await getTranBox()) || {};
|
||||
if (w !== undefined && h !== undefined) {
|
||||
setBoxSize({ w, h });
|
||||
}
|
||||
if (x !== undefined && y !== undefined) {
|
||||
setBoxPosition({
|
||||
x: limitNumber(x, 0, window.innerWidth),
|
||||
y: limitNumber(y, 0, window.innerHeight),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
debouncePutTranBox({ ...boxSize, ...boxPosition });
|
||||
}, [boxSize, boxPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
async function handleMouseup(e) {
|
||||
e.stopPropagation();
|
||||
@@ -167,12 +192,26 @@ export default function Slection({
|
||||
};
|
||||
}, [tranboxShortcut, handleTranbox]);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (showBox) {
|
||||
setShowBox(false);
|
||||
} else {
|
||||
handleTranbox();
|
||||
}
|
||||
}, [showBox, handleTranbox]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(MSG_OPEN_TRANBOX, handleTranbox);
|
||||
return () => {
|
||||
window.removeEventListener(MSG_OPEN_TRANBOX, handleTranbox);
|
||||
const handleStatusUpdate = (event) => {
|
||||
if (event.detail?.action === MSG_OPEN_TRANBOX) {
|
||||
handleToggle();
|
||||
}
|
||||
};
|
||||
}, [handleTranbox]);
|
||||
|
||||
document.addEventListener(EVENT_KISS, handleStatusUpdate);
|
||||
return () => {
|
||||
document.removeEventListener(EVENT_KISS, handleStatusUpdate);
|
||||
};
|
||||
}, [handleToggle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGm) {
|
||||
@@ -217,8 +256,9 @@ export default function Slection({
|
||||
|
||||
return (
|
||||
<>
|
||||
{showBox && (
|
||||
{
|
||||
<TranBox
|
||||
showBox={showBox}
|
||||
text={text}
|
||||
setText={setText}
|
||||
boxSize={boxSize}
|
||||
@@ -237,7 +277,7 @@ export default function Slection({
|
||||
// extStyles={extStyles}
|
||||
langDetector={langDetector}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
|
||||
{showBtn && (
|
||||
<TranBtn
|
||||
|
||||
Reference in New Issue
Block a user