Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bfa12b61c | ||
|
|
79bd776ef9 | ||
|
|
222428ad47 | ||
|
|
4b3853dd22 | ||
|
|
9dd191902c | ||
|
|
3f524ad674 | ||
|
|
7e6376fcb7 | ||
|
|
6f35013faf | ||
|
|
e71acdaaa9 | ||
|
|
fd7c663282 | ||
|
|
89b2bbe9ac | ||
|
|
7eb64a463b | ||
|
|
8971a28abc | ||
|
|
2ff989429f | ||
|
|
24369e2581 | ||
|
|
2bb8a5182c | ||
|
|
629bf9461a | ||
|
|
a56fb6c8d6 | ||
|
|
efb3529c92 | ||
|
|
a372a4173c | ||
|
|
5e46832548 | ||
|
|
91869c42e1 | ||
|
|
d421748bed | ||
|
|
7e5cd7e5a6 | ||
|
|
2b910b2c47 | ||
|
|
814ce4ca11 | ||
|
|
1e63fd1e19 | ||
|
|
4b19902e5c | ||
|
|
fd014a1d34 | ||
|
|
fd91bcf603 | ||
|
|
61a4a8f920 | ||
|
|
ed4275a18b | ||
|
|
7481d65e1e | ||
|
|
0c49cf1af9 | ||
|
|
7f04000739 | ||
|
|
e3da9824b6 | ||
|
|
34370345cd | ||
|
|
6c1a4e851c | ||
|
|
766e3ce7f9 | ||
|
|
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 |
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.3
|
||||
REACT_APP_VERSION=2.0.8
|
||||
|
||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||
|
||||
|
||||
104
README.en.md
104
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,9 +112,20 @@ Personal Rules > Subscription Rules > Global Rules
|
||||
|
||||
Among these, Global Rules have the lowest priority but are very important as they serve as the default rules.
|
||||
|
||||
### Local Ollama interface cannot be used
|
||||
### API (Ollama, etc.) Test Failure
|
||||
|
||||
If encountering a 403 error, refer to: https://github.com/fishjar/kiss-translator/issues/174
|
||||
Common reasons for API test failures include:
|
||||
|
||||
- Incorrect address:
|
||||
- For example, `Ollama` has a native API address and an `Openai`-compatible address. This plugin currently supports the `Openai`-compatible address and does not support the `Ollama` native API address.
|
||||
- Some AI models do not support batch translation:
|
||||
- In this case, you can choose to disable batch translation or use a custom API.
|
||||
- Alternatively, you can use a custom API. For details, please refer to: [Custom API Example Documentation](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
- Some AI models have inconsistent parameters:
|
||||
- For example, the parameters of the `Gemini` native API are highly inconsistent. Some model versions do not support certain parameters, leading to errors.
|
||||
- In this case, you can modify the request body using a `Hook`, or replace it with `Gemini2` (an OpenAI-compatible address).
|
||||
- The server restricts cross-origin access, returning a 403 error:
|
||||
- For example, `Ollama` requires adding the environment variable `OLLAMA_ORIGINS=*` when starting. See: https://github.com/fishjar/kiss-translator/issues/174
|
||||
|
||||
### Custom API doesn't work in Tampermonkey scripts
|
||||
|
||||
@@ -153,6 +137,14 @@ Custom APIs are very powerful and flexible, and can theoretically connect to any
|
||||
|
||||
Example reference: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
|
||||
### How to directly access the Tampermonkey script settings page
|
||||
|
||||
Settings page address: https://fishjar.github.io/kiss-translator/options.html
|
||||
|
||||
### Subtitle Translation Tips
|
||||
|
||||
As long as the KT button is on (blue background with white text), you don't need to click it multiple times. Just click the original subtitle button in the YouTube player and wait for the bilingual subtitles to appear automatically.
|
||||
|
||||
## Future Plans
|
||||
|
||||
This is a side project with no strict timeline. Community contributions are welcome. The following are preliminary feature directions:
|
||||
|
||||
78
README.md
78
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,9 +112,20 @@
|
||||
|
||||
其中全局规则优先级最低,但非常重要,相当于兜底规则。
|
||||
|
||||
### 本地的Ollama接口不能使用
|
||||
### 接口(Ollama等)测试失败
|
||||
|
||||
如果出现403的情况,参考:https://github.com/fishjar/kiss-translator/issues/174
|
||||
一般接口测试失败常见有以下几种原因:
|
||||
|
||||
- 地址填错了:
|
||||
- 比如 `Ollama` 有原生接口地址和 `Openai` 兼容的地址,本插件目前统一支持 `Openai` 兼容的地址,不支持 `Ollama` 原生接口地址
|
||||
- 某些AI模型不支持聚合翻译:
|
||||
- 此种情况可以选择禁用聚合翻译或通过自定义接口的方式来使用。
|
||||
- 或通过自定义接口的方式来使用,详情参考: [自定义接口示例文档](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
- 某些AI模型的参数不一致:
|
||||
- 比如 `Gemini` 原生接口参数非常不一致,部分版本的模型不支持某些参数会导致返回错误。
|
||||
- 此种情况可以通过 `Hook` 修改请求 `body` ,或者更换为 `Gemini2` (`Openai` 兼容的地址)
|
||||
- 服务器跨域限制访问,返回403错误:
|
||||
- 比如 `Ollama` 启动时须添加环境变量 `OLLAMA_ORIGINS=*`, 参考:https://github.com/fishjar/kiss-translator/issues/174
|
||||
|
||||
### 填写的接口在油猴脚本不能使用
|
||||
|
||||
@@ -149,6 +137,14 @@
|
||||
|
||||
示例参考: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
|
||||
### 如何直接进入油猴脚本设置页面
|
||||
|
||||
设置页面地址: https://fishjar.github.io/kiss-translator/options.html
|
||||
|
||||
### 字幕翻译小技巧
|
||||
|
||||
KT按钮只要是开启状态(蓝底白字),无需多次点击,只需点击开启Youtube播放器本来的字幕按钮,然后等待双语字幕自动呈现即可。
|
||||
|
||||
## 未来规划
|
||||
|
||||
本项目为业余开发,无严格时间表,欢迎社区共建。以下为初步设想的功能方向:
|
||||
|
||||
@@ -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";
|
||||
|
||||
110
custom-api_v2.md
110
custom-api_v2.md
@@ -1,4 +1,67 @@
|
||||
# 自定义接口示例
|
||||
# 自定义接口说明及示例
|
||||
|
||||
## 默认接口规范
|
||||
|
||||
如果接口的请求数据和返回数据符合以下规范,
|
||||
则无需填写 `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" // 原文语言
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Prompt 相关
|
||||
|
||||
`Prompt` 可替换占位符:
|
||||
|
||||
```js
|
||||
`{{from}}` // 原文语言名称
|
||||
`{{to}}` // 目标语言名称
|
||||
`{{fromLang}}` // 原文语言代码
|
||||
`{{toLang}}` // 目标语言代码
|
||||
`{{text}}` // 原文
|
||||
`{{tone}}` // 风格
|
||||
`{{title}}` // 页面标题
|
||||
`{{description}}` // 页面描述
|
||||
```
|
||||
|
||||
Hook 中 `Prompt` 类型说明:
|
||||
|
||||
```js
|
||||
`systemPrompt` // 聚合翻译 System Prompt
|
||||
`nobatchPrompt` // 非聚合翻译 System Prompt
|
||||
`nobatchUserPrompt` // 非聚合翻译 User Prompt
|
||||
`subtitlePrompt` // 字幕翻译 System Prompt
|
||||
```
|
||||
|
||||
## 谷歌翻译接口
|
||||
|
||||
@@ -60,9 +123,12 @@ async (args) => {
|
||||
{
|
||||
role: "user",
|
||||
content: JSON.stringify({
|
||||
targetLanguage: args.to,
|
||||
targetLanguage: args.toLang,
|
||||
segments: args.texts.map((text, id) => ({ id, text })),
|
||||
glossary: {},
|
||||
title: "", // 可省略
|
||||
description: "", // 可省略
|
||||
glossary: {}, // 可省略
|
||||
tone: "", // 可省略
|
||||
}),
|
||||
},
|
||||
],
|
||||
@@ -93,9 +159,12 @@ async (args) => {
|
||||
{
|
||||
role: "user",
|
||||
content: JSON.stringify({
|
||||
targetLanguage: args.to,
|
||||
targetLanguage: args.toLang,
|
||||
segments: args.texts.map((text, id) => ({ id, text })),
|
||||
glossary: {},
|
||||
title: "", // 可省略
|
||||
description: "", // 可省略
|
||||
glossary: {}, // 可省略
|
||||
tone: "", // 可省略
|
||||
}),
|
||||
},
|
||||
],
|
||||
@@ -197,6 +266,36 @@ async (args) => {
|
||||
};
|
||||
```
|
||||
|
||||
v2.0.6 版后内置默认 prompt,Response 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", // 或 args.model
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: args.defaultNobatchPrompt, // 或 args.nobatchPrompt
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: args.defaultNobatchUserPrompt, // 或 args.nobatchUserPrompt
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 20480,
|
||||
};
|
||||
|
||||
return { url, body, headers, method };
|
||||
};
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
@@ -226,6 +325,7 @@ Hook参数里面的语言含义说明:
|
||||
["cs", "Czech - Čeština"],
|
||||
["da", "Danish - Dansk"],
|
||||
["nl", "Dutch - Nederlands"],
|
||||
["fa", "Persian - فارسی"],
|
||||
["fi", "Finnish - Suomi"],
|
||||
["fr", "French - Français"],
|
||||
["de", "German - Deutsch"],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kiss-translator",
|
||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||
"version": "2.0.3",
|
||||
"version": "2.0.8",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
||||
310
pnpm-lock.yaml
generated
310
pnpm-lock.yaml
generated
@@ -87,10 +87,6 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@aashutoshrathi/word-wrap@1.2.6':
|
||||
resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -109,6 +105,10 @@ packages:
|
||||
resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/compat-data@7.22.20':
|
||||
resolution: {integrity: sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -128,10 +128,18 @@ packages:
|
||||
resolution: {integrity: sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/generator@7.28.5':
|
||||
resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-annotate-as-pure@7.22.5':
|
||||
resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-annotate-as-pure@7.27.3':
|
||||
resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-builder-binary-assignment-operator-visitor@7.22.15':
|
||||
resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -165,6 +173,10 @@ packages:
|
||||
resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-globals@7.28.0':
|
||||
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-hoist-variables@7.22.5':
|
||||
resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -181,6 +193,10 @@ packages:
|
||||
resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-module-imports@7.27.1':
|
||||
resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-module-transforms@7.22.20':
|
||||
resolution: {integrity: sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -195,8 +211,8 @@ packages:
|
||||
resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-plugin-utils@7.24.0':
|
||||
resolution: {integrity: sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==}
|
||||
'@babel/helper-plugin-utils@7.27.1':
|
||||
resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-remap-async-to-generator@7.22.20':
|
||||
@@ -231,10 +247,18 @@ packages:
|
||||
resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-string-parser@7.27.1':
|
||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.22.20':
|
||||
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5':
|
||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-option@7.22.15':
|
||||
resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -263,6 +287,11 @@ packages:
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/parser@7.28.5':
|
||||
resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15':
|
||||
resolution: {integrity: sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -406,8 +435,8 @@ packages:
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/plugin-syntax-jsx@7.24.1':
|
||||
resolution: {integrity: sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==}
|
||||
'@babel/plugin-syntax-jsx@7.27.1':
|
||||
resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
@@ -852,10 +881,18 @@ packages:
|
||||
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/traverse@7.22.20':
|
||||
resolution: {integrity: sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/traverse@7.28.5':
|
||||
resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.22.19':
|
||||
resolution: {integrity: sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -864,6 +901,10 @@ packages:
|
||||
resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.28.5':
|
||||
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@bcoe/v8-coverage@0.2.3':
|
||||
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
|
||||
|
||||
@@ -1053,8 +1094,14 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||
|
||||
'@eslint-community/regexpp@4.10.0':
|
||||
resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==}
|
||||
'@eslint-community/eslint-utils@4.9.0':
|
||||
resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||
|
||||
'@eslint-community/regexpp@4.12.2':
|
||||
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
|
||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||
|
||||
'@eslint-community/regexpp@4.8.1':
|
||||
@@ -1175,6 +1222,9 @@ packages:
|
||||
resolution: {integrity: sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==}
|
||||
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.3':
|
||||
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -1183,6 +1233,10 @@ packages:
|
||||
resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/set-array@1.1.2':
|
||||
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -1193,9 +1247,15 @@ packages:
|
||||
'@jridgewell/sourcemap-codec@1.4.15':
|
||||
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.19':
|
||||
resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@leichtgewicht/ip-codec@2.0.4':
|
||||
resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==}
|
||||
|
||||
@@ -1697,8 +1757,8 @@ packages:
|
||||
resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
'@ungap/structured-clone@1.2.0':
|
||||
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
|
||||
'@webassemblyjs/ast@1.11.6':
|
||||
resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==}
|
||||
@@ -1790,6 +1850,11 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
acorn@8.15.0:
|
||||
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
address@1.2.2:
|
||||
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -2113,8 +2178,8 @@ packages:
|
||||
caniuse-api@3.0.0:
|
||||
resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
|
||||
|
||||
caniuse-lite@1.0.30001599:
|
||||
resolution: {integrity: sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==}
|
||||
caniuse-lite@1.0.30001754:
|
||||
resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==}
|
||||
|
||||
case-sensitive-paths-webpack-plugin@2.4.0:
|
||||
resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==}
|
||||
@@ -2297,6 +2362,10 @@ packages:
|
||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
crypt@0.0.2:
|
||||
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
|
||||
|
||||
@@ -2463,6 +2532,15 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decimal.js@10.4.3:
|
||||
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
|
||||
|
||||
@@ -2848,8 +2926,8 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
esquery@1.5.0:
|
||||
resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
|
||||
esquery@1.6.0:
|
||||
resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
esrecurse@4.3.0:
|
||||
@@ -2992,8 +3070,8 @@ packages:
|
||||
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
|
||||
flatted@3.3.1:
|
||||
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
|
||||
flatted@3.3.3:
|
||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||
|
||||
follow-redirects@1.15.3:
|
||||
resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==}
|
||||
@@ -3297,8 +3375,8 @@ packages:
|
||||
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
ignore@5.3.1:
|
||||
resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
immer@9.0.21:
|
||||
@@ -3308,6 +3386,10 @@ packages:
|
||||
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
import-local@3.1.0:
|
||||
resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3744,6 +3826,11 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
jsesc@3.1.0:
|
||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
json-buffer@3.0.1:
|
||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||
|
||||
@@ -4224,8 +4311,8 @@ packages:
|
||||
resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
optionator@0.9.3:
|
||||
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
p-limit@2.3.0:
|
||||
@@ -4331,6 +4418,9 @@ packages:
|
||||
picocolors@1.0.0:
|
||||
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
@@ -6021,8 +6111,6 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@aashutoshrathi/word-wrap@1.2.6': {}
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@ampproject/remapping@2.2.1':
|
||||
@@ -6042,6 +6130,12 @@ snapshots:
|
||||
'@babel/highlight': 7.22.20
|
||||
chalk: 2.4.2
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@babel/compat-data@7.22.20': {}
|
||||
|
||||
'@babel/core@7.22.20':
|
||||
@@ -6079,10 +6173,22 @@ snapshots:
|
||||
'@jridgewell/trace-mapping': 0.3.19
|
||||
jsesc: 2.5.2
|
||||
|
||||
'@babel/generator@7.28.5':
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.5
|
||||
'@babel/types': 7.28.5
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jsesc: 3.1.0
|
||||
|
||||
'@babel/helper-annotate-as-pure@7.22.5':
|
||||
dependencies:
|
||||
'@babel/types': 7.22.19
|
||||
|
||||
'@babel/helper-annotate-as-pure@7.27.3':
|
||||
dependencies:
|
||||
'@babel/types': 7.28.5
|
||||
|
||||
'@babel/helper-builder-binary-assignment-operator-visitor@7.22.15':
|
||||
dependencies:
|
||||
'@babel/types': 7.22.19
|
||||
@@ -6133,6 +6239,8 @@ snapshots:
|
||||
'@babel/template': 7.22.15
|
||||
'@babel/types': 7.22.19
|
||||
|
||||
'@babel/helper-globals@7.28.0': {}
|
||||
|
||||
'@babel/helper-hoist-variables@7.22.5':
|
||||
dependencies:
|
||||
'@babel/types': 7.22.19
|
||||
@@ -6149,6 +6257,13 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.24.0
|
||||
|
||||
'@babel/helper-module-imports@7.27.1':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.28.5
|
||||
'@babel/types': 7.28.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-module-transforms@7.22.20(@babel/core@7.22.20)':
|
||||
dependencies:
|
||||
'@babel/core': 7.22.20
|
||||
@@ -6164,7 +6279,7 @@ snapshots:
|
||||
|
||||
'@babel/helper-plugin-utils@7.22.5': {}
|
||||
|
||||
'@babel/helper-plugin-utils@7.24.0': {}
|
||||
'@babel/helper-plugin-utils@7.27.1': {}
|
||||
|
||||
'@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.22.20)':
|
||||
dependencies:
|
||||
@@ -6196,8 +6311,12 @@ snapshots:
|
||||
|
||||
'@babel/helper-string-parser@7.24.1': {}
|
||||
|
||||
'@babel/helper-string-parser@7.27.1': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.22.20': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5': {}
|
||||
|
||||
'@babel/helper-validator-option@7.22.15': {}
|
||||
|
||||
'@babel/helper-wrap-function@7.22.20':
|
||||
@@ -6234,6 +6353,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.22.19
|
||||
|
||||
'@babel/parser@7.28.5':
|
||||
dependencies:
|
||||
'@babel/types': 7.28.5
|
||||
|
||||
'@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15(@babel/core@7.22.20)':
|
||||
dependencies:
|
||||
'@babel/core': 7.22.20
|
||||
@@ -6341,7 +6464,7 @@ snapshots:
|
||||
'@babel/plugin-syntax-flow@7.24.1(@babel/core@7.22.20)':
|
||||
dependencies:
|
||||
'@babel/core': 7.22.20
|
||||
'@babel/helper-plugin-utils': 7.24.0
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
|
||||
'@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.22.20)':
|
||||
dependencies:
|
||||
@@ -6368,10 +6491,10 @@ snapshots:
|
||||
'@babel/core': 7.22.20
|
||||
'@babel/helper-plugin-utils': 7.22.5
|
||||
|
||||
'@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.22.20)':
|
||||
'@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.22.20)':
|
||||
dependencies:
|
||||
'@babel/core': 7.22.20
|
||||
'@babel/helper-plugin-utils': 7.24.0
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
|
||||
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.22.20)':
|
||||
dependencies:
|
||||
@@ -6689,11 +6812,13 @@ snapshots:
|
||||
'@babel/plugin-transform-react-jsx@7.23.4(@babel/core@7.22.20)':
|
||||
dependencies:
|
||||
'@babel/core': 7.22.20
|
||||
'@babel/helper-annotate-as-pure': 7.22.5
|
||||
'@babel/helper-module-imports': 7.24.3
|
||||
'@babel/helper-plugin-utils': 7.24.0
|
||||
'@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.22.20)
|
||||
'@babel/types': 7.24.0
|
||||
'@babel/helper-annotate-as-pure': 7.27.3
|
||||
'@babel/helper-module-imports': 7.27.1
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.22.20)
|
||||
'@babel/types': 7.28.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/plugin-transform-react-pure-annotations@7.22.5(@babel/core@7.22.20)':
|
||||
dependencies:
|
||||
@@ -6918,6 +7043,12 @@ snapshots:
|
||||
'@babel/parser': 7.22.16
|
||||
'@babel/types': 7.22.19
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
'@babel/parser': 7.28.5
|
||||
'@babel/types': 7.28.5
|
||||
|
||||
'@babel/traverse@7.22.20':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.22.13
|
||||
@@ -6933,6 +7064,18 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/traverse@7.28.5':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
'@babel/generator': 7.28.5
|
||||
'@babel/helper-globals': 7.28.0
|
||||
'@babel/parser': 7.28.5
|
||||
'@babel/template': 7.27.2
|
||||
'@babel/types': 7.28.5
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/types@7.22.19':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.22.5
|
||||
@@ -6945,6 +7088,11 @@ snapshots:
|
||||
'@babel/helper-validator-identifier': 7.22.20
|
||||
to-fast-properties: 2.0.0
|
||||
|
||||
'@babel/types@7.28.5':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@bcoe/v8-coverage@0.2.3': {}
|
||||
|
||||
'@buttercup/fetch@0.1.2':
|
||||
@@ -7163,18 +7311,23 @@ snapshots:
|
||||
eslint: 8.57.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/regexpp@4.10.0': {}
|
||||
'@eslint-community/eslint-utils@4.9.0(eslint@8.57.0)':
|
||||
dependencies:
|
||||
eslint: 8.57.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/regexpp@4.12.2': {}
|
||||
|
||||
'@eslint-community/regexpp@4.8.1': {}
|
||||
|
||||
'@eslint/eslintrc@2.1.4':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
debug: 4.3.4
|
||||
debug: 4.4.3
|
||||
espree: 9.6.1
|
||||
globals: 13.24.0
|
||||
ignore: 5.3.1
|
||||
import-fresh: 3.3.0
|
||||
ignore: 5.3.2
|
||||
import-fresh: 3.3.1
|
||||
js-yaml: 4.1.0
|
||||
minimatch: 3.1.2
|
||||
strip-json-comments: 3.1.1
|
||||
@@ -7203,7 +7356,7 @@ snapshots:
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
debug: 4.3.4
|
||||
debug: 4.4.3
|
||||
minimatch: 3.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -7399,6 +7552,11 @@ snapshots:
|
||||
'@types/yargs': 17.0.24
|
||||
chalk: 4.1.2
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.3':
|
||||
dependencies:
|
||||
'@jridgewell/set-array': 1.1.2
|
||||
@@ -7407,6 +7565,8 @@ snapshots:
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.1': {}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/set-array@1.1.2': {}
|
||||
|
||||
'@jridgewell/source-map@0.3.5':
|
||||
@@ -7416,11 +7576,18 @@ snapshots:
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.4.15': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.19':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.1
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@leichtgewicht/ip-codec@2.0.4': {}
|
||||
|
||||
'@mui/base@5.0.0-beta.40(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
@@ -7971,7 +8138,7 @@ snapshots:
|
||||
'@typescript-eslint/types': 5.62.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@ungap/structured-clone@1.2.0': {}
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@webassemblyjs/ast@1.11.6':
|
||||
dependencies:
|
||||
@@ -8069,9 +8236,9 @@ snapshots:
|
||||
dependencies:
|
||||
acorn: 8.10.0
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.11.3):
|
||||
acorn-jsx@5.3.2(acorn@8.15.0):
|
||||
dependencies:
|
||||
acorn: 8.11.3
|
||||
acorn: 8.15.0
|
||||
|
||||
acorn-walk@7.2.0: {}
|
||||
|
||||
@@ -8081,6 +8248,8 @@ snapshots:
|
||||
|
||||
acorn@8.11.3: {}
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
address@1.2.2: {}
|
||||
|
||||
adjust-sourcemap-loader@4.0.0:
|
||||
@@ -8244,7 +8413,7 @@ snapshots:
|
||||
autoprefixer@10.4.16(postcss@8.4.30):
|
||||
dependencies:
|
||||
browserslist: 4.23.0
|
||||
caniuse-lite: 1.0.30001599
|
||||
caniuse-lite: 1.0.30001754
|
||||
fraction.js: 4.3.6
|
||||
normalize-range: 0.1.2
|
||||
picocolors: 1.0.0
|
||||
@@ -8444,7 +8613,7 @@ snapshots:
|
||||
|
||||
browserslist@4.23.0:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001599
|
||||
caniuse-lite: 1.0.30001754
|
||||
electron-to-chromium: 1.4.713
|
||||
node-releases: 2.0.14
|
||||
update-browserslist-db: 1.0.13(browserslist@4.23.0)
|
||||
@@ -8484,11 +8653,11 @@ snapshots:
|
||||
caniuse-api@3.0.0:
|
||||
dependencies:
|
||||
browserslist: 4.23.0
|
||||
caniuse-lite: 1.0.30001599
|
||||
caniuse-lite: 1.0.30001754
|
||||
lodash.memoize: 4.1.2
|
||||
lodash.uniq: 4.5.0
|
||||
|
||||
caniuse-lite@1.0.30001599: {}
|
||||
caniuse-lite@1.0.30001754: {}
|
||||
|
||||
case-sensitive-paths-webpack-plugin@2.4.0: {}
|
||||
|
||||
@@ -8661,6 +8830,12 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
crypt@0.0.2: {}
|
||||
|
||||
crypto-random-string@2.0.0: {}
|
||||
@@ -8823,6 +8998,10 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decimal.js@10.4.3: {}
|
||||
|
||||
decode-named-character-reference@1.0.2:
|
||||
@@ -9270,24 +9449,24 @@ snapshots:
|
||||
|
||||
eslint@8.57.0:
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
|
||||
'@eslint-community/regexpp': 4.10.0
|
||||
'@eslint-community/eslint-utils': 4.9.0(eslint@8.57.0)
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@eslint/eslintrc': 2.1.4
|
||||
'@eslint/js': 8.57.0
|
||||
'@humanwhocodes/config-array': 0.11.14
|
||||
'@humanwhocodes/module-importer': 1.0.1
|
||||
'@nodelib/fs.walk': 1.2.8
|
||||
'@ungap/structured-clone': 1.2.0
|
||||
'@ungap/structured-clone': 1.3.0
|
||||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.3
|
||||
debug: 4.3.4
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3
|
||||
doctrine: 3.0.0
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 7.2.2
|
||||
eslint-visitor-keys: 3.4.3
|
||||
espree: 9.6.1
|
||||
esquery: 1.5.0
|
||||
esquery: 1.6.0
|
||||
esutils: 2.0.3
|
||||
fast-deep-equal: 3.1.3
|
||||
file-entry-cache: 6.0.1
|
||||
@@ -9295,7 +9474,7 @@ snapshots:
|
||||
glob-parent: 6.0.2
|
||||
globals: 13.24.0
|
||||
graphemer: 1.4.0
|
||||
ignore: 5.3.1
|
||||
ignore: 5.3.2
|
||||
imurmurhash: 0.1.4
|
||||
is-glob: 4.0.3
|
||||
is-path-inside: 3.0.3
|
||||
@@ -9305,7 +9484,7 @@ snapshots:
|
||||
lodash.merge: 4.6.2
|
||||
minimatch: 3.1.2
|
||||
natural-compare: 1.4.0
|
||||
optionator: 0.9.3
|
||||
optionator: 0.9.4
|
||||
strip-ansi: 6.0.1
|
||||
text-table: 0.2.0
|
||||
transitivePeerDependencies:
|
||||
@@ -9313,15 +9492,15 @@ snapshots:
|
||||
|
||||
espree@9.6.1:
|
||||
dependencies:
|
||||
acorn: 8.11.3
|
||||
acorn-jsx: 5.3.2(acorn@8.11.3)
|
||||
acorn: 8.15.0
|
||||
acorn-jsx: 5.3.2(acorn@8.15.0)
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
esprima@1.2.2: {}
|
||||
|
||||
esprima@4.0.1: {}
|
||||
|
||||
esquery@1.5.0:
|
||||
esquery@1.6.0:
|
||||
dependencies:
|
||||
estraverse: 5.3.0
|
||||
|
||||
@@ -9508,11 +9687,11 @@ snapshots:
|
||||
|
||||
flat-cache@3.2.0:
|
||||
dependencies:
|
||||
flatted: 3.3.1
|
||||
flatted: 3.3.3
|
||||
keyv: 4.5.4
|
||||
rimraf: 3.0.2
|
||||
|
||||
flatted@3.3.1: {}
|
||||
flatted@3.3.3: {}
|
||||
|
||||
follow-redirects@1.15.3: {}
|
||||
|
||||
@@ -9838,7 +10017,7 @@ snapshots:
|
||||
|
||||
ignore@5.2.4: {}
|
||||
|
||||
ignore@5.3.1: {}
|
||||
ignore@5.3.2: {}
|
||||
|
||||
immer@9.0.21: {}
|
||||
|
||||
@@ -9847,6 +10026,11 @@ snapshots:
|
||||
parent-module: 1.0.1
|
||||
resolve-from: 4.0.0
|
||||
|
||||
import-fresh@3.3.1:
|
||||
dependencies:
|
||||
parent-module: 1.0.1
|
||||
resolve-from: 4.0.0
|
||||
|
||||
import-local@3.1.0:
|
||||
dependencies:
|
||||
pkg-dir: 4.2.0
|
||||
@@ -10527,6 +10711,8 @@ snapshots:
|
||||
|
||||
jsesc@2.5.2: {}
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-buffer@3.0.1: {}
|
||||
|
||||
json-parse-even-better-errors@2.3.1: {}
|
||||
@@ -11079,14 +11265,14 @@ snapshots:
|
||||
type-check: 0.3.2
|
||||
word-wrap: 1.2.5
|
||||
|
||||
optionator@0.9.3:
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
'@aashutoshrathi/word-wrap': 1.2.6
|
||||
deep-is: 0.1.4
|
||||
fast-levenshtein: 2.0.6
|
||||
levn: 0.4.1
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
word-wrap: 1.2.5
|
||||
|
||||
p-limit@2.3.0:
|
||||
dependencies:
|
||||
@@ -11174,6 +11360,8 @@ snapshots:
|
||||
|
||||
picocolors@1.0.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
pify@2.3.0: {}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "2.0.3",
|
||||
"version": "2.0.8",
|
||||
"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.3",
|
||||
"version": "2.0.8",
|
||||
"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.3",
|
||||
"version": "2.0.8",
|
||||
"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,
|
||||
@@ -419,12 +419,12 @@ export const apiTranslate = async ({
|
||||
toLang,
|
||||
apiSetting = DEFAULT_API_SETTING,
|
||||
docInfo = {},
|
||||
glossary = {},
|
||||
glossary,
|
||||
useCache = true,
|
||||
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,17 +22,24 @@ 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,
|
||||
INPUT_PLACE_TONE,
|
||||
INPUT_PLACE_TITLE,
|
||||
INPUT_PLACE_DESCRIPTION,
|
||||
INPUT_PLACE_TO_LANG,
|
||||
INPUT_PLACE_FROM_LANG,
|
||||
} 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";
|
||||
@@ -60,44 +67,74 @@ const keyPick = (apiSlug, key = "", cacheMap) => {
|
||||
return keys[curIndex];
|
||||
};
|
||||
|
||||
const genSystemPrompt = ({ systemPrompt, from, to }) =>
|
||||
const genSystemPrompt = ({
|
||||
systemPrompt,
|
||||
tone,
|
||||
from,
|
||||
to,
|
||||
fromLang,
|
||||
toLang,
|
||||
texts,
|
||||
docInfo: { title = "", description = "" } = {},
|
||||
}) =>
|
||||
systemPrompt
|
||||
.replaceAll(INPUT_PLACE_TITLE, title)
|
||||
.replaceAll(INPUT_PLACE_DESCRIPTION, description)
|
||||
.replaceAll(INPUT_PLACE_TONE, tone)
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to);
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_FROM_LANG, fromLang)
|
||||
.replaceAll(INPUT_PLACE_TO_LANG, toLang)
|
||||
.replaceAll(INPUT_PLACE_TEXT, texts[0]);
|
||||
|
||||
const genUserPrompt = ({
|
||||
// userPrompt,
|
||||
nobatchUserPrompt,
|
||||
useBatchFetch,
|
||||
tone,
|
||||
glossary = {},
|
||||
// from,
|
||||
glossary,
|
||||
from,
|
||||
to,
|
||||
fromLang,
|
||||
toLang,
|
||||
texts,
|
||||
docInfo,
|
||||
docInfo: { title = "", description = "" } = {},
|
||||
}) => {
|
||||
const prompt = JSON.stringify({
|
||||
targetLanguage: to,
|
||||
title: docInfo.title,
|
||||
description: docInfo.description,
|
||||
segments: texts.map((text, i) => ({ id: i, text })),
|
||||
glossary,
|
||||
tone,
|
||||
});
|
||||
if (useBatchFetch) {
|
||||
const promptObj = {
|
||||
targetLanguage: toLang,
|
||||
segments: texts.map((text, i) => ({ id: i, text })),
|
||||
};
|
||||
|
||||
// if (userPrompt.includes(INPUT_PLACE_TEXT)) {
|
||||
// return userPrompt
|
||||
// .replaceAll(INPUT_PLACE_FROM, from)
|
||||
// .replaceAll(INPUT_PLACE_TO, to)
|
||||
// .replaceAll(INPUT_PLACE_TEXT, prompt);
|
||||
// }
|
||||
title && (promptObj.title = title);
|
||||
description && (promptObj.description = description);
|
||||
glossary &&
|
||||
Object.keys(glossary).length !== 0 &&
|
||||
(promptObj.glossary = glossary);
|
||||
tone && (promptObj.tone = tone);
|
||||
|
||||
return prompt;
|
||||
return JSON.stringify(promptObj);
|
||||
}
|
||||
|
||||
return nobatchUserPrompt
|
||||
.replaceAll(INPUT_PLACE_TITLE, title)
|
||||
.replaceAll(INPUT_PLACE_DESCRIPTION, description)
|
||||
.replaceAll(INPUT_PLACE_TONE, tone)
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_FROM_LANG, fromLang)
|
||||
.replaceAll(INPUT_PLACE_TO_LANG, toLang)
|
||||
.replaceAll(INPUT_PLACE_TEXT, texts[0]);
|
||||
};
|
||||
|
||||
const parseAIRes = (raw) => {
|
||||
const parseAIRes = (raw, useBatchFetch = true) => {
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!useBatchFetch) {
|
||||
return [[raw]];
|
||||
}
|
||||
|
||||
try {
|
||||
const jsonString = extractJson(raw);
|
||||
if (!jsonString) return [];
|
||||
@@ -497,7 +534,7 @@ const genOpenRouter = ({
|
||||
};
|
||||
|
||||
const genOllama = ({
|
||||
think,
|
||||
// think,
|
||||
url,
|
||||
key,
|
||||
systemPrompt,
|
||||
@@ -523,7 +560,7 @@ const genOllama = ({
|
||||
],
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
think,
|
||||
// think,
|
||||
stream: false,
|
||||
};
|
||||
|
||||
@@ -552,8 +589,8 @@ const genCloudflareAI = ({ texts, from, to, url, key }) => {
|
||||
return { url, body, headers };
|
||||
};
|
||||
|
||||
const genCustom = ({ texts, from, to, url, key }) => {
|
||||
const body = { texts, from, to };
|
||||
const genCustom = ({ texts, fromLang, toLang, url, key }) => {
|
||||
const body = { texts, from: fromLang, to: toLang };
|
||||
const headers = {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${key}`,
|
||||
@@ -627,15 +664,21 @@ export const genTransReq = async ({ reqHook, ...args }) => {
|
||||
apiSlug,
|
||||
key,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
// userPrompt,
|
||||
nobatchPrompt = defaultNobatchPrompt,
|
||||
nobatchUserPrompt = defaultNobatchUserPrompt,
|
||||
useBatchFetch,
|
||||
from,
|
||||
to,
|
||||
fromLang,
|
||||
toLang,
|
||||
texts,
|
||||
docInfo,
|
||||
glossary,
|
||||
customHeader,
|
||||
customBody,
|
||||
events,
|
||||
tone,
|
||||
} = args;
|
||||
|
||||
if (API_SPE_TYPES.mulkeys.has(apiType)) {
|
||||
@@ -647,15 +690,30 @@ export const genTransReq = async ({ reqHook, ...args }) => {
|
||||
}
|
||||
|
||||
if (API_SPE_TYPES.ai.has(apiType)) {
|
||||
args.systemPrompt = genSystemPrompt({ systemPrompt, from, to });
|
||||
args.userPrompt = !!events
|
||||
? JSON.stringify(events)
|
||||
: genUserPrompt({
|
||||
userPrompt,
|
||||
args.systemPrompt = events
|
||||
? systemPrompt
|
||||
: genSystemPrompt({
|
||||
systemPrompt: useBatchFetch ? systemPrompt : nobatchPrompt,
|
||||
from,
|
||||
to,
|
||||
fromLang,
|
||||
toLang,
|
||||
texts,
|
||||
docInfo,
|
||||
tone,
|
||||
});
|
||||
args.userPrompt = events
|
||||
? JSON.stringify(events)
|
||||
: genUserPrompt({
|
||||
nobatchUserPrompt,
|
||||
useBatchFetch,
|
||||
from,
|
||||
to,
|
||||
fromLang,
|
||||
toLang,
|
||||
texts,
|
||||
docInfo,
|
||||
tone,
|
||||
glossary,
|
||||
});
|
||||
}
|
||||
@@ -681,7 +739,13 @@ export const genTransReq = async ({ reqHook, ...args }) => {
|
||||
try {
|
||||
interpreter.run(`exports.reqHook = ${reqHook}`);
|
||||
const hookResult = await interpreter.exports.reqHook(
|
||||
{ ...args, defaultSystemPrompt, defaultSubtitlePrompt },
|
||||
{
|
||||
...args,
|
||||
defaultSystemPrompt,
|
||||
defaultSubtitlePrompt,
|
||||
defaultNobatchPrompt,
|
||||
defaultNobatchUserPrompt,
|
||||
},
|
||||
{
|
||||
url,
|
||||
body,
|
||||
@@ -717,10 +781,11 @@ export const parseTransRes = async (
|
||||
toLang,
|
||||
langMap,
|
||||
resHook,
|
||||
thinkIgnore,
|
||||
// thinkIgnore,
|
||||
history,
|
||||
userMsg,
|
||||
apiType,
|
||||
useBatchFetch,
|
||||
}
|
||||
) => {
|
||||
// 执行 response hook
|
||||
@@ -811,13 +876,13 @@ export const parseTransRes = async (
|
||||
content: modelMsg.content,
|
||||
});
|
||||
}
|
||||
return parseAIRes(res?.choices?.[0]?.message?.content ?? "");
|
||||
return parseAIRes(modelMsg?.content, useBatchFetch);
|
||||
case OPT_TRANS_GEMINI:
|
||||
modelMsg = res?.candidates?.[0]?.content;
|
||||
if (history && userMsg && modelMsg) {
|
||||
history.add(userMsg, modelMsg);
|
||||
}
|
||||
return parseAIRes(res?.candidates?.[0]?.content?.parts?.[0]?.text ?? "");
|
||||
return parseAIRes(modelMsg?.parts?.[0]?.text ?? "", useBatchFetch);
|
||||
case OPT_TRANS_CLAUDE:
|
||||
modelMsg = { role: res?.role, content: res?.content?.text };
|
||||
if (history && userMsg && modelMsg) {
|
||||
@@ -826,18 +891,18 @@ export const parseTransRes = async (
|
||||
content: modelMsg.content,
|
||||
});
|
||||
}
|
||||
return parseAIRes(res?.content?.[0]?.text ?? "");
|
||||
return parseAIRes(res?.content?.[0]?.text ?? "", useBatchFetch);
|
||||
case OPT_TRANS_CLOUDFLAREAI:
|
||||
return [[res?.result?.translated_text]];
|
||||
case OPT_TRANS_OLLAMA:
|
||||
modelMsg = res?.choices?.[0]?.message;
|
||||
|
||||
const deepModels = thinkIgnore
|
||||
.split(",")
|
||||
.filter((model) => model?.trim());
|
||||
if (deepModels.some((model) => res?.model?.startsWith(model))) {
|
||||
modelMsg?.content.replace(/<think>[\s\S]*<\/think>/i, "");
|
||||
}
|
||||
// const deepModels = thinkIgnore
|
||||
// .split(",")
|
||||
// .filter((model) => model?.trim());
|
||||
// if (deepModels.some((model) => res?.model?.startsWith(model))) {
|
||||
// modelMsg?.content.replace(/<think>[\s\S]*<\/think>/i, "");
|
||||
// }
|
||||
|
||||
if (history && userMsg && modelMsg) {
|
||||
history.add(userMsg, {
|
||||
@@ -845,9 +910,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:
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -118,6 +118,13 @@ async function getFavWords(rule) {
|
||||
*/
|
||||
export async function run(isUserscript = false) {
|
||||
try {
|
||||
// if (document?.documentElement?.tagName?.toUpperCase() !== "HTML") {
|
||||
// return;
|
||||
// }
|
||||
if (!document?.contentType?.includes("html")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取设置信息
|
||||
const setting = await getSettingWithDefault();
|
||||
|
||||
|
||||
@@ -9,7 +9,12 @@ export const DEFAULT_CONTEXT_SIZE = 3; // 上下文会话数量
|
||||
export const INPUT_PLACE_URL = "{{url}}"; // 占位符
|
||||
export const INPUT_PLACE_FROM = "{{from}}"; // 占位符
|
||||
export const INPUT_PLACE_TO = "{{to}}"; // 占位符
|
||||
export const INPUT_PLACE_FROM_LANG = "{{fromLang}}"; // 占位符
|
||||
export const INPUT_PLACE_TO_LANG = "{{toLang}}"; // 占位符
|
||||
export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
|
||||
export const INPUT_PLACE_TONE = "{{tone}}"; // 占位符
|
||||
export const INPUT_PLACE_TITLE = "{{title}}"; // 占位符
|
||||
export const INPUT_PLACE_DESCRIPTION = "{{description}}"; // 占位符
|
||||
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
|
||||
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
|
||||
|
||||
@@ -46,7 +51,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 +87,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,
|
||||
@@ -170,6 +175,7 @@ export const OPT_LANGS_TO = [
|
||||
["cs", "Czech - Čeština"],
|
||||
["da", "Danish - Dansk"],
|
||||
["nl", "Dutch - Nederlands"],
|
||||
["fa", "Persian - فارسی"],
|
||||
["fi", "Finnish - Suomi"],
|
||||
["fr", "French - Français"],
|
||||
["de", "German - Deutsch"],
|
||||
@@ -311,14 +317,14 @@ export const OPT_LANGS_TO_SPEC = {
|
||||
["id", "id"],
|
||||
["vi", "vi"],
|
||||
]),
|
||||
[OPT_TRANS_OPENAI]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_GEMINI]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_GEMINI_2]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_CLAUDE]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_OLLAMA]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_OPENROUTER]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_CLOUDFLAREAI]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_CUSTOMIZE]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_OPENAI]: OPT_LANGS_SPEC_NAME,
|
||||
[OPT_TRANS_GEMINI]: OPT_LANGS_SPEC_NAME,
|
||||
[OPT_TRANS_GEMINI_2]: OPT_LANGS_SPEC_NAME,
|
||||
[OPT_TRANS_CLAUDE]: OPT_LANGS_SPEC_NAME,
|
||||
[OPT_TRANS_OLLAMA]: OPT_LANGS_SPEC_NAME,
|
||||
[OPT_TRANS_OPENROUTER]: OPT_LANGS_SPEC_NAME,
|
||||
[OPT_TRANS_CLOUDFLAREAI]: OPT_LANGS_SPEC_NAME,
|
||||
[OPT_TRANS_CUSTOMIZE]: OPT_LANGS_SPEC_NAME,
|
||||
};
|
||||
|
||||
const specToCode = (m) =>
|
||||
@@ -340,6 +346,9 @@ Object.entries(OPT_LANGS_TO_SPEC).forEach(([t, m]) => {
|
||||
OPT_LANGS_TO_CODE[t] = specToCode(m);
|
||||
});
|
||||
|
||||
export const defaultNobatchPrompt = `You are a professional, authentic machine translation engine.`;
|
||||
export const defaultNobatchUserPrompt = `Translate the following source text to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`;
|
||||
|
||||
export const defaultSystemPrompt = `Act as a translation API. Output a single raw JSON object only. No extra text or fences.
|
||||
|
||||
Input:
|
||||
@@ -430,6 +439,8 @@ const defaultApi = {
|
||||
model: "", // 模型名称
|
||||
systemPrompt: defaultSystemPrompt,
|
||||
subtitlePrompt: defaultSubtitlePrompt,
|
||||
nobatchPrompt: defaultNobatchPrompt,
|
||||
nobatchUserPrompt: defaultNobatchUserPrompt,
|
||||
userPrompt: "",
|
||||
tone: BUILTIN_STONES[0], // 翻译风格
|
||||
placeholder: BUILTIN_PLACEHOLDERS[0], // 占位符
|
||||
@@ -441,7 +452,7 @@ const defaultApi = {
|
||||
resHook: "", // response 钩子函数
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大请求数量
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL, // 请求间隔时间
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 30, // 请求超时时间
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 3, // 请求超时时间
|
||||
batchInterval: DEFAULT_BATCH_INTERVAL, // 批处理请求间隔时间
|
||||
batchSize: DEFAULT_BATCH_SIZE, // 每次最多发送段落数量
|
||||
batchLength: DEFAULT_BATCH_LENGTH, // 每次发送最大文字数量
|
||||
@@ -450,8 +461,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 +510,6 @@ const defaultApiOpts = {
|
||||
[OPT_TRANS_DEEPLX]: {
|
||||
...defaultApi,
|
||||
url: "http://localhost:1188/translate",
|
||||
fetchLimit: 1,
|
||||
},
|
||||
[OPT_TRANS_NIUTRANS]: {
|
||||
...defaultApi,
|
||||
@@ -512,7 +522,6 @@ const defaultApiOpts = {
|
||||
url: "https://api.openai.com/v1/chat/completions",
|
||||
model: "gpt-4",
|
||||
useBatchFetch: true,
|
||||
fetchLimit: 1,
|
||||
},
|
||||
[OPT_TRANS_GEMINI]: {
|
||||
...defaultApi,
|
||||
@@ -557,7 +566,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 +574,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
|
||||
);
|
||||
|
||||
@@ -526,9 +526,9 @@ export const I18N = {
|
||||
zh_TW: `1.其中 BuiltinAI 為瀏覽器內建AI翻譯,目前僅 Chrome 138 以上版本支援。`,
|
||||
},
|
||||
about_api_2: {
|
||||
zh: `2、大部分AI接口都与OpenAI兼容,因此选择添加OpenAI类型即可。`,
|
||||
zh: `2、大部分AI接口都与OpenAI兼容,因此选择添加OpenAI类型即可。It should be noted that Prompt has two types: batch translation and nobatch translation. Not all interfaces support batch translation.`,
|
||||
en: `2. Most AI interfaces are compatible with OpenAI, so just choose to add the OpenAI type.`,
|
||||
zh_TW: `2.大部分AI介面都與OpenAI相容,因此選擇新增OpenAI類型即可。`,
|
||||
zh_TW: `2.大部分AI介面都與OpenAI相容,因此選擇新增OpenAI類型即可。要注意的是Prompt分聚合翻譯和非聚合翻譯兩種,不是所有介面都支援聚合翻譯。`,
|
||||
},
|
||||
about_api_3: {
|
||||
zh: `3、暂未列出的接口,理论上都可以通过自定义接口 (Custom) 的形式支持。`,
|
||||
@@ -569,11 +569,36 @@ export const I18N = {
|
||||
zh: `虚线框`,
|
||||
en: `Dashed Box`,
|
||||
},
|
||||
dash_line_bold: {
|
||||
zh: `下划虚线加粗`,
|
||||
en: `Dashed Underline Bold`,
|
||||
zh_TW: `下劃虛線`,
|
||||
},
|
||||
dash_box_bold: {
|
||||
zh: `虚线框加粗`,
|
||||
en: `Dashed Box Bold`,
|
||||
zh_TW: `虛線框加粗`,
|
||||
},
|
||||
marker: {
|
||||
zh: `马克笔`,
|
||||
en: `Marker`,
|
||||
zh_TW: `馬克筆`,
|
||||
},
|
||||
gradient_marker: {
|
||||
zh: `渐变马克笔`,
|
||||
en: `Gradient Marker`,
|
||||
zh_TW: `漸層馬克筆`,
|
||||
},
|
||||
wavy_line: {
|
||||
zh: `下划波浪线`,
|
||||
en: `Wavy Underline`,
|
||||
zh_TW: `下劃波浪線`,
|
||||
},
|
||||
wavy_line_bold: {
|
||||
zh: `下划波浪线加粗`,
|
||||
en: `Wavy Underline Bold`,
|
||||
zh_TW: `下劃波浪線加粗`,
|
||||
},
|
||||
fuzzy: {
|
||||
zh: `模糊`,
|
||||
en: `Fuzzy`,
|
||||
@@ -604,15 +629,10 @@ export const I18N = {
|
||||
en: `Glow`,
|
||||
zh_TW: `發光`,
|
||||
},
|
||||
diy_style: {
|
||||
zh: `自定义样式`,
|
||||
en: `Custom Style`,
|
||||
zh_TW: `自訂樣式`,
|
||||
},
|
||||
diy_style_helper: {
|
||||
zh: `遵循“CSS”的语法`,
|
||||
en: `Follow the syntax of "CSS"`,
|
||||
zh_TW: `遵循 CSS 語法`,
|
||||
colorful: {
|
||||
zh: `多彩`,
|
||||
en: `Colorful`,
|
||||
zh_TW: `多彩`,
|
||||
},
|
||||
setting: {
|
||||
zh: `设置`,
|
||||
@@ -709,6 +729,11 @@ export const I18N = {
|
||||
en: `1. AI intelligent replacement does not support regular expressions.2. Separate multiple terms with newlines or semicolons ";". 3. Terms and translations are separated by English commas ",". 4. If there is no translation, the term will be deemed not to be translated.`,
|
||||
zh_TW: `1.AI智能替換,不支援正規表示式。2. 多條術語以換行或分號「;」分隔。3. 術語與譯文以英文逗號「,」分隔。4. 無譯文者視為不翻譯該術語。`,
|
||||
},
|
||||
text_ext_style: {
|
||||
zh: `译文附加样式`,
|
||||
en: `Translation additional styles`,
|
||||
zh_TW: `譯文附加樣式`,
|
||||
},
|
||||
selector_style: {
|
||||
zh: `选择器节点样式`,
|
||||
en: `Selector Style`,
|
||||
@@ -745,9 +770,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`,
|
||||
@@ -755,9 +804,9 @@ export const I18N = {
|
||||
zh_TW: `注入 CSS`,
|
||||
},
|
||||
inject_css_helper: {
|
||||
zh: `初始化时注入运行,一个页面仅运行一次。`,
|
||||
en: `Injected and run at initialization, and only run once per page.`,
|
||||
zh_TW: `初始化時注入運行,一個頁面僅運行一次。`,
|
||||
zh: `预加载时注入,一个页面仅运行一次。`,
|
||||
en: `Injected during preload, runs only once per page.`,
|
||||
zh_TW: `預先載入時注入,一個頁面僅運行一次。`,
|
||||
},
|
||||
fixer_function: {
|
||||
zh: `修复函数`,
|
||||
@@ -1020,9 +1069,9 @@ export const I18N = {
|
||||
zh_TW: `隱藏`,
|
||||
},
|
||||
save_rule: {
|
||||
zh: `保存规则`,
|
||||
en: `Save Rule`,
|
||||
zh_TW: `儲存規則`,
|
||||
zh: `保存本站规则`,
|
||||
en: `Save this site rule`,
|
||||
zh_TW: `保存本站規則`,
|
||||
},
|
||||
global_rule: {
|
||||
zh: `全局规则`,
|
||||
@@ -1106,7 +1155,7 @@ export const I18N = {
|
||||
},
|
||||
selection_translate: {
|
||||
zh: `划词翻译`,
|
||||
en: `Selection Translate`,
|
||||
en: `Selection Translation`,
|
||||
zh_TW: `劃詞翻譯`,
|
||||
},
|
||||
toggle_selection_translate: {
|
||||
@@ -1160,9 +1209,9 @@ export const I18N = {
|
||||
zh_TW: `觸控設定`,
|
||||
},
|
||||
touch_translate_shortcut: {
|
||||
zh: `触屏翻译快捷方式`,
|
||||
en: `Touch Translate Shortcut`,
|
||||
zh_TW: `觸控翻譯捷徑`,
|
||||
zh: `触屏翻译快捷方式 (支持多选)`,
|
||||
en: `Touch Translate Shortcut (multiple supported)`,
|
||||
zh_TW: `觸控翻譯捷徑 (支援多選)`,
|
||||
},
|
||||
touch_tap_0: {
|
||||
zh: `禁用`,
|
||||
@@ -1349,15 +1398,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: `翻译完成钩子函数`,
|
||||
@@ -1591,7 +1660,7 @@ export const I18N = {
|
||||
},
|
||||
subtitle_translate: {
|
||||
zh: `字幕翻译`,
|
||||
en: `Subtitle translate`,
|
||||
en: `Subtitle Translation`,
|
||||
zh_TW: `字幕翻譯`,
|
||||
},
|
||||
toggle_subtitle_translate: {
|
||||
@@ -1604,6 +1673,16 @@ export const I18N = {
|
||||
en: `Enable bilingual display`,
|
||||
zh_TW: `雙語顯示`,
|
||||
},
|
||||
is_skip_ad: {
|
||||
zh: `快进广告`,
|
||||
en: `Skip AD`,
|
||||
zh_TW: `快轉廣告`,
|
||||
},
|
||||
download_subtitles: {
|
||||
zh: `下载字幕`,
|
||||
en: `Download subtitles`,
|
||||
zh_TW: `下载字幕`,
|
||||
},
|
||||
background_styles: {
|
||||
zh: `背景样式`,
|
||||
en: `DBackground Style`,
|
||||
@@ -1679,6 +1758,36 @@ export const I18N = {
|
||||
en: `The subtitle data is ready, please click the KT button to load it`,
|
||||
zh_TW: `字幕資料已準備就緒,請點擊KT按鈕加載`,
|
||||
},
|
||||
starting_reprocess_events: {
|
||||
zh: `重新处理字幕数据...`,
|
||||
en: `Reprocess the subtitle data...`,
|
||||
zh_TW: `重新处理字幕数据...`,
|
||||
},
|
||||
waitting_for_subtitle: {
|
||||
zh: `请等待字幕数据`,
|
||||
en: `Please wait for the subtitle data.`,
|
||||
zh_TW: `请等待字幕数据`,
|
||||
},
|
||||
ai_processing_pls_wait: {
|
||||
zh: `AI处理中,请稍等...`,
|
||||
en: `AI processing in progress, please wait...`,
|
||||
zh_TW: `AI处理中,请稍等...`,
|
||||
},
|
||||
processing_subtitles: {
|
||||
zh: `字幕处理中...`,
|
||||
en: `Subtitle processing...`,
|
||||
zh_TW: `字幕处理中...`,
|
||||
},
|
||||
waiting_subtitles: {
|
||||
zh: `等待字幕中`,
|
||||
en: `Waiting for subtitles`,
|
||||
zh_TW: `等待字幕中`,
|
||||
},
|
||||
subtitle_is_not_yet_ready: {
|
||||
zh: `字幕数据尚未准备好`,
|
||||
en: `Subtitle is not yet ready.`,
|
||||
zh_TW: `字幕数据尚未准备好`,
|
||||
},
|
||||
log_level: {
|
||||
zh: `日志级别`,
|
||||
en: `Log Level`,
|
||||
@@ -1735,6 +1844,36 @@ export const I18N = {
|
||||
en: `Highlight after translation`,
|
||||
zh_TW: `翻譯後高亮`,
|
||||
},
|
||||
pagescroll_root_margin: {
|
||||
zh: `滚动加载提前触发 (0-10000px)`,
|
||||
en: `Early triggering of scroll loading (0-10000px)`,
|
||||
zh_TW: `滾動載入提前觸發 (0-10000px)`,
|
||||
},
|
||||
styles_setting: {
|
||||
zh: `样式设置`,
|
||||
en: `Style Setting`,
|
||||
zh_TW: `樣式設定`,
|
||||
},
|
||||
style_name: {
|
||||
zh: `样式名称`,
|
||||
en: `Style Name`,
|
||||
zh_TW: `樣式名稱`,
|
||||
},
|
||||
style_code: {
|
||||
zh: `样式代码`,
|
||||
en: `Style Code`,
|
||||
zh_TW: `樣式程式碼`,
|
||||
},
|
||||
pre_trans_seconds: {
|
||||
zh: `提前翻译时长 (10-36000s)`,
|
||||
en: `Pre translation seconds (10-36000s)`,
|
||||
zh_TW: `提前翻译时长 (10-36000s)`,
|
||||
},
|
||||
throttle_trans_interval: {
|
||||
zh: `节流翻译间隔 (1-3600s)`,
|
||||
en: `Throttling translation interval (1-3600s)`,
|
||||
zh_TW: `节流翻译间隔 (1-3600s)`,
|
||||
},
|
||||
};
|
||||
|
||||
export const newI18n = (lang) => (key) => I18N[key]?.[lang] || "";
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from "./storage";
|
||||
export * from "./url";
|
||||
export * from "./msg";
|
||||
export * from "./client";
|
||||
export * from "./styles";
|
||||
|
||||
@@ -33,3 +33,6 @@ 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";
|
||||
|
||||
export const MSG_MENUS_PROGRESSED = "progressed";
|
||||
export const MSG_MENUS_UPDATEFORM = "updateFormData";
|
||||
|
||||
407
src/config/quotes.js
Normal file
407
src/config/quotes.js
Normal file
@@ -0,0 +1,407 @@
|
||||
const quotes = [
|
||||
{
|
||||
en: "The unexamined life is not worth living.",
|
||||
zh: "未经审视的人生不值得过。",
|
||||
},
|
||||
{
|
||||
en: "I think, therefore I am.",
|
||||
zh: "我思故我在。",
|
||||
},
|
||||
{
|
||||
en: "He who has a why to live for can bear almost any how.",
|
||||
zh: "知道为何而活的人,几乎能忍受任何一种生活。",
|
||||
},
|
||||
{
|
||||
en: "Life is what happens when you're busy making other plans.",
|
||||
zh: "生活就是当你忙着制定其他计划时所发生的事情。",
|
||||
},
|
||||
{
|
||||
en: "Get busy living or get busy dying.",
|
||||
zh: "要么忙着活,要么忙着死。",
|
||||
},
|
||||
{
|
||||
en: "We are what we repeatedly do. Excellence, then, is not an act, but a habit.",
|
||||
zh: "我们由我们反复做的事情构成的。因此,卓越不是一种行为,而是一种习惯。",
|
||||
},
|
||||
{
|
||||
en: "Man is condemned to be free.",
|
||||
zh: "人注定是自由的。",
|
||||
},
|
||||
{
|
||||
en: "To be, or not to be: that is the question.",
|
||||
zh: "生存还是毁灭,这是一个问题。",
|
||||
},
|
||||
{
|
||||
en: "The purpose of life is not to be happy. It is to be useful, to be honorable, to be compassionate, to have it make some difference that you have lived and lived well.",
|
||||
zh: "人生的目的不是快乐,而是有用、高尚、富有同情心,让你活过并且活得好,从而使世界有所不同。",
|
||||
},
|
||||
{
|
||||
en: "Life is 10% what happens to us and 90% how we react to it.",
|
||||
zh: "生活 10% 取决于发生在我们身上的事,90% 取决于我们如何反应。",
|
||||
},
|
||||
{
|
||||
en: "The two most important days in your life are the day you are born and the day you find out why.",
|
||||
zh: "你一生中最重要的两天是:你出生的那天和你明白你为何出生的那天。",
|
||||
},
|
||||
{
|
||||
en: "In three words I can sum up everything I've learned about life: it goes on.",
|
||||
zh: "关于人生,我所学到的一切可以总结为三个词:它在继续。",
|
||||
},
|
||||
{
|
||||
en: "Not all those who wander are lost.",
|
||||
zh: "并非所有流浪者都迷失了方向。",
|
||||
},
|
||||
{
|
||||
en: "Life is simple, but we insist on making it complicated.",
|
||||
zh: "生活本简单,但我们坚持要把它弄复杂。",
|
||||
},
|
||||
{
|
||||
en: "Our life is what our thoughts make it.",
|
||||
zh: "我们的生活是由我们的思想造成的。",
|
||||
},
|
||||
{
|
||||
en: "Find purpose, the means will follow.",
|
||||
zh: "找到目标,方法自会随之而来。",
|
||||
},
|
||||
{
|
||||
en: "The goal of life is living in agreement with nature.",
|
||||
zh: "生活的目标是与自然和谐相处。",
|
||||
},
|
||||
{
|
||||
en: "The only true wisdom is in knowing you know nothing.",
|
||||
zh: "唯一的真正智慧在于知道自己一无所有。",
|
||||
},
|
||||
{
|
||||
en: "Knowledge is power.",
|
||||
zh: "知识就是力量。",
|
||||
},
|
||||
{
|
||||
en: "Knowing yourself is the beginning of all wisdom.",
|
||||
zh: "了解自己是所有智慧的开端。",
|
||||
},
|
||||
{
|
||||
en: "The journey of a thousand miles begins with a single step.",
|
||||
zh: "千里之行,始于足下。",
|
||||
},
|
||||
{
|
||||
en: "The only source of knowledge is experience.",
|
||||
zh: "知识的唯一来源是经验。",
|
||||
},
|
||||
{
|
||||
en: "A fool thinks himself to be wise, but a wise man knows himself to be a fool.",
|
||||
zh: "愚者自以为聪明,智者自知愚蠢。",
|
||||
},
|
||||
{
|
||||
en: "We learn from failure, not from success!",
|
||||
zh: "我们从失败中学习,而不是从成功中!",
|
||||
},
|
||||
{
|
||||
en: "The wise man is one who knows what he does not know.",
|
||||
zh: "智者,知其所不知。",
|
||||
},
|
||||
{
|
||||
en: "To know that we know what we know, and that we do not know what we do not know, that is true knowledge.",
|
||||
zh: "知之为知之,不知为不知,是知也。",
|
||||
},
|
||||
{
|
||||
en: "Curiosity is the wick in the candle of learning.",
|
||||
zh: "好奇心是学习这支蜡烛的灯芯。",
|
||||
},
|
||||
{
|
||||
en: "It is the mark of an educated mind to be able to entertain a thought without accepting it.",
|
||||
zh: "能够容纳一种思想而不同意它,这是一个受过教育的头脑的标志。",
|
||||
},
|
||||
{
|
||||
en: "Never stop questioning.",
|
||||
zh: "永远不要停止提问。",
|
||||
},
|
||||
{
|
||||
en: "The man who asks a question is a fool for a minute, the man who does not ask is a fool for life.",
|
||||
zh: "问问题的人,只傻一分钟;不问的人,傻一生。",
|
||||
},
|
||||
{
|
||||
en: "Wisdom is not a product of schooling but of the lifelong attempt to acquire it.",
|
||||
zh: "智慧不是学校教育的产物,而是终生努力获得的产物。",
|
||||
},
|
||||
{
|
||||
en: "The greatest enemy of knowledge is not ignorance, it is the illusion of knowledge.",
|
||||
zh: "知识最大的敌人不是无知,而是自以为拥有知识的幻觉。",
|
||||
},
|
||||
{
|
||||
en: "True wisdom comes to each of us when we realize how little we understand about life, ourselves, and the world around us.",
|
||||
zh: "当我们认识到自己对生命、对自身、对周围世界了解得多么少时,真正的智慧才会降临到我们每个人身上。",
|
||||
},
|
||||
{
|
||||
en: "Beware of false knowledge; it is more dangerous than ignorance.",
|
||||
zh: "谨防虚假的知识;它比无知更危险。",
|
||||
},
|
||||
{
|
||||
en: "What does not kill me makes me stronger.",
|
||||
zh: "杀不死我的,使我更强大。",
|
||||
},
|
||||
{
|
||||
en: "The only constant in life is change.",
|
||||
zh: "生活中唯一不变的就是变化。",
|
||||
},
|
||||
{
|
||||
en: "If you are going through hell, keep going.",
|
||||
zh: "如果你正在经历地狱,那就继续走下去。",
|
||||
},
|
||||
{
|
||||
en: "In the middle of difficulty lies opportunity.",
|
||||
zh: "机会蕴藏在困难之中。",
|
||||
},
|
||||
{
|
||||
en: "It is not the strongest of the species that survive, nor the most intelligent, but the one most responsive to change.",
|
||||
zh: "存活下来的物种不是最强壮的,也不是最聪明的,而是最能适应变化的。",
|
||||
},
|
||||
{
|
||||
en: "We must become the change we wish to see in the world.",
|
||||
zh: "我们必须成为我们希望在世界上看到的改变。",
|
||||
},
|
||||
{
|
||||
en: "A smooth sea never made a skilled sailor.",
|
||||
zh: "平静的大海练不出熟练的水手。",
|
||||
},
|
||||
{
|
||||
en: "Obstacles don't block the path, they are the path.",
|
||||
zh: "障碍不是挡住了路,障碍本身就是路。",
|
||||
},
|
||||
{
|
||||
en: "Fall seven times, stand up eight.",
|
||||
zh: "七次跌倒,八次站起。",
|
||||
},
|
||||
{
|
||||
en: "The art of life lies in a constant readjustment to our surroundings.",
|
||||
zh: "生活的艺术在于不断地调整自己以适应环境。",
|
||||
},
|
||||
{
|
||||
en: "Adversity introduces a man to himself.",
|
||||
zh: "逆境使人认识自己。",
|
||||
},
|
||||
{
|
||||
en: "The wound is the place where the Light enters you.",
|
||||
zh: "伤口是光进入你内心的入口。",
|
||||
},
|
||||
{
|
||||
en: "When we are no longer able to change a situation, we are challenged to change ourselves.",
|
||||
zh: "当我们无法改变现状时,我们就需要改变自己。",
|
||||
},
|
||||
{
|
||||
en: "Be the change you wish to see in the world.",
|
||||
zh: "成为你希望在世界上看到的改变。",
|
||||
},
|
||||
{
|
||||
en: "Do not pray for an easy life, pray for the strength to endure a difficult one.",
|
||||
zh: "不要祈祷生活安逸,要祈祷有力量去忍受艰难的生活。",
|
||||
},
|
||||
{
|
||||
en: "A pessimist sees the difficulty in every opportunity; an optimist sees the opportunity in every difficulty.",
|
||||
zh: "悲观者在每个机会中都看到困难;乐观者在每个困难中都看到机会。",
|
||||
},
|
||||
{
|
||||
en: "It's not what happens to you, but how you react to it that matters.",
|
||||
zh: "重要的不是发生在你身上的事,而是你如何应对它。",
|
||||
},
|
||||
{
|
||||
en: "To love oneself is the beginning of a lifelong romance.",
|
||||
zh: "爱自己是终身浪漫的开始。",
|
||||
},
|
||||
{
|
||||
en: "Love is composed of a single soul inhabiting two bodies.",
|
||||
zh: "爱是栖息于两个身体中的同一个灵魂。",
|
||||
},
|
||||
{
|
||||
en: "Man is the measure of all things.",
|
||||
zh: "人是万物的尺度。",
|
||||
},
|
||||
{
|
||||
en: "The best and most beautiful things in this world cannot be seen or even heard, but must be felt with the heart.",
|
||||
zh: "世界上最好最美的东西是看不见也听不见的,必须用心去感受。",
|
||||
},
|
||||
{
|
||||
en: "Where there is love there is life.",
|
||||
zh: "有爱的地方就有生命。",
|
||||
},
|
||||
{
|
||||
en: "If you want to be loved, be lovable.",
|
||||
zh: "如果你想被爱,就要变得可爱。",
|
||||
},
|
||||
{
|
||||
en: "We are all in the gutter, but some of us are looking at the stars.",
|
||||
zh: "我们都身处沟渠,但仍有人仰望星空。",
|
||||
},
|
||||
{
|
||||
en: "The only thing we have to fear is fear itself.",
|
||||
zh: "我们唯一需要恐惧的就是恐惧本身。",
|
||||
},
|
||||
{
|
||||
en: "Be kind, for everyone you meet is fighting a hard battle.",
|
||||
zh: "要友善,因为你遇到的每个人都在打一场艰苦的战斗。",
|
||||
},
|
||||
{
|
||||
en: "Man is born free, and everywhere he is in chains.",
|
||||
zh: "人生而自由,却无往不在枷锁之中。",
|
||||
},
|
||||
{
|
||||
en: "We love the things we love for what they are.",
|
||||
zh: "我们爱我们所爱之物,只因它们本来的样子。",
|
||||
},
|
||||
{
|
||||
en: "Darkness cannot drive out darkness; only light can do that. Hate cannot drive out hate; only love can do that.",
|
||||
zh: "黑暗无法驱逐黑暗,只有光明可以;仇恨无法驱逐仇恨,只有爱可以。",
|
||||
},
|
||||
{
|
||||
en: "An eye for an eye only ends up making the whole world blind.",
|
||||
zh: "以眼还眼,只会让整个世界都盲目。",
|
||||
},
|
||||
{
|
||||
en: "Hell is other people.",
|
||||
zh: "他人即地狱。",
|
||||
},
|
||||
{
|
||||
en: "You will not be punished for your anger, you will be punished by your anger.",
|
||||
zh: "你不会因为你的愤怒而受到惩罚,你会被你的愤怒所惩罚。",
|
||||
},
|
||||
{
|
||||
en: "To err is human, to forgive divine.",
|
||||
zh: "犯错是人性,宽恕是神性。",
|
||||
},
|
||||
{
|
||||
en: "Man is the only creature who refuses to be what he is.",
|
||||
zh: "人是唯一拒绝承认自己本质的生物。",
|
||||
},
|
||||
{
|
||||
en: "Beauty is in the eye of the beholder.",
|
||||
zh: "情人眼里出西施。",
|
||||
},
|
||||
{
|
||||
en: "All that we see or seem is but a dream within a dream.",
|
||||
zh: "我们所见所感,皆如梦中之梦。",
|
||||
},
|
||||
{
|
||||
en: "Everything you can imagine is real.",
|
||||
zh: "你能想象的一切都是真实的。",
|
||||
},
|
||||
{
|
||||
en: "The map is not the territory.",
|
||||
zh: "地图并非领土。",
|
||||
},
|
||||
{
|
||||
en: "We don't see things as they are, we see them as we are.",
|
||||
zh: "我们看到的不是事物的原貌,而是我们自己的样子。",
|
||||
},
|
||||
{
|
||||
en: "There are two ways to be fooled. One is to believe what isn't true; the other is to refuse to believe what is true.",
|
||||
zh: "被愚弄有两种方式。一种是相信不真实的东西;另一种是拒绝相信真实的东西。",
|
||||
},
|
||||
{
|
||||
en: "Simplicity is the ultimate sophistication.",
|
||||
zh: "简约是极致的复杂。",
|
||||
},
|
||||
{
|
||||
en: "The truth will set you free.",
|
||||
zh: "真相将使你自由。",
|
||||
},
|
||||
{
|
||||
en: "Reality is merely an illusion, albeit a very persistent one.",
|
||||
zh: "现实只是一种幻觉,尽管是一种非常持久的幻觉。",
|
||||
},
|
||||
{
|
||||
en: "What is rational is actual and what is actual is rational.",
|
||||
zh: "凡是合乎理性的东西都是现实的,凡是现实的东西都是合乎理性的。",
|
||||
},
|
||||
{
|
||||
en: "Truth is like the sun. You can shut it out for a time, but it ain't goin' away.",
|
||||
zh: "真相就像太阳。你可以暂时将它遮住,但它不会消失。",
|
||||
},
|
||||
{
|
||||
en: "Everything we hear is an opinion, not a fact. Everything we see is a perspective, not the truth.",
|
||||
zh: "我们听到的一切都只是观点,而非事实。我们看到的一切都只是视角,而非真相。",
|
||||
},
|
||||
{
|
||||
en: "There is no truth. There is only perception.",
|
||||
zh: "没有真相,只有认知。",
|
||||
},
|
||||
{
|
||||
en: "If you look deep enough into anything, you will find mathematics.",
|
||||
zh: "如果你对任何事物看得足够深入,你都会发现数学。",
|
||||
},
|
||||
{
|
||||
en: "The medium is the message.",
|
||||
zh: "媒介即信息。",
|
||||
},
|
||||
{
|
||||
en: "Nothing is true, everything is permitted.",
|
||||
zh: "没有什么是真实的,一切都被允许。",
|
||||
},
|
||||
{
|
||||
en: "We are what we believe we are.",
|
||||
zh: "我们相信自己是什么,我们就是什么。",
|
||||
},
|
||||
{
|
||||
en: "Yesterday is history, tomorrow is a mystery, but today is a gift. That is why it is called the present.",
|
||||
zh: "昨天是历史,明天是谜团,但今天是礼物。这就是为什么它被称为‘现在’(Present)。",
|
||||
},
|
||||
{
|
||||
en: "Time is money.",
|
||||
zh: "时间就是金钱。",
|
||||
},
|
||||
{
|
||||
en: "The only thing necessary for the triumph of evil is for good men to do nothing.",
|
||||
zh: "邪恶得逞的唯一条件是好人袖手旁观。",
|
||||
},
|
||||
{
|
||||
en: "Carpe diem.",
|
||||
zh: "活在当下。",
|
||||
},
|
||||
{
|
||||
en: "Do not dwell in the past, do not dream of the future, concentrate the mind on the present moment.",
|
||||
zh: "不要沉湎于过去,不要幻想未来,集中精神活在当下。",
|
||||
},
|
||||
{
|
||||
en: "The best time to plant a tree was 20 years ago. The second best time is now.",
|
||||
zh: "种树的最佳时机是20年前。其次是现在。",
|
||||
},
|
||||
{
|
||||
en: "Action speaks louder than words.",
|
||||
zh: "事实胜于雄辩。",
|
||||
},
|
||||
{
|
||||
en: "Honesty is the first chapter in the book of wisdom.",
|
||||
zh: "诚实是智慧之书的第一章。",
|
||||
},
|
||||
{
|
||||
en: "Two things are infinite: the universe and human stupidity; and I'm not sure about the universe.",
|
||||
zh: "有两样东西是无限的:宇宙和人类的愚蠢;而且我不太确定宇宙是否无限。",
|
||||
},
|
||||
{
|
||||
en: "You cannot step twice into the same river.",
|
||||
zh: "人不能两次踏进同一条河流。",
|
||||
},
|
||||
{
|
||||
en: "The future belongs to those who believe in the beauty of their dreams.",
|
||||
zh: "未来属于那些相信梦想之美的人。",
|
||||
},
|
||||
{
|
||||
en: "Procrastination is the thief of time.",
|
||||
zh: "拖延是时间的大敌。",
|
||||
},
|
||||
{
|
||||
en: "An investment in knowledge pays the best interest.",
|
||||
zh: "投资知识,收益最佳。",
|
||||
},
|
||||
{
|
||||
en: "I have not failed. I've just found 10,000 ways that won't work.",
|
||||
zh: "我没有失败。我只是找到了一万种行不通的方法。",
|
||||
},
|
||||
{
|
||||
en: "That which is done, is done.",
|
||||
zh: "木已成舟。",
|
||||
},
|
||||
];
|
||||
|
||||
export function getRandomQuote() {
|
||||
const randomIndex = Math.floor(Math.random() * quotes.length);
|
||||
return quotes[randomIndex];
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { OPT_TRANS_MICROSOFT } from "./api";
|
||||
import { OPT_STYLE_NONE } from "./styles";
|
||||
|
||||
export const GLOBAL_KEY = "*";
|
||||
export const REMAIN_KEY = "-";
|
||||
@@ -7,46 +8,8 @@ export const SHADOW_KEY = ">>>";
|
||||
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
|
||||
|
||||
export const DEFAULT_TRANS_TAG = "font";
|
||||
export const DEFAULT_SELECT_STYLE =
|
||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||
|
||||
export const OPT_STYLE_NONE = "style_none"; // 无
|
||||
export const OPT_STYLE_LINE = "under_line"; // 下划线
|
||||
export const OPT_STYLE_DOTLINE = "dot_line"; // 点状线
|
||||
export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
|
||||
export const OPT_STYLE_DASHBOX = "dash_box"; // 虚线框
|
||||
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
|
||||
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
|
||||
export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
|
||||
export const OPT_STYLE_BLOCKQUOTE = "blockquote"; // 引用
|
||||
export const OPT_STYLE_GRADIENT = "gradient"; // 渐变
|
||||
export const OPT_STYLE_BLINK = "blink"; // 闪现
|
||||
export const OPT_STYLE_GLOW = "glow"; // 发光
|
||||
export const OPT_STYLE_DIY = "diy_style"; // 自定义样式
|
||||
export const OPT_STYLE_ALL = [
|
||||
OPT_STYLE_NONE,
|
||||
OPT_STYLE_LINE,
|
||||
OPT_STYLE_DOTLINE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_DASHBOX,
|
||||
OPT_STYLE_FUZZY,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_BLOCKQUOTE,
|
||||
OPT_STYLE_GRADIENT,
|
||||
OPT_STYLE_BLINK,
|
||||
OPT_STYLE_GLOW,
|
||||
OPT_STYLE_DIY,
|
||||
];
|
||||
export const OPT_STYLE_USE_COLOR = [
|
||||
OPT_STYLE_LINE,
|
||||
OPT_STYLE_DOTLINE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_DASHBOX,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_BLOCKQUOTE,
|
||||
];
|
||||
// export const DEFAULT_SELECT_STYLE =
|
||||
// "-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||
|
||||
export const OPT_TIMING_PAGESCROLL = "mk_pagescroll"; // 滚动加载翻译
|
||||
export const OPT_TIMING_PAGEOPEN = "mk_pageopen"; // 直接翻译到底
|
||||
@@ -81,23 +44,10 @@ export const OPT_HIGHLIGHT_WORDS_ALL = [
|
||||
OPT_HIGHLIGHT_WORDS_AFTERTRANS,
|
||||
];
|
||||
|
||||
export const DEFAULT_DIY_STYLE = `color: #333;
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
LightGreen 20%,
|
||||
LightPink 20% 40%,
|
||||
LightSalmon 40% 60%,
|
||||
LightSeaGreen 60% 80%,
|
||||
LightSkyBlue 80%
|
||||
);
|
||||
&:hover {
|
||||
color: #111;
|
||||
};`;
|
||||
|
||||
export const DEFAULT_SELECTOR =
|
||||
"h1, h2, h3, h4, h5, h6, li, p, dd, blockquote, figcaption, label, legend";
|
||||
export const DEFAULT_IGNORE_SELECTOR = "button, footer, pre, mark, nav";
|
||||
export const DEFAULT_KEEP_SELECTOR = `a:has(code)`;
|
||||
export const DEFAULT_KEEP_SELECTOR = `code, cite, math, .math, a:has(code)`;
|
||||
export const DEFAULT_RULE = {
|
||||
pattern: "", // 匹配网址
|
||||
selector: "", // 选择器
|
||||
@@ -109,15 +59,16 @@ export const DEFAULT_RULE = {
|
||||
toLang: GLOBAL_KEY, // 目标语言
|
||||
textStyle: GLOBAL_KEY, // 译文样式
|
||||
transOpen: GLOBAL_KEY, // 开启翻译
|
||||
bgColor: "", // 译文颜色
|
||||
textDiyStyle: "", // 自定义译文样式
|
||||
// bgColor: "", // 译文颜色 (作废)
|
||||
// textDiyStyle: "", // 自定义译文样式 (作废)
|
||||
textExtStyle: "", // 译文附加样式
|
||||
termsStyle: "", // 专业术语样式
|
||||
highlightStyle: "", // 高亮词汇样式
|
||||
selectStyle: "", // 选择器节点样式
|
||||
parentStyle: "", // 选择器父节点样式
|
||||
grandStyle: "", // 选择器父节点样式
|
||||
injectJs: "", // 注入JS
|
||||
injectCss: "", // 注入CSS
|
||||
// injectCss: "", // 注入CSS (作废)
|
||||
transOnly: GLOBAL_KEY, // 是否仅显示译文
|
||||
// transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译 (暂时作废)
|
||||
transTag: GLOBAL_KEY, // 译文元素标签
|
||||
@@ -152,13 +103,14 @@ export const GLOBLA_RULE = {
|
||||
toLang: "zh-CN", // 目标语言
|
||||
textStyle: OPT_STYLE_NONE, // 译文样式
|
||||
transOpen: "false", // 开启翻译
|
||||
bgColor: "", // 译文颜色
|
||||
textDiyStyle: DEFAULT_DIY_STYLE, // 自定义译文样式
|
||||
// bgColor: DEFAULT_COLOR, // 译文颜色 (作废)
|
||||
// textDiyStyle: DEFAULT_DIY_STYLE, // 自定义译文样式 (作废)
|
||||
textExtStyle: "", // 译文附加样式
|
||||
termsStyle: "font-weight: bold;", // 专业术语样式
|
||||
highlightStyle: "color: red;", // 高亮词汇样式
|
||||
selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
|
||||
parentStyle: DEFAULT_SELECT_STYLE, // 选择器父节点样式
|
||||
grandStyle: DEFAULT_SELECT_STYLE, // 选择器祖节点样式
|
||||
selectStyle: "", // 选择器节点样式
|
||||
parentStyle: "", // 选择器父节点样式
|
||||
grandStyle: "", // 选择器祖节点样式
|
||||
injectJs: "", // 注入JS
|
||||
injectCss: "", // 注入CSS
|
||||
transOnly: "false", // 是否仅显示译文
|
||||
@@ -185,16 +137,6 @@ export const GLOBLA_RULE = {
|
||||
|
||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||
|
||||
export const DEFAULT_OW_RULE = {
|
||||
apiSlug: REMAIN_KEY,
|
||||
fromLang: REMAIN_KEY,
|
||||
toLang: REMAIN_KEY,
|
||||
textStyle: REMAIN_KEY,
|
||||
transOpen: REMAIN_KEY,
|
||||
bgColor: "",
|
||||
textDiyStyle: DEFAULT_DIY_STYLE,
|
||||
};
|
||||
|
||||
// todo: 校验几个内置规则
|
||||
const RULES_MAP = {
|
||||
// "www.google.com/search": {
|
||||
@@ -210,8 +152,9 @@ const RULES_MAP = {
|
||||
autoScan: `false`,
|
||||
},
|
||||
"twitter.com, https://x.com": {
|
||||
selector: `[data-testid='tweetText']`,
|
||||
keepSelector: `img, svg, span:has(a), div:has(a)`,
|
||||
selector: `[data-testid='tweetText'], [data-testid='twitter-article-title'], .public-DraftStyleDefault-block`,
|
||||
keepSelector: `img, svg, a, span:has(a), div:has(a)`,
|
||||
ignoreSelector: `button, [data-testid='videoPlayer'], [role='group']`,
|
||||
autoScan: `false`,
|
||||
},
|
||||
"www.youtube.com/live_chat": {
|
||||
@@ -222,6 +165,14 @@ const RULES_MAP = {
|
||||
"www.youtube.com": {
|
||||
rootsSelector: `ytd-page-manager`,
|
||||
ignoreSelector: `aside, button, footer, form, header, pre, mark, nav, #player, #container, .caption-window, .ytp-settings-menu`,
|
||||
selectStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||
parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||
grandStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||
},
|
||||
"web.telegram.org": {
|
||||
autoScan: `false`,
|
||||
selector: ".text-content, .embedded-text-wrapper",
|
||||
rootsSelector: ".Transition",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
OPT_TRANS_MICROSOFT,
|
||||
DEFAULT_API_LIST,
|
||||
} from "./api";
|
||||
import { DEFAULT_CUSTOM_STYLES } from "./styles";
|
||||
|
||||
// 默认快捷键
|
||||
export const OPT_SHORTCUT_TRANSLATE = "toggleTranslate";
|
||||
@@ -88,6 +89,7 @@ export const DEFAULT_TRANBOX_SETTING = {
|
||||
hideClickAway: false, // 是否点击外部关闭弹窗
|
||||
simpleStyle: false, // 是否简洁界面
|
||||
followSelection: false, // 翻译框是否跟随选中文本
|
||||
autoHeight: false, // 自适应高度
|
||||
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
|
||||
// extStyles: "", // 附加样式
|
||||
enDict: OPT_DICT_BING, // 英文词典
|
||||
@@ -101,18 +103,21 @@ line-height: 1.3;
|
||||
text-shadow: 1px 1px 2px black;
|
||||
display: inline-block`;
|
||||
|
||||
const SUBTITLE_ORIGIN_STYLE = `font-size: clamp(1.5rem, 3cqw, 3rem);`;
|
||||
const SUBTITLE_ORIGIN_STYLE = `font-size: clamp(1rem, 2cqw, 3rem);`;
|
||||
|
||||
const SUBTITLE_TRANSLATION_STYLE = `font-size: clamp(1.5rem, 3cqw, 3rem);`;
|
||||
const SUBTITLE_TRANSLATION_STYLE = `font-size: clamp(1rem, 2cqw, 3rem);`;
|
||||
|
||||
export const DEFAULT_SUBTITLE_SETTING = {
|
||||
enabled: true, // 是否开启
|
||||
apiSlug: OPT_TRANS_MICROSOFT,
|
||||
segSlug: "-", // AI智能断句
|
||||
chunkLength: 1000, // AI处理切割长度
|
||||
preTrans: 90, // 提前翻译时长
|
||||
throttleTrans: 30, // 节流翻译间隔
|
||||
// fromLang: "en",
|
||||
toLang: "zh-CN",
|
||||
isBilingual: true, // 是否双语显示
|
||||
skipAd: false, // 是否快进广告
|
||||
windowStyle: SUBTITLE_WINDOW_STYLE, // 背景样式
|
||||
originStyle: SUBTITLE_ORIGIN_STYLE, // 原文样式
|
||||
translationStyle: SUBTITLE_TRANSLATION_STYLE, // 译文样式
|
||||
@@ -166,7 +171,8 @@ export const DEFAULT_SETTING = {
|
||||
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
||||
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
||||
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
|
||||
touchTranslate: 2, // 触屏翻译 {5:单指双击,6:单指三击,7:双指双击}
|
||||
// touchTranslate: 2, // 触屏翻译 {5:单指双击,6:单指三击,7:双指双击} (作废)
|
||||
touchModes: [2], // 触屏翻译 {5:单指双击,6:单指三击,7:双指双击} (多选)
|
||||
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
|
||||
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
|
||||
orilist: DEFAULT_ORILIST.join(",\n"), // 禁用CSP名单
|
||||
@@ -179,4 +185,6 @@ export const DEFAULT_SETTING = {
|
||||
transAllnow: false, // 是否立即全部翻译
|
||||
subtitleSetting: DEFAULT_SUBTITLE_SETTING, // 字幕设置
|
||||
logLevel: LogLevel.INFO.value, // 日志级别
|
||||
rootMargin: 500, // 提前触发翻译
|
||||
customStyles: DEFAULT_CUSTOM_STYLES, // 自定义样式列表
|
||||
};
|
||||
|
||||
@@ -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`;
|
||||
|
||||
46
src/config/styles.js
Normal file
46
src/config/styles.js
Normal file
@@ -0,0 +1,46 @@
|
||||
export const OPT_STYLE_NONE = "style_none"; // 无
|
||||
export const OPT_STYLE_LINE = "under_line"; // 下划线
|
||||
export const OPT_STYLE_DOTLINE = "dot_line"; // 点状线
|
||||
export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
|
||||
export const OPT_STYLE_DASHLINE_BOLD = "dash_line_bold"; // 虚线加粗
|
||||
export const OPT_STYLE_DASHBOX = "dash_box"; // 虚线框
|
||||
export const OPT_STYLE_DASHBOX_BOLD = "dash_box_bold"; // 虚线框加粗
|
||||
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
|
||||
export const OPT_STYLE_WAVYLINE_BOLD = "wavy_line_bold"; // 波浪线加粗
|
||||
export const OPT_STYLE_MARKER = "marker"; // 马克笔
|
||||
export const OPT_STYLE_GRADIENT_MARKER = "gradient_marker"; // 渐变马克笔
|
||||
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
|
||||
export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
|
||||
export const OPT_STYLE_BLOCKQUOTE = "blockquote"; // 引用
|
||||
export const OPT_STYLE_GRADIENT = "gradient"; // 渐变
|
||||
export const OPT_STYLE_BLINK = "blink"; // 闪现
|
||||
export const OPT_STYLE_GLOW = "glow"; // 发光
|
||||
export const OPT_STYLE_COLORFUL = "colorful"; // 多彩
|
||||
export const OPT_STYLE_ALL = [
|
||||
OPT_STYLE_NONE,
|
||||
OPT_STYLE_LINE,
|
||||
OPT_STYLE_DOTLINE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_DASHLINE_BOLD,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_WAVYLINE_BOLD,
|
||||
OPT_STYLE_DASHBOX,
|
||||
OPT_STYLE_DASHBOX_BOLD,
|
||||
OPT_STYLE_MARKER,
|
||||
OPT_STYLE_GRADIENT_MARKER,
|
||||
OPT_STYLE_FUZZY,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_BLOCKQUOTE,
|
||||
OPT_STYLE_GRADIENT,
|
||||
OPT_STYLE_BLINK,
|
||||
OPT_STYLE_GLOW,
|
||||
OPT_STYLE_COLORFUL,
|
||||
];
|
||||
|
||||
export const DEFAULT_CUSTOM_STYLES = [
|
||||
{
|
||||
styleSlug: "custom",
|
||||
styleName: "Custom Style",
|
||||
styleCode: `color: #209CEE;`,
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,3 @@
|
||||
import { run } from "./common";
|
||||
|
||||
if (document.documentElement && document.documentElement.tagName === "HTML") {
|
||||
run();
|
||||
}
|
||||
run();
|
||||
|
||||
@@ -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>
|
||||
|
||||
84
src/hooks/CustomStyles.js
Normal file
84
src/hooks/CustomStyles.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useSetting } from "./Setting";
|
||||
import { DEFAULT_CUSTOM_STYLES, OPT_STYLE_ALL } from "../config/styles";
|
||||
import { builtinStylesMap } from "../libs/style";
|
||||
import { useI18n } from "./I18n";
|
||||
|
||||
function useStyleState() {
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const customStyles = setting?.customStyles || [];
|
||||
|
||||
return { customStyles, updateSetting };
|
||||
}
|
||||
|
||||
export function useStyleList() {
|
||||
const { customStyles, updateSetting } = useStyleState();
|
||||
|
||||
const addStyle = useCallback(() => {
|
||||
const defaultStyle = DEFAULT_CUSTOM_STYLES[0];
|
||||
const uuid = crypto.randomUUID();
|
||||
const styleSlug = `custom_${crypto.randomUUID()}`;
|
||||
const styleName = `Style_${uuid.slice(0, 8)}`;
|
||||
const newStyle = {
|
||||
...defaultStyle,
|
||||
styleSlug,
|
||||
styleName,
|
||||
};
|
||||
updateSetting((prev) => ({
|
||||
...prev,
|
||||
customStyles: [...(prev?.customStyles || []), newStyle],
|
||||
}));
|
||||
}, [updateSetting]);
|
||||
|
||||
const deleteStyle = useCallback(
|
||||
(styleSlug) => {
|
||||
updateSetting((prev) => ({
|
||||
...prev,
|
||||
customStyles: (prev?.customStyles || []).filter(
|
||||
(item) => item.styleSlug !== styleSlug
|
||||
),
|
||||
}));
|
||||
},
|
||||
[updateSetting]
|
||||
);
|
||||
|
||||
const updateStyle = useCallback(
|
||||
(styleSlug, updateData) => {
|
||||
updateSetting((prev) => ({
|
||||
...prev,
|
||||
customStyles: (prev?.customStyles || []).map((item) =>
|
||||
item.styleSlug === styleSlug ? { ...item, ...updateData } : item
|
||||
),
|
||||
}));
|
||||
},
|
||||
[updateSetting]
|
||||
);
|
||||
|
||||
return {
|
||||
customStyles,
|
||||
addStyle,
|
||||
deleteStyle,
|
||||
updateStyle,
|
||||
};
|
||||
}
|
||||
|
||||
export function useAllTextStyles() {
|
||||
const { customStyles } = useStyleList();
|
||||
const i18n = useI18n();
|
||||
|
||||
const builtinStyles = useMemo(
|
||||
() =>
|
||||
OPT_STYLE_ALL.map((styleSlug) => ({
|
||||
styleSlug,
|
||||
styleName: i18n(styleSlug),
|
||||
styleCode: builtinStylesMap[styleSlug] || "",
|
||||
})),
|
||||
[i18n]
|
||||
);
|
||||
|
||||
const allTextStyles = useMemo(() => {
|
||||
return [...builtinStyles, ...customStyles];
|
||||
}, [builtinStyles, customStyles]);
|
||||
|
||||
return { builtinStyles, customStyles, allTextStyles };
|
||||
}
|
||||
@@ -57,9 +57,9 @@ export function useRules() {
|
||||
const put = useCallback(
|
||||
(pattern, obj) => {
|
||||
save((prev) => {
|
||||
if (pattern !== obj.pattern) {
|
||||
return prev;
|
||||
}
|
||||
// if (pattern !== obj.pattern) {
|
||||
// return prev;
|
||||
// }
|
||||
return prev.map((item) =>
|
||||
item.pattern === pattern ? { ...item, ...obj } : item
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DEFAULT_SUBRULES_LIST, DEFAULT_OW_RULE } from "../config";
|
||||
import { DEFAULT_SUBRULES_LIST } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { loadOrFetchSubRules } from "../libs/subRules";
|
||||
@@ -78,15 +78,3 @@ export function useSubRules() {
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆写订阅规则
|
||||
* @returns
|
||||
*/
|
||||
export function useOwSubRule() {
|
||||
const { setting, updateChild } = useSetting();
|
||||
const owSubrule = setting?.owSubrule || DEFAULT_OW_RULE;
|
||||
const updateOwSubrule = updateChild("owSubrule");
|
||||
|
||||
return { owSubrule, updateOwSubrule };
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { THEME_DARK, THEME_LIGHT } from "../config";
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function Theme({ children, options, styles }) {
|
||||
export default function Theme({ children, options = {}, styles = {} }) {
|
||||
const { darkMode } = useDarkMode();
|
||||
const [systemMode, setSystemMode] = useState(THEME_LIGHT);
|
||||
|
||||
@@ -29,11 +29,8 @@ export default function Theme({ children, options, styles }) {
|
||||
const theme = useMemo(() => {
|
||||
let htmlFontSize = 16;
|
||||
try {
|
||||
const s = window.getComputedStyle(document.body.parentNode).fontSize;
|
||||
const fontSize = parseInt(s.replace("px", ""));
|
||||
if (fontSize > 0 && fontSize < 1000) {
|
||||
htmlFontSize = fontSize;
|
||||
}
|
||||
const s = window.getComputedStyle(document.documentElement).fontSize;
|
||||
htmlFontSize = parseInt(s.replace("px", ""));
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -3,12 +3,12 @@ import { APP_CONSTS } from "../config";
|
||||
import ContentFab from "../views/Action/ContentFab";
|
||||
|
||||
export class FabManager extends ShadowDomManager {
|
||||
constructor({ translator, processActions, fabConfig }) {
|
||||
constructor({ processActions, fabConfig }) {
|
||||
super({
|
||||
id: APP_CONSTS.fabID,
|
||||
className: "notranslate",
|
||||
reactComponent: ContentFab,
|
||||
props: { translator, processActions, fabConfig },
|
||||
props: { processActions, fabConfig },
|
||||
});
|
||||
|
||||
if (!fabConfig?.isHide) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { matchValue, type, isMatch } from "./utils";
|
||||
import {
|
||||
GLOBAL_KEY,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
// OPT_TIMING_ALL,
|
||||
DEFAULT_RULE,
|
||||
GLOBLA_RULE,
|
||||
OPT_SPLIT_PARAGRAPH_ALL,
|
||||
@@ -13,7 +11,6 @@ import {
|
||||
import { loadOrFetchSubRules } from "./subRules";
|
||||
import { getRulesWithDefault, setRules } from "./storage";
|
||||
import { trySyncRules } from "./sync";
|
||||
// import { FIXER_ALL } from "./webfix";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
/**
|
||||
@@ -37,7 +34,7 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
|
||||
}
|
||||
|
||||
const rule = rules.find((r) =>
|
||||
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
|
||||
r.pattern.split(/\n|,/).some((p) => isMatch(href, p.trim()))
|
||||
);
|
||||
const globalRule = {
|
||||
...GLOBLA_RULE,
|
||||
@@ -56,12 +53,12 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
|
||||
"aiTerms",
|
||||
"termsStyle",
|
||||
"highlightStyle",
|
||||
"textExtStyle",
|
||||
"selectStyle",
|
||||
"parentStyle",
|
||||
"grandStyle",
|
||||
"injectJs",
|
||||
"injectCss",
|
||||
// "fixerSelector",
|
||||
"transStartHook",
|
||||
"transEndHook",
|
||||
// "transRemoveHook",
|
||||
@@ -77,16 +74,14 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
|
||||
"toLang",
|
||||
"transOpen",
|
||||
"transOnly",
|
||||
// "transTiming",
|
||||
"autoScan",
|
||||
"hasRichText",
|
||||
"hasShadowroot",
|
||||
"transTag",
|
||||
"transTitle",
|
||||
// "detectRemote",
|
||||
// "fixerFunc",
|
||||
"splitParagraph",
|
||||
"highlightWords",
|
||||
"textStyle",
|
||||
].forEach((key) => {
|
||||
if (!rule[key] || rule[key] === GLOBAL_KEY) {
|
||||
rule[key] = globalRule[key];
|
||||
@@ -99,18 +94,6 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
|
||||
}
|
||||
});
|
||||
|
||||
// if (!rule.skipLangs || rule.skipLangs.length === 0) {
|
||||
// rule.skipLangs = globalRule.skipLangs;
|
||||
// }
|
||||
if (!rule.textStyle || rule.textStyle === GLOBAL_KEY) {
|
||||
rule.textStyle = globalRule.textStyle;
|
||||
rule.bgColor = globalRule.bgColor;
|
||||
rule.textDiyStyle = globalRule.textDiyStyle;
|
||||
} else {
|
||||
rule.bgColor = rule.bgColor?.trim() || globalRule.bgColor;
|
||||
rule.textDiyStyle = rule.textDiyStyle?.trim() || globalRule.textDiyStyle;
|
||||
}
|
||||
|
||||
return rule;
|
||||
};
|
||||
|
||||
@@ -150,6 +133,7 @@ export const checkRules = (rules) => {
|
||||
aiTerms,
|
||||
termsStyle,
|
||||
highlightStyle,
|
||||
textExtStyle,
|
||||
selectStyle,
|
||||
parentStyle,
|
||||
grandStyle,
|
||||
@@ -160,19 +144,12 @@ export const checkRules = (rules) => {
|
||||
toLang,
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
transOnly,
|
||||
autoScan,
|
||||
hasRichText,
|
||||
hasShadowroot,
|
||||
// transTiming,
|
||||
transTag,
|
||||
transTitle,
|
||||
// detectRemote,
|
||||
// skipLangs,
|
||||
// fixerSelector,
|
||||
// fixerFunc,
|
||||
transStartHook,
|
||||
transEndHook,
|
||||
// transRemoveHook,
|
||||
@@ -189,36 +166,34 @@ export const checkRules = (rules) => {
|
||||
aiTerms: type(aiTerms) === "string" ? aiTerms : "",
|
||||
termsStyle: type(termsStyle) === "string" ? termsStyle : "",
|
||||
highlightStyle: type(highlightStyle) === "string" ? highlightStyle : "",
|
||||
textExtStyle: type(textExtStyle) === "string" ? textExtStyle : "",
|
||||
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 : "",
|
||||
bgColor: type(bgColor) === "string" ? bgColor : "",
|
||||
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
|
||||
apiSlug:
|
||||
type(apiSlug) === "string" && apiSlug.trim() !== ""
|
||||
? apiSlug.trim()
|
||||
: GLOBAL_KEY,
|
||||
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
|
||||
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
|
||||
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
|
||||
// textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
|
||||
textStyle:
|
||||
type(textStyle) === "string" && textStyle.trim() !== ""
|
||||
? textStyle.trim()
|
||||
: GLOBAL_KEY,
|
||||
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
|
||||
transOnly: matchValue([GLOBAL_KEY, "true", "false"], transOnly),
|
||||
autoScan: matchValue([GLOBAL_KEY, "true", "false"], autoScan),
|
||||
hasRichText: matchValue([GLOBAL_KEY, "true", "false"], hasRichText),
|
||||
hasShadowroot: matchValue([GLOBAL_KEY, "true", "false"], hasShadowroot),
|
||||
// transTiming: matchValue([GLOBAL_KEY, ...OPT_TIMING_ALL], transTiming),
|
||||
transTag: matchValue([GLOBAL_KEY, "span", "font"], transTag),
|
||||
transTitle: matchValue([GLOBAL_KEY, "true", "false"], transTitle),
|
||||
// detectRemote: matchValue([GLOBAL_KEY, "true", "false"], detectRemote),
|
||||
// skipLangs: type(skipLangs) === "array" ? skipLangs : [],
|
||||
// fixerSelector: type(fixerSelector) === "string" ? fixerSelector : "",
|
||||
transStartHook: type(transStartHook) === "string" ? transStartHook : "",
|
||||
transEndHook: type(transEndHook) === "string" ? transEndHook : "",
|
||||
// transRemoveHook:
|
||||
// type(transRemoveHook) === "string" ? transRemoveHook : "",
|
||||
// fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc),
|
||||
splitParagraph: matchValue(
|
||||
[GLOBAL_KEY, ...OPT_SPLIT_PARAGRAPH_ALL],
|
||||
splitParagraph
|
||||
|
||||
@@ -15,7 +15,13 @@ export default class ShadowDomManager {
|
||||
_ReactComponent;
|
||||
_props;
|
||||
|
||||
constructor({ id, className = "", reactComponent, props = {} }) {
|
||||
constructor({
|
||||
id,
|
||||
className = "",
|
||||
reactComponent,
|
||||
props = {},
|
||||
rootElement = document.body,
|
||||
}) {
|
||||
if (!id || !reactComponent) {
|
||||
throw new Error("ID and a React Component must be provided.");
|
||||
}
|
||||
@@ -23,6 +29,7 @@ export default class ShadowDomManager {
|
||||
this._className = className;
|
||||
this._ReactComponent = reactComponent;
|
||||
this._props = props;
|
||||
this._rootElement = rootElement;
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
@@ -92,22 +99,18 @@ export default class ShadowDomManager {
|
||||
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");
|
||||
this._rootElement.appendChild(host);
|
||||
this.#hostElement = host;
|
||||
const shadowContainer = host.attachShadow({ mode: "open" });
|
||||
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,
|
||||
container: shadowContainer,
|
||||
});
|
||||
|
||||
const enhancedProps = {
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { kissLog } from "./log";
|
||||
|
||||
/**
|
||||
* @class ShadowRootMonitor
|
||||
* @description 通过覆写 Element.prototype.attachShadow 来监控页面上所有新创建的 Shadow DOM
|
||||
*/
|
||||
export default class ShadowRootMonitor {
|
||||
/**
|
||||
* @param {function(ShadowRoot): void} callback - 当一个新的 shadowRoot 被创建时调用的回调函数。
|
||||
*/
|
||||
constructor(callback) {
|
||||
if (typeof callback !== "function") {
|
||||
throw new Error("Callback must be a function.");
|
||||
}
|
||||
|
||||
this.callback = callback;
|
||||
this.isMonitoring = false;
|
||||
this.originalAttachShadow = Element.prototype.attachShadow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始监控 shadowRoot 的创建。
|
||||
*/
|
||||
start() {
|
||||
if (this.isMonitoring) {
|
||||
return;
|
||||
}
|
||||
const monitorInstance = this;
|
||||
|
||||
Element.prototype.attachShadow = function (...args) {
|
||||
const shadowRoot = monitorInstance.originalAttachShadow.apply(this, args);
|
||||
if (shadowRoot) {
|
||||
try {
|
||||
monitorInstance.callback(shadowRoot);
|
||||
} catch (error) {
|
||||
kissLog("Error in ShadowRootMonitor callback", error);
|
||||
}
|
||||
}
|
||||
return shadowRoot;
|
||||
};
|
||||
|
||||
this.isMonitoring = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止监控,并恢复原始的 attachShadow 方法。
|
||||
*/
|
||||
stop() {
|
||||
if (!this.isMonitoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
Element.prototype.attachShadow = this.originalAttachShadow;
|
||||
this.isMonitoring = false;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 数据同步
|
||||
*/
|
||||
|
||||
@@ -12,9 +12,13 @@ import {
|
||||
OPT_STYLE_GRADIENT,
|
||||
OPT_STYLE_BLINK,
|
||||
OPT_STYLE_GLOW,
|
||||
OPT_STYLE_DIY,
|
||||
DEFAULT_DIY_STYLE,
|
||||
OPT_STYLE_COLORFUL,
|
||||
DEFAULT_COLOR,
|
||||
OPT_STYLE_MARKER,
|
||||
OPT_STYLE_GRADIENT_MARKER,
|
||||
OPT_STYLE_DASHBOX_BOLD,
|
||||
OPT_STYLE_DASHLINE_BOLD,
|
||||
OPT_STYLE_WAVYLINE_BOLD,
|
||||
} from "../config";
|
||||
|
||||
const gradientFlow = keyframes`
|
||||
@@ -47,47 +51,63 @@ const glow = keyframes`
|
||||
}
|
||||
`;
|
||||
|
||||
const genLineStyle = (style, color) => `
|
||||
const genLineStyle = (style, color, thickness = 1) => `
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: ${style};
|
||||
text-decoration-color: ${color};
|
||||
text-decoration-thickness: 2px;
|
||||
text-decoration-thickness: ${thickness}px;
|
||||
text-underline-offset: 0.3em;
|
||||
-webkit-text-decoration-line: underline;
|
||||
-webkit-text-decoration-style: ${style};
|
||||
-webkit-text-decoration-color: ${color};
|
||||
-webkit-text-decoration-thickness: 2px;
|
||||
-webkit-text-decoration-thickness: 1px;
|
||||
-webkit-text-underline-offset: 0.3em;
|
||||
|
||||
/* opacity: 0.8;
|
||||
opacity: 0.8;
|
||||
-webkit-opacity: 0.8;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
-webkit-opacity: 1;
|
||||
} */
|
||||
}
|
||||
`;
|
||||
|
||||
const genStyles = ({
|
||||
textDiyStyle = DEFAULT_DIY_STYLE,
|
||||
bgColor = DEFAULT_COLOR,
|
||||
} = {}) => ({
|
||||
const genBuiltinStyles = (color = DEFAULT_COLOR) => ({
|
||||
// 无样式
|
||||
[OPT_STYLE_NONE]: ``,
|
||||
// 下划线
|
||||
[OPT_STYLE_LINE]: genLineStyle("solid", bgColor),
|
||||
[OPT_STYLE_LINE]: genLineStyle("solid", color),
|
||||
// 点状线
|
||||
[OPT_STYLE_DOTLINE]: genLineStyle("dotted", bgColor),
|
||||
[OPT_STYLE_DOTLINE]: genLineStyle("dotted", color),
|
||||
// 虚线
|
||||
[OPT_STYLE_DASHLINE]: genLineStyle("dashed", bgColor),
|
||||
[OPT_STYLE_DASHLINE]: genLineStyle("dashed", color),
|
||||
// 虚线加粗
|
||||
[OPT_STYLE_DASHLINE_BOLD]: genLineStyle("dashed", color, 2),
|
||||
// 波浪线
|
||||
[OPT_STYLE_WAVYLINE]: genLineStyle("wavy", bgColor),
|
||||
[OPT_STYLE_WAVYLINE]: genLineStyle("wavy", color),
|
||||
// 波浪线加粗
|
||||
[OPT_STYLE_WAVYLINE_BOLD]: genLineStyle("wavy", color, 2),
|
||||
// 虚线框
|
||||
[OPT_STYLE_DASHBOX]: `
|
||||
border: 2px dashed ${bgColor || DEFAULT_COLOR};
|
||||
display: inline-block;
|
||||
padding: 0.2em 0.4em;
|
||||
border: 1px dashed ${color};
|
||||
display: block;
|
||||
padding: 0.2em 0.3em;
|
||||
box-sizing: border-box;
|
||||
`,
|
||||
// 虚线框加粗
|
||||
[OPT_STYLE_DASHBOX_BOLD]: `
|
||||
border: 2px dashed ${color};
|
||||
display: block;
|
||||
padding: 0.2em 0.3em;
|
||||
box-sizing: border-box;
|
||||
`,
|
||||
// 马克笔
|
||||
[OPT_STYLE_MARKER]: `
|
||||
background: linear-gradient(to top, ${color} 50%, transparent 50%);
|
||||
`,
|
||||
// 渐变马克笔
|
||||
[OPT_STYLE_GRADIENT_MARKER]: `
|
||||
background: linear-gradient(to top, transparent, ${color} 20%, transparent 60%);
|
||||
`,
|
||||
// 模糊
|
||||
[OPT_STYLE_FUZZY]: `
|
||||
filter: blur(0.2em);
|
||||
@@ -100,7 +120,7 @@ const genStyles = ({
|
||||
// 高亮
|
||||
[OPT_STYLE_HIGHLIGHT]: `
|
||||
color: #fff;
|
||||
background-color: ${bgColor || DEFAULT_COLOR};
|
||||
background-color: ${color};
|
||||
`,
|
||||
// 引用
|
||||
[OPT_STYLE_BLOCKQUOTE]: `
|
||||
@@ -108,7 +128,7 @@ const genStyles = ({
|
||||
-webkit-opacity: 0.8;
|
||||
display: block;
|
||||
padding: 0.25em 0.5em;
|
||||
border-left: 0.5em solid ${bgColor || DEFAULT_COLOR};
|
||||
border-left: 0.25em solid ${color};
|
||||
background: rgb(32, 156, 238, 0.2);
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
@@ -138,14 +158,29 @@ const genStyles = ({
|
||||
[OPT_STYLE_GLOW]: `
|
||||
animation: ${glow} 2s ease-in-out infinite alternate;
|
||||
`,
|
||||
// 自定义
|
||||
[OPT_STYLE_DIY]: `
|
||||
${textDiyStyle}
|
||||
`,
|
||||
// 多彩
|
||||
[OPT_STYLE_COLORFUL]: `
|
||||
color: #333;
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
LightGreen 20%,
|
||||
LightPink 20% 40%,
|
||||
LightSalmon 40% 60%,
|
||||
LightSeaGreen 60% 80%,
|
||||
LightSkyBlue 80%
|
||||
);
|
||||
&:hover {
|
||||
color: #111;
|
||||
};
|
||||
`,
|
||||
});
|
||||
|
||||
export const genTextClass = ({ textDiyStyle, bgColor = DEFAULT_COLOR }) => {
|
||||
const styles = genStyles({ textDiyStyle, bgColor });
|
||||
export const genTextClass = (customStyles = []) => {
|
||||
const styles = genBuiltinStyles();
|
||||
customStyles.forEach((style) => {
|
||||
styles[style.styleSlug] = style.styleCode;
|
||||
});
|
||||
|
||||
const textClass = {};
|
||||
let textStyles = "";
|
||||
Object.entries(styles).forEach(([k, v]) => {
|
||||
@@ -163,4 +198,4 @@ export const genTextClass = ({ textDiyStyle, bgColor = DEFAULT_COLOR }) => {
|
||||
return [textClass, textStyles];
|
||||
};
|
||||
|
||||
export const defaultStyles = genStyles();
|
||||
export const builtinStylesMap = genBuiltinStyles();
|
||||
|
||||
@@ -83,8 +83,8 @@ export function createLogoSVG({
|
||||
const primaryColor = "#209CEE";
|
||||
const secondaryColor = "#E9F5FD";
|
||||
|
||||
const path1Fill = isSelected ? primaryColor : secondaryColor;
|
||||
const path2Fill = isSelected ? secondaryColor : primaryColor;
|
||||
const path1Fill = isSelected ? secondaryColor : primaryColor;
|
||||
const path2Fill = isSelected ? primaryColor : secondaryColor;
|
||||
|
||||
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 ",
|
||||
|
||||
@@ -21,9 +21,7 @@ export class TransboxManager {
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return (
|
||||
!!this.#container && document.body.parentElement.contains(this.#container)
|
||||
);
|
||||
return !!this.#container && document.body.contains(this.#container);
|
||||
}
|
||||
|
||||
enable() {
|
||||
@@ -31,36 +29,28 @@ export class TransboxManager {
|
||||
this.#container = document.createElement("div");
|
||||
this.#container.id = APP_CONSTS.boxID;
|
||||
this.#container.className = "notranslate";
|
||||
this.#container.style.cssText =
|
||||
"font-size: 0; width: 0; height: 0; border: 0; padding: 0; margin: 0;";
|
||||
document.body.parentElement.appendChild(this.#container);
|
||||
|
||||
this.#shadowContainer = this.#container.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
document.body.appendChild(this.#container);
|
||||
this.#shadowContainer = this.#container.attachShadow({ mode: "open" });
|
||||
const shadowRootElement = document.createElement("div");
|
||||
shadowRootElement.className = `${APP_CONSTS.boxID}_warpper notranslate`;
|
||||
this.#shadowContainer.appendChild(emotionRoot);
|
||||
shadowRootElement.className = `${APP_CONSTS.boxID}_wrapper notranslate`;
|
||||
this.#shadowContainer.appendChild(shadowRootElement);
|
||||
|
||||
const cache = createCache({
|
||||
key: APP_CONSTS.boxID,
|
||||
prepend: true,
|
||||
container: emotionRoot,
|
||||
container: this.#shadowContainer,
|
||||
});
|
||||
|
||||
this.#reactRoot = ReactDOM.createRoot(shadowRootElement);
|
||||
this.CacheProvider = ({ children }) => (
|
||||
<CacheProvider value={cache}>{children}</CacheProvider>
|
||||
this.#reactRoot.render(
|
||||
<React.StrictMode>
|
||||
<CacheProvider value={cache}>
|
||||
<Slection {...this.#props} />
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
const AppProvider = this.CacheProvider;
|
||||
this.#reactRoot.render(
|
||||
<React.StrictMode>
|
||||
<AppProvider>
|
||||
<Slection {...this.#props} />
|
||||
</AppProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
disable() {
|
||||
@@ -72,7 +62,6 @@ export class TransboxManager {
|
||||
this.#container = null;
|
||||
this.#reactRoot = null;
|
||||
this.#shadowContainer = null;
|
||||
this.CacheProvider = null;
|
||||
}
|
||||
|
||||
toggle() {
|
||||
|
||||
@@ -2,8 +2,6 @@ import {
|
||||
APP_UPNAME,
|
||||
APP_LCNAME,
|
||||
APP_CONSTS,
|
||||
MSG_INJECT_JS,
|
||||
MSG_INJECT_CSS,
|
||||
OPT_STYLE_FUZZY,
|
||||
GLOBLA_RULE,
|
||||
DEFAULT_SETTING,
|
||||
@@ -15,15 +13,12 @@ import {
|
||||
OPT_SPLIT_PARAGRAPH_PUNCTUATION,
|
||||
OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||
OPT_SPLIT_PARAGRAPH_TEXTLENGTH,
|
||||
MSG_INJECT_CSS,
|
||||
} from "../config";
|
||||
import interpreter from "./interpreter";
|
||||
import ShadowRootMonitor from "./shadowRootMonitor";
|
||||
import { interpreter } from "./interpreter";
|
||||
import { clearFetchPool } from "./pool";
|
||||
import { debounce, scheduleIdle, genEventName, truncateWords } from "./utils";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { sendBgMsg } from "./msg";
|
||||
import { isExt } from "./client";
|
||||
import { injectInlineJs, injectInternalCss } from "./injector";
|
||||
import { kissLog } from "./log";
|
||||
import { clearAllBatchQueue } from "./batchQueue";
|
||||
import { genTextClass } from "./style";
|
||||
@@ -31,6 +26,10 @@ import { createLoadingSVG } from "./svg";
|
||||
import { shortcutRegister } from "./shortcut";
|
||||
import { tryDetectLang } from "./detect";
|
||||
import { trustedTypesHelper } from "./trustedTypes";
|
||||
import { injectJs, INJECTOR } from "../injectors";
|
||||
import { injectInternalCss } from "./injector";
|
||||
import { isExt } from "./client";
|
||||
import { sendBgMsg } from "./msg";
|
||||
|
||||
/**
|
||||
* @class Translator
|
||||
@@ -77,7 +76,7 @@ export class Translator {
|
||||
"VIDEO",
|
||||
]),
|
||||
INLINE: new Set([
|
||||
"A",
|
||||
// "A",
|
||||
"ABBR",
|
||||
"ACRONYM",
|
||||
"B",
|
||||
@@ -106,7 +105,7 @@ export class Translator {
|
||||
"SCRIPT",
|
||||
"SELECT",
|
||||
"SMALL",
|
||||
"SPAN",
|
||||
// "SPAN",
|
||||
"STRONG",
|
||||
"SUB",
|
||||
"SUP",
|
||||
@@ -206,11 +205,17 @@ export class Translator {
|
||||
|
||||
// 14. 包含常见扩展名的文件名 (例如: document.pdf, image.jpeg)
|
||||
/^[^\s\\/:]+?\.[a-zA-Z0-9]{2,5}$/,
|
||||
|
||||
// todo: 数字和特殊字符组成的字符串
|
||||
];
|
||||
|
||||
static DEFAULT_OPTIONS = DEFAULT_SETTING; // 默认配置
|
||||
static DEFAULT_RULE = GLOBLA_RULE; // 默认规则
|
||||
|
||||
static isElement(el) {
|
||||
return el instanceof Element;
|
||||
}
|
||||
|
||||
static isElementOrFragment(el) {
|
||||
return el instanceof Element || el instanceof DocumentFragment;
|
||||
}
|
||||
@@ -221,6 +226,7 @@ export class Translator {
|
||||
|
||||
if (Translator.TAGS.INLINE.has(el.nodeName)) return false;
|
||||
if (Translator.TAGS.BLOCK.has(el.nodeName)) return true;
|
||||
if (el.attributes?.display?.value?.includes("inline")) return false;
|
||||
|
||||
if (Translator.displayCache.has(el)) {
|
||||
return Translator.displayCache.get(el);
|
||||
@@ -231,11 +237,22 @@ export class Translator {
|
||||
return isBlock;
|
||||
}
|
||||
|
||||
// 判断是否包含块级子元素
|
||||
static hasBlockNode(el) {
|
||||
if (!Translator.isElementOrFragment(el)) return false;
|
||||
for (const child of el.childNodes) {
|
||||
if (Translator.isBlockNode(child)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 判断是否直接包含非空文本节点
|
||||
static hasTextNode(el) {
|
||||
if (!Translator.isElementOrFragment(el)) return false;
|
||||
for (const node of el.childNodes) {
|
||||
if (node.nodeType === Node.TEXT_NODE && /\S/.test(node.nodeValue)) {
|
||||
for (const child of el.childNodes) {
|
||||
if (child.nodeType === Node.TEXT_NODE && /\S/.test(child.nodeValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -248,18 +265,23 @@ export class Translator {
|
||||
}
|
||||
|
||||
// 内置忽略元素
|
||||
static BUILTIN_IGNORE_SELECTOR = `abbr, address, area, audio, br, canvas, code,
|
||||
data, datalist, dfn, embed, head, iframe, img, input, kbd, noscript, map,
|
||||
object, option, output, param, picture, progress,
|
||||
samp, select, script, style, sub, sup, svg, track, time, textarea, template,
|
||||
var, video, wbr, .notranslate, [contenteditable], [translate='no'],
|
||||
${APP_LCNAME}, #${APP_CONSTS.fabID}, #${APP_CONSTS.boxID},
|
||||
.${APP_CONSTS.fabID}_warpper, .${APP_CONSTS.boxID}_warpper`;
|
||||
static KISS_IGNORE_SELECTOR = `${APP_LCNAME}, .kiss-caption-container, .kiss-subtitle-controls
|
||||
#${APP_CONSTS.fabID}, .${APP_CONSTS.fabID}_warpper,
|
||||
#${APP_CONSTS.boxID}, .${APP_CONSTS.boxID}_warpper,
|
||||
#${APP_CONSTS.popupID}, .${APP_CONSTS.popupID}_warpper`;
|
||||
|
||||
static BUILTIN_IGNORE_SELECTOR = `address, area, audio, br, canvas,
|
||||
data, datalist, embed, head, iframe, input, noscript, map,
|
||||
object, option, param, picture, progress,
|
||||
select, script, style, track, textarea, template,
|
||||
video, wbr, .notranslate, [contenteditable], [translate='no'],
|
||||
${Translator.KISS_IGNORE_SELECTOR}`;
|
||||
|
||||
#setting; // 设置选项
|
||||
#rule; // 规则
|
||||
#isInitialized = false; // 初始化状态
|
||||
#isJsInjected = false; // 注入用户JS
|
||||
#isShadowRootJsInjected = false; //
|
||||
#mouseHoverEnabled = false; // 鼠标悬停翻译
|
||||
#enabled = false; // 全局默认状态
|
||||
#runId = 0; // 用于中止过期的异步请求
|
||||
@@ -287,17 +309,23 @@ export class Translator {
|
||||
#hoveredNode = null; // 存储当前悬停的可翻译节点
|
||||
#boundMouseMoveHandler; // 鼠标事件
|
||||
#boundKeyDownHandler; // 键盘事件
|
||||
#windowMessageHandler = null;
|
||||
|
||||
#debouncedFindShadowRoot = null;
|
||||
|
||||
#io; // IntersectionObserver
|
||||
#mo; // MutationObserver
|
||||
#dmm; // DebounceMouseMover
|
||||
#srm; // ShadowRootMonitor
|
||||
|
||||
#rescanQueue = new Set(); // “脏容器”队列
|
||||
#isQueueProcessing = false; // 队列处理状态标志
|
||||
|
||||
// 忽略元素
|
||||
get #ignoreSelector() {
|
||||
if (this.#rule.autoScan === "false") {
|
||||
return `${Translator.KISS_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
|
||||
}
|
||||
|
||||
return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
|
||||
}
|
||||
|
||||
@@ -333,7 +361,7 @@ export class Translator {
|
||||
|
||||
this.#eventName = genEventName();
|
||||
this.#docInfo = {
|
||||
title: document.title,
|
||||
title: truncateWords(document.title),
|
||||
description: this.#getDocDescription(),
|
||||
};
|
||||
this.#combinedSkipsRegex = new RegExp(
|
||||
@@ -350,12 +378,12 @@ export class Translator {
|
||||
this.#io = this.#createIntersectionObserver();
|
||||
this.#mo = this.#createMutationObserver();
|
||||
this.#dmm = this.#createDebounceMouseMover();
|
||||
this.#srm = this.#createShadowRootMonitor();
|
||||
|
||||
// 监控shadowroot
|
||||
if (this.#rule.hasShadowroot === "true") {
|
||||
this.#srm.start();
|
||||
}
|
||||
this.#windowMessageHandler = this.#handleWindowMessage.bind(this);
|
||||
this.#debouncedFindShadowRoot = debounce(
|
||||
this.#findAndObserveShadowRoot.bind(this),
|
||||
300
|
||||
);
|
||||
|
||||
// 鼠标悬停翻译
|
||||
if (this.#setting.mouseHoverSetting.useMouseHover) {
|
||||
@@ -392,15 +420,41 @@ export class Translator {
|
||||
this.#startObserveRoot(root);
|
||||
});
|
||||
|
||||
// 查找现有的所有shadowroot
|
||||
if (this.#rule.hasShadowroot === "true") {
|
||||
try {
|
||||
this.#findAllShadowRoots().forEach((shadowRoot) => {
|
||||
this.#startObserveShadowRoot(shadowRoot);
|
||||
});
|
||||
} catch (err) {
|
||||
kissLog("findAllShadowRoots", err);
|
||||
}
|
||||
this.#attachShadowRootListener();
|
||||
this.#findAndObserveShadowRoot();
|
||||
}
|
||||
}
|
||||
|
||||
#handleWindowMessage(event) {
|
||||
if (event.data?.type === "KISS_SHADOW_ROOT_CREATED") {
|
||||
this.#debouncedFindShadowRoot();
|
||||
}
|
||||
}
|
||||
|
||||
#attachShadowRootListener() {
|
||||
if (!this.#isShadowRootJsInjected) {
|
||||
const id = "kiss-translator-inject-shadowroot-js";
|
||||
injectJs(INJECTOR.shadowroot, id);
|
||||
|
||||
this.#isShadowRootJsInjected = true;
|
||||
}
|
||||
|
||||
window.addEventListener("message", this.#windowMessageHandler);
|
||||
}
|
||||
|
||||
#removeShadowRootListener() {
|
||||
window.removeEventListener("message", this.#windowMessageHandler);
|
||||
}
|
||||
|
||||
// 查找现有的所有shadowroot
|
||||
#findAndObserveShadowRoot() {
|
||||
try {
|
||||
this.#findAllShadowRoots().forEach((shadowRoot) => {
|
||||
this.#startObserveShadowRoot(shadowRoot);
|
||||
});
|
||||
} catch (err) {
|
||||
kissLog("findAllShadowRoots", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,7 +470,7 @@ export class Translator {
|
||||
|
||||
// 创建样式
|
||||
#createTextStyles() {
|
||||
const [textClass, textStyles] = genTextClass({ ...this.#rule });
|
||||
const [textClass, textStyles] = genTextClass(this.#setting.customStyles);
|
||||
const textSheet = new CSSStyleSheet();
|
||||
textSheet.replaceSync(textStyles);
|
||||
this.#textClass = textClass;
|
||||
@@ -502,11 +556,13 @@ export class Translator {
|
||||
|
||||
// 监控翻译单元的可见性
|
||||
#createIntersectionObserver() {
|
||||
const { transInterval, rootMargin = 500 } = this.#setting;
|
||||
|
||||
const pending = new Set();
|
||||
const flush = debounce(() => {
|
||||
pending.forEach((node) => this.#performSyncNode(node));
|
||||
pending.clear();
|
||||
}, this.#setting.transInterval);
|
||||
}, transInterval);
|
||||
|
||||
return new IntersectionObserver(
|
||||
(entries) => {
|
||||
@@ -520,7 +576,7 @@ export class Translator {
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.01 }
|
||||
{ threshold: 0.01, rootMargin: `${rootMargin}px 0px ${rootMargin}px 0px` }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -528,33 +584,35 @@ export class Translator {
|
||||
#createMutationObserver() {
|
||||
return new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (this.#skipMoNodes.has(mutation.target)) return;
|
||||
|
||||
if (
|
||||
mutation.type === "characterData" &&
|
||||
mutation.oldValue !== mutation.target.nodeValue
|
||||
this.#skipMoNodes.has(mutation.target) ||
|
||||
mutation.nextSibling?.tagName === this.#translationTagName
|
||||
) {
|
||||
this.#queueForRescan(mutation.target.parentElement);
|
||||
} else if (mutation.type === "childList") {
|
||||
if (mutation.nextSibling?.tagName === this.#translationTagName) {
|
||||
// 恢复原文时插入元素,忽略
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mutation.type === "characterData") {
|
||||
if (
|
||||
mutation.oldValue !== mutation.target.nodeValue &&
|
||||
!this.#combinedSkipsRegex.test(mutation.target.nodeValue)
|
||||
) {
|
||||
this.#queueForRescan(mutation.target.parentElement);
|
||||
}
|
||||
} else if (mutation.type === "childList") {
|
||||
let nodes = new Set();
|
||||
let hasText = false;
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (this.#skipMoNodes.has(node)) return;
|
||||
if (
|
||||
this.#skipMoNodes.has(node) ||
|
||||
node.nodeName === this.#translationTagName
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (/\S/.test(node.nodeValue)) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
hasText = true;
|
||||
} else if (
|
||||
Translator.isElementOrFragment(node) &&
|
||||
node.nodeName !== this.#translationTagName
|
||||
) {
|
||||
nodes.add(node);
|
||||
}
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
hasText = true;
|
||||
} else if (Translator.isElementOrFragment(node)) {
|
||||
nodes.add(node);
|
||||
}
|
||||
});
|
||||
if (hasText) {
|
||||
@@ -591,13 +649,6 @@ export class Translator {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 创建shadowroot的回调
|
||||
#createShadowRootMonitor() {
|
||||
return new ShadowRootMonitor((shadowRoot) => {
|
||||
this.#startObserveShadowRoot(shadowRoot);
|
||||
});
|
||||
}
|
||||
|
||||
// 跟踪鼠标下的可翻译节点
|
||||
#handleMouseMove(event) {
|
||||
let targetNode = event.composedPath()[0];
|
||||
@@ -727,6 +778,9 @@ export class Translator {
|
||||
|
||||
// 开始/重新监控节点
|
||||
#startObserveNode(node) {
|
||||
// todo: DocumentFragment 无法被 this.#io.observe
|
||||
if (!Translator.isElement(node)) return;
|
||||
|
||||
if (this.#rule.highlightWords === OPT_HIGHLIGHT_WORDS_BEFORETRANS) {
|
||||
this.#highlightWordsDeeply(node);
|
||||
}
|
||||
@@ -772,6 +826,7 @@ export class Translator {
|
||||
#scanNode(rootNode) {
|
||||
if (
|
||||
!Translator.isElementOrFragment(rootNode) ||
|
||||
// rootNode.matches?.(this.#rule.keepSelector) ||
|
||||
rootNode.matches?.(this.#ignoreSelector)
|
||||
) {
|
||||
return;
|
||||
@@ -783,13 +838,24 @@ export class Translator {
|
||||
}
|
||||
|
||||
const hasText = Translator.hasTextNode(rootNode);
|
||||
if (hasText) {
|
||||
|
||||
if (!hasText && rootNode.children.length === 1) {
|
||||
this.#scanNode(rootNode.children[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasBlock = Translator.hasBlockNode(rootNode);
|
||||
|
||||
if (hasText || !hasBlock) {
|
||||
this.#startObserveNode(rootNode);
|
||||
}
|
||||
|
||||
for (const child of rootNode.children) {
|
||||
if (!hasText || Translator.isBlockNode(child)) {
|
||||
this.#scanNode(child);
|
||||
if (hasBlock) {
|
||||
for (const child of rootNode.children) {
|
||||
const isBlock = Translator.isBlockNode(child);
|
||||
if (!hasText || isBlock) {
|
||||
this.#scanNode(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1028,6 +1094,7 @@ export class Translator {
|
||||
|
||||
if (
|
||||
Translator.TAGS.BREAK_LINE.has(node.nodeName) ||
|
||||
node.matches?.(this.#ignoreSelector) ||
|
||||
node.nodeName === this.#translationTagName
|
||||
) {
|
||||
return true;
|
||||
@@ -1087,10 +1154,10 @@ export class Translator {
|
||||
const {
|
||||
transTag,
|
||||
textStyle,
|
||||
transStartHook,
|
||||
transEndHook,
|
||||
transOnly,
|
||||
termsStyle,
|
||||
textExtStyle,
|
||||
selectStyle,
|
||||
parentStyle,
|
||||
grandStyle,
|
||||
@@ -1106,20 +1173,6 @@ export class Translator {
|
||||
const parentNode = hostNode.parentElement;
|
||||
const hideOrigin = transOnly === "true";
|
||||
|
||||
// 翻译开始钩子函数
|
||||
if (transStartHook?.trim()) {
|
||||
try {
|
||||
interpreter.run(`exports.transStartHook = ${transStartHook}`);
|
||||
interpreter.exports.transStartHook({
|
||||
hostNode,
|
||||
parentNode,
|
||||
nodes,
|
||||
});
|
||||
} catch (err) {
|
||||
kissLog("transStartHook", err);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const [processedString, placeholderMap] = this.#serializeForTranslation(
|
||||
nodes,
|
||||
@@ -1138,16 +1191,17 @@ export class Translator {
|
||||
}
|
||||
|
||||
const inner = document.createElement(transTag);
|
||||
inner.className = `${Translator.KISS_CLASS.inner} ${this.#textClass[textStyle]}`;
|
||||
inner.className = `${Translator.KISS_CLASS.inner} ${this.#textClass[textStyle] || ""}`;
|
||||
if (textExtStyle?.trim()) {
|
||||
inner.style.cssText = textExtStyle; // 附加内联样式
|
||||
}
|
||||
inner.appendChild(createLoadingSVG());
|
||||
wrapper.appendChild(inner);
|
||||
nodes[nodes.length - 1].after(wrapper);
|
||||
|
||||
const currentRunId = this.#runId;
|
||||
const [translatedText, isSameLang] = await this.#translateFetch(
|
||||
processedString,
|
||||
deLang
|
||||
);
|
||||
const { trText: translatedText, isSame: isSameLang } =
|
||||
await this.#translateFetch(processedString, deLang);
|
||||
if (this.#runId !== currentRunId) {
|
||||
throw new Error("Request terminated");
|
||||
}
|
||||
@@ -1240,10 +1294,7 @@ export class Translator {
|
||||
}
|
||||
|
||||
// 文本节点
|
||||
if (
|
||||
this.#rule.hasRichText === "false" ||
|
||||
node.nodeType === Node.TEXT_NODE
|
||||
) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
let text = node.textContent;
|
||||
|
||||
// 专业术语替换
|
||||
@@ -1269,11 +1320,16 @@ 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") {
|
||||
if (
|
||||
node.tagName?.toUpperCase() === "IMG" ||
|
||||
node.tagName?.toUpperCase() === "SVG"
|
||||
) {
|
||||
node.style.width = `${node.offsetWidth}px`;
|
||||
node.style.height = `${node.offsetHeight}px`;
|
||||
}
|
||||
@@ -1285,7 +1341,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?.toUpperCase())
|
||||
) {
|
||||
wrapCounter++;
|
||||
const startPlaceholder = `<${this.#placeholder.tagName}${wrapCounter}>`;
|
||||
const endPlaceholder = `</${this.#placeholder.tagName}${wrapCounter}>`;
|
||||
@@ -1331,16 +1390,39 @@ export class Translator {
|
||||
|
||||
// 发起翻译请求
|
||||
#translateFetch(text, deLang = "") {
|
||||
const { fromLang, toLang } = this.#rule;
|
||||
const { toLang, transStartHook } = this.#rule;
|
||||
const fromLang = deLang || this.#rule.fromLang;
|
||||
const apiSetting = { ...this.#apiSetting };
|
||||
const docInfo = { ...this.#docInfo };
|
||||
const glossary = { ...this.#glossary };
|
||||
const apisMap = this.#apisMap;
|
||||
|
||||
return apiTranslate({
|
||||
const args = {
|
||||
text,
|
||||
fromLang: deLang || fromLang,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting: this.#apiSetting,
|
||||
docInfo: this.#docInfo,
|
||||
glossary: this.#glossary,
|
||||
});
|
||||
apiSetting,
|
||||
docInfo,
|
||||
glossary,
|
||||
};
|
||||
|
||||
// 翻译开始钩子函数
|
||||
if (transStartHook?.trim()) {
|
||||
try {
|
||||
interpreter.run(`exports.transStartHook = ${transStartHook}`);
|
||||
const hookResult = interpreter.exports.transStartHook({
|
||||
...args,
|
||||
apisMap,
|
||||
});
|
||||
if (hookResult) {
|
||||
Object.assign(args, hookResult);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("transStartHook", err);
|
||||
}
|
||||
}
|
||||
|
||||
return apiTranslate(args);
|
||||
}
|
||||
|
||||
// 查找指定节点下所有译文节点
|
||||
@@ -1489,6 +1571,8 @@ export class Translator {
|
||||
|
||||
// 停止监听,重置参数
|
||||
#resetOptions() {
|
||||
this.#removeShadowRootListener();
|
||||
|
||||
this.#io.disconnect();
|
||||
this.#mo.disconnect();
|
||||
this.#viewNodes.clear();
|
||||
@@ -1534,15 +1618,43 @@ export class Translator {
|
||||
this.#isJsInjected = true;
|
||||
|
||||
try {
|
||||
const { injectJs, injectCss } = this.#rule;
|
||||
// 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, toLang } = 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1587,13 +1699,13 @@ export class Translator {
|
||||
// 翻译页面标题
|
||||
async #translateTitle() {
|
||||
const title = document.title;
|
||||
this.#docInfo.title = title;
|
||||
this.#docInfo.title = truncateWords(title);
|
||||
if (!title) return;
|
||||
|
||||
try {
|
||||
const deLang = await tryDetectLang(title);
|
||||
const [translatedTitle] = await this.#translateFetch(title, deLang);
|
||||
document.title = translatedTitle || title;
|
||||
const { trText } = await this.#translateFetch(title, deLang);
|
||||
document.title = trText || title;
|
||||
} catch (err) {
|
||||
kissLog("tanslate title", err);
|
||||
}
|
||||
@@ -1659,7 +1771,6 @@ export class Translator {
|
||||
stop() {
|
||||
this.disable();
|
||||
this.#resetOptions();
|
||||
this.#srm.stop();
|
||||
this.#disableMouseHover();
|
||||
this.#removeInjector();
|
||||
this.#isInitialized = false;
|
||||
|
||||
@@ -28,7 +28,7 @@ import { logger } from "./log";
|
||||
export default class TranslatorManager {
|
||||
#clearShortcuts = [];
|
||||
#menuCommandIds = [];
|
||||
#clearTouchListener = null;
|
||||
#clearTouchListeners = [];
|
||||
#isActive = false;
|
||||
#isUserscript;
|
||||
#isIframe;
|
||||
@@ -54,15 +54,15 @@ export default class TranslatorManager {
|
||||
isIframe,
|
||||
});
|
||||
|
||||
this._transboxManager = new TransboxManager(setting);
|
||||
|
||||
if (!isIframe) {
|
||||
this._transboxManager = new TransboxManager(setting);
|
||||
this._inputTranslator = new InputTranslator(setting);
|
||||
this._popupManager = new PopupManager({
|
||||
translator: this._translator,
|
||||
processActions: this.#processActions.bind(this),
|
||||
});
|
||||
this._fabManager = new FabManager({
|
||||
translator: this._translator,
|
||||
processActions: this.#processActions.bind(this),
|
||||
fabConfig,
|
||||
});
|
||||
@@ -110,10 +110,8 @@ export default class TranslatorManager {
|
||||
this.#clearShortcuts = [];
|
||||
|
||||
// 触屏
|
||||
if (this.#clearTouchListener) {
|
||||
this.#clearTouchListener();
|
||||
this.#clearTouchListener = null;
|
||||
}
|
||||
this.#clearTouchListeners.forEach((clear) => clear());
|
||||
this.#clearTouchListeners = [];
|
||||
|
||||
// 油猴菜单
|
||||
if (globalThis.GM && this.#menuCommandIds.length > 0) {
|
||||
@@ -139,14 +137,17 @@ export default class TranslatorManager {
|
||||
window.addEventListener("message", this.#windowMessageHandler);
|
||||
} else {
|
||||
browser.runtime.onMessage.addListener(this.#browserMessageHandler);
|
||||
if (this.#isIframe) {
|
||||
window.addEventListener("message", this.#windowMessageHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#setupTouchOperations() {
|
||||
if (this.#isIframe) return;
|
||||
|
||||
const { touchTranslate = 2 } = this._translator.setting;
|
||||
if (touchTranslate === 0) {
|
||||
const { touchModes = [2] } = this._translator.setting;
|
||||
if (touchModes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,35 +155,31 @@ export default class TranslatorManager {
|
||||
this.#processActions({ action: MSG_TRANS_TOGGLE });
|
||||
};
|
||||
|
||||
switch (touchTranslate) {
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
this.#clearTouchListener = touchTapListener(handleTap, {
|
||||
taps: 1,
|
||||
fingers: touchTranslate,
|
||||
});
|
||||
break;
|
||||
case 5:
|
||||
this.#clearTouchListener = touchTapListener(handleTap, {
|
||||
taps: 2,
|
||||
fingers: 1,
|
||||
});
|
||||
break;
|
||||
case 6:
|
||||
this.#clearTouchListener = touchTapListener(handleTap, {
|
||||
taps: 3,
|
||||
fingers: 1,
|
||||
});
|
||||
break;
|
||||
case 7:
|
||||
this.#clearTouchListener = touchTapListener(handleTap, {
|
||||
taps: 2,
|
||||
fingers: 2,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
const handleListener = (mode) => {
|
||||
let options = null;
|
||||
switch (mode) {
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
options = { taps: 1, fingers: mode };
|
||||
break;
|
||||
case 5:
|
||||
options = { taps: 2, fingers: 1 };
|
||||
break;
|
||||
case 6:
|
||||
options = { taps: 3, fingers: 1 };
|
||||
break;
|
||||
case 7:
|
||||
options = { taps: 2, fingers: 2 };
|
||||
break;
|
||||
default:
|
||||
}
|
||||
if (options) {
|
||||
this.#clearTouchListeners.push(touchTapListener(handleTap, options));
|
||||
}
|
||||
};
|
||||
|
||||
touchModes.forEach((mode) => handleListener(mode));
|
||||
}
|
||||
|
||||
#handleWindowMessage(event) {
|
||||
@@ -190,7 +187,7 @@ export default class TranslatorManager {
|
||||
}
|
||||
|
||||
#handleBrowserMessage(message, sender, sendResponse) {
|
||||
const result = this.#processActions(message);
|
||||
const result = this.#processActions(message, true);
|
||||
const response = result || {
|
||||
rule: this._translator.rule,
|
||||
setting: this._translator.setting,
|
||||
@@ -248,9 +245,9 @@ export default class TranslatorManager {
|
||||
];
|
||||
}
|
||||
|
||||
#processActions({ action, args } = {}) {
|
||||
if (this.#isUserscript) {
|
||||
sendIframeMsg(action);
|
||||
#processActions({ action, args } = {}, fromExt = false) {
|
||||
if (!fromExt) {
|
||||
sendIframeMsg(action, args);
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
|
||||
@@ -71,29 +71,65 @@ export const debounce = (func, delay = 200) => {
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {*} func
|
||||
* @param {*} delay
|
||||
* @returns
|
||||
* @param {Function} func 要执行的函数
|
||||
* @param {number} delay 延迟时间
|
||||
* @param {object} options 选项 { leading: boolean, trailing: boolean }
|
||||
* @returns {Function}
|
||||
*/
|
||||
export const throttle = (func, delay = 200) => {
|
||||
let timer = null;
|
||||
let cache = null;
|
||||
return (...args) => {
|
||||
if (!timer) {
|
||||
func(...args);
|
||||
cache = null;
|
||||
timer = setTimeout(() => {
|
||||
if (cache) {
|
||||
func(...cache);
|
||||
cache = null;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}, delay);
|
||||
} else {
|
||||
cache = args;
|
||||
export const throttle = (
|
||||
func,
|
||||
delay,
|
||||
options = { leading: true, trailing: true }
|
||||
) => {
|
||||
let timeoutId = null;
|
||||
let lastArgs = null;
|
||||
let lastThis = null;
|
||||
let result;
|
||||
let previous = 0;
|
||||
|
||||
function later() {
|
||||
previous = options.leading === false ? 0 : Date.now();
|
||||
timeoutId = null;
|
||||
result = func.apply(lastThis, lastArgs);
|
||||
if (!timeoutId) {
|
||||
lastThis = lastArgs = null;
|
||||
}
|
||||
}
|
||||
|
||||
const throttled = function (...args) {
|
||||
const now = Date.now();
|
||||
if (!previous && options.leading === false) {
|
||||
previous = now;
|
||||
}
|
||||
|
||||
const remaining = delay - (now - previous);
|
||||
lastArgs = args;
|
||||
lastThis = this;
|
||||
|
||||
if (remaining <= 0 || remaining > delay) {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
previous = now;
|
||||
result = func.apply(lastThis, lastArgs);
|
||||
if (!timeoutId) {
|
||||
lastThis = lastArgs = null;
|
||||
}
|
||||
} else if (!timeoutId && options.trailing !== false) {
|
||||
timeoutId = setTimeout(later, remaining);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
throttled.cancel = () => {
|
||||
clearTimeout(timeoutId);
|
||||
previous = 0;
|
||||
timeoutId = null;
|
||||
lastThis = lastArgs = null;
|
||||
};
|
||||
|
||||
return throttled;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -373,3 +409,76 @@ export const randomBetween = (min, max, integer = true) => {
|
||||
const value = Math.random() * (max - min) + min;
|
||||
return integer ? Math.floor(value) : value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据文件名自动获取 MIME 类型
|
||||
* @param {*} filename
|
||||
* @returns
|
||||
*/
|
||||
function getMimeTypeFromFilename(filename) {
|
||||
const defaultType = "application/octet-stream";
|
||||
if (!filename || filename.indexOf(".") === -1) {
|
||||
return defaultType;
|
||||
}
|
||||
|
||||
const extension = filename.split(".").pop().toLowerCase();
|
||||
const mimeMap = {
|
||||
// 文本
|
||||
txt: "text/plain;charset=utf-8",
|
||||
html: "text/html;charset=utf-8",
|
||||
css: "text/css;charset=utf-8",
|
||||
js: "text/javascript;charset=utf-8",
|
||||
json: "application/json;charset=utf-8",
|
||||
xml: "application/xml;charset=utf-8",
|
||||
md: "text/markdown;charset=utf-8",
|
||||
vtt: "text/vtt;charset=utf-8",
|
||||
|
||||
// 图像
|
||||
png: "image/png",
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
gif: "image/gif",
|
||||
svg: "image/svg+xml",
|
||||
webp: "image/webp",
|
||||
ico: "image/x-icon",
|
||||
|
||||
// 音频/视频
|
||||
mp3: "audio/mpeg",
|
||||
mp4: "video/mp4",
|
||||
webm: "video/webm",
|
||||
wav: "audio/wav",
|
||||
|
||||
// 应用程序/文档
|
||||
pdf: "application/pdf",
|
||||
zip: "application/zip",
|
||||
doc: "application/msword",
|
||||
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
xls: "application/vnd.ms-excel",
|
||||
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
};
|
||||
|
||||
// 默认值
|
||||
return mimeMap[extension] || defaultType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param {*} str
|
||||
* @param {*} filename
|
||||
*/
|
||||
export function downloadBlobFile(str, filename = "kiss-file.txt") {
|
||||
const mimeType = getMimeTypeFromFilename(filename);
|
||||
const blob = new Blob([str], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
a.href = url;
|
||||
a.download = filename || `kiss-file.txt`;
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
@@ -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 { truncateWords, throttle } from "../libs/utils.js";
|
||||
import { apiTranslate } from "../apis/index.js";
|
||||
|
||||
/**
|
||||
* @class BilingualSubtitleManager
|
||||
@@ -8,29 +9,33 @@ import { truncateWords } from "../libs/utils.js";
|
||||
export class BilingualSubtitleManager {
|
||||
#videoEl;
|
||||
#formattedSubtitles = [];
|
||||
#translationService;
|
||||
#captionWindowEl = null;
|
||||
#paperEl = null;
|
||||
#currentSubtitleIndex = -1;
|
||||
#preTranslateSeconds = 100;
|
||||
// #preTranslateSeconds = 90;
|
||||
// #throttleSeconds = 30;
|
||||
#setting = {};
|
||||
#isAdPlaying = false;
|
||||
#throttledTriggerTranslations;
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
this.#throttledTriggerTranslations = throttle(
|
||||
this.#triggerTranslations.bind(this),
|
||||
(setting.throttleTrans ?? 30) * 1000
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,6 +59,7 @@ export class BilingualSubtitleManager {
|
||||
destroy() {
|
||||
logger.info("Bilingual Subtitle Manager: Destroying...");
|
||||
this.#removeEventListeners();
|
||||
this.#throttledTriggerTranslations?.cancel();
|
||||
this.#captionWindowEl?.parentElement?.parentElement?.remove();
|
||||
this.#formattedSubtitles = [];
|
||||
}
|
||||
@@ -128,15 +134,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 +149,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 +178,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 +197,10 @@ export class BilingualSubtitleManager {
|
||||
}, 50);
|
||||
};
|
||||
|
||||
handleElement.addEventListener("mousedown", onMouseDown);
|
||||
handleElement.addEventListener("mousedown", onDragStart);
|
||||
handleElement.addEventListener("touchstart", onDragStart, {
|
||||
passive: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,7 +233,7 @@ export class BilingualSubtitleManager {
|
||||
this.#updateCaptionDisplay(subtitle);
|
||||
}
|
||||
|
||||
this.#triggerTranslations(currentTimeMs);
|
||||
this.#throttledTriggerTranslations(currentTimeMs);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,6 +241,7 @@ export class BilingualSubtitleManager {
|
||||
*/
|
||||
onSeek() {
|
||||
this.#currentSubtitleIndex = -1;
|
||||
this.#throttledTriggerTranslations.cancel();
|
||||
this.onTimeUpdate();
|
||||
}
|
||||
|
||||
@@ -278,7 +294,8 @@ export class BilingualSubtitleManager {
|
||||
* @param {number} currentTimeMs
|
||||
*/
|
||||
#triggerTranslations(currentTimeMs) {
|
||||
const lookAheadMs = this.#preTranslateSeconds * 1000;
|
||||
const { preTrans = 90 } = this.#setting;
|
||||
const lookAheadMs = preTrans * 1000;
|
||||
|
||||
for (const sub of this.#formattedSubtitles) {
|
||||
const isCurrent = sub.start <= currentTimeMs && sub.end >= currentTimeMs;
|
||||
@@ -300,13 +317,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]";
|
||||
@@ -340,4 +357,8 @@ export class BilingualSubtitleManager {
|
||||
this.#currentSubtitleIndex = -1;
|
||||
this.onTimeUpdate();
|
||||
}
|
||||
|
||||
updateSetting(obj) {
|
||||
this.#setting = { ...this.#setting, ...obj };
|
||||
}
|
||||
}
|
||||
|
||||
173
src/subtitle/Menus.js
Normal file
173
src/subtitle/Menus.js
Normal file
@@ -0,0 +1,173 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { MSG_MENUS_PROGRESSED, MSG_MENUS_UPDATEFORM } from "../config";
|
||||
|
||||
function Label({ children }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuItem({ children, onClick, disabled = false }) {
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "0px 8px",
|
||||
opacity: hover ? 1 : 0.8,
|
||||
background: `rgba(255, 255, 255, ${hover ? 0.1 : 0})`,
|
||||
cursor: disabled ? "default" : "pointer",
|
||||
transition: "background 0.2s, opacity 0.2s",
|
||||
borderRadius: 5,
|
||||
}}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Switch({ label, name, value, onChange, disabled }) {
|
||||
const handleClick = useCallback(() => {
|
||||
if (disabled) return;
|
||||
|
||||
onChange({ name, value: !value });
|
||||
}, [disabled, onChange, name, value]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={handleClick} disabled={disabled}>
|
||||
<Label>{label}</Label>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
background: value ? "rgba(32,156,238,.8)" : "rgba(255,255,255,.3)",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
position: "absolute",
|
||||
left: 2,
|
||||
top: 2,
|
||||
background: "rgba(255,255,255,.9)",
|
||||
transform: `translateX(${value ? 16 : 0}px)`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function Button({ label, onClick, disabled }) {
|
||||
const handleClick = useCallback(() => {
|
||||
if (disabled) return;
|
||||
|
||||
onClick();
|
||||
}, [disabled, onClick]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={handleClick} disabled={disabled}>
|
||||
<Label>{label}</Label>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function Menus({
|
||||
i18n,
|
||||
initData,
|
||||
updateSetting,
|
||||
downloadSubtitle,
|
||||
hasSegApi,
|
||||
eventName,
|
||||
}) {
|
||||
const [formData, setFormData] = useState(initData);
|
||||
const [progressed, setProgressed] = useState(0);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ name, value }) => {
|
||||
setFormData((pre) => ({ ...pre, [name]: value }));
|
||||
updateSetting({ name, value });
|
||||
},
|
||||
[updateSetting]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e) => {
|
||||
const { action, data } = e.detail || {};
|
||||
if (action === MSG_MENUS_PROGRESSED) {
|
||||
setProgressed(data);
|
||||
} else if (action === MSG_MENUS_UPDATEFORM) {
|
||||
setFormData((pre) => ({ ...pre, ...data }));
|
||||
}
|
||||
};
|
||||
window.addEventListener(eventName, handler);
|
||||
return () => window.removeEventListener(eventName, handler);
|
||||
}, [eventName]);
|
||||
|
||||
const status = useMemo(() => {
|
||||
if (progressed === 0) return i18n("waiting_subtitles");
|
||||
if (progressed === 100) return i18n("download_subtitles");
|
||||
return i18n("processing_subtitles");
|
||||
}, [progressed, i18n]);
|
||||
|
||||
const { isAISegment, skipAd, isBilingual } = formData;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
bottom: 100,
|
||||
background: "rgba(0,0,0,.6)",
|
||||
width: 200,
|
||||
lineHeight: "40px",
|
||||
fontSize: 16,
|
||||
padding: 8,
|
||||
borderRadius: 5,
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
onChange={handleChange}
|
||||
name="isAISegment"
|
||||
value={isAISegment}
|
||||
label={i18n("ai_segmentation")}
|
||||
disabled={!hasSegApi}
|
||||
/>
|
||||
<Switch
|
||||
onChange={handleChange}
|
||||
name="isBilingual"
|
||||
value={isBilingual}
|
||||
label={i18n("is_bilingual_view")}
|
||||
/>
|
||||
<Switch
|
||||
onChange={handleChange}
|
||||
name="skipAd"
|
||||
value={skipAd}
|
||||
label={i18n("is_skip_ad")}
|
||||
/>
|
||||
<Button
|
||||
label={`${status} [${progressed}%] `}
|
||||
onClick={downloadSubtitle}
|
||||
disabled={progressed !== 100}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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,39 +1,68 @@
|
||||
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,
|
||||
APP_NAME,
|
||||
OPT_LANGS_TO_CODE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
MSG_MENUS_PROGRESSED,
|
||||
MSG_MENUS_UPDATEFORM,
|
||||
} from "../config";
|
||||
import { sleep } from "../libs/utils.js";
|
||||
import { sleep, genEventName, downloadBlobFile } from "../libs/utils.js";
|
||||
import { createLogoSVG } from "../libs/svg.js";
|
||||
import { randomBetween } from "../libs/utils.js";
|
||||
import { newI18n } from "../config";
|
||||
import ShadowDomManager from "../libs/shadowDomManager.js";
|
||||
import { Menus } from "./Menus.js";
|
||||
import { buildBilingualVtt } from "./vtt.js";
|
||||
|
||||
const VIDEO_SELECT = "#container video";
|
||||
const CONTORLS_SELECT = ".ytp-right-controls";
|
||||
const YT_CAPTION_SELECT = "#ytp-caption-window-container";
|
||||
const YT_AD_SELECT = ".video-ads";
|
||||
const YT_SUBTITLE_BTN_SELECT = "button.ytp-subtitles-button";
|
||||
|
||||
class YouTubeCaptionProvider {
|
||||
#setting = {};
|
||||
#videoId = "";
|
||||
|
||||
#subtitles = [];
|
||||
#flatEvents = [];
|
||||
#progressedNum = 0;
|
||||
#fromLang = "auto";
|
||||
|
||||
#processingId = null;
|
||||
|
||||
#managerInstance = null;
|
||||
#toggleButton = null;
|
||||
#enabled = false;
|
||||
#ytControls = null;
|
||||
#isBusy = false;
|
||||
#fromLang = "auto";
|
||||
#isMenuShow = false;
|
||||
#notificationEl = null;
|
||||
#notificationTimeout = null;
|
||||
#i18n = () => "";
|
||||
#menuEventName = "kiss-event";
|
||||
|
||||
constructor(setting = {}) {
|
||||
this.#setting = setting;
|
||||
this.#setting = { ...setting, isAISegment: false };
|
||||
this.#i18n = newI18n(setting.uiLang || "zh");
|
||||
this.#menuEventName = genEventName();
|
||||
}
|
||||
|
||||
get #videoId() {
|
||||
const docUrl = new URL(document.location.href);
|
||||
return docUrl.searchParams.get("v");
|
||||
}
|
||||
|
||||
get #videoEl() {
|
||||
return document.querySelector(VIDEO_SELECT);
|
||||
}
|
||||
|
||||
set #progressed(num) {
|
||||
this.#progressedNum = num;
|
||||
this.#sendMenusMsg({ action: MSG_MENUS_PROGRESSED, data: num });
|
||||
}
|
||||
|
||||
get #progressed() {
|
||||
return this.#progressedNum;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
@@ -47,33 +76,47 @@ class YouTubeCaptionProvider {
|
||||
});
|
||||
|
||||
window.addEventListener("yt-navigate-finish", () => {
|
||||
setTimeout(() => {
|
||||
if (this.#toggleButton) {
|
||||
this.#toggleButton.style.opacity = "0.5";
|
||||
}
|
||||
this.#destroyManager();
|
||||
this.#doubleClick();
|
||||
}, 1000);
|
||||
logger.debug("Youtube Provider: yt-navigate-finish", this.#videoId);
|
||||
|
||||
this.#destroyManager();
|
||||
|
||||
this.#subtitles = [];
|
||||
this.#flatEvents = [];
|
||||
this.#progressed = 0;
|
||||
this.#fromLang = "auto";
|
||||
this.#setting.isAISegment = false;
|
||||
this.#sendMenusMsg({
|
||||
action: MSG_MENUS_UPDATEFORM,
|
||||
data: { isAISegment: false },
|
||||
});
|
||||
});
|
||||
|
||||
this.#waitForElement(CONTORLS_SELECT, (ytControls) =>
|
||||
this.#injectToggleButton(ytControls)
|
||||
);
|
||||
this.#waitForElement(CONTORLS_SELECT, (ytControls) => {
|
||||
const ytSubtitleBtn = ytControls.querySelector(YT_SUBTITLE_BTN_SELECT);
|
||||
if (ytSubtitleBtn) {
|
||||
ytSubtitleBtn.addEventListener("click", () => {
|
||||
if (ytSubtitleBtn.getAttribute("aria-pressed") === "true") {
|
||||
this.#startManager();
|
||||
} else {
|
||||
this.#destroyManager();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.#injectToggleButton(ytControls);
|
||||
});
|
||||
|
||||
this.#waitForElement(YT_AD_SELECT, (adContainer) => {
|
||||
this.#moAds(adContainer);
|
||||
});
|
||||
}
|
||||
|
||||
get #videoEl() {
|
||||
return document.querySelector(VIDEO_SELECT);
|
||||
}
|
||||
|
||||
#moAds(adContainer) {
|
||||
const adLayoutSelector = ".ytp-ad-player-overlay-layout";
|
||||
const skipBtnSelector =
|
||||
".ytp-skip-ad-button, .ytp-ad-skip-button, .ytp-ad-skip-button-modern";
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
const { skipAd = false } = this.#setting;
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "childList") {
|
||||
const videoEl = this.#videoEl;
|
||||
@@ -83,22 +126,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 +151,7 @@ class YouTubeCaptionProvider {
|
||||
|
||||
if (node.matches(adLayoutSelector)) {
|
||||
logger.debug("Youtube Provider: Ad ends!");
|
||||
if (videoEl) {
|
||||
if (videoEl && skipAd) {
|
||||
videoEl.playbackRate = 1;
|
||||
}
|
||||
if (this.#managerInstance) {
|
||||
@@ -145,61 +190,93 @@ class YouTubeCaptionProvider {
|
||||
});
|
||||
}
|
||||
|
||||
async #doubleClick() {
|
||||
const button = this.#ytControls?.querySelector(
|
||||
"button.ytp-subtitles-button"
|
||||
);
|
||||
if (button) {
|
||||
await sleep(randomBetween(50, 100));
|
||||
button.click();
|
||||
await sleep(randomBetween(500, 1000));
|
||||
button.click();
|
||||
updateSetting({ name, value }) {
|
||||
if (this.#setting[name] === value) return;
|
||||
|
||||
logger.debug("Youtube Provider: update setting", name, value);
|
||||
this.#setting[name] = value;
|
||||
|
||||
if (name === "isBilingual") {
|
||||
this.#managerInstance?.updateSetting({ [name]: value });
|
||||
} else if (name === "isAISegment") {
|
||||
this.#reProcessEvents();
|
||||
}
|
||||
}
|
||||
|
||||
#injectToggleButton(ytControls) {
|
||||
this.#ytControls = ytControls;
|
||||
downloadSubtitle() {
|
||||
if (!this.#subtitles.length || this.#progressed !== 100) {
|
||||
logger.debug("Youtube Provider: The subtitle is not yet ready.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const vtt = buildBilingualVtt(this.#subtitles);
|
||||
downloadBlobFile(
|
||||
vtt,
|
||||
`kiss-subtitles-${this.#videoId}_${Date.now()}.vtt`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.info("Youtube Provider: download subtitles:", error);
|
||||
}
|
||||
}
|
||||
|
||||
#sendMenusMsg({ action, data }) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(this.#menuEventName, { detail: { action, data } })
|
||||
);
|
||||
}
|
||||
|
||||
#injectToggleButton(ytControls) {
|
||||
const kissControls = document.createElement("div");
|
||||
kissControls.className = "kiss-bilingual-subtitle-controls";
|
||||
kissControls.className = "notranslate kiss-subtitle-controls";
|
||||
Object.assign(kissControls.style, {
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
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",
|
||||
opacity: "0.5",
|
||||
});
|
||||
|
||||
toggleButton.appendChild(createLogoSVG());
|
||||
kissControls.appendChild(toggleButton);
|
||||
|
||||
toggleButton.onclick = () => {
|
||||
if (this.#isBusy) {
|
||||
logger.info(`Youtube Provider: It's budy now...`);
|
||||
this.#showNotification(this.#i18n("subtitle_data_processing"));
|
||||
}
|
||||
const { segApiSetting, isAISegment, skipAd, isBilingual } = this.#setting;
|
||||
const menu = new ShadowDomManager({
|
||||
id: "kiss-subtitle-menus",
|
||||
className: "notranslate",
|
||||
reactComponent: Menus,
|
||||
rootElement: kissControls,
|
||||
props: {
|
||||
i18n: this.#i18n,
|
||||
updateSetting: this.updateSetting.bind(this),
|
||||
downloadSubtitle: this.downloadSubtitle.bind(this),
|
||||
hasSegApi: !!segApiSetting,
|
||||
eventName: this.#menuEventName,
|
||||
initData: {
|
||||
isAISegment,
|
||||
skipAd,
|
||||
isBilingual,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!this.#enabled) {
|
||||
logger.info(`Youtube Provider: Feature toggled ON.`);
|
||||
this.#enabled = true;
|
||||
toggleButton.onclick = () => {
|
||||
if (!this.#isMenuShow) {
|
||||
this.#isMenuShow = true;
|
||||
this.#toggleButton?.replaceChildren(
|
||||
createLogoSVG({ isSelected: true })
|
||||
);
|
||||
this.#startManager();
|
||||
menu.show();
|
||||
} else {
|
||||
logger.info(`Youtube Provider: Feature toggled OFF.`);
|
||||
this.#enabled = false;
|
||||
this.#isMenuShow = false;
|
||||
this.#toggleButton?.replaceChildren(createLogoSVG());
|
||||
this.#destroyManager();
|
||||
menu.hide();
|
||||
}
|
||||
};
|
||||
this.#toggleButton = toggleButton;
|
||||
this.#ytControls?.prepend(kissControls);
|
||||
|
||||
ytControls?.prepend(kissControls);
|
||||
}
|
||||
|
||||
#isSameLang(lang1, lang2) {
|
||||
@@ -287,11 +364,6 @@ class YouTubeCaptionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
#getVideoId() {
|
||||
const docUrl = new URL(document.location.href);
|
||||
return docUrl.searchParams.get("v");
|
||||
}
|
||||
|
||||
async #aiSegment({ videoId, fromLang, toLang, chunkEvents, segApiSetting }) {
|
||||
try {
|
||||
const events = chunkEvents.filter((item) => item.text);
|
||||
@@ -323,36 +395,38 @@ class YouTubeCaptionProvider {
|
||||
}
|
||||
|
||||
async #handleInterceptedRequest(url, responseText) {
|
||||
if (this.#isBusy) {
|
||||
logger.info("Youtube Provider is busy...");
|
||||
const videoId = this.#videoId;
|
||||
if (!videoId) {
|
||||
logger.debug("Youtube Provider: videoId not found.");
|
||||
return;
|
||||
}
|
||||
this.#isBusy = true;
|
||||
|
||||
const potUrl = new URL(url);
|
||||
if (videoId !== potUrl.searchParams.get("v")) {
|
||||
logger.debug("Youtube Provider: skip other timedtext:", videoId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#flatEvents.length) {
|
||||
logger.debug("Youtube Provider: video was processed:", videoId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoId === this.#processingId) {
|
||||
logger.debug("Youtube Provider: video is processing:", videoId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#processingId = videoId;
|
||||
|
||||
try {
|
||||
const videoId = this.#getVideoId();
|
||||
if (!videoId) {
|
||||
logger.info("Youtube Provider: videoId not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoId === this.#videoId) {
|
||||
logger.info("Youtube Provider: videoId already processed.");
|
||||
return;
|
||||
}
|
||||
|
||||
const potUrl = new URL(url);
|
||||
if (videoId !== potUrl.searchParams.get("v")) {
|
||||
logger.info("Youtube Provider: skip other timedtext.");
|
||||
return;
|
||||
}
|
||||
|
||||
const { segApiSetting, toLang } = this.#setting;
|
||||
this.#showNotification(this.#i18n("starting_to_process_subtitle"));
|
||||
|
||||
const { toLang } = this.#setting;
|
||||
const captionTracks = await this.#getCaptionTracks(videoId);
|
||||
const captionTrack = this.#findCaptionTrack(captionTracks);
|
||||
if (!captionTrack) {
|
||||
logger.info("Youtube Provider: CaptionTrack not found.");
|
||||
logger.debug("Youtube Provider: CaptionTrack not found:", videoId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -363,7 +437,7 @@ class YouTubeCaptionProvider {
|
||||
responseText
|
||||
);
|
||||
if (!events?.length) {
|
||||
logger.info("Youtube Provider: SubtitleEvents not got.");
|
||||
logger.debug("Youtube Provider: events not got:", videoId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -377,108 +451,131 @@ class YouTubeCaptionProvider {
|
||||
`Youtube Provider: fromLang: ${fromLang}, toLang: ${toLang}`
|
||||
);
|
||||
if (this.#isSameLang(fromLang, toLang)) {
|
||||
logger.info("Youtube Provider: skip same lang", fromLang, toLang);
|
||||
logger.debug("Youtube Provider: skip same lang", fromLang, toLang);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#showNotification(this.#i18n("starting_to_process_subtitle"));
|
||||
const flatEvents = this.#genFlatEvents(events);
|
||||
if (!flatEvents?.length) {
|
||||
logger.debug("Youtube Provider: flatEvents not got:", videoId);
|
||||
return;
|
||||
}
|
||||
|
||||
const flatEvents = this.#flatEvents(events);
|
||||
if (!flatEvents.length) return;
|
||||
this.#flatEvents = flatEvents;
|
||||
this.#fromLang = fromLang;
|
||||
|
||||
if (potUrl.searchParams.get("kind") === "asr" && segApiSetting) {
|
||||
logger.info("Youtube Provider: Starting AI ...");
|
||||
this.#processEvents({
|
||||
videoId,
|
||||
flatEvents,
|
||||
fromLang,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn("Youtube Provider: handle subtitle", error);
|
||||
this.#showNotification(this.#i18n("subtitle_load_failed"));
|
||||
} finally {
|
||||
this.#processingId = null;
|
||||
}
|
||||
}
|
||||
|
||||
const eventChunks = this.#splitEventsIntoChunks(
|
||||
flatEvents,
|
||||
segApiSetting.chunkLength
|
||||
async #processEvents({ videoId, flatEvents, fromLang }) {
|
||||
try {
|
||||
const [subtitles, progressed] = await this.#eventsToSubtitles({
|
||||
videoId,
|
||||
flatEvents,
|
||||
fromLang,
|
||||
});
|
||||
if (!subtitles?.length) {
|
||||
logger.debug(
|
||||
"Youtube Provider: events to subtitles got empty",
|
||||
videoId
|
||||
);
|
||||
const subtitlesFallback = () =>
|
||||
this.#formatSubtitles(flatEvents, fromLang);
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventChunks.length === 0) {
|
||||
this.#onCaptionsReady({
|
||||
videoId,
|
||||
subtitles: subtitlesFallback(),
|
||||
fromLang,
|
||||
isInitialLoad: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const firstChunkEvents = eventChunks[0];
|
||||
const firstBatchSubtitles = await this.#aiSegment({
|
||||
if (videoId !== this.#videoId) {
|
||||
logger.debug(
|
||||
"Youtube Provider: videoId changed!",
|
||||
videoId,
|
||||
this.#videoId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#subtitles = subtitles;
|
||||
this.#progressed = progressed;
|
||||
|
||||
this.#startManager();
|
||||
} catch (error) {
|
||||
logger.info("Youtube Provider: process events", error);
|
||||
this.#showNotification(this.#i18n("subtitle_load_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
#reProcessEvents() {
|
||||
const videoId = this.#videoId;
|
||||
const flatEvents = this.#flatEvents;
|
||||
const fromLang = this.#fromLang;
|
||||
if (!videoId || !flatEvents.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#showNotification(this.#i18n("starting_reprocess_events"));
|
||||
|
||||
this.#destroyManager();
|
||||
|
||||
this.#processEvents({ videoId, flatEvents, fromLang });
|
||||
}
|
||||
|
||||
async #eventsToSubtitles({ videoId, flatEvents, fromLang }) {
|
||||
const { isAISegment, segApiSetting, chunkLength, toLang } = this.#setting;
|
||||
const subtitlesFallback = () => [
|
||||
this.#formatSubtitles(flatEvents, fromLang),
|
||||
100,
|
||||
];
|
||||
|
||||
// potUrl.searchParams.get("kind") === "asr"
|
||||
if (isAISegment && segApiSetting) {
|
||||
logger.info("Youtube Provider: Starting AI ...");
|
||||
this.#showNotification(this.#i18n("ai_processing_pls_wait"));
|
||||
|
||||
const eventChunks = this.#splitEventsIntoChunks(flatEvents, chunkLength);
|
||||
|
||||
if (eventChunks.length === 0) {
|
||||
return subtitlesFallback();
|
||||
}
|
||||
|
||||
const firstChunkEvents = eventChunks[0];
|
||||
const firstBatchSubtitles = await this.#aiSegment({
|
||||
videoId,
|
||||
chunkEvents: firstChunkEvents,
|
||||
fromLang,
|
||||
toLang,
|
||||
segApiSetting,
|
||||
});
|
||||
|
||||
if (!firstBatchSubtitles?.length) {
|
||||
return subtitlesFallback();
|
||||
}
|
||||
|
||||
const chunkCount = eventChunks.length;
|
||||
if (chunkCount > 1) {
|
||||
const remainingChunks = eventChunks.slice(1);
|
||||
this.#processRemainingChunksAsync({
|
||||
chunks: remainingChunks,
|
||||
chunkCount,
|
||||
videoId,
|
||||
chunkEvents: firstChunkEvents,
|
||||
fromLang,
|
||||
toLang,
|
||||
segApiSetting,
|
||||
});
|
||||
|
||||
if (!firstBatchSubtitles?.length) {
|
||||
this.#onCaptionsReady({
|
||||
videoId,
|
||||
subtitles: subtitlesFallback(),
|
||||
fromLang,
|
||||
isInitialLoad: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.#onCaptionsReady({
|
||||
videoId,
|
||||
subtitles: firstBatchSubtitles,
|
||||
fromLang,
|
||||
isInitialLoad: true,
|
||||
});
|
||||
|
||||
if (eventChunks.length > 1) {
|
||||
const remainingChunks = eventChunks.slice(1);
|
||||
this.#processRemainingChunksAsync({
|
||||
chunks: remainingChunks,
|
||||
videoId,
|
||||
fromLang,
|
||||
toLang,
|
||||
segApiSetting,
|
||||
});
|
||||
}
|
||||
return [firstBatchSubtitles, 100 / eventChunks.length];
|
||||
} else {
|
||||
const subtitles = this.#formatSubtitles(flatEvents, fromLang);
|
||||
if (!subtitles?.length) {
|
||||
logger.info("Youtube Provider: No subtitles after format.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.#onCaptionsReady({
|
||||
videoId,
|
||||
subtitles,
|
||||
fromLang,
|
||||
isInitialLoad: true,
|
||||
});
|
||||
return [firstBatchSubtitles, 100];
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn("Youtube Provider: unknow error", error);
|
||||
this.#showNotification(this.#i18n("subtitle_load_failed"));
|
||||
} finally {
|
||||
this.#isBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
#onCaptionsReady({ videoId, subtitles, fromLang }) {
|
||||
this.#subtitles = subtitles;
|
||||
this.#videoId = videoId;
|
||||
this.#fromLang = fromLang;
|
||||
|
||||
if (this.#toggleButton) {
|
||||
this.#toggleButton.style.opacity = subtitles.length ? "1" : "0.5";
|
||||
}
|
||||
|
||||
this.#destroyManager();
|
||||
if (this.#enabled) {
|
||||
this.#startManager();
|
||||
} else {
|
||||
this.#showNotification(this.#i18n("subtitle_data_is_ready"));
|
||||
}
|
||||
return subtitlesFallback();
|
||||
}
|
||||
|
||||
#startManager() {
|
||||
@@ -486,11 +583,8 @@ class YouTubeCaptionProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
const videoId = this.#getVideoId();
|
||||
if (!this.#subtitles?.length || this.#videoId !== videoId) {
|
||||
logger.info("Youtube Provider: No subtitles");
|
||||
this.#showNotification(this.#i18n("try_get_subtitle_data"));
|
||||
this.#doubleClick();
|
||||
if (!this.#subtitles.length) {
|
||||
this.#showNotification(this.#i18n("waitting_for_subtitle"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -505,7 +599,6 @@ class YouTubeCaptionProvider {
|
||||
this.#managerInstance = new BilingualSubtitleManager({
|
||||
videoEl,
|
||||
formattedSubtitles: this.#subtitles,
|
||||
translationService: apiTranslate,
|
||||
setting: { ...this.#setting, fromLang: this.#fromLang },
|
||||
});
|
||||
this.#managerInstance.start();
|
||||
@@ -590,7 +683,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
|
||||
@@ -744,7 +837,7 @@ class YouTubeCaptionProvider {
|
||||
return sentences;
|
||||
}
|
||||
|
||||
#flatEvents(events = []) {
|
||||
#genFlatEvents(events = []) {
|
||||
const segments = [];
|
||||
let buffer = null;
|
||||
|
||||
@@ -827,6 +920,7 @@ class YouTubeCaptionProvider {
|
||||
|
||||
async #processRemainingChunksAsync({
|
||||
chunks,
|
||||
chunkCount,
|
||||
videoId,
|
||||
fromLang,
|
||||
toLang,
|
||||
@@ -837,7 +931,7 @@ class YouTubeCaptionProvider {
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunkEvents = chunks[i];
|
||||
const chunkNum = i + 2;
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`Youtube Provider: Processing subtitle chunk ${chunkNum}/${chunks.length + 1}: ${chunkEvents[0]?.start} --> ${chunkEvents[chunkEvents.length - 1]?.start}`
|
||||
);
|
||||
|
||||
@@ -855,7 +949,7 @@ class YouTubeCaptionProvider {
|
||||
if (aiSubtitles?.length > 0) {
|
||||
subtitlesForThisChunk = aiSubtitles;
|
||||
} else {
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`Youtube Provider: AI segmentation for chunk ${chunkNum} returned no data.`
|
||||
);
|
||||
subtitlesForThisChunk = this.#formatSubtitles(chunkEvents, fromLang);
|
||||
@@ -864,18 +958,29 @@ class YouTubeCaptionProvider {
|
||||
subtitlesForThisChunk = this.#formatSubtitles(chunkEvents, fromLang);
|
||||
}
|
||||
|
||||
if (this.#getVideoId() !== videoId) {
|
||||
logger.info("Youtube Provider: videoId changed!");
|
||||
if (videoId !== this.#videoId) {
|
||||
logger.info(
|
||||
"Youtube Provider: videoId changed!!",
|
||||
videoId,
|
||||
this.#videoId
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (subtitlesForThisChunk.length > 0 && this.#managerInstance) {
|
||||
logger.info(
|
||||
`Youtube Provider: Appending ${subtitlesForThisChunk.length} subtitles from chunk ${chunkNum}.`
|
||||
if (subtitlesForThisChunk.length > 0) {
|
||||
const progressed = (chunkNum * 100) / chunkCount;
|
||||
this.#subtitles.push(...subtitlesForThisChunk);
|
||||
this.#progressed = progressed;
|
||||
|
||||
logger.debug(
|
||||
`Youtube Provider: Appending ${subtitlesForThisChunk.length} subtitles from chunk ${chunkNum} (${this.#progressed}%).`
|
||||
);
|
||||
this.#managerInstance.appendSubtitles(subtitlesForThisChunk);
|
||||
|
||||
if (this.#managerInstance) {
|
||||
this.#managerInstance.appendSubtitles(subtitlesForThisChunk);
|
||||
}
|
||||
} else {
|
||||
logger.info(`Youtube Provider: Chunk ${chunkNum} no subtitles.`);
|
||||
logger.debug(`Youtube Provider: Chunk ${chunkNum} no subtitles.`);
|
||||
}
|
||||
|
||||
await sleep(randomBetween(500, 1000));
|
||||
@@ -913,7 +1018,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(
|
||||
|
||||
@@ -1,39 +1,113 @@
|
||||
function millisecondsStringToNumber(msString) {
|
||||
const cleanString = msString.trim();
|
||||
const milliseconds = parseInt(cleanString, 10);
|
||||
/**
|
||||
* 将多种格式的VTT时间戳字符串转换为毫秒数。
|
||||
* 兼容以下格式:
|
||||
* - mmm (e.g., "291040")
|
||||
* - MM:SS (e.g., "00:03")
|
||||
* - HH:MM:SS (e.g., "01:02:03")
|
||||
* - MM:SS.mmm (e.g., "00:07.980")
|
||||
* - HH:MM:SS.mmm (e.g., "01:02:03.456")
|
||||
* - MM:SS:mmm (e.g., "00:07:536")
|
||||
*
|
||||
* @param {string} timestamp - VTT时间戳字符串.
|
||||
* @returns {number} - 转换后的总毫秒数.
|
||||
*/
|
||||
function parseTimestampToMilliseconds(timestamp) {
|
||||
const ts = timestamp.trim();
|
||||
|
||||
if (isNaN(milliseconds)) {
|
||||
return 0;
|
||||
if (!ts.includes(":") && !ts.includes(".")) {
|
||||
return parseInt(ts, 10) || 0;
|
||||
}
|
||||
|
||||
return milliseconds;
|
||||
let timePart = ts;
|
||||
let msPart = "0";
|
||||
|
||||
if (ts.includes(".")) {
|
||||
const parts = ts.split(".");
|
||||
timePart = parts[0];
|
||||
msPart = parts[1];
|
||||
} else {
|
||||
const colonParts = ts.split(":");
|
||||
if (
|
||||
colonParts.length > 1 &&
|
||||
colonParts[colonParts.length - 1].length === 3
|
||||
) {
|
||||
msPart = colonParts.pop();
|
||||
timePart = colonParts.join(":");
|
||||
}
|
||||
}
|
||||
|
||||
const timeComponents = timePart.split(":").map((p) => parseInt(p, 10) || 0);
|
||||
let hours = 0,
|
||||
minutes = 0,
|
||||
seconds = 0;
|
||||
|
||||
if (timeComponents.length === 3) {
|
||||
[hours, minutes, seconds] = timeComponents;
|
||||
} else if (timeComponents.length === 2) {
|
||||
[minutes, seconds] = timeComponents;
|
||||
} else if (timeComponents.length === 1) {
|
||||
[seconds] = timeComponents;
|
||||
}
|
||||
|
||||
const milliseconds = parseInt(msPart.padEnd(3, "0"), 10) || 0;
|
||||
|
||||
return (hours * 3600 + minutes * 60 + seconds) * 1000 + milliseconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将毫秒数转换为VTT时间戳字符串 (HH:MM:SS.mmm).
|
||||
*
|
||||
* @param {number} ms - 总毫秒数.
|
||||
* @returns {string} - 格式化的VTT时间戳 (HH:MM:SS.mmm).
|
||||
*/
|
||||
function formatMillisecondsToTimestamp(ms) {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
const milliseconds = String(ms % 1000).padStart(3, "0");
|
||||
|
||||
const totalMinutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
||||
|
||||
const hours = String(Math.floor(totalMinutes / 60)).padStart(2, "0");
|
||||
const minutes = String(totalMinutes % 60).padStart(2, "0");
|
||||
|
||||
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析包含双语字幕的VTT文件内容。
|
||||
* @param {string} vttText - VTT文件的文本内容。
|
||||
* @returns {Array<Object>} 一个包含字幕对象的数组,每个对象包含 start, end, text, 和 translation.
|
||||
*/
|
||||
export function parseBilingualVtt(vttText) {
|
||||
const cleanText = vttText.replace(/^\uFEFF/, "").trim();
|
||||
const cues = cleanText.split(/\n\n+/);
|
||||
if (!cleanText) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cues = cleanText.split(/\n\n+/);
|
||||
const result = [];
|
||||
|
||||
for (const cue of cues) {
|
||||
const startIndex = cues[0].toUpperCase().includes("WEBVTT") ? 1 : 0;
|
||||
|
||||
for (let i = startIndex; i < cues.length; i++) {
|
||||
const cue = cues[i];
|
||||
if (!cue.includes("-->")) continue;
|
||||
|
||||
const lines = cue.split("\n");
|
||||
|
||||
const timestampLineIndex = lines.findIndex((line) => line.includes("-->"));
|
||||
if (timestampLineIndex === -1) continue;
|
||||
|
||||
const [startTimeString, endTimeString] =
|
||||
lines[timestampLineIndex].split(" --> ");
|
||||
lines[timestampLineIndex].split("-->");
|
||||
const textLines = lines.slice(timestampLineIndex + 1);
|
||||
|
||||
if (startTimeString && endTimeString && textLines.length > 0) {
|
||||
const originalText = textLines[0].trim();
|
||||
const translatedText = (textLines[1] || "").trim();
|
||||
const originalText = textLines[0]?.trim() || "";
|
||||
const translatedText = textLines[1]?.trim() || "";
|
||||
|
||||
result.push({
|
||||
start: millisecondsStringToNumber(startTimeString),
|
||||
end: millisecondsStringToNumber(endTimeString),
|
||||
start: parseTimestampToMilliseconds(startTimeString),
|
||||
end: parseTimestampToMilliseconds(endTimeString),
|
||||
text: originalText,
|
||||
translation: translatedText,
|
||||
});
|
||||
@@ -42,3 +116,31 @@ export function parseBilingualVtt(vttText) {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 parseBilingualVtt 生成的JSON数据转换回标准的VTT字幕字符串。
|
||||
* @param {Array<Object>} cues - 字幕对象数组,
|
||||
* @returns {string} - 格式化的VTT文件内容字符串。
|
||||
*/
|
||||
export function buildBilingualVtt(cues) {
|
||||
if (!Array.isArray(cues)) {
|
||||
return "WEBVTT";
|
||||
}
|
||||
|
||||
const header = "WEBVTT";
|
||||
|
||||
const cueBlocks = cues.map((cue, index) => {
|
||||
const startTime = formatMillisecondsToTimestamp(cue.start);
|
||||
const endTime = formatMillisecondsToTimestamp(cue.end);
|
||||
|
||||
const cueIndex = index + 1;
|
||||
const timestampLine = `${startTime} --> ${endTime}`;
|
||||
|
||||
const textLine = cue.text || "";
|
||||
const translationLine = cue.translation || "";
|
||||
|
||||
return `${cueIndex}\n${timestampLine}\n${textLine}\n${translationLine}`;
|
||||
});
|
||||
|
||||
return [header, ...cueBlocks].join("\n\n");
|
||||
}
|
||||
|
||||
@@ -5,11 +5,9 @@ import Draggable from "./Draggable";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { SettingProvider } from "../../hooks/Setting";
|
||||
import { MSG_TRANS_TOGGLE, MSG_POPUP_TOGGLE } from "../../config";
|
||||
import { sendIframeMsg } from "../../libs/iframe";
|
||||
import useWindowSize from "../../hooks/WindowSize";
|
||||
|
||||
export default function ContentFab({
|
||||
translator,
|
||||
fabConfig: { x: fabX, y: fabY, fabClickAction = 0 } = {},
|
||||
processActions,
|
||||
}) {
|
||||
@@ -28,13 +26,12 @@ export default function ContentFab({
|
||||
const handleClick = useCallback(() => {
|
||||
if (!moved) {
|
||||
if (fabClickAction === 1) {
|
||||
translator.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
processActions({ action: MSG_TRANS_TOGGLE });
|
||||
} else {
|
||||
processActions({ action: MSG_POPUP_TOGGLE });
|
||||
}
|
||||
}
|
||||
}, [moved, translator, fabClickAction, processActions]);
|
||||
}, [moved, fabClickAction, processActions]);
|
||||
|
||||
const fabProps = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -27,7 +27,7 @@ import ReusableAutocomplete from "./ReusableAutocomplete";
|
||||
import ShowMoreButton from "./ShowMoreButton";
|
||||
import {
|
||||
OPT_TRANS_DEEPLX,
|
||||
OPT_TRANS_OLLAMA,
|
||||
// OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_TRANS_NIUTRANS,
|
||||
OPT_TRANS_BUILTINAI,
|
||||
@@ -38,12 +38,14 @@ import {
|
||||
DEFAULT_BATCH_SIZE,
|
||||
DEFAULT_BATCH_LENGTH,
|
||||
DEFAULT_CONTEXT_SIZE,
|
||||
OPT_ALL_TYPES,
|
||||
OPT_ALL_TRANS_TYPES,
|
||||
API_SPE_TYPES,
|
||||
BUILTIN_STONES,
|
||||
BUILTIN_PLACEHOLDERS,
|
||||
BUILTIN_PLACETAGS,
|
||||
OPT_TRANS_AZUREAI,
|
||||
defaultNobatchPrompt,
|
||||
defaultNobatchUserPrompt,
|
||||
} from "../../config";
|
||||
import ValidationInput from "../../hooks/ValidationInput";
|
||||
|
||||
@@ -54,18 +56,25 @@ function TestButton({ api }) {
|
||||
const handleApiTest = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [text] = await apiTranslate({
|
||||
text: "hello world",
|
||||
const text = "hello world";
|
||||
const { trText } = await apiTranslate({
|
||||
text,
|
||||
fromLang: "en",
|
||||
toLang: "zh-CN",
|
||||
apiSetting: { ...api },
|
||||
useCache: false,
|
||||
usePool: false,
|
||||
});
|
||||
if (!text) {
|
||||
if (!trText) {
|
||||
throw new Error("empty result");
|
||||
}
|
||||
alert.success(i18n("test_success"));
|
||||
alert.success(
|
||||
<>
|
||||
<div>{i18n("test_success")}</div>
|
||||
<div>{text}</div>
|
||||
<div>{trText}</div>
|
||||
</>
|
||||
);
|
||||
} catch (err) {
|
||||
// alert.error(`${i18n("test_failed")}: ${err.message}`);
|
||||
let msg = err.message;
|
||||
@@ -77,24 +86,7 @@ function TestButton({ api }) {
|
||||
alert.error(
|
||||
<>
|
||||
<div>{i18n("test_failed")}</div>
|
||||
{msg === err.message ? (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
{msg}
|
||||
</div>
|
||||
) : (
|
||||
<pre
|
||||
style={{
|
||||
maxWidth: 400,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{msg}
|
||||
</pre>
|
||||
)}
|
||||
{msg === err.message ? <div>{msg}</div> : <pre>{msg}</pre>}
|
||||
</>
|
||||
);
|
||||
} finally {
|
||||
@@ -181,12 +173,14 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
model = "",
|
||||
apiType,
|
||||
systemPrompt = "",
|
||||
nobatchPrompt = defaultNobatchPrompt,
|
||||
nobatchUserPrompt = defaultNobatchUserPrompt,
|
||||
subtitlePrompt = "",
|
||||
// userPrompt = "",
|
||||
customHeader = "",
|
||||
customBody = "",
|
||||
think = false,
|
||||
thinkIgnore = "",
|
||||
// think = false,
|
||||
// thinkIgnore = "",
|
||||
fetchLimit = DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval = DEFAULT_FETCH_INTERVAL,
|
||||
httpTimeout = DEFAULT_HTTP_TIMEOUT,
|
||||
@@ -195,7 +189,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
reqHook = "",
|
||||
resHook = "",
|
||||
temperature = 0,
|
||||
maxTokens = 256,
|
||||
maxTokens = 20480,
|
||||
apiName = "",
|
||||
isDisabled = false,
|
||||
useBatchFetch = false,
|
||||
@@ -309,29 +303,53 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
<ValidationInput
|
||||
size="small"
|
||||
fullWidth
|
||||
label={"Max Tokens"}
|
||||
label={"Max Tokens (0-1000000)"}
|
||||
type="number"
|
||||
name="maxTokens"
|
||||
value={maxTokens}
|
||||
onChange={handleChange}
|
||||
min={0}
|
||||
max={2 ** 15}
|
||||
max={1000000}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}></Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={"SYSTEM PROMPT"}
|
||||
name="systemPrompt"
|
||||
value={systemPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
helperText={i18n("system_prompt_helper")}
|
||||
/>
|
||||
{useBatchFetch ? (
|
||||
<TextField
|
||||
size="small"
|
||||
label={"BATCH SYSTEM PROMPT"}
|
||||
name="systemPrompt"
|
||||
value={systemPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
helperText={i18n("system_prompt_helper")}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"SYSTEM PROMPT"}
|
||||
name="nobatchPrompt"
|
||||
value={nobatchPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"USER PROMPT"}
|
||||
name="nobatchUserPrompt"
|
||||
value={nobatchUserPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={"SUBTITLE PROMPT"}
|
||||
@@ -354,7 +372,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{apiType === OPT_TRANS_OLLAMA && (
|
||||
{/* {apiType === OPT_TRANS_OLLAMA && (
|
||||
<>
|
||||
<TextField
|
||||
select
|
||||
@@ -375,7 +393,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{apiType === OPT_TRANS_NIUTRANS && (
|
||||
<>
|
||||
@@ -570,7 +588,7 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
||||
name="httpTimeout"
|
||||
value={httpTimeout}
|
||||
onChange={handleChange}
|
||||
min={5000}
|
||||
min={1000}
|
||||
max={60000}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -775,7 +793,7 @@ export default function Apis() {
|
||||
|
||||
const apiTypes = useMemo(
|
||||
() =>
|
||||
OPT_ALL_TYPES.map((type) => ({
|
||||
OPT_ALL_TRANS_TYPES.map((type) => ({
|
||||
type,
|
||||
label: type,
|
||||
})),
|
||||
|
||||
@@ -2,6 +2,7 @@ import FileDownloadIcon from "@mui/icons-material/FileDownload";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { useState } from "react";
|
||||
import { kissLog } from "../../libs/log";
|
||||
import { downloadBlobFile } from "../../libs/utils";
|
||||
|
||||
export default function DownloadButton({ handleData, text, fileName }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -10,13 +11,7 @@ export default function DownloadButton({ handleData, text, fileName }) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await handleData();
|
||||
const url = window.URL.createObjectURL(new Blob([data]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", fileName || `${Date.now()}.json`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
downloadBlobFile(data, fileName);
|
||||
} catch (err) {
|
||||
kissLog("download", err);
|
||||
} finally {
|
||||
|
||||
@@ -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,7 @@ import SelectAllIcon from "@mui/icons-material/SelectAll";
|
||||
import EventNoteIcon from "@mui/icons-material/EventNote";
|
||||
import MouseIcon from "@mui/icons-material/Mouse";
|
||||
import SubtitlesIcon from "@mui/icons-material/Subtitles";
|
||||
import FormatColorText from "@mui/icons-material/FormatColorText";
|
||||
|
||||
function LinkItem({ label, url, icon }) {
|
||||
const match = useMatch(url);
|
||||
@@ -42,6 +43,24 @@ export default function Navigator(props) {
|
||||
url: "/rules",
|
||||
icon: <DesignServicesIcon />,
|
||||
},
|
||||
{
|
||||
id: "apis_setting",
|
||||
label: i18n("apis_setting"),
|
||||
url: "/apis",
|
||||
icon: <ApiIcon />,
|
||||
},
|
||||
{
|
||||
id: "styles_setting",
|
||||
label: i18n("styles_setting"),
|
||||
url: "/styles",
|
||||
icon: <FormatColorText />,
|
||||
},
|
||||
{
|
||||
id: "sync",
|
||||
label: i18n("sync_setting"),
|
||||
url: "/sync",
|
||||
icon: <SyncIcon />,
|
||||
},
|
||||
{
|
||||
id: "input_translate",
|
||||
label: i18n("input_translate"),
|
||||
@@ -66,18 +85,6 @@ export default function Navigator(props) {
|
||||
url: "/subtitle",
|
||||
icon: <SubtitlesIcon />,
|
||||
},
|
||||
{
|
||||
id: "apis_setting",
|
||||
label: i18n("apis_setting"),
|
||||
url: "/apis",
|
||||
icon: <ApiIcon />,
|
||||
},
|
||||
{
|
||||
id: "sync",
|
||||
label: i18n("sync_setting"),
|
||||
url: "/sync",
|
||||
icon: <SyncIcon />,
|
||||
},
|
||||
{
|
||||
id: "words",
|
||||
label: i18n("favorite_words"),
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_STYLE_DIY,
|
||||
OPT_STYLE_USE_COLOR,
|
||||
} from "../../config";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { useOwSubRule } from "../../hooks/SubRules";
|
||||
import { useApiList } from "../../hooks/Api";
|
||||
|
||||
export default function OwSubRule() {
|
||||
const i18n = useI18n();
|
||||
const { owSubrule, updateOwSubrule } = useOwSubRule();
|
||||
const { enabledApis } = useApiList();
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
const { name, value } = e.target;
|
||||
updateOwSubrule({ [name]: value });
|
||||
};
|
||||
|
||||
const {
|
||||
apiSlug,
|
||||
fromLang,
|
||||
toLang,
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
} = owSubrule;
|
||||
|
||||
const RemainItem = (
|
||||
<MenuItem key={REMAIN_KEY} value={REMAIN_KEY}>
|
||||
{i18n("remain_unchanged")}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const GlobalItem = (
|
||||
<MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}>
|
||||
{GLOBAL_KEY}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={12}>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transOpen"
|
||||
value={transOpen}
|
||||
label={i18n("translate_switch")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
<MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem>
|
||||
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="apiSlug"
|
||||
value={apiSlug}
|
||||
label={i18n("translate_service")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{enabledApis.map((api) => (
|
||||
<MenuItem key={api.apiSlug} value={api.apiSlug}>
|
||||
{api.apiName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="fromLang"
|
||||
value={fromLang}
|
||||
label={i18n("from_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="toLang"
|
||||
value={toLang}
|
||||
label={i18n("to_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="textStyle"
|
||||
value={textStyle}
|
||||
label={i18n("text_style")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
name="bgColor"
|
||||
value={bgColor}
|
||||
label={i18n("bg_color")}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{textStyle === OPT_STYLE_DIY && (
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("diy_style")}
|
||||
helperText={i18n("diy_style_helper")}
|
||||
name="textDiyStyle"
|
||||
value={textDiyStyle}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export default function ReusableAutocomplete({
|
||||
name: name,
|
||||
value: newValue,
|
||||
},
|
||||
preventDefault: () => {},
|
||||
};
|
||||
onChange(syntheticEvent);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,6 @@ import {
|
||||
GLOBLA_RULE,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_STYLE_DIY,
|
||||
// OPT_STYLE_USE_COLOR,
|
||||
URL_KISS_RULES_NEW_ISSUE,
|
||||
OPT_SYNCTYPE_WORKER,
|
||||
DEFAULT_TRANS_TAG,
|
||||
@@ -53,7 +50,6 @@ import {
|
||||
getSyncWithDefault,
|
||||
getRulesOld,
|
||||
} from "../../libs/storage";
|
||||
// import OwSubRule from "./OwSubRule";
|
||||
import ClearAllIcon from "@mui/icons-material/ClearAll";
|
||||
import HelpButton from "./HelpButton";
|
||||
import { useSyncCaches } from "../../hooks/Sync";
|
||||
@@ -68,7 +64,7 @@ import { kissLog } from "../../libs/log";
|
||||
import { useApiList } from "../../hooks/Api";
|
||||
import ShowMoreButton from "./ShowMoreButton";
|
||||
import { useConfirm } from "../../hooks/Confirm";
|
||||
import { defaultStyles } from "../../libs/style";
|
||||
import { useAllTextStyles } from "../../hooks/CustomStyles";
|
||||
|
||||
const calculateInitialValues = (rule) => {
|
||||
const base = rule?.pattern === "*" ? GLOBLA_RULE : DEFAULT_RULE;
|
||||
@@ -87,6 +83,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
const [formValues, setFormValues] = useState(initialFormValues);
|
||||
const [showMore, setShowMore] = useState(!rules);
|
||||
const { enabledApis } = useApiList();
|
||||
const { allTextStyles } = useAllTextStyles();
|
||||
|
||||
useEffect(() => {
|
||||
const newInitialValues = calculateInitialValues(rule);
|
||||
@@ -104,6 +101,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
aiTerms = "",
|
||||
termsStyle = "",
|
||||
highlightStyle = "color: red;",
|
||||
textExtStyle = "",
|
||||
selectStyle = "",
|
||||
parentStyle = "",
|
||||
grandStyle = "",
|
||||
@@ -114,8 +112,8 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
toLang,
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
// bgColor,
|
||||
// textDiyStyle,
|
||||
transOnly = "false",
|
||||
autoScan = "true",
|
||||
hasRichText = "true",
|
||||
@@ -139,13 +137,6 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
return JSON.stringify(initialFormValues) !== JSON.stringify(formValues);
|
||||
}, [initialFormValues, formValues]);
|
||||
|
||||
const stylesExample = useMemo(() => {
|
||||
return Object.entries(defaultStyles)
|
||||
.filter(([_, v]) => v)
|
||||
.map(([k, v]) => `${i18n(k)}:${v}`)
|
||||
.join("\n");
|
||||
}, [i18n]);
|
||||
|
||||
const hasSamePattern = (str) => {
|
||||
for (const item of rules.list) {
|
||||
if (item.pattern === str && rule?.pattern !== str) {
|
||||
@@ -459,6 +450,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
type="number"
|
||||
name="splitLength"
|
||||
value={splitLength}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
min={0}
|
||||
max={1000}
|
||||
@@ -529,61 +521,16 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
{allTextStyles.map((item) => (
|
||||
<MenuItem key={item.styleSlug} value={item.styleSlug}>
|
||||
{item.styleName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
name="bgColor"
|
||||
value={bgColor}
|
||||
label={i18n("bg_color")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{textStyle === OPT_STYLE_DIY && (
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("diy_style")}
|
||||
FormHelperTextProps={{
|
||||
component: "div",
|
||||
}}
|
||||
helperText={
|
||||
<Box>
|
||||
<Box component="div">{i18n("default_styles_example")}</Box>
|
||||
<Box
|
||||
component="pre"
|
||||
sx={{
|
||||
overflowX: "auto",
|
||||
height: 200,
|
||||
resize: "vertical",
|
||||
minHeight: 100,
|
||||
margin: 0,
|
||||
// border: "1px solid #ccc",
|
||||
}}
|
||||
>
|
||||
{stylesExample}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
name="textDiyStyle"
|
||||
value={textDiyStyle}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
maxRows={10}
|
||||
multiline
|
||||
/>
|
||||
)}
|
||||
|
||||
{showMore && (
|
||||
<>
|
||||
<TextField
|
||||
@@ -629,6 +576,16 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
maxRows={10}
|
||||
multiline
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("text_ext_style")}
|
||||
name="textExtStyle"
|
||||
value={textExtStyle}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
maxRows={10}
|
||||
multiline
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("selector_style")}
|
||||
|
||||
@@ -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 || {};
|
||||
|
||||
@@ -259,7 +260,7 @@ export default function Settings() {
|
||||
name="httpTimeout"
|
||||
value={httpTimeout}
|
||||
onChange={handleChange}
|
||||
min={5000}
|
||||
min={1000}
|
||||
max={60000}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -268,10 +269,13 @@ export default function Settings() {
|
||||
select
|
||||
fullWidth
|
||||
size="small"
|
||||
name="touchTranslate"
|
||||
value={touchTranslate}
|
||||
name="touchModes"
|
||||
value={touchModes}
|
||||
label={i18n("touch_translate_shortcut")}
|
||||
onChange={handleChange}
|
||||
SelectProps={{
|
||||
multiple: true,
|
||||
}}
|
||||
>
|
||||
{[0, 2, 3, 4, 5, 6, 7].map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
@@ -295,34 +299,6 @@ export default function Settings() {
|
||||
<MenuItem value={2}>{i18n("secondary_context_menus")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transAllnow"
|
||||
value={transAllnow}
|
||||
label={i18n("trigger_mode")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("mk_pagescroll")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("mk_pageopen")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
{/* <Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="detectRemote"
|
||||
value={detectRemote}
|
||||
label={i18n("detect_lang_remote")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
||||
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid> */}
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
@@ -341,6 +317,47 @@ export default function Settings() {
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transAllnow"
|
||||
value={transAllnow}
|
||||
label={i18n("trigger_mode")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("mk_pagescroll")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("mk_pageopen")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<ValidationInput
|
||||
fullWidth
|
||||
size="small"
|
||||
label={i18n("pagescroll_root_margin")}
|
||||
type="number"
|
||||
name="rootMargin"
|
||||
value={rootMargin}
|
||||
onChange={handleChange}
|
||||
min={0}
|
||||
max={10000}
|
||||
/>
|
||||
</Grid>
|
||||
{/* <Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="detectRemote"
|
||||
value={detectRemote}
|
||||
label={i18n("detect_lang_remote")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
||||
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid> */}
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
select
|
||||
|
||||
220
src/views/Options/StylesSetting.js
Normal file
220
src/views/Options/StylesSetting.js
Normal file
@@ -0,0 +1,220 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Button from "@mui/material/Button";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import { useConfirm } from "../../hooks/Confirm";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useAllTextStyles, useStyleList } from "../../hooks/CustomStyles";
|
||||
import { css } from "@emotion/css";
|
||||
import { getRandomQuote } from "../../config/quotes";
|
||||
import { useSetting } from "../../hooks/Setting";
|
||||
|
||||
function StyleFields({ customStyle, deleteStyle, updateStyle, isBuiltin }) {
|
||||
const i18n = useI18n();
|
||||
const {
|
||||
setting: { uiLang },
|
||||
} = useSetting();
|
||||
const [formData, setFormData] = useState({});
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
const confirm = useConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
if (customStyle) {
|
||||
setFormData(customStyle);
|
||||
}
|
||||
}, [customStyle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!customStyle) return;
|
||||
const hasChanged = JSON.stringify(customStyle) !== JSON.stringify(formData);
|
||||
setIsModified(hasChanged);
|
||||
}, [customStyle, formData]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
let { name, value } = e.target;
|
||||
|
||||
setFormData((prevData) => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
updateStyle(customStyle.styleSlug, formData);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
const isConfirmed = await confirm({
|
||||
confirmText: i18n("delete"),
|
||||
cancelText: i18n("cancel"),
|
||||
});
|
||||
|
||||
if (isConfirmed) {
|
||||
deleteStyle(customStyle.styleSlug);
|
||||
}
|
||||
};
|
||||
|
||||
const { styleName = "", styleCode = "" } = formData;
|
||||
|
||||
const textClass = useMemo(
|
||||
() => css`
|
||||
${styleCode}
|
||||
`,
|
||||
[styleCode]
|
||||
);
|
||||
|
||||
const quote = useMemo(() => {
|
||||
const q = getRandomQuote();
|
||||
if (uiLang === "en") {
|
||||
return [q.zh, q.en];
|
||||
}
|
||||
return [q.en, q.zh];
|
||||
}, [uiLang]);
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<Box>
|
||||
{quote[0]}
|
||||
<br />
|
||||
<span className={textClass}>{quote[1]}</span>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("style_name")}
|
||||
name="styleName"
|
||||
value={styleName}
|
||||
onChange={handleChange}
|
||||
disabled={isBuiltin}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("style_code")}
|
||||
name="styleCode"
|
||||
value={styleCode}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
disabled={isBuiltin}
|
||||
/>
|
||||
|
||||
{!isBuiltin && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
useFlexGap
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
disabled={!isModified}
|
||||
>
|
||||
{i18n("save")}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{i18n("delete")}
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function StyleAccordion({ customStyle, deleteStyle, updateStyle, isBuiltin }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setExpanded((pre) => !pre);
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion expanded={expanded} onChange={handleChange}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography
|
||||
sx={{
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
>
|
||||
{`${customStyle.styleName}`}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{expanded && (
|
||||
<StyleFields
|
||||
customStyle={customStyle}
|
||||
deleteStyle={deleteStyle}
|
||||
updateStyle={updateStyle}
|
||||
isBuiltin={isBuiltin}
|
||||
/>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StylesSetting() {
|
||||
const i18n = useI18n();
|
||||
const { customStyles, addStyle, deleteStyle, updateStyle } = useStyleList();
|
||||
const { builtinStyles } = useAllTextStyles();
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
addStyle();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Box>
|
||||
<Button
|
||||
size="small"
|
||||
id="add-style-button"
|
||||
variant="contained"
|
||||
onClick={handleClick}
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
{i18n("add")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
{customStyles.map((customStyle) => (
|
||||
<StyleAccordion
|
||||
key={customStyle.styleSlug}
|
||||
customStyle={customStyle}
|
||||
deleteStyle={deleteStyle}
|
||||
updateStyle={updateStyle}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box>
|
||||
{builtinStyles.map((customStyle) => (
|
||||
<StyleAccordion
|
||||
key={customStyle.styleSlug}
|
||||
customStyle={customStyle}
|
||||
deleteStyle={deleteStyle}
|
||||
updateStyle={updateStyle}
|
||||
isBuiltin={true}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -30,8 +30,11 @@ export default function SubtitleSetting() {
|
||||
apiSlug,
|
||||
segSlug,
|
||||
chunkLength,
|
||||
preTrans = 90,
|
||||
throttleTrans = 30,
|
||||
toLang,
|
||||
isBilingual,
|
||||
skipAd = false,
|
||||
windowStyle,
|
||||
originStyle,
|
||||
translationStyle,
|
||||
@@ -113,6 +116,32 @@ export default function SubtitleSetting() {
|
||||
max={20000}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<ValidationInput
|
||||
fullWidth
|
||||
size="small"
|
||||
label={i18n("pre_trans_seconds")}
|
||||
type="number"
|
||||
name="preTrans"
|
||||
value={preTrans}
|
||||
onChange={handleChange}
|
||||
min={10}
|
||||
max={36000}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<ValidationInput
|
||||
fullWidth
|
||||
size="small"
|
||||
label={i18n("throttle_trans_interval")}
|
||||
type="number"
|
||||
name="throttleTrans"
|
||||
value={throttleTrans}
|
||||
onChange={handleChange}
|
||||
min={1}
|
||||
max={3600}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||
<TextField
|
||||
fullWidth
|
||||
@@ -145,6 +174,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
|
||||
|
||||
@@ -25,6 +25,7 @@ import Playgound from "./Playground";
|
||||
import MouseHoverSetting from "./MouseHover";
|
||||
import SubtitleSetting from "./Subtitle";
|
||||
import Loading from "../../hooks/Loading";
|
||||
import StylesSetting from "./StylesSetting";
|
||||
|
||||
export default function Options() {
|
||||
const [error, setError] = useState("");
|
||||
@@ -107,6 +108,7 @@ export default function Options() {
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Setting />} />
|
||||
<Route path="rules" element={<Rules />} />
|
||||
<Route path="styles" element={<StylesSetting />} />
|
||||
<Route path="input" element={<InputSetting />} />
|
||||
<Route path="tranbox" element={<Tranbox />} />
|
||||
<Route path="mousehover" element={<MouseHoverSetting />} />
|
||||
|
||||
@@ -19,12 +19,12 @@ import {
|
||||
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";
|
||||
import { useAllTextStyles } from "../../hooks/CustomStyles";
|
||||
|
||||
export default function PopupCont({
|
||||
rule,
|
||||
@@ -37,6 +37,7 @@ export default function PopupCont({
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [commands, setCommands] = useState({});
|
||||
const { allTextStyles } = useAllTextStyles();
|
||||
|
||||
const handleTransToggle = async (e) => {
|
||||
try {
|
||||
@@ -384,23 +385,13 @@ export default function PopupCont({
|
||||
}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
{allTextStyles.map((item) => (
|
||||
<MenuItem key={item.styleSlug} value={item.styleSlug}>
|
||||
{item.styleName}
|
||||
</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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -115,7 +115,15 @@ export default function TranBox({
|
||||
text,
|
||||
setText,
|
||||
setShowBox,
|
||||
tranboxSetting: { enDict, enSug, apiSlugs, fromLang, toLang, toLang2 },
|
||||
tranboxSetting: {
|
||||
enDict,
|
||||
enSug,
|
||||
apiSlugs,
|
||||
fromLang,
|
||||
toLang,
|
||||
toLang2,
|
||||
autoHeight,
|
||||
},
|
||||
transApis,
|
||||
boxSize,
|
||||
setBoxSize,
|
||||
@@ -141,6 +149,7 @@ export default function TranBox({
|
||||
size={boxSize}
|
||||
setSize={setBoxSize}
|
||||
setPosition={setBoxPosition}
|
||||
autoHeight={autoHeight}
|
||||
header={
|
||||
<Header
|
||||
setShowBox={setShowBox}
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function TranCont({
|
||||
setTrText("");
|
||||
setError("");
|
||||
|
||||
const [trText] = await apiTranslate({
|
||||
const { trText } = await apiTranslate({
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { isMobile } from "../../libs/mobile";
|
||||
import { kissLog } from "../../libs/log";
|
||||
import { useLangMap } from "../../hooks/I18n";
|
||||
import { debouncePutTranBox, getTranBox } from "../../libs/storage";
|
||||
|
||||
export default function Slection({
|
||||
contextMenuType,
|
||||
@@ -107,6 +108,29 @@ export default function Slection({
|
||||
return "onMouseUp";
|
||||
}, [triggerMode]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const { w, h, x, y } = (await getTranBox()) || {};
|
||||
if (w !== undefined && h !== undefined) {
|
||||
setBoxSize({ w, h });
|
||||
}
|
||||
if (x !== undefined && y !== undefined) {
|
||||
setBoxPosition({
|
||||
x: limitNumber(x, 0, window.innerWidth),
|
||||
y: limitNumber(y, 0, window.innerHeight),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
debouncePutTranBox({ ...boxSize, ...boxPosition });
|
||||
}, [boxSize, boxPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
async function handleMouseup(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
Reference in New Issue
Block a user