Compare commits

...

127 Commits

Author SHA1 Message Date
Gabe
864e0651b1 fix: set pnpm version 2025-11-15 22:37:26 +08:00
Gabe
65d328eb38 Update version number: 2.0.10 2025-11-15 22:30:17 +08:00
Gabe
731d360323 fix: unavailable action log 2025-11-15 21:09:24 +08:00
Gabe
c4ccdba268 fix: sync bug 2025-11-15 21:05:06 +08:00
Gabe
4f00492e49 fix: Adjust popup UI 2025-11-15 14:39:03 +08:00
Gabe
abcf2baad6 feat: supports plain text translation 2025-11-15 14:25:05 +08:00
Gabe
49a7698993 fix: update ignore selectors 2025-11-15 01:08:17 +08:00
Gabe
8d2548acaf doc: i18n 2025-11-15 00:41:58 +08:00
Gabe
251deb5886 fix: notice text 2025-11-13 01:49:51 +08:00
Gabe
7a15bdeadc fix: from lang bug 2025-11-13 01:45:11 +08:00
Gabe
1e59d57764 fix: from lang bug 2025-11-13 01:38:16 +08:00
Gabe
12b3768598 doc: readme 2025-11-13 00:14:00 +08:00
Gabe
3abe5b98d0 doc: readme 2025-11-12 23:54:27 +08:00
Gabe
ad004105c3 fix: Solidified build environment 2025-11-12 23:38:29 +08:00
Gabe
f70266197e fix: Solidified build environment 2025-11-12 23:37:40 +08:00
Gabe
cc31a8004a fix: try fix workflows error 2025-11-12 23:14:31 +08:00
Gabe
fa14851596 fix: try fix workflows error 2025-11-12 23:08:08 +08:00
Gabe
d56c46e944 Update version number: 2.0.9 2025-11-12 22:25:59 +08:00
Gabe
9f8bcf1fe1 feat: Added Japanese and Korean language support 2025-11-12 21:41:29 +08:00
Gabe
e50387a796 fix: custom apis 2025-11-12 00:56:27 +08:00
Gabe
3d2eac8772 fix: save rule bug 2025-11-12 00:13:41 +08:00
Gabe
343f529cac fix: Optimize subtitle translation 2025-11-11 23:36:06 +08:00
Gabe
3bfa12b61c Update version number: 2.0.8 2025-11-10 01:17:20 +08:00
Gabe
79bd776ef9 fix: format 2025-11-10 01:15:54 +08:00
Gabe
222428ad47 feat: pre translate time 2025-11-10 01:08:09 +08:00
Gabe
4b3853dd22 fix: download subtitle 2025-11-10 00:35:30 +08:00
Gabe
9dd191902c fix: update dependencies 2025-11-10 00:30:37 +08:00
Gabe
3f524ad674 feat: supports download subtitle 2025-11-10 00:21:07 +08:00
Gabe
7e6376fcb7 fix: rules 2025-11-07 23:40:20 +08:00
Gabe
6f35013faf fix: split pattern (#384) 2025-11-07 21:31:11 +08:00
Gabe
e71acdaaa9 fix: truncate doc title 2025-11-07 00:25:25 +08:00
Gabe
fd7c663282 feat: Restore CSS injection functionality 2025-11-06 23:33:49 +08:00
Gabe
89b2bbe9ac fix: tone error (#382) 2025-11-06 20:15:15 +08:00
Gabe
7eb64a463b Update version number: 2.0.7 2025-11-05 23:26:12 +08:00
Gabe
8971a28abc fix: html font size (#378) 2025-11-05 23:15:40 +08:00
Gabe
2ff989429f doc: i18n 2025-11-05 22:08:44 +08:00
Gabe
24369e2581 fix: Some element tagnames are lowercase. (#377) 2025-11-05 21:41:18 +08:00
Gabe
2bb8a5182c fix: Some element tagnames are lowercase. (#377) 2025-11-05 20:48:12 +08:00
Gabe
629bf9461a fix: AI language code 2025-11-05 01:03:44 +08:00
Gabe
a56fb6c8d6 Update version number: 2.0.6 2025-11-04 23:32:59 +08:00
Gabe
efb3529c92 Update version number: 2.0.6 2025-11-04 23:27:21 +08:00
Gabe
a372a4173c doc: reamde 2025-11-04 22:56:31 +08:00
Gabe
5e46832548 fix: i18n 2025-11-04 22:26:01 +08:00
Gabe
91869c42e1 feat: add more styles 2025-11-04 22:21:26 +08:00
Gabe
d421748bed fix: rules 2025-11-04 22:02:42 +08:00
Gabe
7e5cd7e5a6 feat: supports Persian language 2025-11-04 21:55:50 +08:00
Gabe
2b910b2c47 feat: Supports setting multiple custom styles 2025-11-04 21:22:38 +08:00
Gabe
814ce4ca11 feat: add text additional styles to rule 2025-11-03 19:01:57 +08:00
Gabe
1e63fd1e19 fix: transbox class 2025-11-03 18:44:49 +08:00
Gabe
4b19902e5c fix: translation hooks and custom api doc 2025-11-03 18:42:47 +08:00
Gabe
fd014a1d34 fix: change default httptimeout 2025-11-03 01:15:47 +08:00
Gabe
fd91bcf603 fix: Reset fontsize when the fontsize of the html element is changed. (#348) 2025-11-02 23:37:04 +08:00
Gabe
61a4a8f920 fix: styles 2025-11-02 00:27:46 +08:00
Gabe
ed4275a18b opt: Optimize subtitle processing logic 2025-11-02 00:15:38 +08:00
Gabe
7481d65e1e opt: Optimize subtitle processing logic 2025-11-02 00:10:01 +08:00
Gabe
0c49cf1af9 opt: Optimize subtitle processing logic 2025-11-02 00:04:46 +08:00
Gabe
7f04000739 opt: Optimize subtitle processing logic 2025-11-01 23:43:23 +08:00
Gabe
e3da9824b6 fix: subtitle bug 2025-11-01 22:54:45 +08:00
Gabe
34370345cd fix: parse subtitle time string 2025-11-01 22:45:08 +08:00
Gabe
6c1a4e851c fix: styles 2025-11-01 15:25:40 +08:00
Gabe
766e3ce7f9 feat: add more styles 2025-11-01 15:10:39 +08:00
Gabe
30c2cca2e1 Update version number: 2.0.5 2025-10-31 23:14:01 +08:00
Gabe
af3241b773 doc: readme 2025-10-31 23:12:27 +08:00
Gabe
eca0a63273 fix: subtitle 2025-10-31 20:23:58 +08:00
Gabe
f15cdb38d6 fix: put rule can write different pattern (#367) 2025-10-31 15:33:56 +08:00
Gabe
5b8577aaa7 doc: readme 2025-10-31 14:42:55 +08:00
Gabe
0a4a2b46c1 feat: support ai model with no batch 2025-10-31 14:36:33 +08:00
Gabe
53d441b3f5 doc: readme 2025-10-31 11:25:43 +08:00
Gabe
d4c346e40a fix: Increase the Max Tokens limit 2025-10-31 10:42:08 +08:00
Gabe
2dcacf71e4 fix: apitranslate bug 2025-10-31 10:25:53 +08:00
Gabe
9ded6446a7 fix: input translate 2025-10-31 10:05:01 +08:00
Gabe
0a6f4a9f02 fix: input translate (#342) 2025-10-31 01:39:27 +08:00
Gabe
635e588bcc fix: skip builtin ignore selector when autoscan disabled 2025-10-31 00:22:26 +08:00
Gabe
7343db78a5 fix: iframe bugs 2025-10-30 22:02:43 +08:00
Gabe
ccd457c992 fix: iframe bugs 2025-10-30 22:01:08 +08:00
Gabe
97676f114e fix: remove tink from ollama 2025-10-30 20:07:01 +08:00
Gabe
e83c1eb017 fix: throw error msg 2025-10-30 19:42:00 +08:00
Gabe
e417c0106a fix: change default fetchLimit 2025-10-30 19:10:07 +08:00
Gabe
3c09840d35 feat: can set rootMargin for IntersectionObserver. 2025-10-30 01:05:13 +08:00
Gabe
7361a94f8c feat: can set rootMargin for IntersectionObserver. 2025-10-30 01:03:46 +08:00
Gabe
a9bffe3913 feat: can set rootMargin for IntersectionObserver. 2025-10-30 00:55:17 +08:00
Gabe
5322555eba feat: can set whether skip ads. 2025-10-30 00:31:17 +08:00
Gabe
172dce2867 fix: hooks & injectjs 2025-10-30 00:19:13 +08:00
Gabe
5735fee36e fix: set main width 100% 2025-10-28 00:45:10 +08:00
Gabe
91642d8784 feat: support subtitle dragable on mobile 2025-10-28 00:36:38 +08:00
Gabe
9d8f3f4211 feat: add shadowroot injector 2025-10-28 00:07:44 +08:00
Gabe
66d39da80a fix: check io.observe must be element 2025-10-27 20:45:22 +08:00
Gabe
fbd4a31a9c fix: ignore script ellement 2025-10-27 20:00:05 +08:00
Gabe
3d7e03ddaf Update version number: 2.0.4 2025-10-26 20:26:52 +08:00
Gabe
1f213bf257 fix: styles 2025-10-26 20:10:13 +08:00
Gabe
b38f079611 fix: change showNotification duration 2025-10-26 19:59:51 +08:00
Gabe
21e639cacd fix: createMutationObserver 2025-10-26 18:42:59 +08:00
Gabe
bdaf665b7c feat: The translation box can be set to adaptive height 2025-10-26 16:18:56 +08:00
Gabe
61a515c1d2 feat: Support multi-touch selection 2025-10-26 00:06:52 +08:00
Gabe
1b646df908 feat: Remember the tranbox position and size 2025-10-25 23:18:39 +08:00
Gabe
5550f939b2 doc: readme 2025-10-25 18:41:20 +08:00
Gabe
b34fb5a600 doc: readme 2025-10-25 18:38:55 +08:00
Gabe
c0dce5c0b1 fix: Optimized text scanning logic 2025-10-25 17:46:29 +08:00
Gabe
d56bd2920f fix: isQualityPoor 2025-10-24 21:44:54 +08:00
Gabe
48ad100a64 fix: Optimized the scan node logic 2025-10-24 21:37:26 +08:00
Gabe
ef07a172a9 doc: custom api 2025-10-24 20:58:08 +08:00
Gabe
f492d47719 fix: disable field of rule 2025-10-24 20:57:10 +08:00
Gabe
ac8c07deb4 fix: keepselector for twitter 2025-10-24 01:46:36 +08:00
Gabe
ca48ab639e fix: remove stopPropagation for shortcut 2025-10-23 19:52:18 +08:00
Gabe
7c5232c1a1 fix: Make keepSelector effective even if richText is disabled 2025-10-23 19:32:59 +08:00
Gabe
4fac7fdfe1 fix: update custom api 2025-10-23 14:35:21 +08:00
Gabe
f7fc9560d5 fix: update custom api 2025-10-23 14:33:12 +08:00
Gabe
f7ba744e7f fix: ignore selector 2025-10-23 11:07:25 +08:00
Gabe
315164f142 fix: remove logger 2025-10-22 22:34:55 +08:00
Gabe
429cab5223 Update version number: 2.0.3 2025-10-22 22:27:17 +08:00
Gabe
deecbc874b fix: Adapt to the new UI of youtube 2025-10-22 21:39:17 +08:00
Gabe
504f4cafa0 fix: Adapt to the new UI of youtube 2025-10-22 21:14:39 +08:00
Gabe
6d327d17af fix: mousehover translate (close #331) 2025-10-22 21:01:23 +08:00
Gabe
74290eb52b fix: rules 2025-10-22 01:56:47 +08:00
Gabe
60b9653fd3 feat: more touch operations 2025-10-22 01:50:49 +08:00
Gabe
53e32d3031 refactor: add TranslatorManager 2025-10-21 02:07:33 +08:00
Gabe
ed279cf8a1 fix: highlight fav words 2025-10-19 01:28:29 +08:00
Gabe
296b898bda fix: disable preventDefault in shrotcut 2025-10-19 00:42:13 +08:00
Gabe
2325155b1e feat: highlight fav words && split long paragraph 2025-10-19 00:19:47 +08:00
Gabe
b6ff4aae6a docs: custom api 2025-10-18 11:34:16 +08:00
Gabe
f04002fdb8 fix: ycombinator.com rule 2025-10-18 00:37:55 +08:00
Gabe
b8cb254f56 perf: Optimized api find 2025-10-17 23:02:39 +08:00
Gabe
3983477904 fix: setting ui 2025-10-17 13:11:57 +08:00
Gabe
f011f5ae38 fix: api doc 2025-10-17 12:57:58 +08:00
Gabe
18ecab18df fix: api doc 2025-10-17 12:56:26 +08:00
Gabe
793c481221 fix: rules 2025-10-17 11:53:02 +08:00
Gabe
4fee3688ea fix: popup width change to 360 2025-10-17 10:53:13 +08:00
100 changed files with 6252 additions and 2435 deletions

2
.env
View File

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

View File

@@ -7,15 +7,15 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
version: 9.14.4
- uses: actions/setup-node@v4
with:
node-version: latest
node-version: 24
cache: "pnpm"
- run: pnpm install
- run: pnpm build+zip
@@ -25,7 +25,7 @@ jobs:
path: build
deploy-web:
needs: build
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
@@ -38,7 +38,7 @@ jobs:
folder: build/web
create-release:
needs: build
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
outputs:
upload_url: ${{ steps.create-release.outputs.upload_url }}
steps:
@@ -56,7 +56,7 @@ jobs:
strategy:
matrix:
client: ["chrome", "edge", "firefox", "userscript", "thunderbird"]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4

1
.pnpm-version Normal file
View File

@@ -0,0 +1 @@
9.14.4

View File

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

177
README.ja.md Normal file
View File

@@ -0,0 +1,177 @@
# KISS Translator シンプル翻訳
[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)
シンプルでオープンソースの [バイリンガル対照翻訳拡張機能&ユーザースクリプト](https://github.com/fishjar/kiss-translator)です。
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
## 特徴
- [x] シンプルさを維持
- [x] オープンソース
- [x] 主要なブラウザに対応
- [x] Chrome/Edge
- [x] Firefox
- [x] Kiwi (Android)
- [x] Orion (iOS)
- [x] Safari
- [x] Thunderbird
- [x] 複数の翻訳サービスをサポート
- [x] Google/Microsoft
- [x] Tencent/Volcengine
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
- [x] DeepL/DeepLX/NiuTrans
- [x] AzureAI/CloudflareAI
- [x] Chromeブラウザ内蔵AI翻訳(BuiltinAI)
- [x] 一般的な翻訳シナリオをカバー
- [x] Webページのバイリンガル対照翻訳
- [x] 入力ボックス翻訳
- ショートカットキーで入力ボックス内のテキストを即座に他言語に翻訳
- [x] テキスト選択翻訳
- [x] 任意のページで翻訳ボックスを開き、複数の翻訳サービスで比較翻訳が可能
- [x] 英語辞書翻訳
- [x] 単語のブックマーク
- [x] マウスオーバー翻訳
- [x] YouTube 字幕翻訳
- 任意の翻訳サービスを使用してビデオ字幕を翻訳し、バイリンガル表示をサポート
- 基本的な字幕結合・改行アルゴリズムを内蔵し、翻訳品質を向上
- AIによる改行機能をサポートし、翻訳品質をさらに向上
- 字幕スタイルのカスタマイズ
- [x] 多様な翻訳効果をサポート
- [x] テキスト自動認識と手動ルールの2つのモードをサポート
- テキスト自動認識モードにより、ほとんどのWebサイトでルールを記述しなくても完全な翻訳が可能
- 手動ルールモードで、特定のWebサイトに合わせた最適な最適化が可能
- [x] 翻訳テキストスタイルのカスタマイズ
- [x] リッチテキストの翻訳と表示をサポートし、原文のリンクやその他のテキストスタイルを可能な限り保持
- [x] 翻訳文のみの表示(原文を非表示)をサポート
- [x] 翻訳APIの高度な機能
- [x] カスタムAPIにより、理論上あらゆる翻訳インターフェースをサポート
- [x] 翻訳テキストの統合バッチ送信
- [x] AIコンテキスト会話メモリ機能をサポートし、翻訳品質を向上
- [x] カスタムAI用語集
- [x] すべてのインターフェースがフックやカスタムパラメータなどの高度な機能をサポート
- [x] クライアント間のデータ同期
- [x] KISS-Workercloudflare/docker
- [x] WebDAV
- [x] カスタム翻訳ルール
- [x] ルールの購読/ルール共有
- [x] カスタム専門用語
- [x] カスタムショートカットキー
- `Alt+Q` 翻訳をオン
- `Alt+C` スタイル切り替え
- `Alt+K` 設定ポップアップを開く
- `Alt+S` 翻訳ポップアップを開く/選択テキストを翻訳
- `Alt+O` 設定ページを開く
- `Alt+I` 入力ボックス翻訳
## インストール
> 注:以下の理由により、ブラウザ拡張機能の使用を優先することをお勧めします
>
> - ブラウザ拡張機能の方が機能が完全です(ローカル言語認識、右クリックメニューなど)
> - ユーザースクリプトはより多くの問題(クロスドメイン問題、スクリプトの競合など)に遭遇する可能性があります
- [x] ブラウザ拡張機能
- [x] Chrome [インストール](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Kiwi (Android)
- [x] Orion (iOS)
- [x] Edge [インストール](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [インストール](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [ ] Safari (Mac)
- [ ] Safari (iOS)
- [x] Thunderbird [ダウンロード](https://github.com/fishjar/kiss-translator/releases)
- [x] ユーザースクリプト
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [インストールリンク](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [インストールリンク](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
## 関連プロジェクト
- データ同期サービス: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
- 本プロジェクトのデータ同期サービスとして使用できます。
- 個人のプライベートなルールリストの共有にも使用できます。
- セルフホスト、セルフマネジメント、データはプライベート。
- コミュニティ購読ルール: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
- コミュニティによってメンテナンスされた、最新かつ最も完全な購読ルールリストを提供します。
- ルール関連の問題についての助けを求める。
## よくある質問FAQ
### ショートカットキーの設定方法
拡張機能の管理ページで設定します。例:
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
- firefox [about:addons](about:addons)
### ルール設定の優先順位は?
個人ルール > 購読ルール > グローバルルール
グローバルルールの優先順位は最も低いですが、フォールバックルールとして非常に重要です。
### APIOllamaなどのテストに失敗する
APIテストの失敗には、一般的に以下の原因が考えられます
- アドレスが間違っている:
- 例えば `Ollama` にはネイティブAPIアドレスと `Openai` 互換のアドレスがありますが、本プラグインは現在、`Openai` 互換アドレスをサポートしており、`Ollama` ネイティブAPIアドレスはサポートしていません
- 一部のAIモデルが統合翻訳をサポートしていない
- この場合、統合翻訳を無効にするか、カスタムAPIを使用して対応できます。
- または、カスタムAPIを使用して対応します。詳細は[カスタムAPIサンプルドキュメント](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)を参照してください
- 一部のAIモデルでパラメータが一致しない
- 例えば `Gemini` のネイティブAPIはパラメータの不一致が大きく、一部のバージョンのモデルが特定のパラメータをサポートしていないためエラーが返されることがあります。
- この場合、`Hook` を使用してリクエスト `body` を変更するか、`Gemini2` (`Openai` 互換アドレス) に切り替えることができます
- サーバーのクロスドメイン制限によりアクセスが拒否され、403エラーが返される
- 例えば `Ollama` を起動する際に、環境変数 `OLLAMA_ORIGINS=*` を追加する必要があります。参考https://github.com/fishjar/kiss-translator/issues/174
### 入力したAPIがユーザースクリプトで使用できない
ユーザースクリプトは、リクエストを送信するためにドメインのホワイトリストを追加する必要があります。
### カスタムAPIのhook関数の設定方法
カスタムAPI機能は非常に強力で柔軟性があり、理論的にはどんな翻訳APIにも接続できます。
サンプル参照: [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プレーヤーの字幕ボタンをクリックしてオンにするだけで、バイリンガル字幕が自動的に表示されるのを待つだけです。
## 今後の計画
本プロジェクトは余暇に開発しており、厳密なタイムスケジュールはありません。コミュニティの共同構築を歓迎します。以下は初期段階の機能の方向性です:
- [x] **テキストの統合送信**リクエスト戦略を最適化し、翻訳APIの呼び出し回数を減らし、パフォーマンスを向上させます。
- [x] **リッチテキスト翻訳の強化**:より複雑なページ構造やリッチテキストコンテンツの正確な翻訳をサポートします。
- [x] **カスタム/AI APIの強化**コンテキストメモリ、複数ラウンドの対話など、高度なAI機能をサポートします。
- [x] **英語辞書のフォールバックメカニズム**:翻訳サービスが利用できない場合、他の辞書に切り替えるか、ローカル辞書での検索にフォールバックします。
- [x] **YouTube字幕サポートの最適化**:ストリーミング字幕の結合と翻訳体験を改善し、途切れを減らします。
- [ ] **ルール共同構築メカニズムのアップグレード**:より柔軟なルールの共有、バージョン管理、コミュニティレビュープロセスを導入します。
特定の方向に興味がある場合は、[Issues](https://github.com/fishjar/kiss-translator/issues) で議論したり、PRを送信したりすることを歓迎します
## 開発ガイド
```sh
git clone [https://github.com/fishjar/kiss-translator.git](https://github.com/fishjar/kiss-translator.git)
cd kiss-translator
git checkout dev # PRを送信する場合はdevブランチにプッシュすることをお勧めします
pnpm install
pnpm build
```
## コミュニケーション
- [Telegram グループ](https://t.me/+RRCu_4oNwrM2NmFl)に参加
## 寄付
![appreciate](https://github.com/fishjar/kiss-translator/assets/1157624/ebaecabe-2934-4172-8085-af236f5ee399)

178
README.ko.md Normal file
View File

@@ -0,0 +1,178 @@
# KISS Translator 심플 번역
[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)
심플하고 오픈 소스인 [이중 언어 대조 번역 확장 프로그램 & 유저 스크립트](https://github.com/fishjar/kiss-translator)입니다.
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
## 특징
- [x] 심플함 유지
- [x] 오픈 소스
- [x] 주요 브라우저 지원
- [x] Chrome/Edge
- [x] Firefox
- [x] Kiwi (Android)
- [x] Orion (iOS)
- [x] Safari
- [x] Thunderbird
- [x] 다양한 번역 서비스 지원
- [x] Google/Microsoft
- [x] Tencent/Volcengine
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
- [x] DeepL/DeepLX/NiuTrans
- [x] AzureAI/CloudflareAI
- [x] Chrome 브라우저 내장 AI 번역(BuiltinAI)
- [x] 일반적인 번역 시나리오 지원
- [x] 웹페이지 이중 언어 대조 번역
- [x] 입력창 번역
- 단축키를 통해 입력창 내 텍스트를 즉시 다른 언어로 번역
- [x] 텍스트 선택 번역
- [x] 모든 페이지에서 번역창을 열어 여러 번역 서비스로 비교 번역 가능
- [x] 영어 사전 번역
- [x] 단어 즐겨찾기
- [x] 마우스오버 번역
- [x] YouTube 자막 번역
- 모든 번역 서비스를 사용하여 비디오 자막을 번역하고 이중 언어로 표시 지원
- 기본적인 자막 병합 및 줄 바꿈 알고리즘 내장으로 번역 품질 향상
- AI 줄 바꿈 기능 지원으로 번역 품질 추가 향상
- 사용자 정의 자막 스타일
- [x] 다양한 번역 효과 지원
- [x] 자동 텍스트 인식 및 수동 규칙 두 가지 모드 지원
- 자동 텍스트 인식 모드는 대부분의 웹사이트에서 규칙 작성 없이도 완벽한 번역 가능
- 수동 규칙 모드로 특정 웹사이트에 대한 최적의 최적화 가능
- [x] 번역문 스타일 사용자 정의
- [x] 리치 텍스트 번역 및 표시 지원, 원문의 링크 및 기타 텍스트 스타일 최대한 보존
- [x] 번역문만 표시 (원문 숨기기) 지원
- [x] 번역 인터페이스 고급 기능
- [x] 사용자 정의 인터페이스를 통해 이론상 모든 번역 인터페이스 지원
- [x] 번역 텍스트 일괄 통합 전송
- [x] AI 컨텍스트 (대화 기억) 기능 지원으로 번역 품질 향상
- [x] 사용자 정의 AI 용어 사전
- [x] 모든 인터페이스는 후크 및 사용자 정의 파라미터 등 고급 기능 지원
- [x] 클라이언트 간 데이터 동기화
- [x] KISS-Worker (cloudflare/docker)
- [x] WebDAV
- [x] 사용자 정의 번역 규칙
- [x] 규칙 구독 / 규칙 공유
- [x] 사용자 정의 전문 용어
- [x] 사용자 정의 단축키
- `Alt+Q` 번역 켜기
- `Alt+C` 스타일 전환
- `Alt+K` 설정 팝업 열기
- `Alt+S` 번역 팝업 열기 / 선택한 텍스트 번역
- `Alt+O` 설정 페이지 열기
- `Alt+I` 입력창 번역
## 설치
> 참고: 다음과 같은 이유로 브라우저 확장 프로그램 사용을 우선적으로 권장합니다.
>
> - 브라우저 확장 프로그램의 기능이 더 완전합니다 (로컬 언어 인식, 우클릭 메뉴 등).
> - 유저 스크립트는 사용상 더 많은 문제 (크로스 도메인 문제, 스크립트 충돌 등)를 겪을 수 있습니다.
- [x] 브라우저 확장 프로그램
- [x] Chrome [설치 주소](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Kiwi (Android)
- [x] Orion (iOS)
- [x] Edge [설치 주소](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [설치 주소](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [ ] Safari (Mac)
- [ ] Safari (iOS)
- [x] Thunderbird [다운로드 주소](https://github.com/fishjar/kiss-translator/releases)
- [x] 유저 스크립트
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [설치 링크](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [설치 링크](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
## 관련 프로젝트
- 데이터 동기화 서비스: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
- 본 프로젝트의 데이터 동기화 서비스로 사용할 수 있습니다.
- 개인의 비공개 규칙 목록을 공유하는 데에도 사용할 수 있습니다.
- 직접 배포, 직접 관리, 데이터 비공개.
- 커뮤니티 구독 규칙: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
- 커뮤니티에서 유지 관리하는 최신의 가장 완벽한 구독 규칙 목록을 제공합니다.
- 규칙 관련 문제에 대한 도움 요청.
## 자주 묻는 질문 (FAQ)
### 단축키는 어떻게 설정하나요?
플러그인 관리 페이지에서 설정합니다. 예:
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
- firefox [about:addons](about:addons)
### 규칙 설정의 우선순위는 어떻게 되나요?
개인 규칙 > 구독 규칙 > 전역 규칙
그중 전역 규칙은 우선순위가 가장 낮지만, 예비 규칙으로서 매우 중요합니다.
### 인터페이스 (Ollama 등) 테스트 실패
일반적으로 인터페이스 테스트 실패는 다음과 같은 몇 가지 원인이 있습니다:
- 주소를 잘못 입력한 경우:
- 예를 들어 `Ollama`는 네이티브 인터페이스 주소와 `Openai` 호환 주소가 있습니다. 본 플러그인은 현재 `Openai` 호환 주소를 통일되게 지원하며, `Ollama` 네이티브 인터페이스 주소는 지원하지 않습니다.
- 일부 AI 모델이 통합 번역을 지원하지 않는 경우:
- 이 경우 통합 번역을 비활성화하거나 사용자 정의 인터페이스 방식을 통해 사용할 수 있습니다.
- 또는 사용자 정의 인터페이스 방식을 통해 사용합니다. 자세한 내용은 [사용자 정의 인터페이스 예시 문서](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)를 참조하세요.
- 일부 AI 모델의 파라미터가 일치하지 않는 경우:
- 예를 들어 `Gemini` 네이티브 인터페이스 파라미터는 매우 불일치하며, 일부 버전의 모델은 특정 파라미터를 지원하지 않아 오류를 반환할 수 있습니다.
- 이 경우 `Hook`을 사용하여 요청 `body`를 수정하거나, `Gemini2` (`Openai` 호환 주소)로 변경할 수 있습니다.
- 서버의 크로스 도메인 접근 제한으로 403 오류가 반환되는 경우:
- 예를 들어 `Ollama` 시작 시 환경 변수 `OLLAMA_ORIGINS=*`를 추가해야 합니다. 참고: https://github.com/fishjar/kiss-translator/issues/174
### 입력한 인터페이스를 유저 스크립트에서 사용할 수 없습니다
유저 스크립트는 도메인 화이트리스트를 추가해야 요청을 보낼 수 있습니다.
### 사용자 정의 인터페이스의 hook 함수는 어떻게 설정하나요?
사용자 정의 인터페이스 기능은 매우 강력하고 유연하며, 이론적으로 어떤 번역 인터페이스든 연결할 수 있습니다.
예시 참고: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
### 유저 스크립트 설정 페이지로 바로 이동하는 방법
설정 페이지 주소: https://fishjar.github.io/kiss-translator/options.html
### 자막 번역 팁
KT 버튼이 켜진 상태(파란 바탕에 흰 글씨)이기만 하면, 여러 번 클릭할 필요 없이 Youtube 플레이어의 원래 자막 버튼을 클릭하여 켜기만 하면 이중 언어 자막이 자동으로 나타날 때까지 기다리면 됩니다.
## 향후 계획
본 프로젝트는 여가 시간에 개발되며, 엄격한 시간표는 없습니다. 커뮤니티의 공동 구축을 환영합니다. 다음은 초기 구상 중인 기능 방향입니다:
- [x] **텍스트 통합 전송**: 요청 전략을 최적화하여 번역 인터페이스 호출 횟수를 줄이고 성능을 향상시킵니다.
- [x] **리치 텍스트 번역 강화**: 더 복잡한 페이지 구조와 리치 텍스트 콘텐츠의 정확한 번역을 지원합니다.
- [x] **사용자 정의/AI 인터페이스 강화**: 컨텍스트 기억, 다중 턴 대화 등 고급 AI 기능을 지원합니다.
- [x] **영어 사전 예비 메커니즘**: 번역 서비스가 실패할 경우 다른 사전으로 전환하거나 로컬 사전 조회로 대체합니다.
- [x] **YouTube 자막 지원 최적화**: 스트리밍 자막의 병합 및 번역 경험을 개선하고, 끊김을 줄입니다.
- [ ] **규칙 공동 구축 메커니즘 업그레이드**: 더 유연한 규칙 공유, 버전 관리 및 커뮤니티 검토 프로세스를 도입합니다.
특정 방향에 관심이 있다면, [Issues](https://github.com/fishjar/kiss-translator/issues)에서 토론하거나 PR을 제출해 주세요!
## 개발 가이드
```sh
git clone [https://github.com/fishjar/kiss-translator.git](https://github.com/fishjar/kiss-translator.git)
cd kiss-translator
git checkout dev # PR 제출 시 dev 브랜치로 푸시하는 것을 권장합니다
pnpm install
pnpm build
```
## 커뮤니티
- [Telegram 그룹](https://t.me/+RRCu_4oNwrM2NmFl) 가입
## 후원
![appreciate](https://github.com/fishjar/kiss-translator/assets/1157624/ebaecabe-2934-4172-8085-af236f5ee399)

View File

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

View File

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

View File

@@ -1,4 +1,98 @@
# 自定义接口示例
# 自定义接口说明及示例
## 默认接口规范
如果接口的请求数据和返回数据符合以下规范,
则无需填写 `Request Hook``Response Hook`
### 非聚合翻译 (v2.0.9)
Request body
```json
{
"text": "hello", // 需要翻译的文本列表
"from":"auto", // 原文语言
"to": "zh-CN" // 目标语言
}
```
Response
```json
{
"text": "你好", // 译文
"src": "en" // 原文语言
}
// 或者
{
"text": "你好", // 译文
"from": "en" // 原文语言
}
```
### 聚合翻译
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 +154,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 +190,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: "", // 可省略
}),
},
],
@@ -149,7 +249,7 @@ async ({ res }) => {
v2.0.2 版后内置`parseAIRes`函数Response Hook 可以简化为:
```js
async ({ res, parseAIRes, }) => {
async ({ res, parseAIRes }) => {
const translations = parseAIRes(res?.choices?.[0]?.message?.content);
return { translations };
};
@@ -186,7 +286,37 @@ async (args) => {
},
{
role: "user",
content: `Translate the following source text from to ${args.to}. Output translation directly without any additional text.\n\nSource Text: ${args.texts[0]}\n\nTranslated Text:`,
content: `Translate the following source text to ${args.to}. Output translation directly without any additional text.\n\nSource Text: ${args.texts[0]}\n\nTranslated Text:`,
},
],
temperature: 0,
max_tokens: 20480,
};
return { url, body, headers, method };
};
```
v2.0.6 版后内置默认 promptResponse 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,
@@ -204,3 +334,54 @@ async ({ res }) => {
return { translations: [[res?.choices?.[0]?.message?.content || ""]] };
};
```
## 语言代码表及说明
Hook参数里面的语言含义说明
- `toLang`, `fromLang` 是本插件支持的标准语言代码
- `to`, `from` 是转换后的适用于特定接口的语言代码
如果你的自定义接口与下面的标准语言代码不匹配,需要自行映射转换。
```
["en", "English - English"],
["zh-CN", "Simplified Chinese - 简体中文"],
["zh-TW", "Traditional Chinese - 繁體中文"],
["ar", "Arabic - العربية"],
["bg", "Bulgarian - Български"],
["ca", "Catalan - Català"],
["hr", "Croatian - Hrvatski"],
["cs", "Czech - Čeština"],
["da", "Danish - Dansk"],
["nl", "Dutch - Nederlands"],
["fa", "Persian - فارسی"],
["fi", "Finnish - Suomi"],
["fr", "French - Français"],
["de", "German - Deutsch"],
["el", "Greek - Ελληνικά"],
["hi", "Hindi - हिन्दी"],
["hu", "Hungarian - Magyar"],
["id", "Indonesian - Indonesia"],
["it", "Italian - Italiano"],
["ja", "Japanese - 日本語"],
["ko", "Korean - 한국어"],
["ms", "Malay - Melayu"],
["mt", "Maltese - Malti"],
["nb", "Norwegian - Norsk Bokmål"],
["pl", "Polish - Polski"],
["pt", "Portuguese - Português"],
["ro", "Romanian - Română"],
["ru", "Russian - Русский"],
["sk", "Slovak - Slovenčina"],
["sl", "Slovenian - Slovenščina"],
["es", "Spanish - Español"],
["sv", "Swedish - Svenska"],
["ta", "Tamil - தமிழ்"],
["te", "Telugu - తెలుగు"],
["th", "Thai - ไทย"],
["tr", "Turkish - Türkçe"],
["uk", "Ukrainian - Українська"],
["vi", "Vietnamese - Tiếng Việt"],
```

View File

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

310
pnpm-lock.yaml generated
View File

@@ -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: {}

View File

@@ -0,0 +1,20 @@
{
"app_name": {
"message": "KISS Übersetzer"
},
"app_description": {
"message": "Eine einfache zweisprachige Übersetzungs-Erweiterung und Greasemonkey-Skript"
},
"toggle_translate": {
"message": "Übersetzung umschalten"
},
"toggle_style": {
"message": "Stile umschalten"
},
"open_options": {
"message": "Einstellungen öffnen"
},
"open_tranbox": {
"message": "Popup-Fenster öffnen"
}
}

View File

@@ -6,15 +6,15 @@
"message": "A simple bilingual translation extension & Greasemonkey script"
},
"toggle_translate": {
"message": "Toggle Translate"
"message": "Toggle Translation"
},
"toggle_style": {
"message": "Toggle Style"
"message": "Toggle Styles"
},
"open_options": {
"message": "Open Options"
"message": "Open Setting"
},
"open_tranbox": {
"message": "Translate Popup/Selected"
"message": "Open Popup Box"
}
}

View File

@@ -0,0 +1,20 @@
{
"app_name": {
"message": "KISS Traductor"
},
"app_description": {
"message": "Una sencilla extensión y script de Greasemonkey para traducción bilingüe"
},
"toggle_translate": {
"message": "Alternar traducción"
},
"toggle_style": {
"message": "Cambiar estilo"
},
"open_options": {
"message": "Abrir configuración"
},
"open_tranbox": {
"message": "Abrir ventana emergente"
}
}

View File

@@ -0,0 +1,20 @@
{
"app_name": {
"message": "KISS Traducteur"
},
"app_description": {
"message": "Une extension et un script Greasemonkey de traduction bilingue simple"
},
"toggle_translate": {
"message": "Activer/désactiver la traduction"
},
"toggle_style": {
"message": "Changer de style"
},
"open_options": {
"message": "Ouvrir les paramètres"
},
"open_tranbox": {
"message": "Ouvrir la fenêtre contextuelle"
}
}

View File

@@ -0,0 +1,20 @@
{
"app_name": {
"message": "シンプル翻訳"
},
"app_description": {
"message": "シンプルなバイリンガル対訳翻訳拡張機能Tampermonkeyスクリプト"
},
"toggle_translate": {
"message": "翻訳の切り替え"
},
"toggle_style": {
"message": "スタイル切り替え"
},
"open_options": {
"message": "設定を開く"
},
"open_tranbox": {
"message": "ポップアップを開く"
}
}

View File

@@ -0,0 +1,20 @@
{
"app_name": {
"message": "심플 번역"
},
"app_description": {
"message": "심플한 이중 언어 대조 번역 확장 프로그램 & Tampermonkey 스크립트"
},
"toggle_translate": {
"message": "번역 켜기"
},
"toggle_style": {
"message": "스타일 전환"
},
"open_options": {
"message": "설정 열기"
},
"open_tranbox": {
"message": "팝업 열기"
}
}

View File

@@ -15,6 +15,6 @@
"message": "打开设置"
},
"open_tranbox": {
"message": "翻译弹窗/选中文字"
"message": "打开弹窗"
}
}

View File

@@ -0,0 +1,20 @@
{
"app_name": {
"message": "簡約翻譯"
},
"app_description": {
"message": "一個簡約的雙語對照翻譯擴充功能與 Tampermonkey 腳本"
},
"toggle_translate": {
"message": "開啟翻譯"
},
"toggle_style": {
"message": "切換樣式"
},
"open_options": {
"message": "開啟設定"
},
"open_tranbox": {
"message": "開啟彈出視窗"
}
}

View File

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

View File

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

View File

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

View File

@@ -14,9 +14,9 @@ import {
MSG_BUILTINAI_TRANSLATE,
OPT_TRANS_BUILTINAI,
URL_CACHE_SUBTITLE,
OPT_LANGS_TO_CODE,
} from "../config";
import { sha256, withTimeout } from "../libs/utils";
import { kissLog } from "../libs/log";
import {
handleTranslate,
handleSubtitle,
@@ -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 };
};
// 字幕处理/翻译

View File

@@ -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,10 @@ 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, useBatchFetch }) => {
const body = useBatchFetch
? { texts, from: fromLang, to: toLang }
: { text: texts[0], from: fromLang, to: toLang };
const headers = {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
@@ -627,15 +666,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 +692,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 +741,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 +783,11 @@ export const parseTransRes = async (
toLang,
langMap,
resHook,
thinkIgnore,
// thinkIgnore,
history,
userMsg,
apiType,
useBatchFetch,
}
) => {
// 执行 response hook
@@ -745,6 +812,8 @@ export const parseTransRes = async (
history.add(userMsg, hookResult.modelMsg);
}
return hookResult.translations;
} else if (Array.isArray(hookResult)) {
return hookResult;
}
} catch (err) {
kissLog("run res hook", err);
@@ -811,13 +880,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 +895,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 +914,12 @@ 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]);
if (useBatchFetch) {
return (res?.translations ?? res)?.map((item) => [item.text, item.src]);
}
return [[res.text, res.src || res.from]];
default:
}
@@ -919,7 +991,7 @@ export const handleTranslate = async (
httpTimeout,
});
if (!response) {
throw new Error("tranlate got empty response");
throw new Error("translate got empty response");
}
const result = await parseTransRes(response, {
@@ -934,7 +1006,7 @@ export const handleTranslate = async (
...apiSetting,
});
if (!result?.length) {
throw new Error("tranlate got an unexpected result");
throw new Error("translate got an unexpected result");
}
return result;

View File

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

View File

@@ -1,19 +1,11 @@
import React from "react";
import ReactDOM from "react-dom/client";
import Action from "./views/Action";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { OPT_HIGHLIGHT_WORDS_DISABLE } from "./config";
import {
MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
MSG_TRANS_PUTRULE,
APP_CONSTS,
} from "./config";
import { getFabWithDefault, getSettingWithDefault } from "./libs/storage";
import { Translator } from "./libs/translator";
import { isIframe, sendIframeMsg } from "./libs/iframe";
import { touchTapListener } from "./libs/touch";
import { debounce, genEventName } from "./libs/utils";
getFabWithDefault,
getSettingWithDefault,
getWordsWithDefault,
} from "./libs/storage";
import { isIframe } from "./libs/iframe";
import { genEventName } from "./libs/utils";
import { handlePing, injectScript } from "./libs/gm";
import { matchRule } from "./libs/rules";
import { trySyncAllSubRules } from "./libs/subRules";
@@ -21,6 +13,7 @@ import { isInBlacklist } from "./libs/blacklist";
import { runSubtitle } from "./subtitle/subtitle";
import { logger } from "./libs/log";
import { injectInlineJs } from "./libs/injector";
import TranslatorManager from "./libs/translatorManager";
/**
* 油猴脚本设置页面
@@ -43,62 +36,6 @@ function runSettingPage() {
}
}
/**
* iframe 页面执行
* @param {*} translator
*/
function runIframe(translator) {
window.addEventListener("message", (e) => {
const { action, args } = e.data || {};
switch (action) {
case MSG_TRANS_TOGGLE:
translator?.toggle();
break;
case MSG_TRANS_TOGGLE_STYLE:
translator?.toggleStyle();
break;
case MSG_TRANS_PUTRULE:
translator.updateRule(args || {});
break;
default:
}
});
}
/**
* 悬浮按钮
* @param {*} translator
* @returns
*/
async function showFab(translator) {
const fab = await getFabWithDefault();
const $action = document.createElement("div");
$action.id = APP_CONSTS.fabID;
$action.className = "notranslate";
$action.style.fontSize = "0";
$action.style.width = "0";
$action.style.height = "0";
document.body.parentElement.appendChild($action);
const shadowContainer = $action.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style");
const shadowRootElement = document.createElement("div");
shadowRootElement.className = `${APP_CONSTS.fabID}_warpper notranslate`;
shadowContainer.appendChild(emotionRoot);
shadowContainer.appendChild(shadowRootElement);
const cache = createCache({
key: APP_CONSTS.fabID,
prepend: true,
container: emotionRoot,
});
ReactDOM.createRoot(shadowRootElement).render(
<React.StrictMode>
<CacheProvider value={cache}>
<Action translator={translator} fab={fab} />
</CacheProvider>
</React.StrictMode>
);
}
/**
* 显示错误信息到页面顶部
* @param {*} message
@@ -161,22 +98,19 @@ function showErr(message) {
setTimeout(removeBanner, 10000);
}
/**
* 监听触屏操作
* @param {*} translator
* @returns
*/
function touchOperation(translator) {
const { touchTranslate = 2 } = translator.setting;
if (touchTranslate === 0) {
return;
async function getFavWords(rule) {
if (
rule.highlightWords &&
rule.highlightWords !== OPT_HIGHLIGHT_WORDS_DISABLE
) {
try {
return Object.keys(await getWordsWithDefault());
} catch (err) {
logger.info("get fav words", err);
}
}
const handleTap = debounce(() => {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
});
touchTapListener(handleTap, touchTranslate);
return [];
}
/**
@@ -184,6 +118,13 @@ function touchOperation(translator) {
*/
export async function run(isUserscript = false) {
try {
// if (document?.documentElement?.tagName?.toUpperCase() !== "HTML") {
// return;
// }
if (!document?.contentType?.includes("text")) {
return;
}
// 读取设置信息
const setting = await getSettingWithDefault();
@@ -209,34 +150,28 @@ export async function run(isUserscript = false) {
// 翻译网页
const rule = await matchRule(href, setting);
const translator = new Translator(rule, setting, isUserscript);
const favWords = await getFavWords(rule);
const fabConfig = await getFabWithDefault();
const translatorManager = new TranslatorManager({
setting,
rule,
fabConfig,
favWords,
isIframe,
isUserscript,
});
translatorManager.start();
// 适配iframe
if (isIframe) {
runIframe(translator);
return;
}
// 字幕翻译
runSubtitle({ href, setting, rule, isUserscript });
// 监听消息
// !isUserscript && runtimeListener(translator);
// 输入框翻译
// inputTranslate(setting);
// 划词翻译
// showTransbox(setting, rule);
// 浮球按钮
await showFab(translator);
// 触屏操作
touchOperation(translator);
// 同步订阅规则
isUserscript && (await trySyncAllSubRules(setting));
if (isUserscript) {
trySyncAllSubRules(setting);
}
} catch (err) {
console.error("[KISS-Translator]", err);
showErr(err.message);

View File

@@ -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,
@@ -550,14 +559,13 @@ const defaultApiOpts = {
},
[OPT_TRANS_CUSTOMIZE]: {
...defaultApi,
url: "https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN",
reqHook: defaultRequestHook,
resHook: defaultResponseHook,
},
};
// 内置翻译接口列表(带参数)
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 +573,6 @@ export const DEFAULT_API_LIST = OPT_ALL_TYPES.map((apiType) => ({
}));
export const DEFAULT_API_TYPE = OPT_TRANS_MICROSOFT;
export const DEFAULT_API_SETTING = DEFAULT_API_LIST[DEFAULT_API_TYPE];
export const DEFAULT_API_SETTING = DEFAULT_API_LIST.find(
(a) => a.apiType === DEFAULT_API_TYPE
);

View File

@@ -2,9 +2,11 @@ export const APP_NAME = process.env.REACT_APP_NAME.trim()
.split(/\s+/)
.join("-");
export const APP_LCNAME = APP_NAME.toLowerCase();
export const APP_UPNAME = APP_NAME.toUpperCase();
export const APP_CONSTS = {
fabID: `${APP_LCNAME}-fab`,
boxID: `${APP_LCNAME}-box`,
popupID: `${APP_LCNAME}-popup`,
};
export const APP_VERSION = process.env.REACT_APP_VERSION.split(".");

File diff suppressed because it is too large Load Diff

View File

@@ -7,3 +7,4 @@ export * from "./storage";
export * from "./url";
export * from "./msg";
export * from "./client";
export * from "./styles";

View File

@@ -15,6 +15,7 @@ export const MSG_TRANS_GETRULE = "trans_getrule";
export const MSG_TRANS_PUTRULE = "trans_putrule";
export const MSG_TRANS_CURRULE = "trans_currule";
export const MSG_TRANSBOX_TOGGLE = "transbox_toggle";
export const MSG_POPUP_TOGGLE = "popup_toggle";
export const MSG_MOUSEHOVER_TOGGLE = "mousehover_toggle";
export const MSG_TRANSINPUT_TOGGLE = "transinput_toggle";
export const MSG_CONTEXT_MENUS = "context_menus";
@@ -27,6 +28,11 @@ export const MSG_BUILTINAI_TRANSLATE = "builtinai_translte";
export const MSG_SET_LOGLEVEL = "set_loglevel";
export const MSG_CLEAR_CACHES = "clear_caches";
export const EVENT_KISS = "event_kiss_translate";
export const MSG_XHR_DATA_YOUTUBE = "KISS_XHR_DATA_YOUTUBE";
// export const MSG_GLOBAL_VAR_FETCH = "KISS_GLOBAL_VAR_FETCH";
// export const MSG_GLOBAL_VAR_BACK = "KISS_GLOBAL_VAR_BACK";
export const MSG_MENUS_PROGRESSED = "progressed";
export const MSG_MENUS_UPDATEFORM = "updateFormData";

407
src/config/quotes.js Normal file
View 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];
}

View File

@@ -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"; // 直接翻译到底
@@ -63,23 +26,28 @@ export const OPT_TIMING_ALL = [
OPT_TIMING_ALT,
];
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 OPT_SPLIT_PARAGRAPH_DISABLE = "split_disable";
export const OPT_SPLIT_PARAGRAPH_TEXTLENGTH = "split_textlength";
export const OPT_SPLIT_PARAGRAPH_PUNCTUATION = "split_punctuation";
export const OPT_SPLIT_PARAGRAPH_ALL = [
OPT_SPLIT_PARAGRAPH_DISABLE,
OPT_SPLIT_PARAGRAPH_PUNCTUATION,
OPT_SPLIT_PARAGRAPH_TEXTLENGTH,
];
export const OPT_HIGHLIGHT_WORDS_DISABLE = "highlight_disable";
export const OPT_HIGHLIGHT_WORDS_BEFORETRANS = "highlight_beforetrans";
export const OPT_HIGHLIGHT_WORDS_AFTERTRANS = "highlight_aftertrans";
export const OPT_HIGHLIGHT_WORDS_ALL = [
OPT_HIGHLIGHT_WORDS_DISABLE,
OPT_HIGHLIGHT_WORDS_BEFORETRANS,
OPT_HIGHLIGHT_WORDS_AFTERTRANS,
];
export const DEFAULT_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: "", // 选择器
@@ -91,14 +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, // 译文元素标签
@@ -116,6 +86,9 @@ export const DEFAULT_RULE = {
hasShadowroot: GLOBAL_KEY, // 是否包含shadowroot
rootsSelector: "", // 翻译范围选择器
ignoreSelector: "", // 不翻译的选择器
splitParagraph: GLOBAL_KEY, // 切分段落
splitLength: 0, // 切分段落长度
highlightWords: GLOBAL_KEY, // 高亮词汇
};
// 全局规则
@@ -130,12 +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;", // 专业术语样式
selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
parentStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
grandStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
highlightStyle: "color: red;", // 高亮词汇样式
selectStyle: "", // 选择器节点样式
parentStyle: "", // 选择器节点样式
grandStyle: "", // 选择器祖节点样式
injectJs: "", // 注入JS
injectCss: "", // 注入CSS
transOnly: "false", // 是否仅显示译文
@@ -155,55 +130,56 @@ export const GLOBLA_RULE = {
hasShadowroot: "false", // 是否包含shadowroot
rootsSelector: "body", // 翻译范围选择器
ignoreSelector: DEFAULT_IGNORE_SELECTOR, // 不翻译的选择器
splitParagraph: OPT_SPLIT_PARAGRAPH_DISABLE, // 切分段落
splitLength: 100, // 切分段落长度
highlightWords: OPT_HIGHLIGHT_WORDS_DISABLE, // 高亮词汇
};
export const DEFAULT_RULES = [GLOBLA_RULE];
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": {
rootsSelector: `#rcnt`,
},
// "www.google.com/search": {
// rootsSelector: `#rcnt`,
// },
"en.wikipedia.org": {
ignoreSelector: `.button, code, footer, form, mark, pre, .mwe-math-element, .mw-editsection`,
},
"news.ycombinator.com": {
selector: `p, .titleline, .commtext`,
rootsSelector: `#bigbox`,
selector: `p, .titleline, .commtext, .hn-item-title, .hn-comment-text, .hn-story-title`,
keepSelector: `code, img, svg, pre, .sitebit`,
ignoreSelector: `button, code, footer, form, header, mark, nav, pre, .reply`,
autoScan: `false`,
},
"twitter.com, https://x.com": {
selector: `[data-testid='tweetText']`,
keepSelector: `img, svg, span:has(a), div:has(a)`,
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": {
rootsSelector: `ytd-page-manager`,
ignoreSelector: `aside, button, footer, form, header, pre, mark, nav, #player, #container, .caption-window, .ytp-settings-menu`,
},
"www.youtube.com/live_chat": {
rootsSelector: `div#items`,
selector: `span.yt-live-chat-text-message-renderer`,
autoScan: `false`,
},
"www.youtube.com": {
rootsSelector: `ytd-page-manager`,
ignoreSelector: `aside, button, footer, form, header, pre, mark, nav, #player, #container, .caption-window, .ytp-settings-menu`,
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",
},
};
export const BUILTIN_RULES = Object.entries(RULES_MAP)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([pattern, rule]) => ({
export const BUILTIN_RULES = Object.entries(RULES_MAP).map(
([pattern, rule]) => ({
// ...DEFAULT_RULE,
...rule,
pattern,
}));
})
);

View File

@@ -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, // 触屏翻译
// 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, // 自定义样式列表
};

View File

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

46
src/config/styles.js Normal file
View 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;`,
},
];

View File

@@ -1,5 +1,3 @@
import { run } from "./common";
if (document.documentElement && document.documentElement.tagName === "HTML") {
run();
}
run();

View File

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

84
src/hooks/CustomStyles.js Normal file
View 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 };
}

View File

@@ -13,5 +13,11 @@ export function useDebouncedCallback(callback, delay) {
[delay]
);
useEffect(() => {
return () => {
debouncedCallback.cancel();
};
}, [debouncedCallback]);
return debouncedCallback;
}

View File

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

View File

@@ -25,13 +25,17 @@ const SettingContext = createContext({
reloadSetting: () => {},
});
export function SettingProvider({ children }) {
export function SettingProvider({ children, isSettingPage }) {
const {
data: setting,
isLoading,
update,
reload,
} = useStorage(STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY);
} = useStorage(
STOKEY_SETTING,
DEFAULT_SETTING,
isSettingPage ? KV_SETTING_KEY : ""
);
useEffect(() => {
if (typeof setting?.darkMode === "boolean") {
@@ -43,6 +47,8 @@ export function SettingProvider({ children }) {
}, [setting?.darkMode, update]);
useEffect(() => {
if (!isSettingPage) return;
(async () => {
try {
logger.setLevel(setting?.logLevel);
@@ -53,7 +59,7 @@ export function SettingProvider({ children }) {
logger.error("Failed to fetch log level, using default.", error);
}
})();
}, [setting]);
}, [isSettingPage, setting?.logLevel]);
const updateSetting = useCallback(
(objOrFn) => {

View File

@@ -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 };
}

View File

@@ -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) {
//
}

29
src/hooks/WindowSize.js Normal file
View File

@@ -0,0 +1,29 @@
import { useState, useEffect } from "react";
import { useDebouncedCallback } from "./DebouncedCallback";
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
w: window.innerWidth,
h: window.innerHeight,
});
const debounceWindowResize = useDebouncedCallback(() => {
setWindowSize({
w: window.innerWidth,
h: window.innerHeight,
});
}, 200);
useEffect(() => {
debounceWindowResize();
window.addEventListener("resize", debounceWindowResize);
return () => {
window.removeEventListener("resize", debounceWindowResize);
};
}, [debounceWindowResize]);
return windowSize;
}
export default useWindowSize;

View File

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

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

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

View File

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

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

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

View File

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

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

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

18
src/libs/fabManager.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -62,10 +62,12 @@ class Logger {
return;
}
this.config.level = newLevelObject;
console.log(
`[${this.config.prefix}] Log level dynamically set to ${this.config.level.name}`
);
if (this.config.level.value !== newLevelObject.value) {
this.config.level = newLevelObject;
console.log(
`[${this.config.prefix}] Log level dynamically set to ${this.config.level.name}`
);
}
}
/**

26
src/libs/popupManager.js Normal file
View File

@@ -0,0 +1,26 @@
import ShadowDomManager from "./shadowDomManager";
import { APP_CONSTS, EVENT_KISS, MSG_POPUP_TOGGLE } from "../config";
import Action from "../views/Action";
export class PopupManager extends ShadowDomManager {
constructor({ translator, processActions }) {
super({
id: APP_CONSTS.popupID,
className: "notranslate",
reactComponent: Action,
props: { translator, processActions },
});
}
toggle(props) {
if (this.isVisible) {
document.dispatchEvent(
new CustomEvent(EVENT_KISS, {
detail: { action: MSG_POPUP_TOGGLE },
})
);
} else {
this.show(props || this._props);
}
}
}

View File

@@ -1,17 +1,16 @@
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,
OPT_HIGHLIGHT_WORDS_ALL,
} from "../config";
import { loadOrFetchSubRules } from "./subRules";
import { getRulesWithDefault, setRules } from "./storage";
import { trySyncRules } from "./sync";
// import { FIXER_ALL } from "./webfix";
import { kissLog } from "./log";
/**
@@ -35,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,
@@ -53,12 +52,13 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
"terms",
"aiTerms",
"termsStyle",
"highlightStyle",
"textExtStyle",
"selectStyle",
"parentStyle",
"grandStyle",
"injectJs",
"injectCss",
// "fixerSelector",
"transStartHook",
"transEndHook",
// "transRemoveHook",
@@ -74,31 +74,25 @@ 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];
}
});
// 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;
}
["splitLength"].forEach((key) => {
if (!rule[key]) {
rule[key] = globalRule[key];
}
});
return rule;
};
@@ -138,6 +132,8 @@ export const checkRules = (rules) => {
terms,
aiTerms,
termsStyle,
highlightStyle,
textExtStyle,
selectStyle,
parentStyle,
grandStyle,
@@ -148,22 +144,18 @@ export const checkRules = (rules) => {
toLang,
textStyle,
transOpen,
bgColor,
textDiyStyle,
transOnly,
autoScan,
hasRichText,
hasShadowroot,
// transTiming,
transTag,
transTitle,
// detectRemote,
// skipLangs,
// fixerSelector,
// fixerFunc,
transStartHook,
transEndHook,
// transRemoveHook,
splitParagraph,
splitLength,
highlightWords,
}) => ({
pattern: pattern.trim(),
selector: type(selector) === "string" ? selector : "",
@@ -173,36 +165,44 @@ export const checkRules = (rules) => {
terms: type(terms) === "string" ? terms : "",
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
),
splitLength: Number.isInteger(splitLength) ? splitLength : 0,
highlightWords: matchValue(
[GLOBAL_KEY, ...OPT_HIGHLIGHT_WORDS_ALL],
highlightWords
),
})
);
@@ -226,9 +226,15 @@ export const saveRule = async (curRule) => {
}
const newRule = {};
Object.entries(GLOBLA_RULE).forEach(([key, val]) => {
const globalRule = {
...GLOBLA_RULE,
...(rules.find((r) => r.pattern === GLOBAL_KEY) || {}),
};
Object.keys(GLOBLA_RULE).forEach((key) => {
newRule[key] =
!curRule[key] || curRule[key] === val ? DEFAULT_RULE[key] : curRule[key];
!curRule[key] || curRule[key] === globalRule[key]
? DEFAULT_RULE[key]
: curRule[key];
});
rules.unshift(newRule);

View File

@@ -0,0 +1,131 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { CacheProvider } from "@emotion/react";
import createCache from "@emotion/cache";
import { logger } from "./log";
export default class ShadowDomManager {
#hostElement = null;
#reactRoot = null;
#isVisible = false;
#isProcessing = false;
_id;
_className;
_ReactComponent;
_props;
constructor({
id,
className = "",
reactComponent,
props = {},
rootElement = document.body,
}) {
if (!id || !reactComponent) {
throw new Error("ID and a React Component must be provided.");
}
this._id = id;
this._className = className;
this._ReactComponent = reactComponent;
this._props = props;
this._rootElement = rootElement;
}
get isVisible() {
return this.#isVisible;
}
show(props) {
if (this.#isVisible || this.#isProcessing) {
return;
}
if (!this.#hostElement) {
this.#isProcessing = true;
try {
this.#mount(props || this._props);
} catch (error) {
logger.warn(`Failed to mount component with id "${this._id}":`, error);
this.#isProcessing = false;
return;
} finally {
this.#isProcessing = false;
}
}
this.#hostElement.style.display = "";
this.#isVisible = true;
}
hide() {
if (!this.#isVisible || !this.#hostElement) {
return;
}
this.#hostElement.style.display = "none";
this.#isVisible = false;
}
destroy() {
if (!this.#hostElement) {
return;
}
this.#isProcessing = true;
if (this.#reactRoot) {
this.#reactRoot.unmount();
}
this.#hostElement.remove();
this.#hostElement = null;
this.#reactRoot = null;
this.#isVisible = false;
this.#isProcessing = false;
logger.info(`Component with id "${this._id}" has been destroyed.`);
}
toggle(props) {
if (this.#isVisible) {
this.hide();
} else {
this.show(props || this._props);
}
}
#mount(props) {
const host = document.createElement("div");
host.id = this._id;
if (this._className) {
host.className = this._className;
}
this._rootElement.appendChild(host);
this.#hostElement = host;
const shadowContainer = host.attachShadow({ mode: "open" });
const appRoot = document.createElement("div");
appRoot.className = `${this._id}_wrapper notranslate`;
shadowContainer.appendChild(appRoot);
const cache = createCache({
key: this._id,
prepend: true,
container: shadowContainer,
});
const enhancedProps = {
...props,
onClose: this.hide.bind(this),
};
const ComponentToRender = this._ReactComponent;
this.#reactRoot = ReactDOM.createRoot(appRoot);
this.#reactRoot.render(
<React.StrictMode>
<CacheProvider value={cache}>
<ComponentToRender {...enhancedProps} />
</CacheProvider>
</React.StrictMode>
);
}
}

View File

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

View File

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

View File

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

View File

@@ -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();

View File

@@ -67,9 +67,9 @@ export function createLoadingSVG() {
* @returns
*/
export function createLogoSVG({
width = "100%",
height = "100%",
viewBox = "-20 -20 70 70",
width = "24",
height = "24",
viewBox = "-5 -5 40 40",
isSelected = false,
} = {}) {
const svg = createSVGElement("svg", {
@@ -80,30 +80,26 @@ export function createLogoSVG({
version: "1.1",
});
const primaryColor = "#209CEE";
const secondaryColor = "#E9F5FD";
const path1Fill = isSelected ? 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 ",
fill: "#209CEE",
fill: path1Fill,
transform: "translate(0,0)",
});
const path2 = createSVGElement("path", {
d: "M0 0 C0.66 0 1.32 0 2 0 C2 2.97 2 5.94 2 9 C2.969375 8.2575 3.93875 7.515 4.9375 6.75 C5.48277344 6.33234375 6.02804688 5.9146875 6.58984375 5.484375 C8.39053593 3.83283924 8.39053593 3.83283924 9 0 C13.95 0 18.9 0 24 0 C24 0.99 24 1.98 24 3 C22.68 3 21.36 3 20 3 C20 9.27 20 15.54 20 22 C19.01 22 18.02 22 17 22 C17 15.73 17 9.46 17 3 C15.35 3 13.7 3 12 3 C11.731875 3.598125 11.46375 4.19625 11.1875 4.8125 C10.01506533 6.97224808 8.80630718 8.35790256 7 10 C8.01790655 12.27071461 8.77442829 13.80784632 10.6875 15.4375 C11.120625 15.953125 11.55375 16.46875 12 17 C11.6875 19.6875 11.6875 19.6875 11 22 C10.34 22 9.68 22 9 22 C8.773125 21.236875 8.54625 20.47375 8.3125 19.6875 C6.73268318 16.45263699 5.16717283 15.58358642 2 14 C2 16.64 2 19.28 2 22 C1.34 22 0.68 22 0 22 C0 14.74 0 7.48 0 0 Z ",
fill: "#E9F5FD",
fill: path2Fill,
transform: "translate(4,5)",
});
svg.appendChild(path1);
svg.appendChild(path2);
if (isSelected) {
const redLine = createSVGElement("path", {
d: "M0 36 L32 36",
stroke: "red",
"stroke-width": "3",
"stroke-linecap": "round",
});
svg.appendChild(redLine);
}
return svg;
}

View File

@@ -1,12 +1,47 @@
export function touchTapListener(fn, touchsLength) {
export function touchTapListener(fn, options = {}) {
const config = {
taps: 2,
fingers: 1,
delay: 300,
...options,
};
let maxTouches = 0;
let tapCount = 0;
let tapTimer = null;
const handleTouchStart = (e) => {
maxTouches = Math.max(maxTouches, e.touches.length);
};
const handleTouchend = (e) => {
if (e.touches.length === touchsLength) {
fn();
if (e.touches.length === 0) {
if (maxTouches === config.fingers) {
tapCount++;
clearTimeout(tapTimer);
if (tapCount === config.taps) {
fn(e);
tapCount = 0;
} else {
tapTimer = setTimeout(() => {
tapCount = 0;
}, config.delay);
}
} else {
tapCount = 0;
clearTimeout(tapTimer);
}
maxTouches = 0;
}
};
document.addEventListener("touchstart", handleTouchend);
document.addEventListener("touchstart", handleTouchStart, { passive: true });
document.addEventListener("touchend", handleTouchend, { passive: true });
return () => {
document.removeEventListener("touchstart", handleTouchend);
clearTimeout(tapTimer);
document.removeEventListener("touchstart", handleTouchStart);
document.removeEventListener("touchend", handleTouchend);
};
}

View File

@@ -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() {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,293 @@
import { browser } from "./browser";
import { Translator } from "./translator";
import { InputTranslator } from "./inputTranslate";
import { TransboxManager } from "./tranbox";
import { shortcutRegister } from "./shortcut";
import { sendIframeMsg } from "./iframe";
import { EVENT_KISS, newI18n } from "../config";
import { touchTapListener } from "./touch";
import { PopupManager } from "./popupManager";
import { FabManager } from "./fabManager";
import {
OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP,
OPT_SHORTCUT_SETTING,
MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
MSG_OPEN_TRANBOX,
MSG_TRANSBOX_TOGGLE,
MSG_POPUP_TOGGLE,
MSG_MOUSEHOVER_TOGGLE,
MSG_TRANSINPUT_TOGGLE,
} from "../config";
import { logger } from "./log";
export default class TranslatorManager {
#clearShortcuts = [];
#menuCommandIds = [];
#clearTouchListeners = [];
#isActive = false;
#isUserscript;
#isIframe;
#windowMessageHandler = null;
#browserMessageHandler = null;
_translator;
_transboxManager;
_inputTranslator;
_popupManager;
_fabManager;
constructor({ setting, rule, fabConfig, favWords, isIframe, isUserscript }) {
this.#isIframe = isIframe;
this.#isUserscript = isUserscript;
this._translator = new Translator({
rule,
setting,
favWords,
isUserscript,
isIframe,
});
this._transboxManager = new TransboxManager(setting);
if (!isIframe) {
this._inputTranslator = new InputTranslator(setting);
this._popupManager = new PopupManager({
translator: this._translator,
processActions: this.#processActions.bind(this),
});
this._fabManager = new FabManager({
processActions: this.#processActions.bind(this),
fabConfig,
});
}
this.#windowMessageHandler = this.#handleWindowMessage.bind(this);
this.#browserMessageHandler = this.#handleBrowserMessage.bind(this);
}
start() {
if (this.#isActive) {
logger.info("TranslatorManager is already started.");
return;
}
this.#setupMessageListeners();
this.#setupTouchOperations();
if (!this.#isIframe && this.#isUserscript) {
this.#registerShortcuts();
this.#registerMenus();
}
this.#isActive = true;
logger.info("TranslatorManager started.");
}
stop() {
if (!this.#isActive) {
logger.info("TranslatorManager is not running.");
return;
}
// 移除消息监听器
if (this.#isUserscript) {
window.removeEventListener("message", this.#windowMessageHandler);
} else if (
browser.runtime.onMessage.hasListener(this.#browserMessageHandler)
) {
browser.runtime.onMessage.removeListener(this.#browserMessageHandler);
}
// 已注册的快捷键
this.#clearShortcuts.forEach((clear) => clear());
this.#clearShortcuts = [];
// 触屏
this.#clearTouchListeners.forEach((clear) => clear());
this.#clearTouchListeners = [];
// 油猴菜单
if (globalThis.GM && this.#menuCommandIds.length > 0) {
this.#menuCommandIds.forEach((id) =>
globalThis.GM.unregisterMenuCommand(id)
);
this.#menuCommandIds = [];
}
// 子模块
this._popupManager?.destroy();
this._fabManager?.destroy();
this._transboxManager?.disable();
this._inputTranslator?.disable();
this._translator.stop();
this.#isActive = false;
logger.info("TranslatorManager stopped.");
}
#setupMessageListeners() {
if (this.#isUserscript) {
window.addEventListener("message", this.#windowMessageHandler);
} else {
browser.runtime.onMessage.addListener(this.#browserMessageHandler);
if (this.#isIframe) {
window.addEventListener("message", this.#windowMessageHandler);
}
}
}
#setupTouchOperations() {
if (this.#isIframe) return;
const { touchModes = [2] } = this._translator.setting;
if (touchModes.length === 0) {
return;
}
const handleTap = () => {
this.#processActions({ action: MSG_TRANS_TOGGLE });
};
const handleListener = (mode) => {
let options = null;
switch (mode) {
case 2:
case 3:
case 4:
options = { taps: 1, fingers: mode };
break;
case 5:
options = { taps: 2, fingers: 1 };
break;
case 6:
options = { taps: 3, fingers: 1 };
break;
case 7:
options = { taps: 2, fingers: 2 };
break;
default:
}
if (options) {
this.#clearTouchListeners.push(touchTapListener(handleTap, options));
}
};
touchModes.forEach((mode) => handleListener(mode));
}
#handleWindowMessage(event) {
this.#processActions(event.data);
}
#handleBrowserMessage(message, sender, sendResponse) {
const result = this.#processActions(message, true);
const response = result || {
rule: this._translator.rule,
setting: this._translator.setting,
};
sendResponse(response);
return true;
}
#registerShortcuts() {
const { shortcuts } = this._translator.setting;
this.#clearShortcuts = [
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () =>
this.#processActions({ action: MSG_TRANS_TOGGLE })
),
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () =>
this.#processActions({ action: MSG_TRANS_TOGGLE_STYLE })
),
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () =>
this.#processActions({ action: MSG_POPUP_TOGGLE })
),
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () =>
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank")
),
];
}
#registerMenus() {
if (!globalThis.GM) return;
const { contextMenuType, uiLang } = this._translator.setting;
if (contextMenuType === 0) return;
const i18n = newI18n(uiLang || "zh");
const GM = globalThis.GM;
this.#menuCommandIds = [
GM.registerMenuCommand(
i18n("translate_switch"),
() => this.#processActions({ action: MSG_TRANS_TOGGLE }),
"Q"
),
GM.registerMenuCommand(
i18n("toggle_style"),
() => this.#processActions({ action: MSG_TRANS_TOGGLE_STYLE }),
"C"
),
GM.registerMenuCommand(
i18n("open_menu"),
() => this.#processActions({ action: MSG_POPUP_TOGGLE }),
"K"
),
GM.registerMenuCommand(
i18n("open_setting"),
() => window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank"),
"O"
),
];
}
#processActions({ action, args } = {}, fromExt = false) {
if (!action) return;
if (!fromExt) {
sendIframeMsg(action, args);
}
switch (action) {
case MSG_TRANS_TOGGLE:
this._translator.toggle();
break;
case MSG_TRANS_TOGGLE_STYLE:
this._translator.toggleStyle();
break;
case MSG_TRANS_GETRULE:
break;
case MSG_TRANS_PUTRULE:
this._translator.updateRule(args);
break;
case MSG_OPEN_TRANBOX:
document.dispatchEvent(
new CustomEvent(EVENT_KISS, {
detail: { action: MSG_OPEN_TRANBOX },
})
);
break;
case MSG_POPUP_TOGGLE:
this._popupManager?.toggle();
break;
case MSG_TRANSBOX_TOGGLE:
this._transboxManager?.toggle();
this._translator.toggleTransbox();
break;
case MSG_MOUSEHOVER_TOGGLE:
this._translator.toggleMouseHover();
break;
case MSG_TRANSINPUT_TOGGLE:
this._inputTranslator?.toggle();
this._translator.toggleInputTranslate();
break;
default:
logger.info(`Message action is unavailable: ${action}`);
return { error: `Message action is unavailable: ${action}` };
}
}
}

View File

@@ -1,3 +1,5 @@
import { logger } from "./log";
export const trustedTypesHelper = (() => {
const POLICY_NAME = "kiss-translator-policy";
let policy = null;
@@ -13,7 +15,7 @@ export const trustedTypesHelper = (() => {
if (err.message.includes("already exists")) {
policy = globalThis.trustedTypes.policies.get(POLICY_NAME);
} else {
console.error("cont create Trusted Types", err);
logger.info("cont create Trusted Types", err);
}
}
}

View File

@@ -59,41 +59,84 @@ export const sleep = (delay) =>
*/
export const debounce = (func, delay = 200) => {
let timer = null;
return (...args) => {
const debouncedFunc = (...args) => {
timer && clearTimeout(timer);
timer = setTimeout(() => {
func(...args);
clearTimeout(timer);
timer = null;
}, delay);
};
debouncedFunc.cancel = () => {
clearTimeout(timer);
timer = null;
};
return debouncedFunc;
};
/**
* 节流函数
* @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 +416,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);
}

View File

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

View File

@@ -1,5 +1,6 @@
import { logger } from "../libs/log.js";
import { truncateWords } from "../libs/utils.js";
import { 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 };
}
}

179
src/subtitle/Menus.js Normal file
View File

@@ -0,0 +1,179 @@
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, showOrigin } = 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="showOrigin"
value={showOrigin}
label={i18n("show_origin_subtitle")}
/>
<Switch
onChange={handleChange}
name="skipAd"
value={skipAd}
label={i18n("is_skip_ad")}
/>
<Button
label={`${status} [${progressed}%] `}
onClick={downloadSubtitle}
disabled={progressed !== 100}
/>
</div>
);
}

View File

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

View File

@@ -1,39 +1,69 @@
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,
OPT_LANGS_SPEC_DEFAULT,
} 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 { i18n } from "../config";
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.#i18n = i18n(setting.uiLang || "zh");
this.#setting = { ...setting, isAISegment: false, showOrigin: 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 +77,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 +127,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 +152,11 @@ class YouTubeCaptionProvider {
if (node.matches(adLayoutSelector)) {
logger.debug("Youtube Provider: Ad ends!");
if (videoEl) {
if (!this.#setting.showOrigin) {
this.#hideYtCaption();
}
if (videoEl && skipAd) {
videoEl.playbackRate = 1;
}
if (this.#managerInstance) {
@@ -145,61 +195,109 @@ 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();
} else if (name === "showOrigin") {
this.#toggleShowOrigin();
}
}
#injectToggleButton(ytControls) {
this.#ytControls = ytControls;
#toggleShowOrigin() {
if (this.#setting.showOrigin) {
this.#destroyManager();
} else {
this.#startManager();
}
}
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, showOrigin } =
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, // AI智能断句
skipAd, // 快进广告
isBilingual, // 双语显示
showOrigin, // 显示原字幕
},
},
});
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();
this.#sendMenusMsg({
action: MSG_MENUS_PROGRESSED,
data: this.#progressed,
});
} 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?.before(kissControls);
ytControls?.prepend(kissControls);
}
#isSameLang(lang1, lang2) {
@@ -287,11 +385,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);
@@ -322,37 +415,53 @@ class YouTubeCaptionProvider {
return [];
}
#getFromLang(lang) {
if (lang === "zh") {
return "zh-CN";
}
return (
OPT_LANGS_SPEC_DEFAULT.get(lang) ||
OPT_LANGS_SPEC_DEFAULT.get(lang.slice(0, 2)) ||
OPT_LANGS_TO_CODE[OPT_TRANS_MICROSOFT].get(lang) ||
OPT_LANGS_TO_CODE[OPT_TRANS_MICROSOFT].get(lang.slice(0, 2)) ||
"auto"
);
}
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,122 +472,146 @@ class YouTubeCaptionProvider {
responseText
);
if (!events?.length) {
logger.info("Youtube Provider: SubtitleEvents not got.");
logger.debug("Youtube Provider: events not got:", videoId);
return;
}
const lang = potUrl.searchParams.get("lang");
const fromLang =
OPT_LANGS_TO_CODE[OPT_TRANS_MICROSOFT].get(lang) ||
OPT_LANGS_TO_CODE[OPT_TRANS_MICROSOFT].get(lang.slice(0, 2)) ||
"auto";
const fromLang = this.#getFromLang(lang);
logger.debug(
`Youtube Provider: fromLang: ${fromLang}, toLang: ${toLang}`
`Youtube Provider: lang: ${lang}, 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);
this.#showNotification(this.#i18n("subtitle_same_lang"));
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() {
this.#progressed = 0;
this.#subtitles = [];
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();
}
if (eventChunks.length > 1) {
const remainingChunks = eventChunks.slice(1);
this.#processRemainingChunksAsync({
chunks: remainingChunks,
videoId,
chunkEvents: firstChunkEvents,
fromLang,
toLang,
segApiSetting,
});
if (!firstBatchSubtitles?.length) {
this.#onCaptionsReady({
videoId,
subtitles: subtitlesFallback(),
fromLang,
isInitialLoad: true,
});
return;
}
const processed = Math.floor(100 / eventChunks.length);
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, processed];
} 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 +619,12 @@ 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.#setting.showOrigin) {
return;
}
if (!this.#subtitles.length) {
this.#showNotification(this.#i18n("waitting_for_subtitle"));
return;
}
@@ -505,15 +639,13 @@ class YouTubeCaptionProvider {
this.#managerInstance = new BilingualSubtitleManager({
videoEl,
formattedSubtitles: this.#subtitles,
translationService: apiTranslate,
setting: { ...this.#setting, fromLang: this.#fromLang },
});
this.#managerInstance.start();
this.#showNotification(this.#i18n("subtitle_load_succeed"));
const ytCaption = document.querySelector(YT_CAPTION_SELECT);
ytCaption && (ytCaption.style.display = "none");
this.#hideYtCaption();
}
#destroyManager() {
@@ -526,6 +658,15 @@ class YouTubeCaptionProvider {
this.#managerInstance.destroy();
this.#managerInstance = null;
this.#showYtCaption();
}
#hideYtCaption() {
const ytCaption = document.querySelector(YT_CAPTION_SELECT);
ytCaption && (ytCaption.style.display = "none");
}
#showYtCaption() {
const ytCaption = document.querySelector(YT_CAPTION_SELECT);
ytCaption && (ytCaption.style.display = "block");
}
@@ -545,8 +686,13 @@ class YouTubeCaptionProvider {
if (noSpaceLanguages.some((l) => lang?.startsWith(l))) {
const subtitles = [];
if (this.#isQualityPoor(flatEvents, 5, 0.5)) {
return flatEvents;
}
let currentLine = null;
const MAX_LENGTH = 100;
const MAX_LENGTH = 30;
for (const segment of flatEvents) {
if (segment.text) {
@@ -590,7 +736,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 +890,7 @@ class YouTubeCaptionProvider {
return sentences;
}
#flatEvents(events = []) {
#genFlatEvents(events = []) {
const segments = [];
let buffer = null;
@@ -837,7 +983,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 +1001,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 +1010,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 = Math.floor((chunkNum * 100) / (chunks.length + 1));
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 +1070,7 @@ class YouTubeCaptionProvider {
}
}
#showNotification(message, duration = 3000) {
#showNotification(message, duration = 2000) {
if (!this.#notificationEl) this.#createNotificationElement();
this.#notificationEl.textContent = message;
this.#notificationEl.style.opacity = "1";

View File

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

View File

@@ -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");
}

View File

@@ -0,0 +1,70 @@
import Fab from "@mui/material/Fab";
import TranslateIcon from "@mui/icons-material/Translate";
import ThemeProvider from "../../hooks/Theme";
import Draggable from "./Draggable";
import { useState, useMemo, useCallback } from "react";
import { SettingProvider } from "../../hooks/Setting";
import { MSG_TRANS_TOGGLE, MSG_POPUP_TOGGLE } from "../../config";
import useWindowSize from "../../hooks/WindowSize";
export default function ContentFab({
fabConfig: { x: fabX, y: fabY, fabClickAction = 0 } = {},
processActions,
}) {
const fabWidth = 40;
const windowSize = useWindowSize();
const [moved, setMoved] = useState(false);
const handleStart = useCallback(() => {
setMoved(false);
}, []);
const handleMove = useCallback(() => {
setMoved(true);
}, []);
const handleClick = useCallback(() => {
if (!moved) {
if (fabClickAction === 1) {
processActions({ action: MSG_TRANS_TOGGLE });
} else {
processActions({ action: MSG_POPUP_TOGGLE });
}
}
}, [moved, fabClickAction, processActions]);
const fabProps = useMemo(
() => ({
windowSize,
width: fabWidth,
height: fabWidth,
left: fabX ?? -fabWidth,
top: fabY ?? windowSize.h / 2,
}),
[windowSize, fabWidth, fabX, fabY]
);
return (
<SettingProvider>
<ThemeProvider>
<Draggable
key="fab"
snapEdge
{...fabProps}
onStart={handleStart}
onMove={handleMove}
handler={
<Fab size="small" color="primary" onClick={handleClick}>
<TranslateIcon
sx={{
width: 24,
height: 24,
}}
/>
</Fab>
}
/>
</ThemeProvider>
</SettingProvider>
);
}

View File

@@ -50,7 +50,7 @@ export default function Draggable({
height,
left,
top,
show,
show = true,
snapEdge,
onStart,
onMove,

View File

@@ -1,168 +1,62 @@
import Fab from "@mui/material/Fab";
import TranslateIcon from "@mui/icons-material/Translate";
import ThemeProvider from "../../hooks/Theme";
import Draggable from "./Draggable";
import { useEffect, useState, useMemo, useCallback } from "react";
import { useEffect, useMemo, useCallback, useState } from "react";
import { SettingProvider } from "../../hooks/Setting";
import Popup from "../Popup";
import { debounce } from "../../libs/utils";
import { isGm } from "../../libs/client";
import Header from "../Popup/Header";
import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider";
import {
DEFAULT_SHORTCUTS,
OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP,
OPT_SHORTCUT_SETTING,
MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
} from "../../config";
import { shortcutRegister } from "../../libs/shortcut";
import { sendIframeMsg } from "../../libs/iframe";
import { kissLog } from "../../libs/log";
import { getI18n } from "../../hooks/I18n";
import useWindowSize from "../../hooks/WindowSize";
import { EVENT_KISS, MSG_OPEN_OPTIONS, MSG_POPUP_TOGGLE } from "../../config";
import PopupCont from "../Popup/PopupCont";
import { isExt } from "../../libs/client";
import { sendBgMsg } from "../../libs/msg";
export default function Action({ translator, fab }) {
const fabWidth = 40;
const [showPopup, setShowPopup] = useState(false);
const [windowSize, setWindowSize] = useState({
w: window.innerWidth,
h: window.innerHeight,
});
const [moved, setMoved] = useState(false);
export default function Action({ translator, processActions }) {
const [showPopup, setShowPopup] = useState(true);
const [rule, setRule] = useState(translator.rule);
const [setting, setSetting] = useState(translator.setting);
const windowSize = useWindowSize();
const { fabClickAction = 0 } = fab || {};
const handleWindowResize = useMemo(
() =>
debounce(() => {
setWindowSize({
w: window.innerWidth,
h: window.innerHeight,
});
}),
[]
);
const handleWindowClick = (e) => {
setShowPopup(false);
};
const handleStart = useCallback(() => {
setMoved(false);
}, []);
const handleMove = useCallback(() => {
setMoved(true);
const handleOpenSetting = useCallback(() => {
if (isExt) {
sendBgMsg(MSG_OPEN_OPTIONS);
} else {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
}
}, []);
useEffect(() => {
if (!isGm) {
return;
}
// 注册快捷键
const shortcuts = translator.setting.shortcuts || DEFAULT_SHORTCUTS;
const clearShortcuts = [
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () => {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
setShowPopup(false);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () => {
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
setShowPopup(false);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () => {
setShowPopup((pre) => !pre);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () => {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
}),
];
return () => {
clearShortcuts.forEach((fn) => {
fn();
});
const handleWindowClick = () => {
setShowPopup(false);
};
}, [translator]);
useEffect(() => {
if (!isGm) {
return;
}
// 注册菜单
try {
const menuCommandIds = [];
const { contextMenuType, uiLang } = translator.setting;
contextMenuType !== 0 &&
menuCommandIds.push(
GM.registerMenuCommand(
getI18n(uiLang, "translate_switch"),
(event) => {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
setShowPopup(false);
},
"Q"
),
GM.registerMenuCommand(
getI18n(uiLang, "toggle_style"),
(event) => {
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
setShowPopup(false);
},
"C"
),
GM.registerMenuCommand(
getI18n(uiLang, "open_menu"),
(event) => {
setShowPopup((pre) => !pre);
},
"K"
),
GM.registerMenuCommand(
getI18n(uiLang, "open_setting"),
(event) => {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
},
"O"
)
);
return () => {
menuCommandIds.forEach((id) => {
GM.unregisterMenuCommand(id);
});
};
} catch (err) {
kissLog("registerMenuCommand", err);
}
}, [translator]);
useEffect(() => {
window.addEventListener("resize", handleWindowResize);
return () => {
window.removeEventListener("resize", handleWindowResize);
};
}, [handleWindowResize]);
useEffect(() => {
window.addEventListener("click", handleWindowClick);
return () => {
window.removeEventListener("click", handleWindowClick);
};
}, []);
useEffect(() => {
const handleStatusUpdate = (event) => {
if (event.detail?.action === MSG_POPUP_TOGGLE) {
setShowPopup((pre) => !pre);
}
};
document.addEventListener(EVENT_KISS, handleStatusUpdate);
return () => {
document.removeEventListener(EVENT_KISS, handleStatusUpdate);
};
}, []);
useEffect(() => {
if (showPopup) {
setRule(translator.rule);
setSetting(translator.setting);
}
}, [showPopup, translator]);
const popProps = useMemo(() => {
const width = Math.min(windowSize.w, 300);
const width = Math.min(windowSize.w, 360);
const height = Math.min(windowSize.h, 442);
const left = (windowSize.w - width) / 2;
const top = (windowSize.h - height) / 2;
@@ -175,67 +69,38 @@ export default function Action({ translator, fab }) {
};
}, [windowSize]);
const fabProps = {
windowSize,
width: fabWidth,
height: fabWidth,
left: fab.x ?? -fabWidth,
top: fab.y ?? windowSize.h / 2,
};
return (
<SettingProvider>
<ThemeProvider>
<Draggable
key="pop"
{...popProps}
show={showPopup}
onStart={handleStart}
onMove={handleMove}
usePaper
handler={
<Box style={{ cursor: "move" }}>
<Header setShowPopup={setShowPopup} />
<Divider />
</Box>
}
>
{showPopup && (
<Popup setShowPopup={setShowPopup} translator={translator} />
)}
</Draggable>
<Draggable
key="fab"
snapEdge
{...fabProps}
show={fab.isHide ? false : !showPopup}
onStart={handleStart}
onMove={handleMove}
handler={
<Fab
size="small"
color="primary"
onClick={(e) => {
if (!moved) {
if (fabClickAction === 1) {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
{showPopup && (
<Draggable
key="pop"
{...popProps}
usePaper
handler={
<Box style={{ cursor: "move" }}>
<Header
onClose={() => {
setShowPopup(false);
} else {
setShowPopup((pre) => !pre);
}
}
}}
>
<TranslateIcon
sx={{
width: 24,
height: 24,
}}
}}
/>
<Divider />
</Box>
}
>
<Box width={360}>
<PopupCont
rule={rule}
setting={setting}
setRule={setRule}
setSetting={setSetting}
handleOpenSetting={handleOpenSetting}
processActions={processActions}
isContent={true}
/>
</Fab>
}
/>
</Box>
</Draggable>
)}
</ThemeProvider>
</SettingProvider>
);

View File

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

View File

@@ -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 {

View File

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

View File

@@ -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"),

View File

@@ -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>
);
}

View File

@@ -32,6 +32,7 @@ export default function ReusableAutocomplete({
name: name,
value: newValue,
},
preventDefault: () => {},
};
onChange(syntheticEvent);
}

View File

@@ -10,12 +10,13 @@ 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,
OPT_SPLIT_PARAGRAPH_DISABLE,
OPT_HIGHLIGHT_WORDS_DISABLE,
OPT_SPLIT_PARAGRAPH_ALL,
OPT_HIGHLIGHT_WORDS_ALL,
} from "../../config";
import { useState, useEffect, useMemo } from "react";
import { useI18n } from "../../hooks/I18n";
@@ -49,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";
@@ -59,11 +59,12 @@ import AddIcon from "@mui/icons-material/Add";
import EditIcon from "@mui/icons-material/Edit";
import CancelIcon from "@mui/icons-material/Cancel";
import SaveIcon from "@mui/icons-material/Save";
import ValidationInput from "../../hooks/ValidationInput";
import { kissLog } from "../../libs/log";
import { useApiList } from "../../hooks/Api";
import ShowMoreButton from "./ShowMoreButton";
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;
@@ -82,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);
@@ -98,6 +100,8 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
terms = "",
aiTerms = "",
termsStyle = "",
highlightStyle = "color: red;",
textExtStyle = "",
selectStyle = "",
parentStyle = "",
grandStyle = "",
@@ -108,8 +112,8 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
toLang,
textStyle,
transOpen,
bgColor,
textDiyStyle,
// bgColor,
// textDiyStyle,
transOnly = "false",
autoScan = "true",
hasRichText = "true",
@@ -124,19 +128,15 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
transStartHook = "",
transEndHook = "",
// transRemoveHook = "",
splitParagraph = OPT_SPLIT_PARAGRAPH_DISABLE,
splitLength = 0,
highlightWords = OPT_HIGHLIGHT_WORDS_DISABLE,
} = formValues;
const isModified = useMemo(() => {
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) {
@@ -423,6 +423,59 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="splitParagraph"
value={splitParagraph}
label={i18n("split_paragraph")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{OPT_SPLIT_PARAGRAPH_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<ValidationInput
fullWidth
size="small"
label={i18n("split_length")}
type="number"
name="splitLength"
value={splitLength}
disabled={disabled}
onChange={handleChange}
min={0}
max={1000}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="highlightWords"
value={highlightWords}
label={i18n("highlight_words")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{OPT_HIGHLIGHT_WORDS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
@@ -468,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
@@ -558,6 +566,26 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
maxRows={10}
multiline
/>
<TextField
size="small"
label={i18n("highlight_style")}
name="highlightStyle"
value={highlightStyle}
disabled={disabled}
onChange={handleChange}
maxRows={10}
multiline
/>
<TextField
size="small"
label={i18n("text_ext_style")}
name="textExtStyle"
value={textExtStyle}
disabled={disabled}
onChange={handleChange}
maxRows={10}
multiline
/>
<TextField
size="small"
label={i18n("selector_style")}

View File

@@ -94,7 +94,7 @@ export default function Settings() {
newlineLength = TRANS_NEWLINE_LENGTH,
httpTimeout = DEFAULT_HTTP_TIMEOUT,
contextMenuType = 1,
touchTranslate = 2,
touchModes = [2],
blacklist = DEFAULT_BLACKLIST.join(",\n"),
csplist = DEFAULT_CSPLIST.join(",\n"),
orilist = DEFAULT_ORILIST.join(",\n"),
@@ -105,6 +105,7 @@ export default function Settings() {
skipLangs = [],
// detectRemote = true,
transAllnow = false,
rootMargin = 500,
} = setting;
const { isHide = false, fabClickAction = 0 } = fab || {};
@@ -259,7 +260,7 @@ export default function Settings() {
name="httpTimeout"
value={httpTimeout}
onChange={handleChange}
min={5000}
min={1000}
max={60000}
/>
</Grid>
@@ -268,12 +269,15 @@ export default function Settings() {
select
fullWidth
size="small"
name="touchTranslate"
value={touchTranslate}
name="touchModes"
value={touchModes}
label={i18n("touch_translate_shortcut")}
onChange={handleChange}
SelectProps={{
multiple: true,
}}
>
{[0, 2, 3, 4].map((item) => (
{[0, 2, 3, 4, 5, 6, 7].map((item) => (
<MenuItem key={item} value={item}>
{i18n(`touch_tap_${item}`)}
</MenuItem>
@@ -295,34 +299,6 @@ export default function Settings() {
<MenuItem value={2}>{i18n("secondary_context_menus")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="transAllnow"
value={transAllnow}
label={i18n("trigger_mode")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("mk_pagescroll")}</MenuItem>
<MenuItem value={true}>{i18n("mk_pageopen")}</MenuItem>
</TextField>
</Grid>
{/* <Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="detectRemote"
value={detectRemote}
label={i18n("detect_lang_remote")}
onChange={handleChange}
>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
</TextField>
</Grid> */}
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
@@ -341,6 +317,47 @@ export default function Settings() {
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="transAllnow"
value={transAllnow}
label={i18n("trigger_mode")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("mk_pagescroll")}</MenuItem>
<MenuItem value={true}>{i18n("mk_pageopen")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<ValidationInput
fullWidth
size="small"
label={i18n("pagescroll_root_margin")}
type="number"
name="rootMargin"
value={rootMargin}
onChange={handleChange}
min={0}
max={10000}
/>
</Grid>
{/* <Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="detectRemote"
value={detectRemote}
label={i18n("detect_lang_remote")}
onChange={handleChange}
>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
</TextField>
</Grid> */}
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
@@ -379,18 +396,19 @@ export default function Settings() {
</MenuItem>
))}
</TextField>
<TextField
size="small"
label={i18n("translate_blacklist")}
helperText={i18n("pattern_helper")}
name="blacklist"
value={blacklist}
onChange={handleChange}
maxRows={10}
multiline
/>
{isExt ? (
<>
<TextField
size="small"
label={i18n("disabled_orilist")}
helperText={i18n("pattern_helper")}
name="orilist"
value={orilist}
onChange={handleChange}
multiline
/>
<TextField
select
fullWidth
@@ -409,6 +427,15 @@ export default function Settings() {
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
</TextField>
<TextField
size="small"
label={i18n("disabled_orilist")}
helperText={i18n("pattern_helper")}
name="orilist"
value={orilist}
onChange={handleChange}
multiline
/>
<TextField
size="small"
label={i18n("disabled_csplist")}
@@ -453,17 +480,6 @@ export default function Settings() {
</Box>
</>
)}
<TextField
size="small"
label={i18n("translate_blacklist")}
helperText={i18n("pattern_helper")}
name="blacklist"
value={blacklist}
onChange={handleChange}
maxRows={10}
multiline
/>
</Stack>
</Box>
);

View 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>
);
}

View File

@@ -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>

View File

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

View File

@@ -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("");
@@ -98,7 +99,7 @@ export default function Options() {
}
return (
<SettingProvider>
<SettingProvider isSettingPage={true}>
<ThemeProvider>
<AlertProvider>
<ConfirmProvider>
@@ -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 />} />

View File

@@ -5,7 +5,7 @@ import Stack from "@mui/material/Stack";
import DarkModeButton from "../Options/DarkModeButton";
import Typography from "@mui/material/Typography";
export default function Header({ setShowPopup }) {
export default function Header({ onClose }) {
const handleHomepage = () => {
window.open(process.env.REACT_APP_HOMEPAGE, "_blank");
};
@@ -33,10 +33,10 @@ export default function Header({ setShowPopup }) {
</Typography>
</Stack>
{setShowPopup ? (
{onClose ? (
<IconButton
onClick={() => {
setShowPopup(false);
onClose();
}}
>
<CloseIcon />

View File

@@ -0,0 +1,439 @@
import { useState, useEffect, useMemo } from "react";
import Stack from "@mui/material/Stack";
import MenuItem from "@mui/material/MenuItem";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Button from "@mui/material/Button";
import Grid from "@mui/material/Grid";
import { sendBgMsg, sendTabMsg, getCurTab } from "../../libs/msg";
import { isExt } from "../../libs/client";
import { useI18n } from "../../hooks/I18n";
import TextField from "@mui/material/TextField";
import {
MSG_TRANS_TOGGLE,
MSG_TRANS_PUTRULE,
MSG_SAVE_RULE,
MSG_COMMAND_SHORTCUTS,
MSG_TRANSBOX_TOGGLE,
MSG_MOUSEHOVER_TOGGLE,
MSG_TRANSINPUT_TOGGLE,
OPT_LANGS_FROM,
OPT_LANGS_TO,
} 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,
setting,
setRule,
setSetting,
handleOpenSetting,
processActions,
isContent = false,
}) {
const i18n = useI18n();
const [commands, setCommands] = useState({});
const { allTextStyles } = useAllTextStyles();
const handleTransToggle = async (e) => {
try {
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
if (!processActions) {
await sendTabMsg(MSG_TRANS_TOGGLE);
} else {
processActions({ action: MSG_TRANS_TOGGLE });
}
} catch (err) {
kissLog("toggle trans", err);
}
};
const handleTransboxToggle = async (e) => {
try {
setSetting((pre) => ({
...pre,
tranboxSetting: { ...pre.tranboxSetting, transOpen: e.target.checked },
}));
if (!processActions) {
await sendTabMsg(MSG_TRANSBOX_TOGGLE);
} else {
processActions({ action: MSG_TRANSBOX_TOGGLE });
}
} catch (err) {
kissLog("toggle transbox", err);
}
};
const handleMousehoverToggle = async (e) => {
try {
setSetting((pre) => ({
...pre,
mouseHoverSetting: {
...pre.mouseHoverSetting,
useMouseHover: e.target.checked,
},
}));
if (!processActions) {
await sendTabMsg(MSG_MOUSEHOVER_TOGGLE);
} else {
processActions({ action: MSG_MOUSEHOVER_TOGGLE });
}
} catch (err) {
kissLog("toggle mousehover", err);
}
};
const handleInputTransToggle = async (e) => {
try {
setSetting((pre) => ({
...pre,
inputRule: {
...pre.inputRule,
transOpen: e.target.checked,
},
}));
if (!processActions) {
await sendTabMsg(MSG_TRANSINPUT_TOGGLE);
} else {
processActions({ action: MSG_TRANSINPUT_TOGGLE });
}
} catch (err) {
kissLog("toggle inputtrans", err);
}
};
const handleChange = async (e) => {
try {
let { name, value, checked } = e.target;
if (name === "isPlainText") {
value = checked;
}
setRule((pre) => ({ ...pre, [name]: value }));
if (!processActions) {
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
} else {
processActions({ action: MSG_TRANS_PUTRULE, args: { [name]: value } });
}
} catch (err) {
kissLog("update rule", err);
}
};
const handleClearCache = () => {
tryClearCaches();
};
const handleSaveRule = async () => {
try {
let href = "";
if (!isContent) {
const tab = await getCurTab();
href = tab.url;
} else {
href = window.location?.href;
}
if (!href || typeof href !== "string") {
return;
}
const pattern = parseUrlPattern(href);
const curRule = { ...rule, pattern };
if (isExt && isContent) {
sendBgMsg(MSG_SAVE_RULE, curRule);
} else {
saveRule(curRule);
}
} catch (err) {
kissLog("save rule", err);
}
};
useEffect(() => {
(async () => {
try {
const commands = {};
if (isExt) {
const res = await sendBgMsg(MSG_COMMAND_SHORTCUTS);
res.forEach(({ name, shortcut }) => {
commands[name] = shortcut;
});
} else {
const shortcuts = setting.shortcuts;
if (shortcuts) {
Object.entries(shortcuts).forEach(([key, val]) => {
commands[key] = val.join("+");
});
}
}
setCommands(commands);
} catch (err) {
kissLog("query cmds", err);
}
})();
}, [setting.shortcuts]);
const optApis = useMemo(
() =>
setting.transApis
.filter((api) => !api.isDisabled)
.map((api) => ({
key: api.apiSlug,
name: api.apiName || api.apiSlug,
})),
[setting.transApis]
);
const tranboxEnabled = setting.tranboxSetting.transOpen;
const mouseHoverEnabled = setting.mouseHoverSetting.useMouseHover;
const inputTransEnabled = setting.inputRule.transOpen;
const {
transOpen,
apiSlug,
fromLang,
toLang,
textStyle,
autoScan,
transOnly,
hasRichText,
hasShadowroot,
isPlainText = false,
} = rule;
return (
<Stack sx={{ p: 2 }} spacing={2}>
<Grid container columns={12} spacing={1}>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch
checked={transOpen === "true"}
onChange={handleTransToggle}
/>
}
label={
commands["toggleTranslate"]
? `${i18n("translate_alt")}(${commands["toggleTranslate"]})`
: i18n("translate_alt")
}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="autoScan"
value={autoScan === "true" ? "false" : "true"}
checked={autoScan === "true"}
onChange={handleChange}
/>
}
label={i18n("autoscan_alt")}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="hasShadowroot"
value={hasShadowroot === "true" ? "false" : "true"}
checked={hasShadowroot === "true"}
onChange={handleChange}
/>
}
label={i18n("shadowroot_alt")}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="hasRichText"
value={hasRichText === "true" ? "false" : "true"}
checked={hasRichText === "true"}
onChange={handleChange}
/>
}
label={i18n("richtext_alt")}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="transOnly"
value={transOnly === "true" ? "false" : "true"}
checked={transOnly === "true"}
onChange={handleChange}
/>
}
label={i18n("transonly_alt")}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="tranboxEnabled"
value={!tranboxEnabled}
checked={tranboxEnabled}
onChange={handleTransboxToggle}
/>
}
label={i18n("selection_translate")}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="mouseHoverEnabled"
value={!mouseHoverEnabled}
checked={mouseHoverEnabled}
onChange={handleMousehoverToggle}
/>
}
label={i18n("mousehover_translate")}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="inputTransEnabled"
value={!inputTransEnabled}
checked={inputTransEnabled}
onChange={handleInputTransToggle}
/>
}
label={i18n("input_translate")}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="isPlainText"
value={!isPlainText}
checked={isPlainText}
onChange={handleChange}
/>
}
label={i18n("plain_text_translate")}
/>
</Grid>
</Grid>
<Stack direction="row" spacing={2}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
size="small"
value={fromLang}
name="fromLang"
label={i18n("from_lang")}
onChange={handleChange}
fullWidth
>
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
size="small"
value={toLang}
name="toLang"
label={i18n("to_lang")}
onChange={handleChange}
fullWidth
>
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Stack>
<Stack direction="row" spacing={2}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
size="small"
value={apiSlug}
name="apiSlug"
label={i18n("translate_service")}
onChange={handleChange}
fullWidth
>
{optApis.map(({ key, name }) => (
<MenuItem key={key} value={key}>
{name}
</MenuItem>
))}
</TextField>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
size="small"
value={textStyle}
name="textStyle"
label={
commands["toggleStyle"]
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
: i18n("text_style_alt")
}
onChange={handleChange}
fullWidth
>
{allTextStyles.map((item) => (
<MenuItem key={item.styleSlug} value={item.styleSlug}>
{item.styleName}
</MenuItem>
))}
</TextField>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Button variant="text" onClick={handleSaveRule}>
{i18n("save_rule")}
</Button>
<Button variant="text" onClick={handleClearCache}>
{i18n("clear_cache")}
</Button>
<Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")}
</Button>
</Stack>
</Stack>
);
}

View File

@@ -1,182 +1,26 @@
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect, useCallback } from "react";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import MenuItem from "@mui/material/MenuItem";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Button from "@mui/material/Button";
import Grid from "@mui/material/Grid";
import { sendBgMsg, sendTabMsg, getCurTab } from "../../libs/msg";
import { sendTabMsg } from "../../libs/msg";
import { browser } from "../../libs/browser";
import { isExt } from "../../libs/client";
import { useI18n } from "../../hooks/I18n";
import TextField from "@mui/material/TextField";
import Divider from "@mui/material/Divider";
import Header from "./Header";
import {
MSG_TRANS_TOGGLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
MSG_OPEN_OPTIONS,
MSG_SAVE_RULE,
MSG_COMMAND_SHORTCUTS,
MSG_TRANSBOX_TOGGLE,
MSG_MOUSEHOVER_TOGGLE,
MSG_TRANSINPUT_TOGGLE,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_STYLE_ALL,
} from "../../config";
import { sendIframeMsg } from "../../libs/iframe";
import { saveRule } from "../../libs/rules";
import { tryClearCaches } from "../../libs/cache";
import { MSG_TRANS_GETRULE } from "../../config";
import { kissLog } from "../../libs/log";
import { parseUrlPattern } from "../../libs/utils";
import PopupCont from "./PopupCont";
// 插件popup没有参数
// 网页弹框有
export default function Popup({ setShowPopup, translator }) {
export default function Popup() {
const i18n = useI18n();
const [rule, setRule] = useState(translator?.rule);
const [setting, setSetting] = useState(translator?.setting);
const [commands, setCommands] = useState({});
const [rule, setRule] = useState(null);
const [setting, setSetting] = useState(null);
const handleOpenSetting = () => {
if (!translator) {
browser?.runtime.openOptionsPage();
} else if (isExt) {
sendBgMsg(MSG_OPEN_OPTIONS);
} else {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
}
setShowPopup && setShowPopup(false);
};
const handleTransToggle = async (e) => {
try {
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
if (!translator) {
await sendTabMsg(MSG_TRANS_TOGGLE);
} else {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
}
} catch (err) {
kissLog("toggle trans", err);
}
};
const handleTransboxToggle = async (e) => {
try {
setSetting((pre) => ({
...pre,
tranboxSetting: { ...pre.tranboxSetting, transOpen: e.target.checked },
}));
if (!translator) {
await sendTabMsg(MSG_TRANSBOX_TOGGLE);
} else {
translator.toggleTransbox();
sendIframeMsg(MSG_TRANSBOX_TOGGLE);
}
} catch (err) {
kissLog("toggle transbox", err);
}
};
const handleMousehoverToggle = async (e) => {
try {
setSetting((pre) => ({
...pre,
mouseHoverSetting: {
...pre.mouseHoverSetting,
useMouseHover: e.target.checked,
},
}));
if (!translator) {
await sendTabMsg(MSG_MOUSEHOVER_TOGGLE);
} else {
translator.toggleMouseHover();
sendIframeMsg(MSG_MOUSEHOVER_TOGGLE);
}
} catch (err) {
kissLog("toggle mousehover", err);
}
};
const handleInputTransToggle = async (e) => {
try {
setSetting((pre) => ({
...pre,
inputRule: {
...pre.inputRule,
transOpen: e.target.checked,
},
}));
if (!translator) {
await sendTabMsg(MSG_TRANSINPUT_TOGGLE);
} else {
translator.toggleInputTranslate();
sendIframeMsg(MSG_TRANSINPUT_TOGGLE);
}
} catch (err) {
kissLog("toggle inputtrans", err);
}
};
const handleChange = async (e) => {
try {
const { name, value } = e.target;
setRule((pre) => ({ ...pre, [name]: value }));
if (!translator) {
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
} else {
translator.updateRule({ [name]: value });
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
}
} catch (err) {
kissLog("update rule", err);
}
};
const handleClearCache = () => {
tryClearCaches();
};
const handleSaveRule = async () => {
try {
let href = "";
if (!translator) {
const tab = await getCurTab();
href = tab.url;
} else {
href = window.location?.href;
}
if (!href || typeof href !== "string") {
return;
}
const pattern = parseUrlPattern(href);
const curRule = { ...rule, pattern };
if (isExt && translator) {
sendBgMsg(MSG_SAVE_RULE, curRule);
} else {
saveRule(curRule);
}
} catch (err) {
kissLog("save rule", err);
}
};
const handleOpenSetting = useCallback(() => {
browser?.runtime.openOptionsPage();
}, []);
useEffect(() => {
if (translator) {
return;
}
(async () => {
try {
const res = await sendTabMsg(MSG_TRANS_GETRULE);
@@ -188,297 +32,27 @@ export default function Popup({ setShowPopup, translator }) {
kissLog("query rule", err);
}
})();
}, [translator]);
}, []);
useEffect(() => {
(async () => {
try {
const commands = {};
if (isExt) {
const res = await sendBgMsg(MSG_COMMAND_SHORTCUTS);
res.forEach(({ name, shortcut }) => {
commands[name] = shortcut;
});
} else {
const shortcuts = translator.setting.shortcuts;
if (shortcuts) {
Object.entries(shortcuts).forEach(([key, val]) => {
commands[key] = val.join("+");
});
}
}
setCommands(commands);
} catch (err) {
kissLog("query cmds", err);
}
})();
}, [translator]);
const optApis = useMemo(
() =>
setting?.transApis
.filter((api) => !api.isDisabled)
.map((api) => ({
key: api.apiSlug,
name: api.apiName || api.apiSlug,
})),
[setting]
);
const tranboxEnabled = setting?.tranboxSetting.transOpen;
const mouseHoverEnabled = setting?.mouseHoverSetting.useMouseHover;
const inputTransEnabled = setting?.inputRule.transOpen;
if (!rule) {
return (
<Box minWidth={300}>
{!translator && (
<>
<Header />
<Divider />
</>
)}
return (
<Box width={360}>
<Header />
<Divider />
{rule && setting ? (
<PopupCont
rule={rule}
setting={setting}
setRule={setRule}
setSetting={setSetting}
handleOpenSetting={handleOpenSetting}
/>
) : (
<Stack sx={{ p: 2 }} spacing={3}>
<Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")}
</Button>
</Stack>
</Box>
);
}
const {
transOpen,
apiSlug,
fromLang,
toLang,
textStyle,
autoScan,
transOnly,
hasRichText,
hasShadowroot,
} = rule;
return (
<Box width={360}>
{!translator && (
<>
<Header />
<Divider />
</>
)}
<Stack sx={{ p: 2 }} spacing={2}>
<Grid container columns={12} spacing={1}>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch
checked={transOpen === "true"}
onChange={handleTransToggle}
/>
}
label={
commands["toggleTranslate"]
? `${i18n("translate_alt")}(${commands["toggleTranslate"]})`
: i18n("translate_alt")
}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="autoScan"
value={autoScan === "true" ? "false" : "true"}
checked={autoScan === "true"}
onChange={handleChange}
/>
}
label={i18n("autoscan_alt")}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="hasShadowroot"
value={hasShadowroot === "true" ? "false" : "true"}
checked={hasShadowroot === "true"}
onChange={handleChange}
/>
}
label={i18n("shadowroot_alt")}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="hasRichText"
value={hasRichText === "true" ? "false" : "true"}
checked={hasRichText === "true"}
onChange={handleChange}
/>
}
label={i18n("richtext_alt")}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="transOnly"
value={transOnly === "true" ? "false" : "true"}
checked={transOnly === "true"}
onChange={handleChange}
/>
}
label={i18n("transonly_alt")}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="tranboxEnabled"
value={!tranboxEnabled}
checked={tranboxEnabled}
onChange={handleTransboxToggle}
/>
}
label={i18n("selection_translate")}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="mouseHoverEnabled"
value={!mouseHoverEnabled}
checked={mouseHoverEnabled}
onChange={handleMousehoverToggle}
/>
}
label={i18n("mousehover_translate")}
/>
</Grid>
<Grid item xs={6}>
<FormControlLabel
control={
<Switch
size="small"
name="inputTransEnabled"
value={!inputTransEnabled}
checked={inputTransEnabled}
onChange={handleInputTransToggle}
/>
}
label={i18n("input_translate")}
/>
</Grid>
</Grid>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
size="small"
value={apiSlug}
name="apiSlug"
label={i18n("translate_service")}
onChange={handleChange}
>
{optApis.map(({ key, name }) => (
<MenuItem key={key} value={key}>
{name}
</MenuItem>
))}
</TextField>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
size="small"
value={fromLang}
name="fromLang"
label={i18n("from_lang")}
onChange={handleChange}
>
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
size="small"
value={toLang}
name="toLang"
label={i18n("to_lang")}
onChange={handleChange}
>
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
size="small"
value={textStyle}
name="textStyle"
label={
commands["toggleStyle"]
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
: i18n("text_style_alt")
}
onChange={handleChange}
>
{OPT_STYLE_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</TextField>
{/* {OPT_STYLE_USE_COLOR.includes(textStyle) && (
<TextField
size="small"
name="bgColor"
value={bgColor}
label={i18n("bg_color")}
onChange={handleChange}
/>
)} */}
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Button variant="text" onClick={handleSaveRule}>
{i18n("save_rule")}
</Button>
<Button variant="text" onClick={handleClearCache}>
{i18n("clear_cache")}
</Button>
<Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")}
</Button>
</Stack>
</Stack>
</Box>
);
}

View File

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

View File

@@ -20,7 +20,7 @@ import { isMobile } from "../../libs/mobile";
import TranForm from "./TranForm.js";
function Header({
setShowPopup,
setShowBox,
simpleStyle,
setSimpleStyle,
hideClickAway,
@@ -98,7 +98,7 @@ function Header({
<IconButton
size="small"
onClick={() => {
setShowPopup(false);
setShowBox(false);
}}
>
<CloseIcon fontSize="small" />
@@ -111,10 +111,19 @@ function Header({
}
export default function TranBox({
showBox,
text,
setText,
setShowBox,
tranboxSetting: { enDict, enSug, apiSlugs, fromLang, toLang, toLang2 },
tranboxSetting: {
enDict,
enSug,
apiSlugs,
fromLang,
toLang,
toLang2,
autoHeight,
},
transApis,
boxSize,
setBoxSize,
@@ -134,43 +143,46 @@ export default function TranBox({
return (
<SettingProvider>
<ThemeProvider styles={extStyles}>
<DraggableResizable
position={boxPosition}
size={boxSize}
setSize={setBoxSize}
setPosition={setBoxPosition}
header={
<Header
setShowPopup={setShowBox}
simpleStyle={simpleStyle}
setSimpleStyle={setSimpleStyle}
hideClickAway={hideClickAway}
setHideClickAway={setHideClickAway}
followSelection={followSelection}
setFollowSelection={setFollowSelection}
mouseHover={mouseHover}
/>
}
onClick={(e) => e.stopPropagation()}
onMouseEnter={() => setMouseHover(true)}
onMouseLeave={() => setMouseHover(false)}
>
<Box sx={{ p: simpleStyle ? 1 : 2 }}>
<TranForm
text={text}
setText={setText}
apiSlugs={apiSlugs}
fromLang={fromLang}
toLang={toLang}
toLang2={toLang2}
transApis={transApis}
simpleStyle={simpleStyle}
langDetector={langDetector}
enDict={enDict}
enSug={enSug}
/>
</Box>
</DraggableResizable>
{showBox && (
<DraggableResizable
position={boxPosition}
size={boxSize}
setSize={setBoxSize}
setPosition={setBoxPosition}
autoHeight={autoHeight}
header={
<Header
setShowBox={setShowBox}
simpleStyle={simpleStyle}
setSimpleStyle={setSimpleStyle}
hideClickAway={hideClickAway}
setHideClickAway={setHideClickAway}
followSelection={followSelection}
setFollowSelection={setFollowSelection}
mouseHover={mouseHover}
/>
}
onClick={(e) => e.stopPropagation()}
onMouseEnter={() => setMouseHover(true)}
onMouseLeave={() => setMouseHover(false)}
>
<Box sx={{ p: simpleStyle ? 1 : 2 }}>
<TranForm
text={text}
setText={setText}
apiSlugs={apiSlugs}
fromLang={fromLang}
toLang={toLang}
toLang2={toLang2}
transApis={transApis}
simpleStyle={simpleStyle}
langDetector={langDetector}
enDict={enDict}
enSug={enSug}
/>
</Box>
</DraggableResizable>
)}
</ThemeProvider>
</SettingProvider>
);

View File

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

View File

@@ -10,10 +10,12 @@ import {
OPT_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_HOVER,
OPT_TRANBOX_TRIGGER_SELECT,
EVENT_KISS,
} from "../../config";
import { isMobile } from "../../libs/mobile";
import { kissLog } from "../../libs/log";
import { useLangMap } from "../../hooks/I18n";
import { debouncePutTranBox, getTranBox } from "../../libs/storage";
export default function Slection({
contextMenuType,
@@ -106,6 +108,29 @@ export default function Slection({
return "onMouseUp";
}, [triggerMode]);
useEffect(() => {
(async () => {
try {
const { w, h, x, y } = (await getTranBox()) || {};
if (w !== undefined && h !== undefined) {
setBoxSize({ w, h });
}
if (x !== undefined && y !== undefined) {
setBoxPosition({
x: limitNumber(x, 0, window.innerWidth),
y: limitNumber(y, 0, window.innerHeight),
});
}
} catch (err) {
//
}
})();
}, []);
useEffect(() => {
debouncePutTranBox({ ...boxSize, ...boxPosition });
}, [boxSize, boxPosition]);
useEffect(() => {
async function handleMouseup(e) {
e.stopPropagation();
@@ -167,12 +192,26 @@ export default function Slection({
};
}, [tranboxShortcut, handleTranbox]);
const handleToggle = useCallback(() => {
if (showBox) {
setShowBox(false);
} else {
handleTranbox();
}
}, [showBox, handleTranbox]);
useEffect(() => {
window.addEventListener(MSG_OPEN_TRANBOX, handleTranbox);
return () => {
window.removeEventListener(MSG_OPEN_TRANBOX, handleTranbox);
const handleStatusUpdate = (event) => {
if (event.detail?.action === MSG_OPEN_TRANBOX) {
handleToggle();
}
};
}, [handleTranbox]);
document.addEventListener(EVENT_KISS, handleStatusUpdate);
return () => {
document.removeEventListener(EVENT_KISS, handleStatusUpdate);
};
}, [handleToggle]);
useEffect(() => {
if (!isGm) {
@@ -217,8 +256,9 @@ export default function Slection({
return (
<>
{showBox && (
{
<TranBox
showBox={showBox}
text={text}
setText={setText}
boxSize={boxSize}
@@ -237,7 +277,7 @@ export default function Slection({
// extStyles={extStyles}
langDetector={langDetector}
/>
)}
}
{showBtn && (
<TranBtn