Compare commits
270 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71bbd2e54a | ||
|
|
3083d8e147 | ||
|
|
e74883e9c2 | ||
|
|
0816a9d167 | ||
|
|
4b3e91fa84 | ||
|
|
0973a0b60e | ||
|
|
de5f61126d | ||
|
|
0c20ca761f | ||
|
|
4bce56207e | ||
|
|
dca54e0033 | ||
|
|
309646bf1d | ||
|
|
18b9961b39 | ||
|
|
1e51ff17f2 | ||
|
|
63b5f707e2 | ||
|
|
30efb6ee7a | ||
|
|
61b017618a | ||
|
|
1e0397adc9 | ||
|
|
48b34bf95f | ||
|
|
d5fc69e210 | ||
|
|
59f9dd697f | ||
|
|
c9d72323f1 | ||
|
|
e87f7f3abe | ||
|
|
82ebbcb6d6 | ||
|
|
2db11070c5 | ||
|
|
5efd2517e7 | ||
|
|
c0ba654678 | ||
|
|
546a5a549b | ||
|
|
cbf02c34e3 | ||
|
|
74a7258f10 | ||
|
|
1006c044bc | ||
|
|
ef4ea719f3 | ||
|
|
8b34afe69f | ||
|
|
01292af298 | ||
|
|
cff8b2fe39 | ||
|
|
2cb20b5cc0 | ||
|
|
8f2aed18fe | ||
|
|
d85831cc9a | ||
|
|
55dc3a5556 | ||
|
|
591afe08bd | ||
|
|
748f2002ab | ||
|
|
d2d18a2384 | ||
|
|
35f4fa6aa7 | ||
|
|
66fc2d22ed | ||
|
|
16cf9ee1ed | ||
|
|
d9d97bf14c | ||
|
|
dc811bd3c7 | ||
|
|
b939d1849a | ||
|
|
beca31f55d | ||
|
|
c7df103950 | ||
|
|
4bf7972ad5 | ||
|
|
534eaed1ed | ||
|
|
7e014e7385 | ||
|
|
34adb2660b | ||
|
|
b6bc165cf0 | ||
|
|
bdd5ed7fc7 | ||
|
|
95d19417c3 | ||
|
|
30ebebdd71 | ||
|
|
e9c557776d | ||
|
|
535a43b698 | ||
|
|
59752ed4aa | ||
|
|
b3e7b8f3f1 | ||
|
|
c4e9365512 | ||
|
|
7d3972d3a8 | ||
|
|
52ca4306fd | ||
|
|
da368ee612 | ||
|
|
22c50e7765 | ||
|
|
7bc39dd1bc | ||
|
|
c80ead6116 | ||
|
|
67e76e4009 | ||
|
|
b213218a30 | ||
|
|
c629a1252c | ||
|
|
64d2481e93 | ||
|
|
e7d6a6add8 | ||
|
|
edc25f7da4 | ||
|
|
5bff84ace1 | ||
|
|
f8bfcba317 | ||
|
|
013a05201b | ||
|
|
433e811821 | ||
|
|
df4cfc0fbc | ||
|
|
1bfb465fd6 | ||
|
|
d5d5ec3fef | ||
|
|
0a32f94d32 | ||
|
|
8067f34ce6 | ||
|
|
214c189a7c | ||
|
|
1f67afc8d8 | ||
|
|
7d4af27919 | ||
|
|
2d651abfdd | ||
|
|
6e06fe79cd | ||
|
|
6093577591 | ||
|
|
4b23ee733f | ||
|
|
46428b7c7f | ||
|
|
6805340a9a | ||
|
|
df36ca8d8b | ||
|
|
fe13de7c30 | ||
|
|
b00f636b72 | ||
|
|
8d074e63e1 | ||
|
|
37989b0089 | ||
|
|
477361eb40 | ||
|
|
94288b5dc3 | ||
|
|
84de1e0f12 | ||
|
|
06f93c1c10 | ||
|
|
450283b80a | ||
|
|
44aeed03a6 | ||
|
|
fa4569415d | ||
|
|
a341bf30ba | ||
|
|
34a7354c84 | ||
|
|
21b5dfbe98 | ||
|
|
c1920f5cdd | ||
|
|
3e24568df9 | ||
|
|
b785cfe854 | ||
|
|
15367bd117 | ||
|
|
d7eaac5aca | ||
|
|
d4526d605c | ||
|
|
52979356ca | ||
|
|
c6d3d6454f | ||
|
|
0d7112187d | ||
|
|
045ff3c3d9 | ||
|
|
dd68a73efd | ||
|
|
5947dc182e | ||
|
|
e185bbdb4d | ||
|
|
9368320c38 | ||
|
|
f65314bc2d | ||
|
|
1791e36038 | ||
|
|
8d93094af5 | ||
|
|
43f34fe6ed | ||
|
|
83911b2164 | ||
|
|
94c7494e90 | ||
|
|
65f2177299 | ||
|
|
f033b11e63 | ||
|
|
02f26af592 | ||
|
|
4125aba808 | ||
|
|
e89da9120c | ||
|
|
160fc218fc | ||
|
|
507d54dba0 | ||
|
|
f3029a0f76 | ||
|
|
53181588cf | ||
|
|
f88aa159fc | ||
|
|
fb2b517a67 | ||
|
|
6e952a9530 | ||
|
|
d9acef8d56 | ||
|
|
113a4d8eca | ||
|
|
6d5b93c01b | ||
|
|
5746911651 | ||
|
|
7173692db7 | ||
|
|
b13a63e568 | ||
|
|
791ec65579 | ||
|
|
dd99fddc07 | ||
|
|
5cd6977a6e | ||
|
|
5af66204c4 | ||
|
|
595efe808f | ||
|
|
7b4b3b020c | ||
|
|
3e96540b56 | ||
|
|
88b791bd73 | ||
|
|
7817019e70 | ||
|
|
6013bbd32c | ||
|
|
40e0b96f39 | ||
|
|
16560fbdf0 | ||
|
|
16fdd704aa | ||
|
|
44e84d9259 | ||
|
|
18d29461ce | ||
|
|
31fb749e93 | ||
|
|
e17931493b | ||
|
|
a395f0b31b | ||
|
|
c819896b43 | ||
|
|
733ec92c9c | ||
|
|
7c67bb7181 | ||
|
|
87f099dd7f | ||
|
|
5306d81284 | ||
|
|
471a4a3159 | ||
|
|
a2f99da3b4 | ||
|
|
accab22d56 | ||
|
|
6ea5228a5f | ||
|
|
a07d2cafb6 | ||
|
|
1b38f19cc1 | ||
|
|
aa5b286e0b | ||
|
|
6b6bbed330 | ||
|
|
489bc9534b | ||
|
|
01ebc184ad | ||
|
|
f591d66365 | ||
|
|
80782287d8 | ||
|
|
3494bb1297 | ||
|
|
92ffda5220 | ||
|
|
fbaeff6b7b | ||
|
|
248d3726dd | ||
|
|
1553559b1a | ||
|
|
8935ced75a | ||
|
|
a865d6d74f | ||
|
|
6d976554fd | ||
|
|
189b7f480a | ||
|
|
5e3aa7e2d1 | ||
|
|
730be678ef | ||
|
|
9293f422f3 | ||
|
|
6e8158bb34 | ||
|
|
3078d3ca91 | ||
|
|
947e1c7f08 | ||
|
|
938c123412 | ||
|
|
e7a57ad3b2 | ||
|
|
1e40f81bf7 | ||
|
|
72b2f44e32 | ||
|
|
76f54461e7 | ||
|
|
14ca13e31d | ||
|
|
556fd71275 | ||
|
|
a8002bba9f | ||
|
|
ddd9371fbd | ||
|
|
0ea97b73e3 | ||
|
|
f8c8a4ebeb | ||
|
|
5f613ab558 | ||
|
|
56281f9e82 | ||
|
|
5e8743dbb7 | ||
|
|
f4e4c84712 | ||
|
|
c57a0a11fa | ||
|
|
fa244b2097 | ||
|
|
79612f8a1b | ||
|
|
2bf79dbc51 | ||
|
|
c2658d5dd0 | ||
|
|
13684884c7 | ||
|
|
f216a9254e | ||
|
|
dbdbcbba2d | ||
|
|
2ee4609192 | ||
|
|
0d93cf78f7 | ||
|
|
3398ca0dd7 | ||
|
|
c1778fbcbb | ||
|
|
1ef9974c05 | ||
|
|
399c6b6fed | ||
|
|
62a60eee44 | ||
|
|
54339af885 | ||
|
|
06cfd33e60 | ||
|
|
08c9d78d2a | ||
|
|
e7a5e5dce1 | ||
|
|
3a59a127d1 | ||
|
|
26f213cad2 | ||
|
|
7b6148302d | ||
|
|
38c781b8f3 | ||
|
|
64d827fdcd | ||
|
|
74ad812f37 | ||
|
|
364c829119 | ||
|
|
1ac2c5b61e | ||
|
|
0766199353 | ||
|
|
878bccf151 | ||
|
|
acbd258296 | ||
|
|
54a14e6e5a | ||
|
|
298e4b52f0 | ||
|
|
bee1fbcf88 | ||
|
|
345a34287e | ||
|
|
441a2ca2da | ||
|
|
1ff1b21355 | ||
|
|
117ca4e05b | ||
|
|
07d457be4e | ||
|
|
d48296046e | ||
|
|
56350de2cf | ||
|
|
850dc0e83b | ||
|
|
35f01478b1 | ||
|
|
f9a3ec012f | ||
|
|
3b9b404482 | ||
|
|
d8b0cc4834 | ||
|
|
da13f5e218 | ||
|
|
08e14ae11c | ||
|
|
c2902dff28 | ||
|
|
c4fb39f02f | ||
|
|
b7df44c35a | ||
|
|
9a2b21eee5 | ||
|
|
bdac67df88 | ||
|
|
0b8f19bfad | ||
|
|
c7c5866131 | ||
|
|
f772fa000c | ||
|
|
93fd82fcd9 | ||
|
|
3ae10bfd04 | ||
|
|
a44747ccad | ||
|
|
87ab45f936 | ||
|
|
37b046eb46 |
4
.env
4
.env
@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
|
||||
|
||||
REACT_APP_NAME=KISS Translator
|
||||
REACT_APP_NAME_CN=简约翻译
|
||||
REACT_APP_VERSION=1.6.7
|
||||
REACT_APP_VERSION=1.8.2
|
||||
|
||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||
|
||||
@@ -17,6 +17,8 @@ REACT_APP_RULESURL=https://fishjar.github.io/kiss-rules/kiss-rules.json
|
||||
REACT_APP_RULESURL_ON=https://fishjar.github.io/kiss-rules/kiss-rules-on.json
|
||||
REACT_APP_RULESURL_OFF=https://fishjar.github.io/kiss-rules/kiss-rules-off.json
|
||||
|
||||
REACT_APP_WEBFIXURL=https://fishjar.github.io/kiss-rules/kiss-webfix.json
|
||||
|
||||
REACT_APP_VERSIONFILE=https://fishjar.github.io/kiss-translator/version.txt
|
||||
REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt
|
||||
|
||||
|
||||
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@@ -10,12 +10,15 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8.7.6
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.17.0"
|
||||
cache: "yarn"
|
||||
- run: yarn install
|
||||
- run: yarn build
|
||||
cache: "pnpm"
|
||||
- run: pnpm install
|
||||
- run: pnpm build
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-artifacts
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
nodeLinker: node-modules
|
||||
108
README.en.md
108
README.en.md
@@ -1,28 +1,63 @@
|
||||
# KISS Translator
|
||||
|
||||
A minimalist [bilingual translation Extension & Greasemonkey Script](https://github.com/fishjar/kiss-translator).
|
||||
A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
|
||||
|
||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||
|
||||
## Inspiration
|
||||
|
||||
The inspiration for this project comes from [Immersive Translate](https://github.com/immersive-translate/immersive-translate). After trying it out, I found that it can be used together with the [Webpage Word Translation Extension](https://github.com/fishjar/kiss-dictionary) developed by me earlier, which just forms a very good supplement.
|
||||
|
||||
But the function of this extension is a bit complicated for me, and only the compiled and obfuscated installation package is provided, and the source code is not provided, which cannot meet some of my personalized customization needs.
|
||||
|
||||
It just so happens that I am obsessed with translation tools. Based on the concept of "mainly for personal use, as long as you can use it", I made one. At present, the first version is completed, which basically meets the needs of personal use.
|
||||
|
||||
If you also like a little more simplicity, welcome to pick it up.
|
||||
|
||||
## Features
|
||||
|
||||
- Keep it simple, smart
|
||||
- [x] Keep it simple, smart
|
||||
- [x] Open source
|
||||
- [x] Adapt to common browsers
|
||||
- [x] Chrome/Edge/Firefox/Kiwi
|
||||
- [ ] Safari
|
||||
- [x] Supports multiple translation services
|
||||
- [x] Google/Microsoft/DeepL/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
|
||||
- [x] Custom translation interface
|
||||
- [x] Covers common translation scenarios
|
||||
- [x] Web bilingual translation
|
||||
- [x] Input box translation
|
||||
- [x] Seletction translation
|
||||
- [x] Favorite Words
|
||||
- [x] Mouseover translation
|
||||
- [x] YouTube subtitle translation
|
||||
- [x] Cross-client data synchronization
|
||||
- [x] KISS-Worker(cloudflare/docker)
|
||||
- [x] WebDAV
|
||||
- [x] Custom translation rules
|
||||
- [x] Rule subscription/rule sharing
|
||||
- [x] Customized terminology
|
||||
- [x] Custom translation style
|
||||
- [x] Custom shortcut keys
|
||||
- `Alt+Q` Toggle Translation
|
||||
- `Alt+C` Toggle Styles
|
||||
- `Alt+K` Open Setting Popup
|
||||
- `Alt+S` Open Translate Popup / Translate Selected Text
|
||||
- `Alt+O` Open Options Page
|
||||
- `Alt+I` Input Box Translation
|
||||
|
||||
## Associated ProjectS
|
||||
## Install
|
||||
|
||||
> Note: For the following reasons, it is recommended to use browser extensions first
|
||||
>
|
||||
> - Browser extensions have more complete functions (local language recognition, context menu, etc.)
|
||||
> - Grease Monkey script will encounter more usage problems (cross domain issues, script conflicts, etc.)
|
||||
|
||||
- [x] Browser extension
|
||||
- [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
||||
- [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
|
||||
- [x] Firefox [Installation address](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
|
||||
- [ ] Safari
|
||||
- [x] GreaseMonkey Script
|
||||
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
|
||||
- Greasy Fork [Installation address](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
||||
|
||||
## Associated Projects
|
||||
|
||||
- Data synchronization service: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||
- Data synchronization service available for this project.
|
||||
- You can also share your own private rule list.
|
||||
- Can also be used to share personal private rule lists.
|
||||
- Deploy by yourself, manage by yourself, data is private.
|
||||
- Community subscription rules: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
||||
- Provides the latest and most complete list of subscription rules maintained by the community.
|
||||
@@ -38,50 +73,19 @@ If you also like a little more simplicity, welcome to pick it up.
|
||||
- Supports query of English words, sentences and Chinese characters.
|
||||
- Supports history records and word collections.
|
||||
|
||||
## Description
|
||||
|
||||
### Support shortcut keys
|
||||
|
||||
- `Alt+Q` Toggle Translation
|
||||
- `Alt+C` Toggle Styles
|
||||
- `Alt+K` Open Menu
|
||||
|
||||
## Schedule
|
||||
|
||||
- [x] Provide trial installation package
|
||||
- [x] Adapt browser
|
||||
- [x] Chrome
|
||||
- [x] Edge
|
||||
- [x] Firefox
|
||||
- [ ] Safari
|
||||
- [x] Kiwi
|
||||
- [x] Support translation services
|
||||
- [x] Google
|
||||
- [x] Microsoft
|
||||
- [x] DeepL
|
||||
- [x] OpenAI
|
||||
- [x] Upload to app Store
|
||||
- [x] Chrome [Install Link](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
|
||||
- [x] Edge [Install Link](https://microsoftedge.microsoft.com/addons/detail/kiss-translator/jemckldkclkinpjighnoilpbldbdmmlh)
|
||||
- [x] Firefox [Install Link](https://addons.mozilla.org/en-US/firefox/addon/kiss-translator/)
|
||||
- [ ] Safari
|
||||
- [x] Greasy Fork [Install Link](https://greasyfork.org/en/scripts/472840-kiss-translator)
|
||||
- [x] Open source
|
||||
- [x] Data Synchronization Function
|
||||
- [x] Greasemonkey Script ([Setting Page 1](https://fishjar.github.io/kiss-translator/options.html)、[Setting Page 2](https://kiss-translator.rayjar.com/options))
|
||||
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
|
||||
- [x] [Violentmonkey](https://violentmonkey.github.io/) (Chrome/Edge/Firefox) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
|
||||
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
|
||||
|
||||
## Guide
|
||||
## Development Guidelines
|
||||
|
||||
```sh
|
||||
git clone https://github.com/fishjar/kiss-translator.git
|
||||
cd kiss-translator
|
||||
yarn install
|
||||
yarn build
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Discussion
|
||||
|
||||
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||
|
||||
## Appreciate
|
||||
|
||||

|
||||
|
||||
104
README.md
104
README.md
@@ -1,28 +1,63 @@
|
||||
# 简约翻译
|
||||
|
||||
一个简约的 [双语网页翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
||||
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
||||
|
||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||
|
||||
## 缘由
|
||||
## 特性
|
||||
|
||||
本项目灵感来源于 [Immersive Translate](https://github.com/immersive-translate/immersive-translate),在试用了后,发现搭配本人早前开发的 [网页划词翻译扩展](https://github.com/fishjar/kiss-dictionary) 一起使用,刚好形成很好补充。
|
||||
- [x] 保持简约
|
||||
- [x] 开放源代码
|
||||
- [x] 适配常见浏览器
|
||||
- [x] Chrome/Edge/Firefox/Kiwi
|
||||
- [ ] Safari
|
||||
- [x] 支持多种翻译服务
|
||||
- [x] Google/Microsoft/DeepL/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
|
||||
- [x] 自定义翻译接口
|
||||
- [x] 覆盖常见翻译场景
|
||||
- [x] 网页双语对照翻译
|
||||
- [x] 输入框翻译
|
||||
- [x] 划词翻译
|
||||
- [x] 收藏词汇
|
||||
- [x] 鼠标悬停翻译
|
||||
- [x] YouTube 字幕翻译
|
||||
- [x] 跨客户端数据同步
|
||||
- [x] KISS-Worker(cloudflare/docker)
|
||||
- [x] WebDAV
|
||||
- [x] 自定义翻译规则
|
||||
- [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] 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
|
||||
- [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)
|
||||
- 提供社区维护的,最新最全的订阅规则列表。
|
||||
@@ -31,57 +66,26 @@
|
||||
- 针对一些特殊网站的修正脚本。
|
||||
- 以便翻译软件得到更好的展示效果。
|
||||
- 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
|
||||
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮你到你。
|
||||
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮到你。
|
||||
- 自己部署,自己管理。
|
||||
- 简约词典插件: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
|
||||
- 搭配本项目一起使用的划词翻译插件。
|
||||
- 支持英文单词、句子、汉字的查询。
|
||||
- 支持历史记录、单词收藏。
|
||||
|
||||
## 简要说明
|
||||
|
||||
### 支持快捷键
|
||||
|
||||
- `Alt+Q` 开启翻译
|
||||
- `Alt+C` 切换样式
|
||||
- `Alt+K` 打开菜单
|
||||
|
||||
## 进度
|
||||
|
||||
- [x] 提供试用安装包
|
||||
- [x] 适配浏览器
|
||||
- [x] Chrome
|
||||
- [x] Edge
|
||||
- [x] Firefox
|
||||
- [ ] Safari
|
||||
- [x] Kiwi
|
||||
- [x] 支持翻译服务
|
||||
- [x] Google
|
||||
- [x] Microsoft
|
||||
- [x] DeepL
|
||||
- [x] OpenAI
|
||||
- [x] 上架应用市场
|
||||
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
||||
- [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
|
||||
- [x] Greasy Fork [安装地址](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||
- [x] 开放源代码
|
||||
- [x] 数据同步功能
|
||||
- [x] 油猴脚本 ([设置页面 1](https://fishjar.github.io/kiss-translator/options.html)、[设置页面 2](https://kiss-translator.rayjar.com/options))
|
||||
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
|
||||
- [x] [Violentmonkey](https://violentmonkey.github.io/) (Chrome/Edge/Firefox) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
|
||||
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
|
||||
|
||||
## 指引
|
||||
## 开发指引
|
||||
|
||||
```sh
|
||||
git clone https://github.com/fishjar/kiss-translator.git
|
||||
cd kiss-translator
|
||||
yarn install
|
||||
yarn build
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 交流
|
||||
|
||||
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||
|
||||
## 赞赏
|
||||
|
||||

|
||||
|
||||
@@ -75,7 +75,7 @@ const userscriptWebpack = (config, env) => {
|
||||
// @name ${process.env.REACT_APP_NAME}
|
||||
// @namespace ${process.env.REACT_APP_HOMEPAGE}
|
||||
// @version ${process.env.REACT_APP_VERSION}
|
||||
// @description A minimalist bilingual translation Extension & Greasemonkey Script (一个简约的双语网页翻译扩展 & 油猴脚本)
|
||||
// @description A simple bilingual translation extension & Greasemonkey script (一个简约的双语对照翻译扩展 & 油猴脚本)
|
||||
// @author Gabe<yugang2002@gmail.com>
|
||||
// @homepageURL ${process.env.REACT_APP_HOMEPAGE}
|
||||
// @license GPL-3.0
|
||||
@@ -85,6 +85,7 @@ const userscriptWebpack = (config, env) => {
|
||||
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||
// @grant GM.xmlHttpRequest
|
||||
// @grant GM.registerMenuCommand
|
||||
// @grant GM.unregisterMenuCommand
|
||||
// @grant GM.setValue
|
||||
// @grant GM.getValue
|
||||
// @grant GM.deleteValue
|
||||
@@ -95,14 +96,22 @@ const userscriptWebpack = (config, env) => {
|
||||
// @connect edge.microsoft.com
|
||||
// @connect api-free.deepl.com
|
||||
// @connect api.deepl.com
|
||||
// @connect www2.deepl.com
|
||||
// @connect api.openai.com
|
||||
// @connect generativelanguage.googleapis.com
|
||||
// @connect openai.azure.com
|
||||
// @connect workers.dev
|
||||
// @connect github.io
|
||||
// @connect githubusercontent.com
|
||||
// @connect kiss-translator.rayjar.com
|
||||
// @connect ghproxy.com
|
||||
// @connect dav.jianguoyun.com
|
||||
// @connect fanyi.baidu.com
|
||||
// @connect transmart.qq.com
|
||||
// @connect localhost:3000
|
||||
// @connect 127.0.0.1:3000
|
||||
// @connect localhost:1188
|
||||
// @connect 127.0.0.1:1188
|
||||
// @run-at document-end
|
||||
// ==/UserScript==
|
||||
|
||||
|
||||
15
package.json
15
package.json
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"name": "kiss-translator",
|
||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||
"version": "1.6.7",
|
||||
"version": "1.8.2",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.10.8",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.12",
|
||||
"@violentmonkey/shortcut": "^1.3.0",
|
||||
"query-string": "^8.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-router-dom": "^6.10.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"styled-components": "^6.0.7",
|
||||
"webdav": "^5.3.0",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -29,8 +29,7 @@
|
||||
"build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
|
||||
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
|
||||
"build:rules": "babel-node src/rules.js",
|
||||
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript-ios && yarn build:userscript && yarn build:rules",
|
||||
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
|
||||
"build": "pnpm build:chrome && pnpm build:edge && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
@@ -41,7 +40,8 @@
|
||||
],
|
||||
"globals": {
|
||||
"GM": true,
|
||||
"unsafeWindow": true
|
||||
"unsafeWindow": true,
|
||||
"globalThis": true
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -61,7 +61,6 @@
|
||||
"@babel/node": "^7.22.10",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"wrangler": "^3.4.0"
|
||||
"react-app-rewired": "^2.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
10267
pnpm-lock.yaml
generated
Normal file
10267
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,12 +3,18 @@
|
||||
"message": "KISS Translator"
|
||||
},
|
||||
"app_description": {
|
||||
"message": "A minimalist bilingual translation Extension & Greasemonkey Script"
|
||||
"message": "A simple bilingual translation extension & Greasemonkey script"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "Toggle Translate"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "Toggle Style"
|
||||
},
|
||||
"open_options": {
|
||||
"message": "Open Options"
|
||||
},
|
||||
"open_tranbox": {
|
||||
"message": "Translate Popup/Selected"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,18 @@
|
||||
"message": "简约翻译"
|
||||
},
|
||||
"app_description": {
|
||||
"message": "一个简约的双语网页翻译扩展 & 油猴脚本"
|
||||
"message": "一个简约的双语对照翻译扩展 & 油猴脚本"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "切换翻译"
|
||||
"message": "启停翻译"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "切换样式"
|
||||
},
|
||||
"open_options": {
|
||||
"message": "打开设置"
|
||||
},
|
||||
"open_tranbox": {
|
||||
"message": "翻译弹窗/选中文字"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,45 @@
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">
|
||||
<p>You need to enable <code>JavaScript</code> to run <span>this app.</span></p>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<div id="content">
|
||||
<p>You need to enable JavaScript to run <span>this app.</span></p>
|
||||
The <span>embargo</span> has just lifted to confirm that AmpereOne is
|
||||
@@ -84,6 +123,11 @@
|
||||
>
|
||||
</p>
|
||||
</h2>
|
||||
<hr />
|
||||
<input id="input1" style="width: 80%;" />
|
||||
<hr />
|
||||
<textarea id="textarea1" style="width: 80%;">test</textarea>
|
||||
<hr />
|
||||
<div id="addtitle"></div>
|
||||
<h2>Shadow 1</h2>
|
||||
<div id="shadow1"></div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.6.7",
|
||||
"version": "1.8.2",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -28,14 +28,23 @@
|
||||
},
|
||||
"description": "__MSG_toggle_translate__"
|
||||
},
|
||||
"openTranbox": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+S"
|
||||
},
|
||||
"description": "__MSG_open_tranbox__"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
},
|
||||
"openOptions": {
|
||||
"description": "__MSG_open_options__"
|
||||
}
|
||||
},
|
||||
"permissions": ["<all_urls>", "storage"],
|
||||
"permissions": ["<all_urls>", "storage", "contextMenus"],
|
||||
"icons": {
|
||||
"16": "images/logo16.png",
|
||||
"32": "images/logo32.png",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.6.7",
|
||||
"version": "1.8.2",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -29,14 +29,23 @@
|
||||
},
|
||||
"description": "__MSG_toggle_translate__"
|
||||
},
|
||||
"openTranbox": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+S"
|
||||
},
|
||||
"description": "__MSG_open_tranbox__"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
},
|
||||
"openOptions": {
|
||||
"description": "__MSG_open_options__"
|
||||
}
|
||||
},
|
||||
"permissions": ["storage"],
|
||||
"permissions": ["storage", "contextMenus"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"icons": {
|
||||
"16": "images/logo16.png",
|
||||
|
||||
258
src/apis/baidu.js
Normal file
258
src/apis/baidu.js
Normal file
@@ -0,0 +1,258 @@
|
||||
import queryString from "query-string";
|
||||
import { getBdauth, setBdauth } from "../libs/storage";
|
||||
import {
|
||||
URL_BAIDU_WEB,
|
||||
URL_BAIDU_TRANSAPI_V2,
|
||||
URL_BAIDU_TRANSAPI,
|
||||
} from "../config";
|
||||
import { fetchApi } from "../libs/fetch";
|
||||
|
||||
/* eslint-disable */
|
||||
function n(t, e) {
|
||||
for (var n = 0; n < e.length - 2; n += 3) {
|
||||
var r = e.charAt(n + 2);
|
||||
(r = "a" <= r ? r.charCodeAt(0) - 87 : Number(r)),
|
||||
(r = "+" === e.charAt(n + 1) ? t >>> r : t << r),
|
||||
(t = "+" === e.charAt(n) ? (t + r) & 4294967295 : t ^ r);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
function e(t, e) {
|
||||
(null == e || e > t.length) && (e = t.length);
|
||||
for (var n = 0, r = new Array(e); n < e; n++) r[n] = t[n];
|
||||
return r;
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
function getSign(t, gtk, r = null) {
|
||||
var o,
|
||||
i = t.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
|
||||
if (null === i) {
|
||||
var a = t.length;
|
||||
a > 30 &&
|
||||
(t = ""
|
||||
.concat(t.substr(0, 10))
|
||||
.concat(t.substr(Math.floor(a / 2) - 5, 10))
|
||||
.concat(t.substr(-10, 10)));
|
||||
} else {
|
||||
for (
|
||||
var s = t.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/),
|
||||
c = 0,
|
||||
u = s.length,
|
||||
l = [];
|
||||
c < u;
|
||||
c++
|
||||
)
|
||||
"" !== s[c] &&
|
||||
l.push.apply(
|
||||
l,
|
||||
(function (t) {
|
||||
if (Array.isArray(t)) return e(t);
|
||||
})((o = s[c].split(""))) ||
|
||||
(function (t) {
|
||||
if (
|
||||
("undefined" != typeof Symbol && null != t[Symbol.iterator]) ||
|
||||
null != t["@@iterator"]
|
||||
)
|
||||
return Array.from(t);
|
||||
})(o) ||
|
||||
(function (t, n) {
|
||||
if (t) {
|
||||
if ("string" == typeof t) return e(t, n);
|
||||
var r = Object.prototype.toString.call(t).slice(8, -1);
|
||||
return (
|
||||
"Object" === r && t.constructor && (r = t.constructor.name),
|
||||
"Map" === r || "Set" === r
|
||||
? Array.from(t)
|
||||
: "Arguments" === r ||
|
||||
/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)
|
||||
? e(t, n)
|
||||
: void 0
|
||||
);
|
||||
}
|
||||
})(o) ||
|
||||
(function () {
|
||||
throw new TypeError(
|
||||
"Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
|
||||
);
|
||||
})()
|
||||
),
|
||||
c !== u - 1 && l.push(i[c]);
|
||||
var p = l.length;
|
||||
p > 30 &&
|
||||
(t =
|
||||
l.slice(0, 10).join("") +
|
||||
l.slice(Math.floor(p / 2) - 5, Math.floor(p / 2) + 5).join("") +
|
||||
l.slice(-10).join(""));
|
||||
}
|
||||
for (
|
||||
var d = ""
|
||||
.concat(String.fromCharCode(103))
|
||||
.concat(String.fromCharCode(116))
|
||||
.concat(String.fromCharCode(107)),
|
||||
h = (null !== r ? r : (r = gtk || "") || "").split("."),
|
||||
f = Number(h[0]) || 0,
|
||||
m = Number(h[1]) || 0,
|
||||
g = [],
|
||||
y = 0,
|
||||
v = 0;
|
||||
v < t.length;
|
||||
v++
|
||||
) {
|
||||
var _ = t.charCodeAt(v);
|
||||
_ < 128
|
||||
? (g[y++] = _)
|
||||
: (_ < 2048
|
||||
? (g[y++] = (_ >> 6) | 192)
|
||||
: (55296 == (64512 & _) &&
|
||||
v + 1 < t.length &&
|
||||
56320 == (64512 & t.charCodeAt(v + 1))
|
||||
? ((_ = 65536 + ((1023 & _) << 10) + (1023 & t.charCodeAt(++v))),
|
||||
(g[y++] = (_ >> 18) | 240),
|
||||
(g[y++] = ((_ >> 12) & 63) | 128))
|
||||
: (g[y++] = (_ >> 12) | 224),
|
||||
(g[y++] = ((_ >> 6) & 63) | 128)),
|
||||
(g[y++] = (63 & _) | 128));
|
||||
}
|
||||
for (
|
||||
var b = f,
|
||||
w =
|
||||
""
|
||||
.concat(String.fromCharCode(43))
|
||||
.concat(String.fromCharCode(45))
|
||||
.concat(String.fromCharCode(97)) +
|
||||
""
|
||||
.concat(String.fromCharCode(94))
|
||||
.concat(String.fromCharCode(43))
|
||||
.concat(String.fromCharCode(54)),
|
||||
k =
|
||||
""
|
||||
.concat(String.fromCharCode(43))
|
||||
.concat(String.fromCharCode(45))
|
||||
.concat(String.fromCharCode(51)) +
|
||||
""
|
||||
.concat(String.fromCharCode(94))
|
||||
.concat(String.fromCharCode(43))
|
||||
.concat(String.fromCharCode(98)) +
|
||||
""
|
||||
.concat(String.fromCharCode(43))
|
||||
.concat(String.fromCharCode(45))
|
||||
.concat(String.fromCharCode(102)),
|
||||
x = 0;
|
||||
x < g.length;
|
||||
x++
|
||||
)
|
||||
b = n((b += g[x]), w);
|
||||
return (
|
||||
(b = n(b, k)),
|
||||
(b ^= m) < 0 && (b = 2147483648 + (2147483647 & b)),
|
||||
"".concat((b %= 1e6).toString(), ".").concat(b ^ f)
|
||||
);
|
||||
}
|
||||
|
||||
const getToken = async () => {
|
||||
const res = await fetchApi({
|
||||
input: URL_BAIDU_WEB,
|
||||
init: {
|
||||
headers: {
|
||||
"Content-type": "text/html; charset=utf-8",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
const token = text.match(/token: '(.*)',/)[1];
|
||||
const gtk = text.match(/gtk = "(.*)";/)[1];
|
||||
const exp = Date.now() + 8 * 60 * 60 * 1000;
|
||||
|
||||
if (!token || !gtk) {
|
||||
throw new Error("[baidu] get token error");
|
||||
}
|
||||
|
||||
return { token, gtk, exp };
|
||||
};
|
||||
|
||||
/**
|
||||
* 闭包缓存token,减少对storage查询
|
||||
* @returns
|
||||
*/
|
||||
const _bdAuth = () => {
|
||||
let store;
|
||||
|
||||
return async () => {
|
||||
const now = Date.now();
|
||||
|
||||
// 查询内存缓存
|
||||
if (store && store.exp > now) {
|
||||
return store;
|
||||
}
|
||||
|
||||
// 查询storage缓存
|
||||
store = await getBdauth();
|
||||
if (store && store.exp > now) {
|
||||
return store;
|
||||
}
|
||||
|
||||
// 缓存没有或失效,查询接口
|
||||
store = await getToken();
|
||||
await setBdauth(store);
|
||||
return store;
|
||||
};
|
||||
};
|
||||
|
||||
const bdAuth = _bdAuth();
|
||||
|
||||
/**
|
||||
* 失效作废
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const genBaiduV2 = async ({ text, from, to }) => {
|
||||
const { token, gtk } = await bdAuth();
|
||||
const sign = getSign(text, gtk);
|
||||
const data = {
|
||||
from,
|
||||
to,
|
||||
query: text,
|
||||
simple_means_flag: 3,
|
||||
sign,
|
||||
token,
|
||||
domain: "common",
|
||||
ts: Date.now(),
|
||||
};
|
||||
|
||||
const input = `${URL_BAIDU_TRANSAPI_V2}?from=${from}&to=${to}`;
|
||||
const init = {
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
},
|
||||
method: "POST",
|
||||
body: queryString.stringify(data),
|
||||
};
|
||||
|
||||
return [input, init];
|
||||
};
|
||||
|
||||
export const genBaidu = async ({ text, from, to }) => {
|
||||
const data = {
|
||||
from,
|
||||
to,
|
||||
query: text,
|
||||
source: "txt",
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
},
|
||||
method: "POST",
|
||||
body: queryString.stringify(data),
|
||||
};
|
||||
|
||||
return [URL_BAIDU_TRANSAPI, init];
|
||||
};
|
||||
58
src/apis/deepl.js
Normal file
58
src/apis/deepl.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { URL_DEEPLFREE_TRAN } from "../config";
|
||||
|
||||
let id = 1e4 * Math.round(1e4 * Math.random());
|
||||
|
||||
export const genDeeplFree = ({ text, from, to }) => {
|
||||
const iCount = (text.match(/[i]/g) || []).length + 1;
|
||||
let timestamp = Date.now();
|
||||
timestamp = timestamp + (iCount - (timestamp % iCount));
|
||||
id++;
|
||||
|
||||
let body = JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
method: "LMT_handle_texts",
|
||||
params: {
|
||||
splitting: "newlines",
|
||||
lang: {
|
||||
target_lang: to,
|
||||
source_lang_user_selected: from,
|
||||
},
|
||||
commonJobParams: {
|
||||
wasSpoken: false,
|
||||
transcribe_as: "",
|
||||
},
|
||||
id,
|
||||
timestamp,
|
||||
texts: [
|
||||
{
|
||||
text,
|
||||
requestAlternatives: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
body = body.replace(
|
||||
'method":"',
|
||||
(id + 3) % 13 === 0 || (id + 5) % 29 === 0 ? 'method" : "' : 'method": "'
|
||||
);
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "*/*",
|
||||
"x-app-os-name": "iOS",
|
||||
"x-app-os-version": "16.3.0",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"x-app-device": "iPhone13,2",
|
||||
"User-Agent": "DeepL-iOS/2.9.1 iOS 16.3.0 (iPhone13,2)",
|
||||
"x-app-build": "510265",
|
||||
"x-app-version": "2.9.1",
|
||||
},
|
||||
method: "POST",
|
||||
body,
|
||||
};
|
||||
|
||||
return [URL_DEEPLFREE_TRAN, init];
|
||||
};
|
||||
@@ -4,14 +4,22 @@ import {
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_DEEPLX,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_OPENAI,
|
||||
URL_MICROSOFT_TRANS,
|
||||
OPT_LANGS_SPECIAL,
|
||||
PROMPT_PLACE_FROM,
|
||||
PROMPT_PLACE_TO,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
URL_CACHE_TRAN,
|
||||
KV_SALT_SYNC,
|
||||
URL_BAIDU_LANGDETECT,
|
||||
OPT_LANGS_BAIDU,
|
||||
URL_TENCENT_TRANSMART,
|
||||
OPT_LANGS_TENCENT,
|
||||
OPT_LANGS_SPECIAL,
|
||||
} from "../config";
|
||||
import { tryDetectLang } from "../libs";
|
||||
import { sha256 } from "../libs/utils";
|
||||
|
||||
/**
|
||||
@@ -21,7 +29,7 @@ import { sha256 } from "../libs/utils";
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
export const apiSyncData = async (url, key, data, isBg = false) =>
|
||||
export const apiSyncData = async (url, key, data) =>
|
||||
fetchPolyfill(url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
@@ -29,140 +37,62 @@ export const apiSyncData = async (url, key, data, isBg = false) =>
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
isBg,
|
||||
});
|
||||
|
||||
/**
|
||||
* 下载订阅规则
|
||||
* 下载数据
|
||||
* @param {*} url
|
||||
* @param {*} isBg
|
||||
* @returns
|
||||
*/
|
||||
export const apiFetchRules = (url, isBg = false) =>
|
||||
fetchPolyfill(url, { isBg });
|
||||
export const apiFetch = (url) => fetchPolyfill(url);
|
||||
|
||||
/**
|
||||
* 谷歌翻译
|
||||
* 百度语言识别
|
||||
* @param {*} text
|
||||
* @param {*} to
|
||||
* @param {*} from
|
||||
* @returns
|
||||
*/
|
||||
const apiGoogleTranslate = async (translator, text, to, from, setting) => {
|
||||
const { googleUrl } = setting;
|
||||
const params = {
|
||||
client: "gtx",
|
||||
dt: "t",
|
||||
dj: 1,
|
||||
ie: "UTF-8",
|
||||
sl: from,
|
||||
tl: to,
|
||||
q: text,
|
||||
};
|
||||
const input = `${googleUrl}?${queryString.stringify(params)}`;
|
||||
return fetchPolyfill(input, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
useCache: true,
|
||||
usePool: true,
|
||||
translator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 微软翻译
|
||||
* @param {*} text
|
||||
* @param {*} to
|
||||
* @param {*} from
|
||||
* @returns
|
||||
*/
|
||||
const apiMicrosoftTranslate = (translator, text, to, from) => {
|
||||
const params = {
|
||||
from,
|
||||
to,
|
||||
"api-version": "3.0",
|
||||
};
|
||||
const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`;
|
||||
return fetchPolyfill(input, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify([{ Text: text }]),
|
||||
useCache: true,
|
||||
usePool: true,
|
||||
translator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* DeepL翻译
|
||||
* @param {*} text
|
||||
* @param {*} to
|
||||
* @param {*} from
|
||||
* @returns
|
||||
*/
|
||||
const apiDeepLTranslate = (translator, text, to, from, setting) => {
|
||||
const { deeplUrl, deeplKey } = setting;
|
||||
const data = {
|
||||
text: [text],
|
||||
target_lang: to,
|
||||
split_sentences: "0",
|
||||
};
|
||||
if (from) {
|
||||
data.source_lang = from;
|
||||
}
|
||||
return fetchPolyfill(deeplUrl, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
useCache: true,
|
||||
usePool: true,
|
||||
translator,
|
||||
token: deeplKey,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* OpenAI 翻译
|
||||
* @param {*} text
|
||||
* @param {*} to
|
||||
* @param {*} from
|
||||
* @returns
|
||||
*/
|
||||
const apiOpenaiTranslate = async (translator, text, to, from, setting) => {
|
||||
const { openaiUrl, openaiKey, openaiModel, openaiPrompt } = setting;
|
||||
let prompt = openaiPrompt
|
||||
.replaceAll(PROMPT_PLACE_FROM, from)
|
||||
.replaceAll(PROMPT_PLACE_TO, to);
|
||||
return fetchPolyfill(openaiUrl, {
|
||||
export const apiBaiduLangdetect = async (text) => {
|
||||
const res = await fetchPolyfill(URL_BAIDU_LANGDETECT, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
model: openaiModel,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: prompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: text,
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 256,
|
||||
query: text,
|
||||
}),
|
||||
useCache: true,
|
||||
usePool: true,
|
||||
translator,
|
||||
token: openaiKey,
|
||||
});
|
||||
|
||||
if (res.error === 0) {
|
||||
return OPT_LANGS_BAIDU.get(res.lan) ?? res.lan;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* 腾讯语言识别
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiTencentLangdetect = async (text) => {
|
||||
const body = JSON.stringify({
|
||||
header: {
|
||||
fn: "text_analysis",
|
||||
},
|
||||
text,
|
||||
});
|
||||
|
||||
const res = await fetchPolyfill(URL_TENCENT_TRANSMART, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body,
|
||||
useCache: true,
|
||||
});
|
||||
|
||||
return OPT_LANGS_TENCENT.get(res.language) ?? res.language;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -172,36 +102,114 @@ const apiOpenaiTranslate = async (translator, text, to, from, setting) => {
|
||||
*/
|
||||
export const apiTranslate = async ({
|
||||
translator,
|
||||
q,
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
setting,
|
||||
apiSetting = {},
|
||||
useCache = true,
|
||||
usePool = true,
|
||||
}) => {
|
||||
let trText = "";
|
||||
let isSame = false;
|
||||
|
||||
let from = OPT_LANGS_SPECIAL?.[translator]?.get(fromLang) ?? fromLang;
|
||||
let to = OPT_LANGS_SPECIAL?.[translator]?.get(toLang) ?? toLang;
|
||||
|
||||
if (translator === OPT_TRANS_GOOGLE) {
|
||||
const res = await apiGoogleTranslate(translator, q, to, from, setting);
|
||||
trText = res.sentences.map((item) => item.trans).join(" ");
|
||||
isSame = to === res.src;
|
||||
} else if (translator === OPT_TRANS_MICROSOFT) {
|
||||
const res = await apiMicrosoftTranslate(translator, q, to, from);
|
||||
trText = res[0].translations[0].text;
|
||||
isSame = to === res[0].detectedLanguage.language;
|
||||
} else if (translator === OPT_TRANS_DEEPL) {
|
||||
const res = await apiDeepLTranslate(translator, q, to, from, setting);
|
||||
trText = res.translations.map((item) => item.text).join(" ");
|
||||
isSame = to === res.translations[0].detected_source_language;
|
||||
} else if (translator === OPT_TRANS_OPENAI) {
|
||||
const res = await apiOpenaiTranslate(translator, q, to, from, setting);
|
||||
trText = res?.choices?.[0].message.content;
|
||||
const sLang = await tryDetectLang(q);
|
||||
const tLang = await tryDetectLang(trText);
|
||||
isSame = q === trText || (sLang && tLang && sLang === tLang);
|
||||
if (!text) {
|
||||
return [trText, true];
|
||||
}
|
||||
|
||||
return [trText, isSame];
|
||||
const from =
|
||||
OPT_LANGS_SPECIAL[translator].get(fromLang) ??
|
||||
OPT_LANGS_SPECIAL[translator].get("auto");
|
||||
const to = OPT_LANGS_SPECIAL[translator].get(toLang);
|
||||
if (!to) {
|
||||
console.log(`[trans] target lang: ${toLang} not support`);
|
||||
return [trText, isSame];
|
||||
}
|
||||
|
||||
// 版本号一/二位升级,旧缓存失效
|
||||
const [v1, v2] = process.env.REACT_APP_VERSION.split(".");
|
||||
const cacheOpts = {
|
||||
translator,
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
version: [v1, v2].join("."),
|
||||
};
|
||||
|
||||
const transOpts = {
|
||||
translator,
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
};
|
||||
|
||||
const res = await fetchPolyfill(
|
||||
`${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`,
|
||||
{
|
||||
useCache,
|
||||
usePool,
|
||||
transOpts,
|
||||
apiSetting,
|
||||
}
|
||||
);
|
||||
|
||||
switch (translator) {
|
||||
case OPT_TRANS_GOOGLE:
|
||||
trText = res.sentences.map((item) => item.trans).join(" ");
|
||||
isSame = to === res.src;
|
||||
break;
|
||||
case OPT_TRANS_MICROSOFT:
|
||||
trText = res
|
||||
.map((item) => item.translations.map((item) => item.text).join(" "))
|
||||
.join(" ");
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_DEEPL:
|
||||
trText = res.translations.map((item) => item.text).join(" ");
|
||||
isSame = to === res.translations[0].detected_source_language;
|
||||
break;
|
||||
case OPT_TRANS_DEEPLFREE:
|
||||
trText = res.result?.texts.map((item) => item.text).join(" ");
|
||||
isSame = to === res.result?.lang;
|
||||
break;
|
||||
case OPT_TRANS_DEEPLX:
|
||||
trText = res.data;
|
||||
isSame = to === res.source_lang;
|
||||
break;
|
||||
case OPT_TRANS_BAIDU:
|
||||
// trText = res.trans_result?.data.map((item) => item.dst).join(" ");
|
||||
// isSame = res.trans_result?.to === res.trans_result?.from;
|
||||
if (res.type === 1) {
|
||||
trText = Object.keys(JSON.parse(res.result).content[0].mean[0].cont)[0];
|
||||
isSame = to === res.from;
|
||||
} else if (res.type === 2) {
|
||||
trText = res.data.map((item) => item.dst).join(" ");
|
||||
isSame = to === res.from;
|
||||
}
|
||||
break;
|
||||
case OPT_TRANS_TENCENT:
|
||||
trText = res.auto_translation;
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_OPENAI:
|
||||
trText = res?.choices?.map((item) => item.message.content).join(" ");
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_GEMINI:
|
||||
trText = res?.candidates
|
||||
?.map((item) => item.content.parts.map((item) => item.text).join(" "))
|
||||
.join(" ");
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_CLOUDFLAREAI:
|
||||
trText = res?.result?.translated_text;
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_CUSTOMIZE:
|
||||
trText = res.text;
|
||||
isSame = to === res.from;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
return [trText, isSame, res];
|
||||
};
|
||||
|
||||
@@ -4,9 +4,16 @@ import {
|
||||
MSG_FETCH_LIMIT,
|
||||
MSG_FETCH_CLEAR,
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_OPEN_OPTIONS,
|
||||
MSG_SAVE_RULE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_OPEN_TRANBOX,
|
||||
MSG_CONTEXT_MENUS,
|
||||
MSG_COMMAND_SHORTCUTS,
|
||||
CMD_TOGGLE_TRANSLATE,
|
||||
CMD_TOGGLE_STYLE,
|
||||
CMD_OPEN_OPTIONS,
|
||||
CMD_OPEN_TRANBOX,
|
||||
} from "./config";
|
||||
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
|
||||
import { trySyncSettingAndRules } from "./libs/sync";
|
||||
@@ -14,31 +21,91 @@ import { fetchData, fetchPool } from "./libs/fetch";
|
||||
import { sendTabMsg } from "./libs/msg";
|
||||
import { trySyncAllSubRules } from "./libs/subRules";
|
||||
import { tryClearCaches } from "./libs";
|
||||
import { saveRule } from "./libs/rules";
|
||||
|
||||
globalThis.ContextType = "BACKGROUND";
|
||||
|
||||
/**
|
||||
* 添加右键菜单
|
||||
*/
|
||||
async function addContextMenus(contextMenuType = 1) {
|
||||
// 添加前先删除,避免重复ID的错误
|
||||
try {
|
||||
await browser.contextMenus.removeAll();
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
|
||||
switch (contextMenuType) {
|
||||
case 1:
|
||||
browser.contextMenus.create({
|
||||
id: CMD_TOGGLE_TRANSLATE,
|
||||
title: browser.i18n.getMessage("toggle_translate"),
|
||||
contexts: ["page", "selection"],
|
||||
});
|
||||
break;
|
||||
case 2:
|
||||
browser.contextMenus.create({
|
||||
id: CMD_TOGGLE_TRANSLATE,
|
||||
title: browser.i18n.getMessage("toggle_translate"),
|
||||
contexts: ["page", "selection"],
|
||||
});
|
||||
browser.contextMenus.create({
|
||||
id: CMD_TOGGLE_STYLE,
|
||||
title: browser.i18n.getMessage("toggle_style"),
|
||||
contexts: ["page", "selection"],
|
||||
});
|
||||
browser.contextMenus.create({
|
||||
id: CMD_OPEN_TRANBOX,
|
||||
title: browser.i18n.getMessage("open_tranbox"),
|
||||
contexts: ["page", "selection"],
|
||||
});
|
||||
browser.contextMenus.create({
|
||||
id: "options_separator",
|
||||
type: "separator",
|
||||
contexts: ["page", "selection"],
|
||||
});
|
||||
browser.contextMenus.create({
|
||||
id: CMD_OPEN_OPTIONS,
|
||||
title: browser.i18n.getMessage("open_options"),
|
||||
contexts: ["page", "selection"],
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件安装
|
||||
*/
|
||||
browser.runtime.onInstalled.addListener(() => {
|
||||
tryInitDefaultData();
|
||||
|
||||
// 右键菜单
|
||||
addContextMenus();
|
||||
});
|
||||
|
||||
/**
|
||||
* 浏览器启动
|
||||
*/
|
||||
browser.runtime.onStartup.addListener(async () => {
|
||||
console.log("browser onStartup");
|
||||
|
||||
// 同步数据
|
||||
await trySyncSettingAndRules(true);
|
||||
await trySyncSettingAndRules();
|
||||
|
||||
const { clearCache, contextMenuType, subrulesList } =
|
||||
await getSettingWithDefault();
|
||||
|
||||
// 清除缓存
|
||||
const setting = await getSettingWithDefault();
|
||||
if (setting.clearCache) {
|
||||
if (clearCache) {
|
||||
tryClearCaches();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
// firefox重启后菜单会消失,故重复添加
|
||||
addContextMenus(contextMenuType);
|
||||
|
||||
// 同步订阅规则
|
||||
trySyncAllSubRules(setting, true);
|
||||
trySyncAllSubRules({ subrulesList });
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -54,7 +121,7 @@ browser.runtime.onMessage.addListener(
|
||||
sendResponse({ data });
|
||||
})
|
||||
.catch((error) => {
|
||||
sendResponse({ error: error.message });
|
||||
sendResponse({ error: error.message, cause: error.cause });
|
||||
});
|
||||
break;
|
||||
case MSG_FETCH_LIMIT:
|
||||
@@ -66,6 +133,26 @@ browser.runtime.onMessage.addListener(
|
||||
fetchPool.clear();
|
||||
sendResponse({ data: "ok" });
|
||||
break;
|
||||
case MSG_OPEN_OPTIONS:
|
||||
browser.runtime.openOptionsPage();
|
||||
break;
|
||||
case MSG_SAVE_RULE:
|
||||
saveRule(args);
|
||||
break;
|
||||
case MSG_CONTEXT_MENUS:
|
||||
const { contextMenuType } = args;
|
||||
addContextMenus(contextMenuType);
|
||||
break;
|
||||
case MSG_COMMAND_SHORTCUTS:
|
||||
browser.commands
|
||||
.getAll()
|
||||
.then((commands) => {
|
||||
sendResponse({ data: commands });
|
||||
})
|
||||
.catch((error) => {
|
||||
sendResponse({ error: error.message });
|
||||
});
|
||||
break;
|
||||
default:
|
||||
sendResponse({ error: `message action is unavailable: ${action}` });
|
||||
}
|
||||
@@ -82,9 +169,36 @@ browser.commands.onCommand.addListener((command) => {
|
||||
case CMD_TOGGLE_TRANSLATE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
break;
|
||||
case CMD_OPEN_TRANBOX:
|
||||
sendTabMsg(MSG_OPEN_TRANBOX);
|
||||
break;
|
||||
case CMD_TOGGLE_STYLE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
case CMD_OPEN_OPTIONS:
|
||||
browser.runtime.openOptionsPage();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听右键菜单
|
||||
*/
|
||||
browser.contextMenus.onClicked.addListener(({ menuItemId }) => {
|
||||
switch (menuItemId) {
|
||||
case CMD_TOGGLE_TRANSLATE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
break;
|
||||
case CMD_TOGGLE_STYLE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
case CMD_OPEN_TRANBOX:
|
||||
sendTabMsg(MSG_OPEN_TRANBOX);
|
||||
break;
|
||||
case CMD_OPEN_OPTIONS:
|
||||
browser.runtime.openOptionsPage();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
283
src/common.js
Normal file
283
src/common.js
Normal file
@@ -0,0 +1,283 @@
|
||||
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 {
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
MSG_OPEN_TRANBOX,
|
||||
APP_LCNAME,
|
||||
DEFAULT_TRANBOX_SETTING,
|
||||
} from "./config";
|
||||
import { getFabWithDefault, getSettingWithDefault } from "./libs/storage";
|
||||
import { Translator } from "./libs/translator";
|
||||
import { isIframe, sendIframeMsg, sendParentMsg } from "./libs/iframe";
|
||||
import Slection from "./views/Selection";
|
||||
import { touchTapListener } from "./libs/touch";
|
||||
import { debounce, genEventName } from "./libs/utils";
|
||||
import { handlePing, injectScript } from "./libs/gm";
|
||||
import { browser } from "./libs/browser";
|
||||
import { matchFixer } from "./libs/webfix";
|
||||
import { matchRule } from "./libs/rules";
|
||||
import { trySyncAllSubRules } from "./libs/subRules";
|
||||
import { isInBlacklist } from "./libs/blacklist";
|
||||
import inputTranslate from "./libs/inputTranslate";
|
||||
|
||||
/**
|
||||
* 油猴脚本设置页面
|
||||
*/
|
||||
function runSettingPage() {
|
||||
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
|
||||
unsafeWindow.GM = GM;
|
||||
unsafeWindow.APP_INFO = {
|
||||
name: process.env.REACT_APP_NAME,
|
||||
version: process.env.REACT_APP_VERSION,
|
||||
};
|
||||
} else {
|
||||
const ping = genEventName();
|
||||
window.addEventListener(ping, handlePing);
|
||||
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
|
||||
const script = document.createElement("script");
|
||||
script.textContent = `(${injectScript})("${ping}")`;
|
||||
document.head.append(script);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件监听后端事件
|
||||
* @param {*} translator
|
||||
*/
|
||||
function runtimeListener(translator) {
|
||||
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
translator.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
translator.toggleStyle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
case MSG_TRANS_GETRULE:
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
translator.updateRule(args);
|
||||
sendIframeMsg(MSG_TRANS_PUTRULE, args);
|
||||
break;
|
||||
case MSG_OPEN_TRANBOX:
|
||||
window.dispatchEvent(new CustomEvent(MSG_OPEN_TRANBOX));
|
||||
break;
|
||||
default:
|
||||
return { error: `message action is unavailable: ${action}` };
|
||||
}
|
||||
return { data: translator.rule };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* iframe 页面执行
|
||||
* @param {*} setting
|
||||
*/
|
||||
function runIframe(setting) {
|
||||
let 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:
|
||||
if (!translator) {
|
||||
translator = new Translator(args, setting);
|
||||
} else {
|
||||
translator.updateRule(args || {});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
sendParentMsg(MSG_TRANS_GETRULE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 悬浮按钮
|
||||
* @param {*} translator
|
||||
* @returns
|
||||
*/
|
||||
async function showFab(translator) {
|
||||
const fab = await getFabWithDefault();
|
||||
const $action = document.createElement("div");
|
||||
$action.setAttribute("id", APP_LCNAME);
|
||||
document.body.parentElement.appendChild($action);
|
||||
const shadowContainer = $action.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
const shadowRootElement = document.createElement("div");
|
||||
shadowContainer.appendChild(emotionRoot);
|
||||
shadowContainer.appendChild(shadowRootElement);
|
||||
const cache = createCache({
|
||||
key: APP_LCNAME,
|
||||
prepend: true,
|
||||
container: emotionRoot,
|
||||
});
|
||||
ReactDOM.createRoot(shadowRootElement).render(
|
||||
<React.StrictMode>
|
||||
<CacheProvider value={cache}>
|
||||
<Action translator={translator} fab={fab} />
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 划词翻译
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
function showTransbox({
|
||||
contextMenuType,
|
||||
tranboxSetting = DEFAULT_TRANBOX_SETTING,
|
||||
transApis,
|
||||
}) {
|
||||
if (!tranboxSetting?.transOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $tranbox = document.createElement("div");
|
||||
$tranbox.setAttribute("id", "kiss-transbox");
|
||||
document.body.parentElement.appendChild($tranbox);
|
||||
const shadowContainer = $tranbox.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
const shadowRootElement = document.createElement("div");
|
||||
shadowContainer.appendChild(emotionRoot);
|
||||
shadowContainer.appendChild(shadowRootElement);
|
||||
const cache = createCache({
|
||||
key: "kiss-transbox",
|
||||
prepend: true,
|
||||
container: emotionRoot,
|
||||
});
|
||||
ReactDOM.createRoot(shadowRootElement).render(
|
||||
<React.StrictMode>
|
||||
<CacheProvider value={cache}>
|
||||
<Slection
|
||||
contextMenuType={contextMenuType}
|
||||
tranboxSetting={tranboxSetting}
|
||||
transApis={transApis}
|
||||
/>
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听来自iframe页面消息
|
||||
* @param {*} rule
|
||||
*/
|
||||
function windowListener(rule) {
|
||||
window.addEventListener("message", (e) => {
|
||||
const { action } = e.data || {};
|
||||
switch (action) {
|
||||
case MSG_TRANS_GETRULE:
|
||||
sendIframeMsg(MSG_TRANS_PUTRULE, rule);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误信息到页面顶部
|
||||
* @param {*} message
|
||||
*/
|
||||
function showErr(message) {
|
||||
const $err = document.createElement("div");
|
||||
$err.innerText = `KISS-Translator: ${message}`;
|
||||
$err.style.cssText = "background:red; color:#fff;";
|
||||
document.body.prepend($err);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听触屏操作
|
||||
* @param {*} translator
|
||||
* @returns
|
||||
*/
|
||||
function touchOperation(translator) {
|
||||
const { touchTranslate = 2 } = translator.setting;
|
||||
if (touchTranslate === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleTap = debounce(() => {
|
||||
translator.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
});
|
||||
touchTapListener(handleTap, touchTranslate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 入口函数
|
||||
*/
|
||||
export async function run(isUserscript = false) {
|
||||
try {
|
||||
const href = document.location.href;
|
||||
|
||||
// 设置页面
|
||||
if (
|
||||
isUserscript &&
|
||||
(href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
|
||||
href.includes(process.env.REACT_APP_OPTIONSPAGE) ||
|
||||
href.includes(process.env.REACT_APP_OPTIONSPAGE2))
|
||||
) {
|
||||
runSettingPage();
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取设置信息
|
||||
const setting = await getSettingWithDefault();
|
||||
|
||||
// 黑名单
|
||||
if (isInBlacklist(href, setting)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 适配iframe
|
||||
if (isIframe) {
|
||||
runIframe(setting);
|
||||
return;
|
||||
}
|
||||
|
||||
// 不规范网页修复
|
||||
const fixerSetting = await matchFixer(href, setting);
|
||||
|
||||
// 翻译网页
|
||||
const rule = await matchRule(href, setting);
|
||||
const translator = new Translator(rule, setting, fixerSetting);
|
||||
|
||||
// 监听消息
|
||||
windowListener(rule);
|
||||
!isUserscript && runtimeListener(translator);
|
||||
|
||||
// 输入框翻译
|
||||
inputTranslate(setting);
|
||||
|
||||
// 划词翻译
|
||||
showTransbox(setting);
|
||||
|
||||
// 浮球按钮
|
||||
await showFab(translator);
|
||||
|
||||
// 触屏操作
|
||||
touchOperation(translator);
|
||||
|
||||
// 同步订阅规则
|
||||
isUserscript && (await trySyncAllSubRules(setting));
|
||||
} catch (err) {
|
||||
console.error("[KISS-Translator]", err);
|
||||
showErr(err.message);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,101 @@ export const UI_LANGS = [
|
||||
["zh", "中文"],
|
||||
];
|
||||
|
||||
const customApiLangs = `["en", "English - English"],
|
||||
["zh-CN", "Simplified Chinese - 简体中文"],
|
||||
["zh-TW", "Traditional Chinese - 繁體中文"],
|
||||
["ar", "Arabic - العربية"],
|
||||
["bg", "Bulgarian - Български"],
|
||||
["ca", "Catalan - Català"],
|
||||
["hr", "Croatian - Hrvatski"],
|
||||
["cs", "Czech - Čeština"],
|
||||
["da", "Danish - Dansk"],
|
||||
["nl", "Dutch - Nederlands"],
|
||||
["fi", "Finnish - Suomi"],
|
||||
["fr", "French - Français"],
|
||||
["de", "German - Deutsch"],
|
||||
["el", "Greek - Ελληνικά"],
|
||||
["hi", "Hindi - हिन्दी"],
|
||||
["hu", "Hungarian - Magyar"],
|
||||
["id", "Indonesian - Indonesia"],
|
||||
["it", "Italian - Italiano"],
|
||||
["ja", "Japanese - 日本語"],
|
||||
["ko", "Korean - 한국어"],
|
||||
["ms", "Malay - Melayu"],
|
||||
["mt", "Maltese - Malti"],
|
||||
["nb", "Norwegian - Norsk Bokmål"],
|
||||
["pl", "Polish - Polski"],
|
||||
["pt", "Portuguese - Português"],
|
||||
["ro", "Romanian - Română"],
|
||||
["ru", "Russian - Русский"],
|
||||
["sk", "Slovak - Slovenčina"],
|
||||
["sl", "Slovenian - Slovenščina"],
|
||||
["es", "Spanish - Español"],
|
||||
["sv", "Swedish - Svenska"],
|
||||
["ta", "Tamil - தமிழ்"],
|
||||
["te", "Telugu - తెలుగు"],
|
||||
["th", "Thai - ไทย"],
|
||||
["tr", "Turkish - Türkçe"],
|
||||
["uk", "Ukrainian - Українська"],
|
||||
["vi", "Vietnamese - Tiếng Việt"],
|
||||
`;
|
||||
|
||||
const customApiHelpZH = `/// 自定义翻译源接口说明
|
||||
|
||||
// 请求(Request)数据将按下面规范发送
|
||||
{
|
||||
url: {{YOUR_URL}},
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"Authorization": "Bearer {{YOUR_KEY}}",
|
||||
},
|
||||
body: {
|
||||
text: "", // 需要翻译的文字
|
||||
from: "", // 源语言,可能为空,表示需要接口自动识别语言
|
||||
to: "", // 目标语言
|
||||
}
|
||||
}
|
||||
|
||||
// 返回(Response)数据需符合下面的JSON规范
|
||||
{
|
||||
text: "", // 翻译后的文字
|
||||
from: "", // 识别的源语言
|
||||
to: "", // 目标语言(可选)
|
||||
}
|
||||
|
||||
// 支持的语言代码如下
|
||||
${customApiLangs}
|
||||
`;
|
||||
|
||||
const customApiHelpEN = `/// Custom translation source interface description
|
||||
|
||||
// Request data will be sent according to the following specifications
|
||||
{
|
||||
url: {{YOUR_URL}},
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"Authorization": "Bearer {{YOUR_KEY}}",
|
||||
},
|
||||
body: {
|
||||
text: "", // text to be translated
|
||||
from: "", // Source language, may be empty
|
||||
to: "", // Target language
|
||||
}
|
||||
}
|
||||
|
||||
// The returned data must conform to the following JSON specification
|
||||
{
|
||||
text: "", // translated text
|
||||
from: "", // Recognized source language
|
||||
to: "", // Target language (optional)
|
||||
}
|
||||
|
||||
// The supported language codes are as follows
|
||||
${customApiLangs}
|
||||
`;
|
||||
|
||||
export const I18N = {
|
||||
app_name: {
|
||||
zh: `简约翻译`,
|
||||
@@ -12,9 +107,13 @@ export const I18N = {
|
||||
zh: `翻译`,
|
||||
en: `Translate`,
|
||||
},
|
||||
custom_api_help: {
|
||||
zh: customApiHelpZH,
|
||||
en: customApiHelpEN,
|
||||
},
|
||||
translate_alt: {
|
||||
zh: `翻译 (Alt+Q)`,
|
||||
en: `Translate (Alt+Q)`,
|
||||
zh: `翻译`,
|
||||
en: `Translate`,
|
||||
},
|
||||
basic_setting: {
|
||||
zh: `基本设置`,
|
||||
@@ -24,10 +123,26 @@ export const I18N = {
|
||||
zh: `规则设置`,
|
||||
en: `Rules Setting`,
|
||||
},
|
||||
apis_setting: {
|
||||
zh: `接口设置`,
|
||||
en: `Apis Setting`,
|
||||
},
|
||||
sync_setting: {
|
||||
zh: `同步设置`,
|
||||
en: `Sync Setting`,
|
||||
},
|
||||
patch_setting: {
|
||||
zh: `补丁设置`,
|
||||
en: `Patch Setting`,
|
||||
},
|
||||
patch_setting_help: {
|
||||
zh: `针对一些特殊网站的修正脚本,以便翻译软件得到更好的展示效果。`,
|
||||
en: `Corrected scripts for some special websites so that the translation software can get better display results.`,
|
||||
},
|
||||
inject_webfix: {
|
||||
zh: `注入修复补丁`,
|
||||
en: `Inject Webfix`,
|
||||
},
|
||||
about: {
|
||||
zh: `关于`,
|
||||
en: `About`,
|
||||
@@ -68,6 +183,34 @@ export const I18N = {
|
||||
zh: `翻译服务`,
|
||||
en: `Translate Service`,
|
||||
},
|
||||
translate_timing: {
|
||||
zh: `翻译时机`,
|
||||
en: `Translate Timing`,
|
||||
},
|
||||
mk_disable: {
|
||||
zh: `滚动加载(建议)`,
|
||||
en: `Rolling Loading (Suggested)`,
|
||||
},
|
||||
mk_pageopen: {
|
||||
zh: `页面打开`,
|
||||
en: `Page Open`,
|
||||
},
|
||||
mk_mouseover: {
|
||||
zh: `鼠标悬停`,
|
||||
en: `Mouseover`,
|
||||
},
|
||||
mk_ctrlKey: {
|
||||
zh: `Control + 鼠标悬停`,
|
||||
en: `Control + Mouseover`,
|
||||
},
|
||||
mk_shiftKey: {
|
||||
zh: `Shift + 鼠标悬停`,
|
||||
en: `Shift + Mouseover`,
|
||||
},
|
||||
mk_altKey: {
|
||||
zh: `Alt + 鼠标悬停`,
|
||||
en: `Alt + Mouseover`,
|
||||
},
|
||||
from_lang: {
|
||||
zh: `原文语言`,
|
||||
en: `Source Language`,
|
||||
@@ -76,13 +219,21 @@ export const I18N = {
|
||||
zh: `目标语言`,
|
||||
en: `Target Language`,
|
||||
},
|
||||
to_lang2: {
|
||||
zh: `第二目标语言`,
|
||||
en: `Target Language 2`,
|
||||
},
|
||||
to_lang2_helper: {
|
||||
zh: `设定后,与目标语言产生互译效果,但依赖远程语言识别。`,
|
||||
en: `After setting, it will produce mutual translation effect with the target language, but it relies on remote language recognition.`,
|
||||
},
|
||||
text_style: {
|
||||
zh: `文字样式`,
|
||||
en: `Text Style`,
|
||||
},
|
||||
text_style_alt: {
|
||||
zh: `文字样式 (Alt+C)`,
|
||||
en: `Text Style (Alt+C)`,
|
||||
zh: `文字样式`,
|
||||
en: `Text Style`,
|
||||
},
|
||||
bg_color: {
|
||||
zh: `样式颜色`,
|
||||
@@ -134,15 +285,15 @@ export const I18N = {
|
||||
},
|
||||
personal_rules: {
|
||||
zh: `个人规则`,
|
||||
en: `Personal Rules`,
|
||||
en: `Rules`,
|
||||
},
|
||||
subscribe_rules: {
|
||||
zh: `订阅规则`,
|
||||
en: `Subscribe Rules`,
|
||||
en: `Subscribe`,
|
||||
},
|
||||
overwrite_subscribe_rules: {
|
||||
zh: `覆写订阅规则`,
|
||||
en: `Overwrite Subscribe Rules`,
|
||||
en: `Overwrite`,
|
||||
},
|
||||
subscribe_url: {
|
||||
zh: `订阅地址`,
|
||||
@@ -196,13 +347,17 @@ export const I18N = {
|
||||
zh: `高亮`,
|
||||
en: `Highlight`,
|
||||
},
|
||||
blockquote: {
|
||||
zh: `引用`,
|
||||
en: `Blockquote`,
|
||||
},
|
||||
diy_style: {
|
||||
zh: `自定义样式`,
|
||||
en: `Custom Style`,
|
||||
},
|
||||
diy_style_helper: {
|
||||
zh: `遵循“styled-components”的语法`,
|
||||
en: `Follow the syntax of "styled-components"`,
|
||||
zh: `遵循“CSS”的语法`,
|
||||
en: `Follow the syntax of "CSS"`,
|
||||
},
|
||||
setting: {
|
||||
zh: `设置`,
|
||||
@@ -213,15 +368,15 @@ export const I18N = {
|
||||
en: `URL pattern`,
|
||||
},
|
||||
pattern_helper: {
|
||||
zh: `1、支持星号(*)通配符。2、多个URL支持英文逗号“,”分隔。`,
|
||||
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`,
|
||||
zh: `1、支持星号(*)通配符。2、多个URL用换行或英文逗号“,”分隔。`,
|
||||
en: `1. Supports the asterisk (*) wildcard character. 2. Separate multiple URLs with newlines or English commas ",".`,
|
||||
},
|
||||
selector_helper: {
|
||||
zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`,
|
||||
en: `1. Follow CSS selector syntax. 2. Leave blank to adopt the global setting. 3. Separate multiple CSS selectors with ";". 4. The "shadow root" selector and the internal selector are separated by ">>>".`,
|
||||
},
|
||||
translate_switch: {
|
||||
zh: `开启翻译`,
|
||||
zh: `启停翻译`,
|
||||
en: `Translate Switch`,
|
||||
},
|
||||
default_enabled: {
|
||||
@@ -236,6 +391,30 @@ export const I18N = {
|
||||
zh: `选择器`,
|
||||
en: `Selector`,
|
||||
},
|
||||
keep_selector: {
|
||||
zh: `保留元素选择器`,
|
||||
en: `Keep unchanged selector`,
|
||||
},
|
||||
keep_selector_helper: {
|
||||
zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。3、子元素选择器用“>>>”隔开。`,
|
||||
en: `1. Follow CSS selector syntax. 2. Leave blank to adopt the global setting. 3.Sub-element selectors are separated by ">>>".`,
|
||||
},
|
||||
terms: {
|
||||
zh: `专业术语`,
|
||||
en: `Terms`,
|
||||
},
|
||||
terms_helper: {
|
||||
zh: `0、支持正则表达式匹配。1、多条术语用换行或分号“;”隔开。2、术语和译文用英文逗号“,”隔开。3、没有译文视为不翻译术语。4、留空表示采用全局设置。`,
|
||||
en: `0. Supports regular expression matching. 1. Separate multiple terms with newlines or semicolons ";". 2. Terms and translations are separated by English commas ",". 3. If there is no translation, the term will be deemed not to be translated. 4. Leave blank to adopt the global setting.`,
|
||||
},
|
||||
root_selector: {
|
||||
zh: `根选择器`,
|
||||
en: `Root Selector`,
|
||||
},
|
||||
fixer_function: {
|
||||
zh: `修复函数`,
|
||||
en: `Fixer Function`,
|
||||
},
|
||||
import: {
|
||||
zh: `导入`,
|
||||
en: `Import`,
|
||||
@@ -285,8 +464,8 @@ export const I18N = {
|
||||
en: `OpenAI Prompt`,
|
||||
},
|
||||
if_clear_cache: {
|
||||
zh: `是否清除缓存 (仅用于扩展)`,
|
||||
en: `Whether clear cache (only for extension)`,
|
||||
zh: `是否清除缓存`,
|
||||
en: `Whether clear cache`,
|
||||
},
|
||||
clear_cache_never: {
|
||||
zh: `不清除缓存`,
|
||||
@@ -296,17 +475,25 @@ export const I18N = {
|
||||
zh: `重启浏览器时清除缓存`,
|
||||
en: `Clear cache when restarting browser`,
|
||||
},
|
||||
data_sync_type: {
|
||||
zh: `数据同步方式`,
|
||||
en: `Data Sync Type`,
|
||||
},
|
||||
data_sync_url: {
|
||||
zh: `数据同步接口`,
|
||||
en: `Data Sync API`,
|
||||
},
|
||||
data_sync_user: {
|
||||
zh: `数据同步账户`,
|
||||
en: `Data Sync User`,
|
||||
},
|
||||
data_sync_key: {
|
||||
zh: `数据同步密钥`,
|
||||
en: `Data Sync Key`,
|
||||
},
|
||||
data_sync_test: {
|
||||
zh: `数据同步测试`,
|
||||
en: `Data Sync Test`,
|
||||
sync_now: {
|
||||
zh: `立即同步`,
|
||||
en: `Sync Now`,
|
||||
},
|
||||
sync_success: {
|
||||
zh: `同步成功!`,
|
||||
@@ -321,8 +508,8 @@ export const I18N = {
|
||||
en: `Sorry, something went wrong!`,
|
||||
},
|
||||
error_sync_setting: {
|
||||
zh: `您的同步设置未填写,无法在线分享。`,
|
||||
en: `Your sync settings are missing and cannot be shared online.`,
|
||||
zh: `您的同步类型必须为“KISS-Worker”,且需填写完整`,
|
||||
en: `Your sync type must be "KISS-Worker" and must be filled in completely`,
|
||||
},
|
||||
click_test: {
|
||||
zh: `点击测试`,
|
||||
@@ -356,8 +543,208 @@ export const I18N = {
|
||||
zh: `分享`,
|
||||
en: `Share`,
|
||||
},
|
||||
clear_all: {
|
||||
zh: `清空`,
|
||||
en: `Clear All`,
|
||||
},
|
||||
help: {
|
||||
zh: `求助`,
|
||||
en: `Help`,
|
||||
},
|
||||
restore_default: {
|
||||
zh: `恢复默认`,
|
||||
en: `Restore Default`,
|
||||
},
|
||||
shortcuts_setting: {
|
||||
zh: `快捷键设置`,
|
||||
en: `Shortcuts Setting`,
|
||||
},
|
||||
toggle_translate_shortcut: {
|
||||
zh: `"启停翻译"快捷键`,
|
||||
en: `"Toggle Translate" Shortcut`,
|
||||
},
|
||||
toggle_style_shortcut: {
|
||||
zh: `"切换样式"快捷键`,
|
||||
en: `"Toggle Style" Shortcut`,
|
||||
},
|
||||
toggle_popup_shortcut: {
|
||||
zh: `"打开弹窗"快捷键`,
|
||||
en: `"Open Popup" Shortcut`,
|
||||
},
|
||||
open_setting_shortcut: {
|
||||
zh: `"打开设置"快捷键`,
|
||||
en: `"Open Setting" Shortcut`,
|
||||
},
|
||||
hide_fab_button: {
|
||||
zh: `隐藏悬浮按钮`,
|
||||
en: `Hide Fab Button`,
|
||||
},
|
||||
hide_tran_button: {
|
||||
zh: `隐藏翻译按钮`,
|
||||
en: `Hide Translate Button`,
|
||||
},
|
||||
show: {
|
||||
zh: `显示`,
|
||||
en: `Show`,
|
||||
},
|
||||
hide: {
|
||||
zh: `隐藏`,
|
||||
en: `Hide`,
|
||||
},
|
||||
save_rule: {
|
||||
zh: `保存规则`,
|
||||
en: `Save Rule`,
|
||||
},
|
||||
global_rule: {
|
||||
zh: `全局规则`,
|
||||
en: `Global Rule`,
|
||||
},
|
||||
input_translate: {
|
||||
zh: `输入框翻译`,
|
||||
en: `Input Box Translation`,
|
||||
},
|
||||
use_input_box_translation: {
|
||||
zh: `启用输入框翻译`,
|
||||
en: `Input Box Translation`,
|
||||
},
|
||||
input_selector: {
|
||||
zh: `输入框选择器`,
|
||||
en: `Input Selector`,
|
||||
},
|
||||
input_selector_helper: {
|
||||
zh: `用于输入框翻译。`,
|
||||
en: `Used for input box translation.`,
|
||||
},
|
||||
trigger_trans_shortcut: {
|
||||
zh: `触发翻译快捷键`,
|
||||
en: `Trigger Translation Shortcut Keys`,
|
||||
},
|
||||
trigger_trans_shortcut_help: {
|
||||
zh: `默认为单击“AltLeft+KeyI”`,
|
||||
en: `Default is "AltLeft+KeyI"`,
|
||||
},
|
||||
shortcut_press_count: {
|
||||
zh: `快捷键连击次数`,
|
||||
en: `Shortcut Press Number`,
|
||||
},
|
||||
combo_timeout: {
|
||||
zh: `连击超时时间 (10-1000ms)`,
|
||||
en: `Combo Timeout (10-1000ms)`,
|
||||
},
|
||||
input_trans_start_sign: {
|
||||
zh: `翻译起始标识`,
|
||||
en: `Translation Start Sign`,
|
||||
},
|
||||
input_trans_start_sign_help: {
|
||||
zh: `标识后面可以加目标语言代码,如: “/en 你好”、“/zh hello”`,
|
||||
en: `The target language code can be added after the sign, such as: "/en 你好", "/zh hello"`,
|
||||
},
|
||||
detect_lang_remote: {
|
||||
zh: `远程语言检测`,
|
||||
en: `Remote language detection`,
|
||||
},
|
||||
detect_lang_remote_help: {
|
||||
zh: `启用后检测准确度增加,但会降低翻译速度,请酌情开启`,
|
||||
en: `After enabling, the detection accuracy will increase, but it will reduce the translation speed. Please enable it as appropriate.`,
|
||||
},
|
||||
disable: {
|
||||
zh: `禁用`,
|
||||
en: `Disable`,
|
||||
},
|
||||
enable: {
|
||||
zh: `启用`,
|
||||
en: `Enable`,
|
||||
},
|
||||
selection_translate: {
|
||||
zh: `划词翻译`,
|
||||
en: `Selection Translate`,
|
||||
},
|
||||
toggle_selection_translate: {
|
||||
zh: `启用划词翻译`,
|
||||
en: `Use Selection Translate`,
|
||||
},
|
||||
trigger_tranbox_shortcut: {
|
||||
zh: `显示翻译框/翻译选中文字快捷键`,
|
||||
en: `Open Translate Popup/Translate Selected Shortcut`,
|
||||
},
|
||||
tranbtn_offset_x: {
|
||||
zh: `翻译按钮偏移X(0-100)`,
|
||||
en: `Translate Button Offset X (0-100)`,
|
||||
},
|
||||
tranbtn_offset_y: {
|
||||
zh: `翻译按钮偏移Y(0-100)`,
|
||||
en: `Translate Button Offset Y (0-100)`,
|
||||
},
|
||||
translated_text: {
|
||||
zh: `译文`,
|
||||
en: `Translated Text`,
|
||||
},
|
||||
original_text: {
|
||||
zh: `原文`,
|
||||
en: `Original Text`,
|
||||
},
|
||||
favorite_words: {
|
||||
zh: `收藏词汇`,
|
||||
en: `Favorite Words`,
|
||||
},
|
||||
touch_setting: {
|
||||
zh: `触屏设置`,
|
||||
en: `Touch Setting`,
|
||||
},
|
||||
touch_translate_shortcut: {
|
||||
zh: `触屏翻译快捷方式`,
|
||||
en: `Touch Translate Shortcut`,
|
||||
},
|
||||
touch_tap_0: {
|
||||
zh: `禁用`,
|
||||
en: `Disable`,
|
||||
},
|
||||
touch_tap_2: {
|
||||
zh: `双指轻触`,
|
||||
en: `Two finger tap`,
|
||||
},
|
||||
touch_tap_3: {
|
||||
zh: `三指轻触`,
|
||||
en: `Three finger tap`,
|
||||
},
|
||||
touch_tap_4: {
|
||||
zh: `四指轻触`,
|
||||
en: `Four finger tap`,
|
||||
},
|
||||
translate_blacklist: {
|
||||
zh: `禁用翻译名单`,
|
||||
en: `Translate Blacklist`,
|
||||
},
|
||||
disable_langs: {
|
||||
zh: `不翻译的语言`,
|
||||
en: `Disable Languages`,
|
||||
},
|
||||
disable_langs_helper: {
|
||||
zh: `此功能依赖准确的语言检测,建议启用远程语言检测。`,
|
||||
en: `This feature relies on accurate language detection. It is recommended to enable remote language detection.`,
|
||||
},
|
||||
context_menus: {
|
||||
zh: `右键菜单`,
|
||||
en: `Context Menus`,
|
||||
},
|
||||
hide_context_menus: {
|
||||
zh: `隐藏右键菜单`,
|
||||
en: `Hide Context Menus`,
|
||||
},
|
||||
simple_context_menus: {
|
||||
zh: `简单右键菜单`,
|
||||
en: `Simple_context_menus Context Menus`,
|
||||
},
|
||||
secondary_context_menus: {
|
||||
zh: `二级右键菜单`,
|
||||
en: `Secondary Context Menus`,
|
||||
},
|
||||
mulkeys_help: {
|
||||
zh: `支持用换行或英文逗号“,”分隔多个KEY轮询调用。`,
|
||||
en: `Supports multiple KEY polling calls separated by newlines or English commas ",".`,
|
||||
},
|
||||
translate_page_title: {
|
||||
zh: `是否同时翻译页面标题`,
|
||||
en: `Translate Page Title`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
DEFAULT_SELECTOR,
|
||||
DEFAULT_KEEP_SELECTOR,
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
SHADOW_KEY,
|
||||
@@ -20,14 +21,20 @@ export {
|
||||
};
|
||||
|
||||
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
|
||||
export const STOKEY_BDAUTH = `${APP_NAME}_bdauth`;
|
||||
export const STOKEY_SETTING = `${APP_NAME}_setting`;
|
||||
export const STOKEY_RULES = `${APP_NAME}_rules`;
|
||||
export const STOKEY_WFRULES = `${APP_NAME}_webfix_rules`;
|
||||
export const STOKEY_WORDS = `${APP_NAME}_words`;
|
||||
export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
||||
export const STOKEY_FAB = `${APP_NAME}_fab`;
|
||||
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
|
||||
export const STOKEY_WEBFIXCACHE_PREFIX = `${APP_NAME}_webfixcache_`;
|
||||
|
||||
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
|
||||
export const CMD_TOGGLE_STYLE = "toggleStyle";
|
||||
export const CMD_OPEN_OPTIONS = "openOptions";
|
||||
export const CMD_OPEN_TRANBOX = "openTranbox";
|
||||
|
||||
export const CLIENT_WEB = "web";
|
||||
export const CLIENT_CHROME = "chrome";
|
||||
@@ -36,9 +43,11 @@ export const CLIENT_FIREFOX = "firefox";
|
||||
export const CLIENT_USERSCRIPT = "userscript";
|
||||
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
|
||||
|
||||
export const KV_RULES_KEY = "KT_RULES";
|
||||
export const KV_RULES_SHARE_KEY = "KT_RULES_SHARE";
|
||||
export const KV_SETTING_KEY = "KT_SETTING";
|
||||
export const KV_RULES_KEY = "kiss-rules.json";
|
||||
export const KV_WFRULES_KEY = "kiss-webfix.json";
|
||||
export const KV_WORDS_KEY = "kiss-words.json";
|
||||
export const KV_RULES_SHARE_KEY = "kiss-rules-share.json";
|
||||
export const KV_SETTING_KEY = "kiss-setting.json";
|
||||
export const KV_SALT_SYNC = "KISS-Translator-SYNC";
|
||||
export const KV_SALT_SHARE = "KISS-Translator-SHARE";
|
||||
|
||||
@@ -47,11 +56,16 @@ export const CACHE_NAME = `${APP_NAME}_cache`;
|
||||
export const MSG_FETCH = "fetch";
|
||||
export const MSG_FETCH_LIMIT = "fetch_limit";
|
||||
export const MSG_FETCH_CLEAR = "fetch_clear";
|
||||
export const MSG_OPEN_OPTIONS = "open_options";
|
||||
export const MSG_SAVE_RULE = "save_rule";
|
||||
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
||||
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
|
||||
export const MSG_OPEN_TRANBOX = "open_tranbox";
|
||||
export const MSG_TRANS_GETRULE = "trans_getrule";
|
||||
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
||||
export const MSG_TRANS_CURRULE = "trans_currule";
|
||||
export const MSG_CONTEXT_MENUS = "context_menus";
|
||||
export const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
|
||||
|
||||
export const THEME_LIGHT = "light";
|
||||
export const THEME_DARK = "dark";
|
||||
@@ -63,19 +77,41 @@ export const URL_KISS_RULES_NEW_ISSUE =
|
||||
"https://github.com/fishjar/kiss-rules/issues/new";
|
||||
export const URL_RAW_PREFIX =
|
||||
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
|
||||
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
|
||||
export const URL_MICROSOFT_TRANS =
|
||||
|
||||
export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
|
||||
export const URL_MICROSOFT_TRAN =
|
||||
"https://api-edge.cognitive.microsofttranslator.com/translate";
|
||||
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
|
||||
export const URL_BAIDU_LANGDETECT = "https://fanyi.baidu.com/langdetect";
|
||||
export const URL_BAIDU_WEB = "https://fanyi.baidu.com/";
|
||||
export const URL_BAIDU_TRANSAPI = "https://fanyi.baidu.com/transapi";
|
||||
export const URL_BAIDU_TRANSAPI_V2 = "https://fanyi.baidu.com/v2transapi";
|
||||
export const URL_DEEPLFREE_TRAN = "https://www2.deepl.com/jsonrpc";
|
||||
export const URL_TENCENT_TRANSMART = "https://transmart.qq.com/api/imt";
|
||||
|
||||
export const OPT_TRANS_GOOGLE = "Google";
|
||||
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
||||
export const OPT_TRANS_DEEPL = "DeepL";
|
||||
export const OPT_TRANS_DEEPLX = "DeepLX";
|
||||
export const OPT_TRANS_DEEPLFREE = "DeepLFree";
|
||||
export const OPT_TRANS_BAIDU = "Baidu";
|
||||
export const OPT_TRANS_TENCENT = "Tencent";
|
||||
export const OPT_TRANS_OPENAI = "OpenAI";
|
||||
export const OPT_TRANS_GEMINI = "Gemini";
|
||||
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
|
||||
export const OPT_TRANS_CUSTOMIZE = "Custom";
|
||||
export const OPT_TRANS_ALL = [
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_DEEPLX,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
];
|
||||
|
||||
export const OPT_LANGS_TO = [
|
||||
@@ -119,7 +155,9 @@ export const OPT_LANGS_TO = [
|
||||
];
|
||||
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
|
||||
export const OPT_LANGS_SPECIAL = {
|
||||
[OPT_TRANS_GOOGLE]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])),
|
||||
[OPT_TRANS_MICROSOFT]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["auto", ""],
|
||||
["zh-CN", "zh-Hans"],
|
||||
["zh-TW", "zh-Hant"],
|
||||
@@ -130,10 +168,106 @@ export const OPT_LANGS_SPECIAL = {
|
||||
["zh-CN", "ZH"],
|
||||
["zh-TW", "ZH"],
|
||||
]),
|
||||
[OPT_TRANS_DEEPLFREE]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
|
||||
["auto", "auto"],
|
||||
["zh-CN", "ZH"],
|
||||
["zh-TW", "ZH"],
|
||||
]),
|
||||
[OPT_TRANS_DEEPLX]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
|
||||
["auto", ""],
|
||||
["zh-CN", "ZH"],
|
||||
["zh-TW", "ZH"],
|
||||
]),
|
||||
[OPT_TRANS_BAIDU]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["zh-CN", "zh"],
|
||||
["zh-TW", "cht"],
|
||||
["ar", "ara"],
|
||||
["bg", "bul"],
|
||||
["ca", "cat"],
|
||||
["hr", "hrv"],
|
||||
["da", "dan"],
|
||||
["fi", "fin"],
|
||||
["fr", "fra"],
|
||||
["hi", "mai"],
|
||||
["ja", "jp"],
|
||||
["ko", "kor"],
|
||||
["ms", "may"],
|
||||
["mt", "mlt"],
|
||||
["nb", "nor"],
|
||||
["ro", "rom"],
|
||||
["ru", "ru"],
|
||||
["sl", "slo"],
|
||||
["es", "spa"],
|
||||
["sv", "swe"],
|
||||
["ta", "tam"],
|
||||
["te", "tel"],
|
||||
["uk", "ukr"],
|
||||
["vi", "vie"],
|
||||
]),
|
||||
[OPT_TRANS_TENCENT]: new Map([
|
||||
["auto", "auto"],
|
||||
["zh-CN", "zh"],
|
||||
["zh-TW", "zh"],
|
||||
["en", "en"],
|
||||
["ar", "ar"],
|
||||
["de", "de"],
|
||||
["ru", "ru"],
|
||||
["fr", "fr"],
|
||||
["fi", "fil"],
|
||||
["ko", "ko"],
|
||||
["ms", "ms"],
|
||||
["pt", "pt"],
|
||||
["ja", "ja"],
|
||||
["th", "th"],
|
||||
["tr", "tr"],
|
||||
["es", "es"],
|
||||
["it", "it"],
|
||||
["hi", "hi"],
|
||||
["id", "id"],
|
||||
["vi", "vi"],
|
||||
]),
|
||||
[OPT_TRANS_OPENAI]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_GEMINI]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_CLOUDFLAREAI]: new Map([
|
||||
["auto", ""],
|
||||
["zh-CN", "chinese"],
|
||||
["zh-TW", "chinese"],
|
||||
["en", "english"],
|
||||
["ar", "arabic"],
|
||||
["de", "german"],
|
||||
["ru", "russian"],
|
||||
["fr", "french"],
|
||||
["pt", "portuguese"],
|
||||
["ja", "japanese"],
|
||||
["es", "spanish"],
|
||||
["hi", "hindi"],
|
||||
]),
|
||||
[OPT_TRANS_CUSTOMIZE]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["auto", ""],
|
||||
]),
|
||||
};
|
||||
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
|
||||
export const OPT_LANGS_BAIDU = new Map(
|
||||
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_BAIDU].entries()).map(([k, v]) => [
|
||||
v,
|
||||
k,
|
||||
])
|
||||
);
|
||||
export const OPT_LANGS_TENCENT = new Map(
|
||||
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_TENCENT].entries()).map(([k, v]) => [
|
||||
v,
|
||||
k,
|
||||
])
|
||||
);
|
||||
OPT_LANGS_TENCENT.set("zh", "zh-CN");
|
||||
|
||||
export const OPT_STYLE_NONE = "style_none"; // 无
|
||||
export const OPT_STYLE_LINE = "under_line"; // 下划线
|
||||
@@ -142,6 +276,7 @@ export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
|
||||
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_DIY = "diy_style"; // 自定义样式
|
||||
export const OPT_STYLE_ALL = [
|
||||
OPT_STYLE_NONE,
|
||||
@@ -151,6 +286,7 @@ export const OPT_STYLE_ALL = [
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_FUZZY,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_BLOCKQUOTE,
|
||||
OPT_STYLE_DIY,
|
||||
];
|
||||
export const OPT_STYLE_USE_COLOR = [
|
||||
@@ -159,6 +295,22 @@ export const OPT_STYLE_USE_COLOR = [
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_BLOCKQUOTE,
|
||||
];
|
||||
|
||||
export const OPT_MOUSEKEY_DISABLE = "mk_disable"; // 滚动加载翻译
|
||||
export const OPT_MOUSEKEY_PAGEOPEN = "mk_pageopen"; // 直接翻译到底
|
||||
export const OPT_MOUSEKEY_MOUSEOVER = "mk_mouseover";
|
||||
export const OPT_MOUSEKEY_CONTROL = "mk_ctrlKey";
|
||||
export const OPT_MOUSEKEY_SHIFT = "mk_shiftKey";
|
||||
export const OPT_MOUSEKEY_ALT = "mk_altKey";
|
||||
export const OPT_MOUSEKEY_ALL = [
|
||||
OPT_MOUSEKEY_DISABLE,
|
||||
OPT_MOUSEKEY_PAGEOPEN,
|
||||
OPT_MOUSEKEY_MOUSEOVER,
|
||||
OPT_MOUSEKEY_CONTROL,
|
||||
OPT_MOUSEKEY_SHIFT,
|
||||
OPT_MOUSEKEY_ALT,
|
||||
];
|
||||
|
||||
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
|
||||
@@ -166,6 +318,7 @@ export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
|
||||
|
||||
export const PROMPT_PLACE_FROM = "{{from}}"; // 占位符
|
||||
export const PROMPT_PLACE_TO = "{{to}}"; // 占位符
|
||||
export const PROMPT_PLACE_TEXT = "{{text}}"; // 占位符
|
||||
|
||||
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
|
||||
|
||||
@@ -173,6 +326,8 @@ export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
|
||||
export const GLOBLA_RULE = {
|
||||
pattern: "*",
|
||||
selector: DEFAULT_SELECTOR,
|
||||
keepSelector: DEFAULT_KEEP_SELECTOR,
|
||||
terms: "",
|
||||
translator: OPT_TRANS_MICROSOFT,
|
||||
fromLang: "auto",
|
||||
toLang: "zh-CN",
|
||||
@@ -182,6 +337,34 @@ export const GLOBLA_RULE = {
|
||||
textDiyStyle: "",
|
||||
};
|
||||
|
||||
// 输入框翻译
|
||||
export const OPT_INPUT_TRANS_SIGNS = ["/", "//", "\\", "\\\\", ">", ">>"];
|
||||
export const DEFAULT_INPUT_SHORTCUT = ["AltLeft", "KeyI"];
|
||||
export const DEFAULT_INPUT_RULE = {
|
||||
transOpen: true,
|
||||
translator: OPT_TRANS_MICROSOFT,
|
||||
fromLang: "auto",
|
||||
toLang: "en",
|
||||
triggerShortcut: DEFAULT_INPUT_SHORTCUT,
|
||||
triggerCount: 1,
|
||||
triggerTime: 200,
|
||||
transSign: OPT_INPUT_TRANS_SIGNS[0],
|
||||
};
|
||||
|
||||
// 划词翻译
|
||||
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
|
||||
export const DEFAULT_TRANBOX_SETTING = {
|
||||
transOpen: true,
|
||||
translator: OPT_TRANS_MICROSOFT,
|
||||
fromLang: "auto",
|
||||
toLang: "zh-CN",
|
||||
toLang2: "en",
|
||||
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
|
||||
btnOffsetX: 10,
|
||||
btnOffsetY: 10,
|
||||
hideTranBtn: false,
|
||||
};
|
||||
|
||||
// 订阅列表
|
||||
export const DEFAULT_SUBRULES_LIST = [
|
||||
{
|
||||
@@ -198,9 +381,64 @@ export const DEFAULT_SUBRULES_LIST = [
|
||||
},
|
||||
];
|
||||
|
||||
// 翻译接口
|
||||
export const DEFAULT_TRANS_APIS = {
|
||||
[OPT_TRANS_GOOGLE]: {
|
||||
url: "https://translate.googleapis.com/translate_a/single",
|
||||
key: "",
|
||||
},
|
||||
[OPT_TRANS_DEEPL]: {
|
||||
url: "https://api-free.deepl.com/v2/translate",
|
||||
key: "",
|
||||
},
|
||||
[OPT_TRANS_DEEPLX]: {
|
||||
url: "http://localhost:1188/translate",
|
||||
key: "",
|
||||
},
|
||||
[OPT_TRANS_OPENAI]: {
|
||||
url: "https://api.openai.com/v1/chat/completions",
|
||||
key: "",
|
||||
model: "gpt-4",
|
||||
prompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
|
||||
},
|
||||
[OPT_TRANS_GEMINI]: {
|
||||
url: "https://generativelanguage.googleapis.com/v1/models",
|
||||
key: "",
|
||||
model: "gemini-pro",
|
||||
prompt: `Translate the following text from ${PROMPT_PLACE_FROM} to ${PROMPT_PLACE_TO}:\n\n${PROMPT_PLACE_TEXT}`,
|
||||
},
|
||||
[OPT_TRANS_CLOUDFLAREAI]: {
|
||||
url: "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/@cf/meta/m2m100-1.2b",
|
||||
key: "",
|
||||
},
|
||||
[OPT_TRANS_CUSTOMIZE]: {
|
||||
url: "",
|
||||
key: "",
|
||||
},
|
||||
};
|
||||
|
||||
// 默认快捷键
|
||||
export const OPT_SHORTCUT_TRANSLATE = "toggleTranslate";
|
||||
export const OPT_SHORTCUT_STYLE = "toggleStyle";
|
||||
export const OPT_SHORTCUT_POPUP = "togglePopup";
|
||||
export const OPT_SHORTCUT_SETTING = "openSetting";
|
||||
export const DEFAULT_SHORTCUTS = {
|
||||
[OPT_SHORTCUT_TRANSLATE]: ["AltLeft", "KeyQ"],
|
||||
[OPT_SHORTCUT_STYLE]: ["AltLeft", "KeyC"],
|
||||
[OPT_SHORTCUT_POPUP]: ["AltLeft", "KeyK"],
|
||||
[OPT_SHORTCUT_SETTING]: ["AltLeft", "KeyO"],
|
||||
};
|
||||
|
||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
||||
export const TRANS_NEWLINE_LENGTH = 40; // 换行字符数
|
||||
export const TRANS_NEWLINE_LENGTH = 20; // 换行字符数
|
||||
export const DEFAULT_BLACKLIST = [
|
||||
"https://fishjar.github.io/kiss-translator/options.html",
|
||||
"https://translate.google.com",
|
||||
"https://www.deepl.com/translator",
|
||||
"oapi.dingtalk.com",
|
||||
"login.dingtalk.com",
|
||||
]; // 禁用翻译名单
|
||||
|
||||
export const DEFAULT_SETTING = {
|
||||
darkMode: false, // 深色模式
|
||||
@@ -212,25 +450,35 @@ export const DEFAULT_SETTING = {
|
||||
newlineLength: TRANS_NEWLINE_LENGTH,
|
||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||
injectRules: true, // 是否注入订阅规则
|
||||
injectWebfix: true, // 是否注入修复补丁
|
||||
detectRemote: false, // 是否使用远程语言检测
|
||||
contextMenus: true, // 是否添加右键菜单(作废)
|
||||
contextMenuType: 1, // 右键菜单类型(0不显示,1简单菜单,2多级菜单)
|
||||
transTitle: false, // 是否同时翻译页面标题
|
||||
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
||||
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
|
||||
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口
|
||||
deeplUrl: "https://api-free.deepl.com/v2/translate",
|
||||
deeplKey: "",
|
||||
openaiUrl: "https://api.openai.com/v1/chat/completions",
|
||||
openaiKey: "",
|
||||
openaiModel: "gpt-4",
|
||||
openaiPrompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
|
||||
transApis: DEFAULT_TRANS_APIS, // 翻译接口
|
||||
mouseKey: OPT_MOUSEKEY_DISABLE, // 翻译时机/鼠标悬停翻译
|
||||
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
||||
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
||||
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
|
||||
touchTranslate: 2, // 触屏翻译
|
||||
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
|
||||
disableLangs: [], // 不翻译的语言
|
||||
};
|
||||
|
||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||
|
||||
export const OPT_SYNCTYPE_WORKER = "KISS-Worker";
|
||||
export const OPT_SYNCTYPE_WEBDAV = "WebDAV";
|
||||
export const OPT_SYNCTYPE_ALL = [OPT_SYNCTYPE_WORKER, OPT_SYNCTYPE_WEBDAV];
|
||||
|
||||
export const DEFAULT_SYNC = {
|
||||
syncType: OPT_SYNCTYPE_WORKER, // 同步方式
|
||||
syncUrl: "", // 数据同步接口
|
||||
syncUser: "", // 数据同步用户名
|
||||
syncKey: "", // 数据同步密钥
|
||||
settingUpdateAt: 0,
|
||||
settingSyncAt: 0,
|
||||
rulesUpdateAt: 0,
|
||||
rulesSyncAt: 0,
|
||||
syncMeta: {}, // 数据更新及同步信息
|
||||
subRulesSyncAt: 0, // 订阅规则同步时间
|
||||
dataCaches: {}, // 缓存同步时间
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const els = `li, p, h1, h2, h3, h4, h5, h6, dd`;
|
||||
|
||||
export const DEFAULT_SELECTOR = `:is(${els})`;
|
||||
export const DEFAULT_SELECTOR = `:is(li, p, h1, h2, h3, h4, h5, h6, dd, blockquote)`;
|
||||
export const DEFAULT_KEEP_SELECTOR = `code, img, svg`;
|
||||
|
||||
export const GLOBAL_KEY = "*";
|
||||
export const REMAIN_KEY = "-";
|
||||
@@ -10,6 +9,8 @@ export const SHADOW_KEY = ">>>";
|
||||
export const DEFAULT_RULE = {
|
||||
pattern: "",
|
||||
selector: "",
|
||||
keepSelector: "",
|
||||
terms: "",
|
||||
translator: GLOBAL_KEY,
|
||||
fromLang: GLOBAL_KEY,
|
||||
toLang: GLOBAL_KEY,
|
||||
@@ -42,145 +43,156 @@ export const DEFAULT_OW_RULE = {
|
||||
textDiyStyle: DEFAULT_DIY_STYLE,
|
||||
};
|
||||
|
||||
const RULES = [
|
||||
{
|
||||
pattern: `www.google.com/search`,
|
||||
selector: `h3, .IsZvec, .VwiC3b`,
|
||||
},
|
||||
{
|
||||
pattern: `news.google.com`,
|
||||
selector: `h4`,
|
||||
},
|
||||
{
|
||||
pattern: `www.foxnews.com`,
|
||||
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
|
||||
},
|
||||
{
|
||||
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,
|
||||
selector: DEFAULT_SELECTOR,
|
||||
},
|
||||
{
|
||||
pattern: `themessenger.com`,
|
||||
selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.telegraph.co.uk`,
|
||||
selector: `article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.theguardian.com`,
|
||||
selector: `.show-underline, .dcr-hup5wm div, .dcr-7vl6y8 div, .dcr-12evv1c, figcaption, article ${DEFAULT_SELECTOR}, [data-cy="mostviewed-footer"] h4`,
|
||||
},
|
||||
{
|
||||
pattern: `www.semafor.com`,
|
||||
selector: `${DEFAULT_SELECTOR}, .styles_intro__IYj__, [class*="styles_description"]`,
|
||||
},
|
||||
{
|
||||
pattern: `www.noemamag.com`,
|
||||
selector: `.splash__title, .single-card__title, .single-card__type, .single-card__topic, .highlighted-content__title, .single-card__author, article ${DEFAULT_SELECTOR}, .quote__text, .wp-caption-text div`,
|
||||
},
|
||||
{
|
||||
pattern: `restofworld.org`,
|
||||
selector: `${DEFAULT_SELECTOR}, .recirc-story__headline, .recirc-story__dek`,
|
||||
},
|
||||
{
|
||||
pattern: `www.axios.com`,
|
||||
selector: `.h7, ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.newyorker.com`,
|
||||
selector: `.summary-item__hed, .summary-item__dek, .summary-collection-grid__dek, .dqtvfu, .rubric__link, .caption, article ${DEFAULT_SELECTOR}, .HEhan ${DEFAULT_SELECTOR}, .ContributorBioBio-fBolsO`,
|
||||
},
|
||||
{
|
||||
pattern: `https://time.com/`,
|
||||
selector: `h1, h3, .summary, .video-title, #article-body ${DEFAULT_SELECTOR}, .image-wrap-container .credit.body-caption, .media-heading`,
|
||||
},
|
||||
{
|
||||
pattern: `www.dw.com`,
|
||||
selector: `.ts-teaser-title a, .news-title a, .title a, .teaser-description a, .hbudab h3, .hbudab p, figcaption ,article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.bbc.com`,
|
||||
selector: `h1, h2, .media__link, .media__summary, article ${DEFAULT_SELECTOR}, .ssrcss-y7krbn-Stack, .ssrcss-1mrs5ns-PromoLink, .ssrcss-18cjaf3-Headline, .gs-c-promo-heading__title, .gs-c-promo-summary, .media__content h3, .article__intro`,
|
||||
},
|
||||
{
|
||||
pattern: `www.chinadaily.com.cn`,
|
||||
selector: `h1, .tMain [shape="rect"], .cMain [shape="rect"], .photo_art [shape="rect"], .mai_r [shape="rect"], .lisBox li, #Content ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.facebook.com`,
|
||||
selector: `[role="main"] [dir="auto"]`,
|
||||
},
|
||||
{
|
||||
pattern: `www.reddit.com`,
|
||||
selector: `[slot="title"], [slot="text-body"] ${DEFAULT_SELECTOR}, #-post-rtjson-content p`,
|
||||
},
|
||||
{
|
||||
pattern: `www.quora.com`,
|
||||
selector: `.qu-wordBreak--break-word`,
|
||||
},
|
||||
{
|
||||
pattern: `edition.cnn.com`,
|
||||
selector: `.container__title, .container__headline, .headline__text, .image__caption, [data-type="Title"], .article__content ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.reuters.com`,
|
||||
selector: `#main-content [data-testid="Heading"], #main-content [data-testid="Body"], .article-body__content__17Yit ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.bloomberg.com`,
|
||||
selector: `[data-component="headline"], [data-component="related-item-headline"], [data-component="title"], article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `deno.land, docs.github.com`,
|
||||
selector: `main ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `doc.rust-lang.org`,
|
||||
selector: `#content ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.indiehackers.com`,
|
||||
selector: `h1, h3, .content ${DEFAULT_SELECTOR}, .feed-item__title-link`,
|
||||
},
|
||||
{
|
||||
pattern: `platform.openai.com/docs`,
|
||||
selector: `.docs-body ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `en.wikipedia.org`,
|
||||
selector: `h1, .mw-parser-output ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `stackoverflow.com`,
|
||||
selector: `h1, .s-prose p, .comment-body .comment-copy`,
|
||||
},
|
||||
{
|
||||
pattern: `www.npmjs.com/package/, developer.chrome.com/docs, medium.com, developers.cloudflare.com, react.dev, create-react-app.dev, pytorch.org/`,
|
||||
selector: `article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `news.ycombinator.com`,
|
||||
selector: `.title, .commtext`,
|
||||
},
|
||||
{
|
||||
pattern: `https://github.com/`,
|
||||
selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop='description'], .markdown-title, bdi`,
|
||||
},
|
||||
{
|
||||
pattern: `twitter.com`,
|
||||
selector: `[data-testid='tweetText']`,
|
||||
},
|
||||
{
|
||||
pattern: `youtube.com`,
|
||||
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`,
|
||||
},
|
||||
];
|
||||
const RULES_MAP = {
|
||||
"www.google.com/search": [`h3, .IsZvec, .VwiC3b`],
|
||||
"news.google.com": [`[role="link"], .DY5T1d, .ifw3f, ${DEFAULT_SELECTOR}`],
|
||||
"www.foxnews.com": [
|
||||
`h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
|
||||
],
|
||||
"bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php": [
|
||||
`${DEFAULT_SELECTOR}`,
|
||||
],
|
||||
"themessenger.com": [
|
||||
`.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
|
||||
],
|
||||
"www.telegraph.co.uk, go.dev/doc/": [`article ${DEFAULT_SELECTOR}`],
|
||||
"www.theguardian.com": [
|
||||
`.show-underline, .dcr-hup5wm div, .dcr-7vl6y8 div, .dcr-12evv1c, figcaption, article ${DEFAULT_SELECTOR}, [data-cy="mostviewed-footer"] h4`,
|
||||
],
|
||||
"www.semafor.com": [
|
||||
`${DEFAULT_SELECTOR}, .styles_intro__IYj__, [class*="styles_description"]`,
|
||||
],
|
||||
"www.noemamag.com": [
|
||||
`.splash__title, .single-card__title, .single-card__type, .single-card__topic, .highlighted-content__title, .single-card__author, article ${DEFAULT_SELECTOR}, .quote__text, .wp-caption-text div`,
|
||||
],
|
||||
"restofworld.org": [
|
||||
`${DEFAULT_SELECTOR}, .recirc-story__headline, .recirc-story__dek`,
|
||||
],
|
||||
"www.axios.com": [`.h7, ${DEFAULT_SELECTOR}`],
|
||||
"www.newyorker.com": [
|
||||
`.summary-item__hed, .summary-item__dek, .summary-collection-grid__dek, .dqtvfu, .rubric__link, .caption, article ${DEFAULT_SELECTOR}, .HEhan ${DEFAULT_SELECTOR}, .ContributorBioBio-fBolsO, .BaseText-ewhhUZ`,
|
||||
],
|
||||
"time.com": [
|
||||
`h1, h3, .summary, .video-title, #article-body ${DEFAULT_SELECTOR}, .image-wrap-container .credit.body-caption, .media-heading`,
|
||||
],
|
||||
"www.dw.com": [
|
||||
`.ts-teaser-title a, .news-title a, .title a, .teaser-description a, .hbudab h3, .hbudab p, figcaption ,article ${DEFAULT_SELECTOR}`,
|
||||
],
|
||||
"www.bbc.com": [
|
||||
`h1, h2, .media__link, .media__summary, article ${DEFAULT_SELECTOR}, .ssrcss-y7krbn-Stack, .ssrcss-17zglt8-PromoHeadline, .ssrcss-18cjaf3-Headline, .gs-c-promo-heading__title, .gs-c-promo-summary, .media__content h3, .article__intro, .lx-c-summary-points>li`,
|
||||
],
|
||||
"www.chinadaily.com.cn": [
|
||||
`h1, .tMain [shape="rect"], .cMain [shape="rect"], .photo_art [shape="rect"], .mai_r [shape="rect"], .lisBox li, #Content ${DEFAULT_SELECTOR}`,
|
||||
],
|
||||
"www.facebook.com": [`[role="main"] [dir="auto"]`],
|
||||
"www.reddit.com": [
|
||||
`div:is(.tbIApBd2DM_drfZQJjIum, ._1zPvgKHteTOub9dKkvrOl4,.ULWj94BYSOqoJDetxgcnU),a:is([class^="_334yl59"],[class^="_2GrMpxD"]),h1,h2,h3,h4,h5,h6,p,button`,
|
||||
],
|
||||
"www.quora.com": [`.qu-wordBreak--break-word`],
|
||||
"edition.cnn.com": [
|
||||
`.container__title, .container__headline, .headline__text, .image__caption, [data-type="Title"], .article__content ${DEFAULT_SELECTOR}`,
|
||||
],
|
||||
"www.reuters.com": [
|
||||
`#main-content [data-testid="Heading"], #main-content [data-testid="Body"], .article-body__content__17Yit ${DEFAULT_SELECTOR}`,
|
||||
],
|
||||
"www.bloomberg.com": [
|
||||
`[data-component="headline"], [data-component="related-item-headline"], [data-component="title"], article ${DEFAULT_SELECTOR}`,
|
||||
],
|
||||
"deno.land, docs.github.com": [`main ${DEFAULT_SELECTOR}`, `code, img, svg`],
|
||||
"doc.rust-lang.org": [`.content ${DEFAULT_SELECTOR}`, `code, img, svg`],
|
||||
"www.indiehackers.com": [
|
||||
`h1, h3, .content ${DEFAULT_SELECTOR}, .feed-item__title-link`,
|
||||
],
|
||||
"platform.openai.com/docs": [
|
||||
`.docs-body ${DEFAULT_SELECTOR}`,
|
||||
`code, img, svg`,
|
||||
],
|
||||
"en.wikipedia.org": [
|
||||
`h1, .mw-parser-output ${DEFAULT_SELECTOR}`,
|
||||
`.mwe-math-element`,
|
||||
],
|
||||
"stackoverflow.com": [
|
||||
`h1, .s-prose p, .comment-body .comment-copy`,
|
||||
`code, img, svg`,
|
||||
],
|
||||
"www.npmjs.com/package, developer.chrome.com/docs, medium.com, developers.cloudflare.com, react.dev, create-react-app.dev, pytorch.org":
|
||||
[`article ${DEFAULT_SELECTOR}`],
|
||||
"news.ycombinator.com": [`.title, .commtext`],
|
||||
"github.com": [
|
||||
`.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop="description"], .markdown-title, bdi, .ws-pre-wrap, .status-meta, span.status-meta, .col-10.color-fg-muted, .TimelineItem-body, .pinned-item-list-item-content .color-fg-muted, .markdown-body td, .markdown-body th`,
|
||||
`code, img, svg`,
|
||||
],
|
||||
"twitter.com": [
|
||||
`[data-testid="tweetText"], [data-testid="birdwatch-pivot"]>div.css-1rynq56`,
|
||||
`img, a, .r-18u37iz, .css-175oi2r`,
|
||||
],
|
||||
"m.youtube.com": [
|
||||
`.slim-video-information-title .yt-core-attributed-string, .media-item-headline .yt-core-attributed-string, .comment-text .yt-core-attributed-string, .typography-body-2b .yt-core-attributed-string, #ytp-caption-window-container .ytp-caption-segment`,
|
||||
],
|
||||
"www.youtube.com": [
|
||||
`h1, #video-title, #content-text, #title, yt-attributed-string>span>span, #ytp-caption-window-container .ytp-caption-segment`,
|
||||
],
|
||||
"bard.google.com": [
|
||||
`.query-content ${DEFAULT_SELECTOR}, message-content ${DEFAULT_SELECTOR}`,
|
||||
],
|
||||
"www.bing.com": [
|
||||
`.b_algoSlug, .rwrl_padref; .cib-serp-main >>> .ac-textBlock ${DEFAULT_SELECTOR}, .text-message-content div`,
|
||||
],
|
||||
"www.phoronix.com": [`article ${DEFAULT_SELECTOR}`],
|
||||
"wx2.qq.com": [`.js_message_plain`],
|
||||
"app.slack.com/client/": [
|
||||
`.p-rich_text_section, .c-message_attachment__text, .p-rich_text_list li`,
|
||||
],
|
||||
"discord.com/channels/": [
|
||||
`li[id^=chat-messages] div[id^=message-content], div[class^=headerText], div[class^=name_], section[aria-label='Search Results'] div[id^=message-content]`,
|
||||
],
|
||||
"t.me/s/": [`.js-message_text ${DEFAULT_SELECTOR}`],
|
||||
"web.telegram.org/k/": [
|
||||
`.message, .bot-commands-list-element-description, .reply-markup-button-text`,
|
||||
],
|
||||
"web.telegram.org/a/": [
|
||||
`.message, .text-content, .bot-commands-list-element-description, .reply-markup-button-text`,
|
||||
],
|
||||
"chromereleases.googleblog.com": [
|
||||
`.title, .publishdate, p, i, .header-desc, .header-title, .text`,
|
||||
],
|
||||
"www.instagram.com/": [`h1, article span[dir=auto] > span[dir=auto], ._ab1y`],
|
||||
"www.instagram.com/p/,www.instagram.com/reels/": [
|
||||
`h1, div[class='x9f619 xjbqb8w x78zum5 x168nmei x13lgxp2 x5pf9jr xo71vjh x1uhb9sk x1plvlek xryxfnj x1c4vz4f x2lah0s xdt5ytf xqjyukv x1cy8zhl x1oa3qoh x1nhvcw1'] > span[class='x1lliihq x1plvlek xryxfnj x1n2onr6 x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs x1s928wv xhkezso x1gmr53x x1cpjm7i x1fgarty x1943h6x x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj'], span[class='x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs xt0psk2 x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj']`,
|
||||
],
|
||||
"mail.google.com": [
|
||||
`${DEFAULT_SELECTOR}, h2[data-thread-perm-id], span[data-thread-id], div[data-message-id] div[class=''], .messageBody, #views`,
|
||||
],
|
||||
"web.whatsapp.com": [`.copyable-text > span`],
|
||||
"chat.openai.com": [
|
||||
`div[data-message-author-role] > div ${DEFAULT_SELECTOR}`,
|
||||
],
|
||||
"forum.ru-board.com": [`.tit, .dats, span.post, .lgf ${DEFAULT_SELECTOR}`],
|
||||
"education.github.com": [
|
||||
`${DEFAULT_SELECTOR}, a, summary, span.Button-content`,
|
||||
],
|
||||
"blogs.windows.com": [`${DEFAULT_SELECTOR}, .c-uhf-nav-link, figcaption`],
|
||||
"developer.apple.com/documentation/": [
|
||||
`#main ${DEFAULT_SELECTOR}, #main .abstract .content, #main .abstract.content, #main .link span`,
|
||||
`code, img, svg`,
|
||||
],
|
||||
"greasyfork.org": [
|
||||
`h2, .script-link, .script-description, #additional-info ${DEFAULT_SELECTOR}`,
|
||||
],
|
||||
"www.fmkorea.com": [`#container ${DEFAULT_SELECTOR}`],
|
||||
"forum.arduino.cc": [
|
||||
`.top-row>.title, .featured-topic>.title, .link-top-line>.title, .category-description, .topic-excerpt, .fancy-title, .cooked ${DEFAULT_SELECTOR}`,
|
||||
],
|
||||
"docs.arduino.cc": [`[class^="tutorial-module--left"] ${DEFAULT_SELECTOR}`],
|
||||
"www.historydefined.net": [`.wp-element-caption, ${DEFAULT_SELECTOR}`],
|
||||
};
|
||||
|
||||
export const BUILTIN_RULES = RULES.sort((a, b) =>
|
||||
a.pattern.localeCompare(b.pattern)
|
||||
).map((item) => ({
|
||||
...DEFAULT_RULE,
|
||||
...item,
|
||||
transOpen: "true",
|
||||
}));
|
||||
export const BUILTIN_RULES = Object.entries(RULES_MAP)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([pattern, [selector, keepSelector = "", terms = ""]]) => ({
|
||||
...DEFAULT_RULE,
|
||||
pattern,
|
||||
selector,
|
||||
keepSelector,
|
||||
terms,
|
||||
}));
|
||||
|
||||
@@ -1,53 +1,3 @@
|
||||
import { browser } from "./libs/browser";
|
||||
import {
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
} from "./config";
|
||||
import { getSettingWithDefault, getRulesWithDefault } from "./libs/storage";
|
||||
import { Translator } from "./libs/translator";
|
||||
import { isIframe } from "./libs/iframe";
|
||||
import { matchRule } from "./libs/rules";
|
||||
import { run } from "./common";
|
||||
|
||||
/**
|
||||
* 入口函数
|
||||
*/
|
||||
const init = async () => {
|
||||
const href = isIframe ? document.referrer : document.location.href;
|
||||
const setting = await getSettingWithDefault();
|
||||
const rules = await getRulesWithDefault();
|
||||
const rule = await matchRule(rules, href, setting);
|
||||
const translator = new Translator(rule, setting);
|
||||
|
||||
// 监听消息
|
||||
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
translator.toggle();
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
translator.toggleStyle();
|
||||
break;
|
||||
case MSG_TRANS_GETRULE:
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
translator.updateRule(args);
|
||||
break;
|
||||
default:
|
||||
return { error: `message action is unavailable: ${action}` };
|
||||
}
|
||||
return { data: translator.rule };
|
||||
});
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await init();
|
||||
} catch (err) {
|
||||
const $err = document.createElement("div");
|
||||
$err.innerText = `KISS-Translator: ${err.message}`;
|
||||
$err.style.cssText = "background:red; color:#fff; z-index:10000;";
|
||||
document.body.prepend($err);
|
||||
}
|
||||
})();
|
||||
run();
|
||||
|
||||
24
src/hooks/Api.js
Normal file
24
src/hooks/Api.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useCallback } from "react";
|
||||
import { DEFAULT_TRANS_APIS } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
export function useApi(translator) {
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const transApis = setting?.transApis || DEFAULT_TRANS_APIS;
|
||||
|
||||
const updateApi = useCallback(
|
||||
async (obj) => {
|
||||
const api = transApis[translator] || {};
|
||||
Object.assign(transApis, { [translator]: { ...api, ...obj } });
|
||||
await updateSetting({ transApis });
|
||||
},
|
||||
[translator, transApis, updateSetting]
|
||||
);
|
||||
|
||||
const resetApi = useCallback(async () => {
|
||||
Object.assign(transApis, { [translator]: DEFAULT_TRANS_APIS[translator] });
|
||||
await updateSetting({ transApis });
|
||||
}, [translator, transApis, updateSetting]);
|
||||
|
||||
return { api: transApis[translator] || {}, updateApi, resetApi };
|
||||
}
|
||||
11
src/hooks/Fab.js
Normal file
11
src/hooks/Fab.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { STOKEY_FAB } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
|
||||
/**
|
||||
* fab hook
|
||||
* @returns
|
||||
*/
|
||||
export function useFab() {
|
||||
const { data, update } = useStorage(STOKEY_FAB);
|
||||
return { fab: data, updateFab: update };
|
||||
}
|
||||
67
src/hooks/FavWords.js
Normal file
67
src/hooks/FavWords.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { KV_WORDS_KEY } from "../config";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { trySyncWords } from "../libs/sync";
|
||||
import { getWordsWithDefault, setWords } from "../libs/storage";
|
||||
import { useSyncMeta } from "./Sync";
|
||||
|
||||
export function useFavWords() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [favWords, setFavWords] = useState({});
|
||||
const { updateSyncMeta } = useSyncMeta();
|
||||
|
||||
const toggleFav = useCallback(
|
||||
async (word) => {
|
||||
const favs = { ...favWords };
|
||||
if (favs[word]) {
|
||||
delete favs[word];
|
||||
} else {
|
||||
favs[word] = { createdAt: Date.now() };
|
||||
}
|
||||
await setWords(favs);
|
||||
await updateSyncMeta(KV_WORDS_KEY);
|
||||
await trySyncWords();
|
||||
setFavWords(favs);
|
||||
},
|
||||
[updateSyncMeta, favWords]
|
||||
);
|
||||
|
||||
const mergeWords = useCallback(
|
||||
async (newWords) => {
|
||||
const favs = { ...favWords };
|
||||
newWords.forEach((word) => {
|
||||
if (!favs[word]) {
|
||||
favs[word] = { createdAt: Date.now() };
|
||||
}
|
||||
});
|
||||
await setWords(favs);
|
||||
await updateSyncMeta(KV_WORDS_KEY);
|
||||
await trySyncWords();
|
||||
setFavWords(favs);
|
||||
},
|
||||
[updateSyncMeta, favWords]
|
||||
);
|
||||
|
||||
const clearWords = useCallback(async () => {
|
||||
await setWords({});
|
||||
await updateSyncMeta(KV_WORDS_KEY);
|
||||
await trySyncWords();
|
||||
setFavWords({});
|
||||
}, [updateSyncMeta]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await trySyncWords();
|
||||
const favWords = await getWordsWithDefault();
|
||||
setFavWords(favWords);
|
||||
} catch (err) {
|
||||
console.log("[query fav]", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return { loading, favWords, toggleFav, mergeWords, clearWords };
|
||||
}
|
||||
18
src/hooks/InputRule.js
Normal file
18
src/hooks/InputRule.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useCallback } from "react";
|
||||
import { DEFAULT_INPUT_RULE } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
export function useInputRule() {
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const inputRule = setting?.inputRule || DEFAULT_INPUT_RULE;
|
||||
|
||||
const updateInputRule = useCallback(
|
||||
async (obj) => {
|
||||
Object.assign(inputRule, obj);
|
||||
await updateSetting({ inputRule });
|
||||
},
|
||||
[inputRule, updateSetting]
|
||||
);
|
||||
|
||||
return { inputRule, updateInputRule };
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { STOKEY_RULES, DEFAULT_RULES } from "../config";
|
||||
import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
import { trySyncRules } from "../libs/sync";
|
||||
import { useSync } from "./Sync";
|
||||
import { checkRules } from "../libs/rules";
|
||||
import { useCallback } from "react";
|
||||
import { useSyncMeta } from "./Sync";
|
||||
|
||||
/**
|
||||
* 规则 hook
|
||||
@@ -11,19 +11,15 @@ import { useCallback } from "react";
|
||||
*/
|
||||
export function useRules() {
|
||||
const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
|
||||
const {
|
||||
sync: { rulesUpdateAt },
|
||||
updateSync,
|
||||
} = useSync();
|
||||
const { updateSyncMeta } = useSyncMeta();
|
||||
|
||||
const updateRules = useCallback(
|
||||
async (rules) => {
|
||||
const updateAt = rulesUpdateAt ? Date.now() : 0;
|
||||
await save(rules);
|
||||
await updateSync({ rulesUpdateAt: updateAt });
|
||||
await updateSyncMeta(KV_RULES_KEY);
|
||||
trySyncRules();
|
||||
},
|
||||
[rulesUpdateAt, save, updateSync]
|
||||
[save, updateSyncMeta]
|
||||
);
|
||||
|
||||
const add = useCallback(
|
||||
@@ -53,6 +49,12 @@ export function useRules() {
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
const clear = useCallback(async () => {
|
||||
let rules = [...list];
|
||||
rules = rules.filter((item) => item.pattern === "*");
|
||||
await updateRules(rules);
|
||||
}, [list, updateRules]);
|
||||
|
||||
const put = useCallback(
|
||||
async (pattern, obj) => {
|
||||
const rules = [...list];
|
||||
@@ -85,5 +87,5 @@ export function useRules() {
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
return { list, add, del, put, merge };
|
||||
return { list, add, del, clear, put, merge };
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { STOKEY_SETTING, DEFAULT_SETTING } from "../config";
|
||||
import { STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
import { useSync } from "./Sync";
|
||||
import { trySyncSetting } from "../libs/sync";
|
||||
import { createContext, useCallback, useContext, useMemo } from "react";
|
||||
import { debounce } from "../libs/utils";
|
||||
import { useSyncMeta } from "./Sync";
|
||||
|
||||
const SettingContext = createContext({
|
||||
setting: null,
|
||||
@@ -13,10 +13,7 @@ const SettingContext = createContext({
|
||||
|
||||
export function SettingProvider({ children }) {
|
||||
const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
|
||||
const {
|
||||
sync: { settingUpdateAt },
|
||||
updateSync,
|
||||
} = useSync();
|
||||
const { updateSyncMeta } = useSyncMeta();
|
||||
|
||||
const syncSetting = useMemo(
|
||||
() =>
|
||||
@@ -28,14 +25,17 @@ export function SettingProvider({ children }) {
|
||||
|
||||
const updateSetting = useCallback(
|
||||
async (obj) => {
|
||||
const updateAt = settingUpdateAt ? Date.now() : 0;
|
||||
await update(obj);
|
||||
await updateSync({ settingUpdateAt: updateAt });
|
||||
await updateSyncMeta(KV_SETTING_KEY);
|
||||
syncSetting();
|
||||
},
|
||||
[settingUpdateAt, update, updateSync, syncSetting]
|
||||
[update, syncSetting, updateSyncMeta]
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContext.Provider
|
||||
value={{
|
||||
|
||||
19
src/hooks/Shortcut.js
Normal file
19
src/hooks/Shortcut.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useCallback } from "react";
|
||||
import { DEFAULT_SHORTCUTS } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
export function useShortcut(action) {
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const shortcuts = setting?.shortcuts || DEFAULT_SHORTCUTS;
|
||||
const shortcut = shortcuts[action] || [];
|
||||
|
||||
const setShortcut = useCallback(
|
||||
async (val) => {
|
||||
Object.assign(shortcuts, { [action]: val });
|
||||
await updateSetting({ shortcuts });
|
||||
},
|
||||
[action, shortcuts, updateSetting]
|
||||
);
|
||||
|
||||
return { shortcut, setShortcut };
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { storage } from "../libs/storage";
|
||||
|
||||
export function useStorage(key, defaultVal = null) {
|
||||
const [data, setData] = useState(defaultVal);
|
||||
/**
|
||||
*
|
||||
* @param {*} key
|
||||
* @param {*} defaultVal 需为调用hook外的常量
|
||||
* @returns
|
||||
*/
|
||||
export function useStorage(key, defaultVal) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
const save = useCallback(
|
||||
async (val) => {
|
||||
@@ -14,7 +21,7 @@ export function useStorage(key, defaultVal = null) {
|
||||
|
||||
const update = useCallback(
|
||||
async (obj) => {
|
||||
setData((pre) => ({ ...pre, ...obj }));
|
||||
setData((pre = {}) => ({ ...pre, ...obj }));
|
||||
await storage.putObj(key, obj);
|
||||
},
|
||||
[key]
|
||||
@@ -26,19 +33,37 @@ export function useStorage(key, defaultVal = null) {
|
||||
}, [key]);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
const val = await storage.getObj(key);
|
||||
if (val) {
|
||||
setData(val);
|
||||
} else if (defaultVal) {
|
||||
await storage.setObj(key, defaultVal);
|
||||
try {
|
||||
setLoading(true);
|
||||
const val = await storage.getObj(key);
|
||||
if (val) {
|
||||
setData(val);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[storage reload]", err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [key, defaultVal]);
|
||||
}, [key]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await reload();
|
||||
try {
|
||||
setLoading(true);
|
||||
const val = await storage.getObj(key);
|
||||
if (val) {
|
||||
setData(val);
|
||||
} else if (defaultVal) {
|
||||
setData(defaultVal);
|
||||
await storage.setObj(key, defaultVal);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[storage load]", err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [reload]);
|
||||
}, [key, defaultVal]);
|
||||
|
||||
return { data, save, update, remove, reload };
|
||||
return { data, save, update, remove, reload, loading };
|
||||
}
|
||||
|
||||
@@ -32,6 +32,19 @@ export function useSubRules() {
|
||||
[list, updateSetting]
|
||||
);
|
||||
|
||||
const updateSub = useCallback(
|
||||
async (url, obj) => {
|
||||
const subrulesList = [...list];
|
||||
subrulesList.forEach((item) => {
|
||||
if (item.url === url) {
|
||||
Object.assign(item, obj);
|
||||
}
|
||||
});
|
||||
await updateSetting({ subrulesList });
|
||||
},
|
||||
[list, updateSetting]
|
||||
);
|
||||
|
||||
const addSub = useCallback(
|
||||
async (url) => {
|
||||
const subrulesList = [...list];
|
||||
@@ -70,6 +83,7 @@ export function useSubRules() {
|
||||
return {
|
||||
subList: list,
|
||||
selectSub,
|
||||
updateSub,
|
||||
addSub,
|
||||
delSub,
|
||||
selectedSub,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useCallback } from "react";
|
||||
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
|
||||
@@ -6,6 +7,57 @@ import { useStorage } from "./Storage";
|
||||
* @returns
|
||||
*/
|
||||
export function useSync() {
|
||||
const { data, update } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
|
||||
return { sync: data, updateSync: update };
|
||||
const { data, update, reload } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
|
||||
return { sync: data, updateSync: update, reloadSync: reload };
|
||||
}
|
||||
|
||||
/**
|
||||
* update syncmeta hook
|
||||
* @returns
|
||||
*/
|
||||
export function useSyncMeta() {
|
||||
const { sync, updateSync } = useSync();
|
||||
const updateSyncMeta = useCallback(
|
||||
async (key) => {
|
||||
const syncMeta = sync?.syncMeta || {};
|
||||
syncMeta[key] = { ...(syncMeta[key] || {}), updateAt: Date.now() };
|
||||
await updateSync({ syncMeta });
|
||||
},
|
||||
[sync?.syncMeta, updateSync]
|
||||
);
|
||||
return { updateSyncMeta };
|
||||
}
|
||||
|
||||
/**
|
||||
* caches sync hook
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export function useSyncCaches() {
|
||||
const { sync, updateSync, reloadSync } = useSync();
|
||||
|
||||
const updateDataCache = useCallback(
|
||||
async (url) => {
|
||||
const dataCaches = sync?.dataCaches || {};
|
||||
dataCaches[url] = Date.now();
|
||||
await updateSync({ dataCaches });
|
||||
},
|
||||
[sync, updateSync]
|
||||
);
|
||||
|
||||
const deleteDataCache = useCallback(
|
||||
async (url) => {
|
||||
const dataCaches = sync?.dataCaches || {};
|
||||
delete dataCaches[url];
|
||||
await updateSync({ dataCaches });
|
||||
},
|
||||
[sync, updateSync]
|
||||
);
|
||||
|
||||
return {
|
||||
dataCaches: sync?.dataCaches || {},
|
||||
updateDataCache,
|
||||
deleteDataCache,
|
||||
reloadSync,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,10 +12,24 @@ import { THEME_DARK, THEME_LIGHT } from "../config";
|
||||
export default function Theme({ children, options }) {
|
||||
const { darkMode } = useDarkMode();
|
||||
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;
|
||||
}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
|
||||
return createTheme({
|
||||
palette: {
|
||||
mode: darkMode ? THEME_DARK : THEME_LIGHT,
|
||||
},
|
||||
typography: {
|
||||
htmlFontSize,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}, [darkMode, options]);
|
||||
|
||||
18
src/hooks/Tranbox.js
Normal file
18
src/hooks/Tranbox.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useCallback } from "react";
|
||||
import { DEFAULT_TRANBOX_SETTING } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
export function useTranbox() {
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const tranboxSetting = setting?.tranboxSetting || DEFAULT_TRANBOX_SETTING;
|
||||
|
||||
const updateTranbox = useCallback(
|
||||
async (obj) => {
|
||||
Object.assign(tranboxSetting, obj);
|
||||
await updateSetting({ tranboxSetting });
|
||||
},
|
||||
[tranboxSetting, updateSetting]
|
||||
);
|
||||
|
||||
return { tranboxSetting, updateTranbox };
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { tryDetectLang } from "../libs";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { DEFAULT_TRANS_APIS } from "../config";
|
||||
|
||||
/**
|
||||
* 翻译hook
|
||||
@@ -22,16 +23,27 @@ export function useTranslate(q, rule, setting) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const deLang = await tryDetectLang(q);
|
||||
if (deLang && toLang.includes(deLang)) {
|
||||
if (!q.replace(/\[(\d+)\]/g, "").trim()) {
|
||||
setText(q);
|
||||
setSamelang(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const deLang = await tryDetectLang(q, setting.detectRemote);
|
||||
const disableLangs = setting.disableLangs || [];
|
||||
if (
|
||||
deLang &&
|
||||
(toLang.includes(deLang) || disableLangs.includes(deLang))
|
||||
) {
|
||||
setSamelang(true);
|
||||
} else {
|
||||
const [trText, isSame] = await apiTranslate({
|
||||
translator,
|
||||
q,
|
||||
text: q,
|
||||
fromLang,
|
||||
toLang,
|
||||
setting,
|
||||
apiSetting:
|
||||
setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator],
|
||||
});
|
||||
setText(trText);
|
||||
setSamelang(isSame);
|
||||
|
||||
58
src/hooks/WebfixRules.js
Normal file
58
src/hooks/WebfixRules.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { STOKEY_WFRULES, KV_WFRULES_KEY } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
import { trySyncWebfixRules } from "../libs/sync";
|
||||
import { useCallback } from "react";
|
||||
import { useSyncMeta } from "./Sync";
|
||||
|
||||
const DEFAULT_WFRULES = [];
|
||||
|
||||
/**
|
||||
* 修复规则 hook
|
||||
* @returns
|
||||
*/
|
||||
export function useWebfixRules() {
|
||||
const { data: list, save } = useStorage(STOKEY_WFRULES, DEFAULT_WFRULES);
|
||||
const { updateSyncMeta } = useSyncMeta();
|
||||
|
||||
const updateRules = useCallback(
|
||||
async (rules) => {
|
||||
await save(rules);
|
||||
await updateSyncMeta(KV_WFRULES_KEY);
|
||||
trySyncWebfixRules();
|
||||
},
|
||||
[save, updateSyncMeta]
|
||||
);
|
||||
|
||||
const add = useCallback(
|
||||
async (rule) => {
|
||||
const rules = [...list];
|
||||
if (rules.map((item) => item.pattern).includes(rule.pattern)) {
|
||||
return;
|
||||
}
|
||||
rules.unshift(rule);
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
const del = useCallback(
|
||||
async (pattern) => {
|
||||
let rules = [...list];
|
||||
rules = rules.filter((item) => item.pattern !== pattern);
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
const put = useCallback(
|
||||
async (pattern, obj) => {
|
||||
const rules = [...list];
|
||||
const rule = rules.find((r) => r.pattern === pattern);
|
||||
rule && Object.assign(rule, obj);
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
return { list, add, del, put };
|
||||
}
|
||||
26
src/index.js
26
src/index.js
@@ -32,25 +32,23 @@ function App() {
|
||||
href={process.env.REACT_APP_HOMEPAGE}
|
||||
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||
</Divider>
|
||||
<Stack spacing={2} direction="row" useFlexGap flexWrap="wrap">
|
||||
<Stack spacing={2}>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
||||
Install Userscript 1
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
|
||||
Install Userscript 2
|
||||
Install/Update Userscript for Tampermonkey/Violentmonkey
|
||||
</Link>
|
||||
{/* <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
|
||||
Install/Update Userscript for Tampermonkey/Violentmonkey 2
|
||||
</Link> */}
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
|
||||
Install Userscript Safari 1
|
||||
Install/Update Userscript for iOS Safari
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
|
||||
Install Userscript Safari 2
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
|
||||
Open Options Page 1
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
|
||||
{/* <Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
|
||||
Install/Update Userscript for iOS Safari 2
|
||||
</Link> */}
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE}>Open Options Page</Link>
|
||||
{/* <Link href={process.env.REACT_APP_OPTIONSPAGE2}>
|
||||
Open Options Page 2
|
||||
</Link>
|
||||
</Link> */}
|
||||
</Stack>
|
||||
|
||||
{loading ? (
|
||||
|
||||
13
src/libs/blacklist.js
Normal file
13
src/libs/blacklist.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { isMatch } from "./utils";
|
||||
import { DEFAULT_BLACKLIST } from "../config";
|
||||
|
||||
/**
|
||||
* 检查是否在黑名单中
|
||||
* @param {*} href
|
||||
* @param {*} param1
|
||||
* @returns
|
||||
*/
|
||||
export const isInBlacklist = (
|
||||
href,
|
||||
{ blacklist = DEFAULT_BLACKLIST.join(",\n") }
|
||||
) => blacklist.split(/\n|,/).some((url) => isMatch(href, url.trim()));
|
||||
@@ -13,3 +13,5 @@ function _browser() {
|
||||
}
|
||||
|
||||
export const browser = _browser();
|
||||
|
||||
export const isBg = () => globalThis?.ContextType === "BACKGROUND";
|
||||
|
||||
@@ -6,13 +6,11 @@ import {
|
||||
MSG_FETCH_LIMIT,
|
||||
MSG_FETCH_CLEAR,
|
||||
CACHE_NAME,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
DEFAULT_FETCH_INTERVAL,
|
||||
DEFAULT_FETCH_LIMIT,
|
||||
} from "../config";
|
||||
import { msAuth } from "./auth";
|
||||
import { isBg } from "./browser";
|
||||
import { newCacheReq, newTransReq } from "./req";
|
||||
|
||||
/**
|
||||
* 油猴脚本的请求封装
|
||||
@@ -27,53 +25,38 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
||||
url: input,
|
||||
headers,
|
||||
data: body,
|
||||
onload: (response) => {
|
||||
if (response.status === 200) {
|
||||
const headers = new Headers();
|
||||
response.responseHeaders.split("\n").forEach((line) => {
|
||||
const [name, value] = line.split(":").map((item) => item.trim());
|
||||
if (name && value) {
|
||||
headers.append(name, value);
|
||||
}
|
||||
});
|
||||
resolve(new Response(response.response, { headers }));
|
||||
} else {
|
||||
reject(new Error(`[${response.status}] ${response.responseText}`));
|
||||
}
|
||||
// withCredentials: true,
|
||||
onload: ({ response, responseHeaders, status, statusText, ...opts }) => {
|
||||
const headers = {};
|
||||
responseHeaders.split("\n").forEach((line) => {
|
||||
const [name, value] = line.split(":").map((item) => item.trim());
|
||||
if (name && value) {
|
||||
headers[name] = value;
|
||||
}
|
||||
});
|
||||
resolve({
|
||||
body: response,
|
||||
headers,
|
||||
status,
|
||||
statusText,
|
||||
});
|
||||
},
|
||||
onerror: reject,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 构造缓存 request
|
||||
* @param {*} request
|
||||
* @returns
|
||||
*/
|
||||
const newCacheReq = async (request) => {
|
||||
if (request.method !== "GET") {
|
||||
const body = await request.text();
|
||||
const cacheUrl = new URL(request.url);
|
||||
cacheUrl.pathname += body;
|
||||
request = new Request(cacheUrl.toString(), { method: "GET" });
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
/**
|
||||
* 发起请求
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const fetchApi = async ({ input, init = {}, translator, token }) => {
|
||||
if (translator === OPT_TRANS_MICROSOFT) {
|
||||
init.headers["Authorization"] = `Bearer ${token}`; // Microsoft
|
||||
} else if (translator === OPT_TRANS_DEEPL) {
|
||||
init.headers["Authorization"] = `DeepL-Auth-Key ${token}`; // DeepL
|
||||
} else if (translator === OPT_TRANS_OPENAI) {
|
||||
init.headers["Authorization"] = `Bearer ${token}`; // OpenAI
|
||||
init.headers["api-key"] = token; // Azure OpenAI
|
||||
export const fetchApi = async ({ input, init, transOpts, apiSetting }) => {
|
||||
if (transOpts?.translator) {
|
||||
[input, init] = await newTransReq(transOpts, apiSetting);
|
||||
}
|
||||
|
||||
if (!input) {
|
||||
throw new Error("url is empty");
|
||||
}
|
||||
|
||||
if (isGm) {
|
||||
@@ -83,19 +66,26 @@ const fetchApi = async ({ input, init = {}, translator, token }) => {
|
||||
} else {
|
||||
info = GM.info;
|
||||
}
|
||||
|
||||
// Tampermonkey --> .connects
|
||||
// Violentmonkey --> .connect
|
||||
const connects = info?.script?.connects || info?.script?.connect || [];
|
||||
const url = new URL(input);
|
||||
const isSafe = connects.find((item) => url.hostname.endsWith(item));
|
||||
|
||||
if (isSafe) {
|
||||
if (window.KISS_GM) {
|
||||
return window.KISS_GM.fetch(input, init);
|
||||
} else {
|
||||
return fetchGM(input, init);
|
||||
}
|
||||
const { body, headers, status, statusText } = window.KISS_GM
|
||||
? await window.KISS_GM.fetch(input, init)
|
||||
: await fetchGM(input, init);
|
||||
|
||||
return new Response(body, {
|
||||
headers: new Headers(headers),
|
||||
status,
|
||||
statusText,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(input, init);
|
||||
};
|
||||
|
||||
@@ -104,13 +94,7 @@ const fetchApi = async ({ input, init = {}, translator, token }) => {
|
||||
*/
|
||||
export const fetchPool = taskPool(
|
||||
fetchApi,
|
||||
async ({ translator }) => {
|
||||
if (translator === OPT_TRANS_MICROSOFT) {
|
||||
const [token] = await msAuth();
|
||||
return { token };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
null,
|
||||
DEFAULT_FETCH_INTERVAL,
|
||||
DEFAULT_FETCH_LIMIT
|
||||
);
|
||||
@@ -123,9 +107,9 @@ export const fetchPool = taskPool(
|
||||
*/
|
||||
export const fetchData = async (
|
||||
input,
|
||||
{ useCache, usePool, translator, token, ...init } = {}
|
||||
{ useCache, usePool, transOpts, apiSetting, ...init } = {}
|
||||
) => {
|
||||
const cacheReq = await newCacheReq(new Request(input, init));
|
||||
const cacheReq = await newCacheReq(input, init);
|
||||
let res;
|
||||
|
||||
// 查询缓存
|
||||
@@ -141,13 +125,19 @@ export const fetchData = async (
|
||||
if (!res) {
|
||||
// 发送请求
|
||||
if (usePool) {
|
||||
res = await fetchPool.push({ input, init, translator, token });
|
||||
res = await fetchPool.push({ input, init, transOpts, apiSetting });
|
||||
} else {
|
||||
res = await fetchApi({ input, init, translator, token });
|
||||
res = await fetchApi({ input, init, transOpts, apiSetting });
|
||||
}
|
||||
|
||||
if (!res?.ok) {
|
||||
throw new Error(`response: ${res.statusText}`);
|
||||
const cause = {
|
||||
status: res.status,
|
||||
};
|
||||
if (res.headers.get("Content-Type")?.includes("json")) {
|
||||
cause.body = await res.json();
|
||||
}
|
||||
throw new Error(`response: [${res.status}] ${res.statusText}`, { cause });
|
||||
}
|
||||
|
||||
// 插入缓存
|
||||
@@ -174,12 +164,16 @@ export const fetchData = async (
|
||||
* @param {*} opts
|
||||
* @returns
|
||||
*/
|
||||
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
|
||||
export const fetchPolyfill = async (input, opts) => {
|
||||
if (!input?.trim()) {
|
||||
throw new Error("URL is empty");
|
||||
}
|
||||
|
||||
// 插件
|
||||
if (isExt && !isBg) {
|
||||
if (isExt && !isBg()) {
|
||||
const res = await sendBgMsg(MSG_FETCH, { input, opts });
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
throw new Error(res.error, { cause: res.cause });
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -5,3 +5,7 @@ export const sendIframeMsg = (action, args) => {
|
||||
iframe.contentWindow.postMessage({ action, args }, "*");
|
||||
});
|
||||
};
|
||||
|
||||
export const sendParentMsg = (action, args) => {
|
||||
window.parent.postMessage({ action, args }, "*");
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CACHE_NAME } from "../config";
|
||||
import { browser } from "./browser";
|
||||
import { apiBaiduLangdetect } from "../apis";
|
||||
|
||||
/**
|
||||
* 清除缓存数据
|
||||
@@ -13,15 +14,29 @@ export const tryClearCaches = async () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 本地语言识别
|
||||
* 语言识别
|
||||
* @param {*} q
|
||||
* @returns
|
||||
*/
|
||||
export const tryDetectLang = async (q) => {
|
||||
try {
|
||||
const res = await browser?.i18n?.detectLanguage(q);
|
||||
return res?.languages?.[0]?.language;
|
||||
} catch (err) {
|
||||
console.log("[detect lang]", err.message);
|
||||
export const tryDetectLang = async (q, useRemote = false) => {
|
||||
let lang = "";
|
||||
|
||||
if (useRemote) {
|
||||
try {
|
||||
lang = await apiBaiduLangdetect(q);
|
||||
} catch (err) {
|
||||
console.log("[detect lang remote]", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!lang) {
|
||||
try {
|
||||
const res = await browser?.i18n?.detectLanguage(q);
|
||||
lang = res?.languages?.[0]?.language;
|
||||
} catch (err) {
|
||||
console.log("[detect lang local]", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
return lang;
|
||||
};
|
||||
|
||||
205
src/libs/inputTranslate.js
Normal file
205
src/libs/inputTranslate.js
Normal file
@@ -0,0 +1,205 @@
|
||||
import {
|
||||
DEFAULT_INPUT_RULE,
|
||||
DEFAULT_TRANS_APIS,
|
||||
DEFAULT_INPUT_SHORTCUT,
|
||||
OPT_LANGS_LIST,
|
||||
} from "../config";
|
||||
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
|
||||
import { stepShortcutRegister } from "./shortcut";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { tryDetectLang } from ".";
|
||||
import { loadingSvg } from "./svg";
|
||||
|
||||
function isInputNode(node) {
|
||||
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
|
||||
}
|
||||
|
||||
function isEditAbleNode(node) {
|
||||
return node.hasAttribute("contenteditable");
|
||||
}
|
||||
|
||||
function selectContent(node) {
|
||||
node.focus();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
function pasteContentEvent(node, text) {
|
||||
node.focus();
|
||||
const data = new DataTransfer();
|
||||
data.setData("text/plain", text);
|
||||
|
||||
const event = new ClipboardEvent("paste", { clipboardData: data });
|
||||
document.dispatchEvent(event);
|
||||
data.clearData();
|
||||
}
|
||||
|
||||
function pasteContentCommand(node, text) {
|
||||
node.focus();
|
||||
document.execCommand("insertText", false, text);
|
||||
}
|
||||
|
||||
function collapseToEnd(node) {
|
||||
node.focus();
|
||||
const selection = window.getSelection();
|
||||
selection.collapseToEnd();
|
||||
}
|
||||
|
||||
function getNodeText(node) {
|
||||
if (isInputNode(node)) {
|
||||
return node.value;
|
||||
}
|
||||
return node.innerText || node.textContent || "";
|
||||
}
|
||||
|
||||
function addLoading(node, loadingId) {
|
||||
const div = document.createElement("div");
|
||||
div.id = loadingId;
|
||||
div.innerHTML = loadingSvg;
|
||||
div.style.cssText = `
|
||||
width: ${node.offsetWidth}px;
|
||||
height: ${node.offsetHeight}px;
|
||||
line-height: ${node.offsetHeight}px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
left: ${node.offsetLeft}px;
|
||||
top: ${node.offsetTop}px;
|
||||
z-index: 2147483647;
|
||||
`;
|
||||
node.offsetParent?.appendChild(div);
|
||||
}
|
||||
|
||||
function removeLoading(node, loadingId) {
|
||||
const div = node.offsetParent.querySelector(`#${loadingId}`);
|
||||
if (div) {
|
||||
div.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入框翻译
|
||||
*/
|
||||
export default function inputTranslate ({
|
||||
inputRule: {
|
||||
transOpen,
|
||||
triggerShortcut,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
triggerCount,
|
||||
triggerTime,
|
||||
transSign,
|
||||
} = DEFAULT_INPUT_RULE,
|
||||
transApis,
|
||||
detectRemote,
|
||||
}) {
|
||||
if (!transOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apiSetting = transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
|
||||
if (triggerShortcut.length === 0) {
|
||||
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
||||
triggerCount = 1;
|
||||
}
|
||||
|
||||
stepShortcutRegister(
|
||||
triggerShortcut,
|
||||
async () => {
|
||||
let node = document.activeElement;
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (node.shadowRoot) {
|
||||
node = node.shadowRoot.activeElement;
|
||||
}
|
||||
|
||||
if (!isInputNode(node) && !isEditAbleNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let initText = getNodeText(node);
|
||||
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
|
||||
// todo: remove multiple char
|
||||
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
|
||||
}
|
||||
if (!initText.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let text = initText;
|
||||
if (transSign) {
|
||||
const res = matchInputStr(text, transSign);
|
||||
if (res) {
|
||||
let lang = res[1];
|
||||
if (lang === "zh" || lang === "cn") {
|
||||
lang = "zh-CN";
|
||||
} else if (lang === "tw" || lang === "hk") {
|
||||
lang = "zh-TW";
|
||||
}
|
||||
if (lang && OPT_LANGS_LIST.includes(lang)) {
|
||||
toLang = lang;
|
||||
}
|
||||
text = res[2];
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("input -->", text);
|
||||
|
||||
const loadingId = "kiss-" + genEventName();
|
||||
try {
|
||||
addLoading(node, loadingId);
|
||||
|
||||
const deLang = await tryDetectLang(text, detectRemote);
|
||||
if (deLang && toLang.includes(deLang)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [trText, isSame] = await apiTranslate({
|
||||
translator,
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting,
|
||||
});
|
||||
if (!trText || isSame) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInputNode(node)) {
|
||||
node.value = trText;
|
||||
node.dispatchEvent(
|
||||
new Event("input", { bubbles: true, cancelable: true })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
selectContent(node);
|
||||
await sleep(200);
|
||||
|
||||
pasteContentEvent(node, trText);
|
||||
await sleep(200);
|
||||
|
||||
// todo: use includes?
|
||||
if (getNodeText(node).startsWith(initText)) {
|
||||
pasteContentCommand(node, trText);
|
||||
await sleep(100);
|
||||
} else {
|
||||
collapseToEnd(node);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[translate input]", err.message);
|
||||
} finally {
|
||||
removeLoading(node, loadingId);
|
||||
}
|
||||
},
|
||||
triggerCount,
|
||||
triggerTime
|
||||
);
|
||||
}
|
||||
@@ -19,3 +19,12 @@ export const sendTabMsg = async (action, args) => {
|
||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
return browser.tabs.sendMessage(tabs[0].id, { action, args });
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前tab信息
|
||||
* @returns
|
||||
*/
|
||||
export const getTabInfo = async () => {
|
||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
return tabs[0];
|
||||
};
|
||||
|
||||
@@ -6,7 +6,13 @@
|
||||
* @param {*} _limit
|
||||
* @returns
|
||||
*/
|
||||
export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
||||
export const taskPool = (
|
||||
fn,
|
||||
preFn,
|
||||
_interval = 100,
|
||||
_limit = 100,
|
||||
_retryInteral = 1000
|
||||
) => {
|
||||
const pool = [];
|
||||
const maxRetry = 2; // 最大重试次数
|
||||
let maxCount = _limit; // 最大数量
|
||||
@@ -14,23 +20,6 @@ export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
||||
let interval = _interval; // 间隔时间
|
||||
let timer = null;
|
||||
|
||||
const handleTask = async (item, preArgs) => {
|
||||
curCount++;
|
||||
const { args, resolve, reject, retry } = item;
|
||||
try {
|
||||
const res = await fn({ ...args, ...preArgs });
|
||||
resolve(res);
|
||||
} catch (err) {
|
||||
if (retry < maxRetry) {
|
||||
pool.push({ args, resolve, reject, retry: retry + 1 });
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
} finally {
|
||||
curCount--;
|
||||
}
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
// console.log("timer", timer);
|
||||
timer && clearTimeout(timer);
|
||||
@@ -39,12 +28,24 @@ export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
||||
if (curCount < maxCount) {
|
||||
const item = pool.shift();
|
||||
if (item) {
|
||||
curCount++;
|
||||
const { args, resolve, reject, retry } = item;
|
||||
try {
|
||||
const preArgs = await preFn(item.args);
|
||||
handleTask(item, preArgs);
|
||||
const preArgs = preFn ? await preFn(item.args) : {};
|
||||
const res = await fn({ ...args, ...preArgs });
|
||||
resolve(res);
|
||||
} catch (err) {
|
||||
console.log("[preFn]", err);
|
||||
pool.push(item);
|
||||
console.log("[task]", retry, err);
|
||||
if (retry < maxRetry) {
|
||||
const retryTimer = setTimeout(() => {
|
||||
clearTimeout(retryTimer);
|
||||
pool.push({ args, resolve, reject, retry: retry + 1 });
|
||||
}, _retryInteral);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
} finally {
|
||||
curCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
317
src/libs/req.js
Normal file
317
src/libs/req.js
Normal file
@@ -0,0 +1,317 @@
|
||||
import queryString from "query-string";
|
||||
import {
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_DEEPLX,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
URL_MICROSOFT_TRAN,
|
||||
URL_TENCENT_TRANSMART,
|
||||
PROMPT_PLACE_FROM,
|
||||
PROMPT_PLACE_TO,
|
||||
PROMPT_PLACE_TEXT,
|
||||
} from "../config";
|
||||
import { msAuth } from "./auth";
|
||||
import { genDeeplFree } from "../apis/deepl";
|
||||
import { genBaidu } from "../apis/baidu";
|
||||
|
||||
const keyMap = new Map();
|
||||
|
||||
// 轮询key
|
||||
const keyPick = (translator, key = "") => {
|
||||
const keys = key
|
||||
.split(/\n|,/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (keys.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const preIndex = keyMap.get(translator) ?? -1;
|
||||
const curIndex = (preIndex + 1) % keys.length;
|
||||
keyMap.set(translator, curIndex);
|
||||
|
||||
return keys[curIndex];
|
||||
};
|
||||
|
||||
/**
|
||||
* 构造缓存 request
|
||||
* @param {*} request
|
||||
* @returns
|
||||
*/
|
||||
export const newCacheReq = async (input, init) => {
|
||||
let request = new Request(input, init);
|
||||
if (request.method !== "GET") {
|
||||
const body = await request.text();
|
||||
const cacheUrl = new URL(request.url);
|
||||
cacheUrl.pathname += body;
|
||||
request = new Request(cacheUrl.toString(), { method: "GET" });
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
const genGoogle = ({ text, from, to, url, key }) => {
|
||||
const params = {
|
||||
client: "gtx",
|
||||
dt: "t",
|
||||
dj: 1,
|
||||
ie: "UTF-8",
|
||||
sl: from,
|
||||
tl: to,
|
||||
q: text,
|
||||
};
|
||||
const input = `${url}?${queryString.stringify(params)}`;
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
};
|
||||
if (key) {
|
||||
init.headers.Authorization = `Bearer ${key}`;
|
||||
}
|
||||
|
||||
return [input, init];
|
||||
};
|
||||
|
||||
const genMicrosoft = async ({ text, from, to }) => {
|
||||
const [token] = await msAuth();
|
||||
const params = {
|
||||
from,
|
||||
to,
|
||||
"api-version": "3.0",
|
||||
};
|
||||
const input = `${URL_MICROSOFT_TRAN}?${queryString.stringify(params)}`;
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify([{ Text: text }]),
|
||||
};
|
||||
|
||||
return [input, init];
|
||||
};
|
||||
|
||||
const genDeepl = ({ text, from, to, url, key }) => {
|
||||
const data = {
|
||||
text: [text],
|
||||
target_lang: to,
|
||||
source_lang: from,
|
||||
// split_sentences: "0",
|
||||
};
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `DeepL-Auth-Key ${key}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genDeeplX = ({ text, from, to, url, key }) => {
|
||||
const data = {
|
||||
text,
|
||||
target_lang: to,
|
||||
source_lang: from,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
if (key) {
|
||||
init.headers.Authorization = `Bearer ${key}`;
|
||||
}
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genTencent = ({ text, from, to }) => {
|
||||
const data = {
|
||||
header: {
|
||||
fn: "auto_translation_block",
|
||||
},
|
||||
source: {
|
||||
text_block: text,
|
||||
lang: from,
|
||||
},
|
||||
target: {
|
||||
lang: to,
|
||||
},
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [URL_TENCENT_TRANSMART, init];
|
||||
};
|
||||
|
||||
const genOpenAI = ({ text, from, to, url, key, prompt, model }) => {
|
||||
prompt = prompt
|
||||
.replaceAll(PROMPT_PLACE_FROM, from)
|
||||
.replaceAll(PROMPT_PLACE_TO, to);
|
||||
|
||||
const data = {
|
||||
model,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: prompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: text,
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 256,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${key}`, // OpenAI
|
||||
"api-key": key, // Azure OpenAI
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genGemini = ({ text, from, to, url, key, prompt, model }) => {
|
||||
prompt = prompt
|
||||
.replaceAll(PROMPT_PLACE_FROM, from)
|
||||
.replaceAll(PROMPT_PLACE_TO, to)
|
||||
.replaceAll(PROMPT_PLACE_TEXT, text);
|
||||
|
||||
const data = {
|
||||
contents: [
|
||||
{
|
||||
// role: "user",
|
||||
parts: [
|
||||
{
|
||||
text: prompt,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const input = `${url}/${model}:generateContent?key=${key}`;
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [input, init];
|
||||
};
|
||||
|
||||
const genCloudflareAI = ({ text, from, to, url, key }) => {
|
||||
const data = {
|
||||
text,
|
||||
source_lang: from,
|
||||
target_lang: to,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genCustom = ({ text, from, to, url, key }) => {
|
||||
const data = {
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
};
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
if (key) {
|
||||
init.headers.Authorization = `Bearer ${key}`;
|
||||
}
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
/**
|
||||
* 构造翻译接口 request
|
||||
* @param {*}
|
||||
* @returns
|
||||
*/
|
||||
export const newTransReq = ({ translator, text, from, to }, apiSetting) => {
|
||||
const args = { text, from, to, ...apiSetting };
|
||||
|
||||
switch (translator) {
|
||||
case OPT_TRANS_DEEPL:
|
||||
case OPT_TRANS_OPENAI:
|
||||
case OPT_TRANS_GEMINI:
|
||||
case OPT_TRANS_CLOUDFLAREAI:
|
||||
args.key = keyPick(translator, args.key);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
switch (translator) {
|
||||
case OPT_TRANS_GOOGLE:
|
||||
return genGoogle(args);
|
||||
case OPT_TRANS_MICROSOFT:
|
||||
return genMicrosoft(args);
|
||||
case OPT_TRANS_DEEPL:
|
||||
return genDeepl(args);
|
||||
case OPT_TRANS_DEEPLFREE:
|
||||
return genDeeplFree(args);
|
||||
case OPT_TRANS_DEEPLX:
|
||||
return genDeeplX(args);
|
||||
case OPT_TRANS_BAIDU:
|
||||
return genBaidu(args);
|
||||
case OPT_TRANS_TENCENT:
|
||||
return genTencent(args);
|
||||
case OPT_TRANS_OPENAI:
|
||||
return genOpenAI(args);
|
||||
case OPT_TRANS_GEMINI:
|
||||
return genGemini(args);
|
||||
case OPT_TRANS_CLOUDFLAREAI:
|
||||
return genCloudflareAI(args);
|
||||
case OPT_TRANS_CUSTOMIZE:
|
||||
return genCustom(args);
|
||||
default:
|
||||
throw new Error(`[trans] translator: ${translator} not support`);
|
||||
}
|
||||
};
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
DEFAULT_OW_RULE,
|
||||
} from "../config";
|
||||
import { loadOrFetchSubRules } from "./subRules";
|
||||
import { getRulesWithDefault, setRules } from "./storage";
|
||||
import { trySyncRules } from "./sync";
|
||||
|
||||
/**
|
||||
* 根据href匹配规则
|
||||
@@ -19,7 +21,6 @@ import { loadOrFetchSubRules } from "./subRules";
|
||||
* @returns
|
||||
*/
|
||||
export const matchRule = async (
|
||||
rules,
|
||||
href,
|
||||
{
|
||||
injectRules = true,
|
||||
@@ -27,7 +28,7 @@ export const matchRule = async (
|
||||
owSubrule = DEFAULT_OW_RULE,
|
||||
}
|
||||
) => {
|
||||
rules = [...rules];
|
||||
const rules = await getRulesWithDefault();
|
||||
if (injectRules) {
|
||||
try {
|
||||
const selectedSub = subrulesList.find((item) => item.selected);
|
||||
@@ -47,9 +48,8 @@ export const matchRule = async (
|
||||
mixRule[key] = val;
|
||||
});
|
||||
|
||||
const subRules = (await loadOrFetchSubRules(selectedSub.url)).map(
|
||||
(item) => ({ ...item, ...mixRule })
|
||||
);
|
||||
let subRules = await loadOrFetchSubRules(selectedSub.url);
|
||||
subRules = subRules.map((item) => ({ ...item, ...mixRule }));
|
||||
rules.splice(-1, 0, ...subRules);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -60,32 +60,27 @@ export const matchRule = async (
|
||||
const rule = rules.find((r) =>
|
||||
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
|
||||
);
|
||||
|
||||
const globalRule =
|
||||
rules.find((r) =>
|
||||
r.pattern.split(",").some((p) => p.trim() === GLOBAL_KEY)
|
||||
) || GLOBLA_RULE;
|
||||
|
||||
const globalRule = rules.find((r) => r.pattern === GLOBAL_KEY) || GLOBLA_RULE;
|
||||
if (!rule) {
|
||||
return globalRule;
|
||||
}
|
||||
|
||||
rule.selector =
|
||||
rule?.selector?.trim() ||
|
||||
globalRule?.selector?.trim() ||
|
||||
GLOBLA_RULE.selector;
|
||||
|
||||
rule.bgColor = rule?.bgColor?.trim() || globalRule?.bgColor?.trim();
|
||||
rule.textDiyStyle =
|
||||
rule?.textDiyStyle?.trim() || globalRule?.textDiyStyle?.trim();
|
||||
|
||||
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach(
|
||||
(key) => {
|
||||
if (rule[key] === GLOBAL_KEY) {
|
||||
rule[key] = globalRule[key];
|
||||
}
|
||||
rule.selector = rule.selector?.trim() || globalRule.selector;
|
||||
rule.keepSelector = rule.keepSelector?.trim() || globalRule.keepSelector;
|
||||
rule.terms = rule.terms?.trim() || globalRule.terms;
|
||||
if (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;
|
||||
}
|
||||
["translator", "fromLang", "toLang", "transOpen"].forEach((key) => {
|
||||
if (rule[key] === GLOBAL_KEY) {
|
||||
rule[key] = globalRule[key];
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return rule;
|
||||
};
|
||||
@@ -119,6 +114,8 @@ export const checkRules = (rules) => {
|
||||
({
|
||||
pattern,
|
||||
selector,
|
||||
keepSelector,
|
||||
terms,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
@@ -129,6 +126,8 @@ export const checkRules = (rules) => {
|
||||
}) => ({
|
||||
pattern: pattern.trim(),
|
||||
selector: type(selector) === "string" ? selector : "",
|
||||
keepSelector: type(keepSelector) === "string" ? keepSelector : "",
|
||||
terms: type(terms) === "string" ? terms : "",
|
||||
bgColor: type(bgColor) === "string" ? bgColor : "",
|
||||
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
|
||||
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
|
||||
@@ -141,3 +140,19 @@ export const checkRules = (rules) => {
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存或更新rule
|
||||
* @param {*} newRule
|
||||
*/
|
||||
export const saveRule = async (newRule) => {
|
||||
const rules = await getRulesWithDefault();
|
||||
const rule = rules.find((item) => isMatch(newRule.pattern, item.pattern));
|
||||
if (rule && rule.pattern !== GLOBAL_KEY) {
|
||||
Object.assign(rule, { ...newRule, pattern: rule.pattern });
|
||||
} else {
|
||||
rules.unshift(newRule);
|
||||
}
|
||||
await setRules(rules);
|
||||
trySyncRules();
|
||||
};
|
||||
|
||||
112
src/libs/shortcut.js
Normal file
112
src/libs/shortcut.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { isSameSet } from "./utils";
|
||||
|
||||
/**
|
||||
* 键盘快捷键监听
|
||||
* @param {*} fn
|
||||
* @param {*} target
|
||||
* @param {*} timeout
|
||||
* @returns
|
||||
*/
|
||||
export const shortcutListener = (fn, target = document, timeout = 3000) => {
|
||||
const allkeys = new Set();
|
||||
const curkeys = new Set();
|
||||
let timer = null;
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
timer && clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
allkeys.clear();
|
||||
curkeys.clear();
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}, timeout);
|
||||
|
||||
if (e.code) {
|
||||
allkeys.add(e.code);
|
||||
curkeys.add(e.code);
|
||||
fn([...curkeys], [...allkeys]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyup = (e) => {
|
||||
curkeys.delete(e.code);
|
||||
if (curkeys.size === 0) {
|
||||
fn([...curkeys], [...allkeys]);
|
||||
allkeys.clear();
|
||||
}
|
||||
};
|
||||
|
||||
target.addEventListener("keydown", handleKeydown, true);
|
||||
target.addEventListener("keyup", handleKeyup, true);
|
||||
return () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
target.removeEventListener("keydown", handleKeydown);
|
||||
target.removeEventListener("keyup", handleKeyup);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册键盘快捷键
|
||||
* @param {*} targetKeys
|
||||
* @param {*} fn
|
||||
* @param {*} target
|
||||
* @returns
|
||||
*/
|
||||
export const shortcutRegister = (targetKeys = [], fn, target = document) => {
|
||||
return shortcutListener((curkeys) => {
|
||||
if (
|
||||
targetKeys.length > 0 &&
|
||||
isSameSet(new Set(targetKeys), new Set(curkeys))
|
||||
) {
|
||||
fn();
|
||||
}
|
||||
}, target);
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册连续快捷键
|
||||
* @param {*} targetKeys
|
||||
* @param {*} fn
|
||||
* @param {*} step
|
||||
* @param {*} timeout
|
||||
* @param {*} target
|
||||
* @returns
|
||||
*/
|
||||
export const stepShortcutRegister = (
|
||||
targetKeys = [],
|
||||
fn,
|
||||
step = 3,
|
||||
timeout = 500,
|
||||
target = document
|
||||
) => {
|
||||
let count = 0;
|
||||
let pre = Date.now();
|
||||
let timer;
|
||||
return shortcutListener((curkeys, allkeys) => {
|
||||
timer && clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
clearTimeout(timer);
|
||||
count = 0;
|
||||
}, timeout);
|
||||
|
||||
if (targetKeys.length > 0 && curkeys.length === 0) {
|
||||
const now = Date.now();
|
||||
if (
|
||||
(count === 0 || now - pre < timeout) &&
|
||||
isSameSet(new Set(targetKeys), new Set(allkeys))
|
||||
) {
|
||||
count++;
|
||||
if (count === step) {
|
||||
count = 0;
|
||||
fn();
|
||||
}
|
||||
} else {
|
||||
count = 0;
|
||||
}
|
||||
pre = now;
|
||||
}
|
||||
}, target);
|
||||
};
|
||||
@@ -1,10 +1,14 @@
|
||||
import {
|
||||
STOKEY_SETTING,
|
||||
STOKEY_RULES,
|
||||
STOKEY_WFRULES,
|
||||
STOKEY_WORDS,
|
||||
STOKEY_FAB,
|
||||
STOKEY_SYNC,
|
||||
STOKEY_MSAUTH,
|
||||
STOKEY_BDAUTH,
|
||||
STOKEY_RULESCACHE_PREFIX,
|
||||
STOKEY_WEBFIXCACHE_PREFIX,
|
||||
DEFAULT_SETTING,
|
||||
DEFAULT_RULES,
|
||||
DEFAULT_SYNC,
|
||||
@@ -82,10 +86,8 @@ export const storage = {
|
||||
* 设置信息
|
||||
*/
|
||||
export const getSetting = () => getObj(STOKEY_SETTING);
|
||||
export const getSettingWithDefault = async () => ({
|
||||
...DEFAULT_SETTING,
|
||||
...((await getSetting()) || {}),
|
||||
});
|
||||
export const getSettingWithDefault = async () =>
|
||||
(await getSetting()) || DEFAULT_SETTING;
|
||||
export const setSetting = (val) => setObj(STOKEY_SETTING, val);
|
||||
export const updateSetting = (obj) => putObj(STOKEY_SETTING, obj);
|
||||
|
||||
@@ -97,6 +99,21 @@ export const getRulesWithDefault = async () =>
|
||||
(await getRules()) || DEFAULT_RULES;
|
||||
export const setRules = (val) => setObj(STOKEY_RULES, val);
|
||||
|
||||
/**
|
||||
* 修复规则列表
|
||||
*/
|
||||
export const getWebfixRules = () => getObj(STOKEY_WFRULES);
|
||||
export const getWebfixRulesWithDefault = async () =>
|
||||
(await getWebfixRules()) || [];
|
||||
export const setWebfixRules = (val) => setObj(STOKEY_WFRULES, val);
|
||||
|
||||
/**
|
||||
* 词汇列表
|
||||
*/
|
||||
export const getWords = () => getObj(STOKEY_WORDS);
|
||||
export const getWordsWithDefault = async () => (await getWords()) || {};
|
||||
export const setWords = (val) => setObj(STOKEY_WORDS, val);
|
||||
|
||||
/**
|
||||
* 订阅规则
|
||||
*/
|
||||
@@ -106,12 +123,21 @@ export const delSubRules = (url) => del(STOKEY_RULESCACHE_PREFIX + url);
|
||||
export const setSubRules = (url, val) =>
|
||||
setObj(STOKEY_RULESCACHE_PREFIX + url, val);
|
||||
|
||||
/**
|
||||
* 修复站点
|
||||
*/
|
||||
export const getWebfix = (url) => getObj(STOKEY_WEBFIXCACHE_PREFIX + url);
|
||||
export const getWebfixWithDefault = async () => (await getWebfix()) || [];
|
||||
export const setWebfix = (url, val) =>
|
||||
setObj(STOKEY_WEBFIXCACHE_PREFIX + url, val);
|
||||
|
||||
/**
|
||||
* fab位置
|
||||
*/
|
||||
export const getFab = () => getObj(STOKEY_FAB);
|
||||
export const getFabWithDefault = async () => (await getFab()) || {};
|
||||
export const setFab = (obj) => setObj(STOKEY_FAB, obj);
|
||||
export const updateFab = (obj) => putObj(STOKEY_FAB, obj);
|
||||
|
||||
/**
|
||||
* 数据同步
|
||||
@@ -126,6 +152,12 @@ export const updateSync = (obj) => putObj(STOKEY_SYNC, obj);
|
||||
export const getMsauth = () => getObj(STOKEY_MSAUTH);
|
||||
export const setMsauth = (val) => setObj(STOKEY_MSAUTH, val);
|
||||
|
||||
/**
|
||||
* baidu auth
|
||||
*/
|
||||
export const getBdauth = () => getObj(STOKEY_BDAUTH);
|
||||
export const setBdauth = (val) => setObj(STOKEY_BDAUTH, val);
|
||||
|
||||
/**
|
||||
* 存入默认数据
|
||||
*/
|
||||
|
||||
@@ -5,18 +5,30 @@ import {
|
||||
setSubRules,
|
||||
getSubRules,
|
||||
} from "./storage";
|
||||
import { apiFetchRules } from "../apis";
|
||||
import { apiFetch } from "../apis";
|
||||
import { checkRules } from "./rules";
|
||||
import { isAllchar } from "./utils";
|
||||
import { syncWebfix } from "./webfix";
|
||||
|
||||
/**
|
||||
* 更新缓存同步时间
|
||||
* @param {*} url
|
||||
*/
|
||||
const updateSyncDataCache = async (url) => {
|
||||
const { dataCaches = {} } = await getSyncWithDefault();
|
||||
dataCaches[url] = Date.now();
|
||||
await updateSync({ dataCaches });
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const syncSubRules = async (url, isBg = false) => {
|
||||
const res = await apiFetchRules(url, isBg);
|
||||
export const syncSubRules = async (url) => {
|
||||
const res = await apiFetch(url);
|
||||
const rules = checkRules(res).filter(
|
||||
(rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== ""
|
||||
({ pattern }) => !isAllchar(pattern, GLOBAL_KEY)
|
||||
);
|
||||
if (rules.length > 0) {
|
||||
await setSubRules(url, rules);
|
||||
@@ -29,10 +41,11 @@ export const syncSubRules = async (url, isBg = false) => {
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const syncAllSubRules = async (subrulesList, isBg = false) => {
|
||||
for (let subrules of subrulesList) {
|
||||
export const syncAllSubRules = async (subrulesList) => {
|
||||
for (const subrules of subrulesList) {
|
||||
try {
|
||||
await syncSubRules(subrules.url, isBg);
|
||||
await syncSubRules(subrules.url);
|
||||
await updateSyncDataCache(subrules.url);
|
||||
} catch (err) {
|
||||
console.log(`[sync subrule error]: ${subrules.url}`, err);
|
||||
}
|
||||
@@ -44,14 +57,18 @@ export const syncAllSubRules = async (subrulesList, isBg = false) => {
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
|
||||
export const trySyncAllSubRules = async ({ subrulesList }) => {
|
||||
try {
|
||||
const { subRulesSyncAt } = await getSyncWithDefault();
|
||||
const now = Date.now();
|
||||
const interval = 24 * 60 * 60 * 1000; // 间隔一天
|
||||
if (now - subRulesSyncAt > interval) {
|
||||
await syncAllSubRules(subrulesList, isBg);
|
||||
// 同步订阅规则
|
||||
await syncAllSubRules(subrulesList);
|
||||
await updateSync({ subRulesSyncAt: now });
|
||||
|
||||
// 同步修复规则
|
||||
await syncWebfix(process.env.REACT_APP_WEBFIXURL);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[try sync all subrules]", err);
|
||||
@@ -64,9 +81,10 @@ export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
|
||||
* @returns
|
||||
*/
|
||||
export const loadOrFetchSubRules = async (url) => {
|
||||
const rules = await getSubRules(url);
|
||||
if (rules?.length) {
|
||||
return rules;
|
||||
let rules = await getSubRules(url);
|
||||
if (!rules || rules.length === 0) {
|
||||
rules = await syncSubRules(url);
|
||||
await updateSyncDataCache(url);
|
||||
}
|
||||
return syncSubRules(url);
|
||||
return rules || [];
|
||||
};
|
||||
|
||||
34
src/libs/svg.js
Normal file
34
src/libs/svg.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export const loadingSvg = `
|
||||
<svg viewBox="0 0 100 100" style="display:inline-block; width:100%; height: 100%;">
|
||||
<circle fill="#209CEE" stroke="none" cx="6" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 15 ; 0 -15; 0 15"
|
||||
repeatCount="indefinite"
|
||||
begin="0.1"
|
||||
/>
|
||||
</circle>
|
||||
<circle fill="#209CEE" stroke="none" cx="30" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 10 ; 0 -10; 0 10"
|
||||
repeatCount="indefinite"
|
||||
begin="0.2"
|
||||
/>
|
||||
</circle>
|
||||
<circle fill="#209CEE" stroke="none" cx="54" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 5 ; 0 -5; 0 5"
|
||||
repeatCount="indefinite"
|
||||
begin="0.3"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
`;
|
||||
221
src/libs/sync.js
221
src/libs/sync.js
@@ -1,57 +1,122 @@
|
||||
import {
|
||||
APP_LCNAME,
|
||||
KV_SETTING_KEY,
|
||||
KV_RULES_KEY,
|
||||
KV_WFRULES_KEY,
|
||||
KV_WORDS_KEY,
|
||||
KV_RULES_SHARE_KEY,
|
||||
KV_SALT_SHARE,
|
||||
OPT_SYNCTYPE_WEBDAV,
|
||||
} from "../config";
|
||||
import {
|
||||
getSyncWithDefault,
|
||||
updateSync,
|
||||
getSettingWithDefault,
|
||||
getRulesWithDefault,
|
||||
getWordsWithDefault,
|
||||
getWebfixRulesWithDefault,
|
||||
setSetting,
|
||||
setRules,
|
||||
setWebfixRules,
|
||||
setWords,
|
||||
} from "./storage";
|
||||
import { apiSyncData } from "../apis";
|
||||
import { sha256 } from "./utils";
|
||||
import { sha256, removeEndchar } from "./utils";
|
||||
import { createClient, getPatcher } from "webdav";
|
||||
import { fetchApi } from "./fetch";
|
||||
|
||||
getPatcher().patch("request", (opts) => {
|
||||
return fetchApi({
|
||||
input: opts.url,
|
||||
init: { method: opts.method, headers: opts.headers, body: opts.data },
|
||||
});
|
||||
});
|
||||
|
||||
const syncByWebdav = async (data, { syncUrl, syncUser, syncKey }) => {
|
||||
const client = createClient(syncUrl, {
|
||||
username: syncUser,
|
||||
password: syncKey,
|
||||
});
|
||||
const pathname = `/${APP_LCNAME}`;
|
||||
const filename = `/${APP_LCNAME}/${data.key}`;
|
||||
|
||||
if ((await client.exists(pathname)) === false) {
|
||||
await client.createDirectory(pathname);
|
||||
}
|
||||
|
||||
const isExist = await client.exists(filename);
|
||||
if (isExist) {
|
||||
const cont = await client.getFileContents(filename, { format: "text" });
|
||||
const webData = JSON.parse(cont);
|
||||
if (webData.updateAt >= data.updateAt) {
|
||||
return webData;
|
||||
}
|
||||
}
|
||||
|
||||
await client.putFileContents(filename, JSON.stringify(data, null, 2));
|
||||
return data;
|
||||
};
|
||||
|
||||
const syncByWorker = async (data, { syncUrl, syncKey }) => {
|
||||
syncUrl = removeEndchar(syncUrl, "/");
|
||||
return await apiSyncData(`${syncUrl}/sync`, syncKey, data);
|
||||
};
|
||||
|
||||
const syncData = async (key, valueFn) => {
|
||||
const {
|
||||
syncType,
|
||||
syncUrl,
|
||||
syncUser,
|
||||
syncKey,
|
||||
syncMeta = {},
|
||||
} = await getSyncWithDefault();
|
||||
if (!syncUrl || !syncKey || (syncType === OPT_SYNCTYPE_WEBDAV && !syncUser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { updateAt = 0, syncAt = 0 } = syncMeta[key] || {};
|
||||
syncAt === 0 && (updateAt = 0);
|
||||
|
||||
const value = await valueFn();
|
||||
const data = {
|
||||
key,
|
||||
value: JSON.stringify(value),
|
||||
updateAt,
|
||||
};
|
||||
const args = {
|
||||
syncUrl,
|
||||
syncUser,
|
||||
syncKey,
|
||||
};
|
||||
|
||||
const res =
|
||||
syncType === OPT_SYNCTYPE_WEBDAV
|
||||
? await syncByWebdav(data, args)
|
||||
: await syncByWorker(data, args);
|
||||
|
||||
syncMeta[key] = {
|
||||
updateAt: res.updateAt,
|
||||
syncAt: Date.now(),
|
||||
};
|
||||
await updateSync({ syncMeta });
|
||||
|
||||
return { value: JSON.parse(res.value), isNew: res.updateAt > updateAt };
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步设置
|
||||
* @returns
|
||||
*/
|
||||
const syncSetting = async (isBg = false) => {
|
||||
const { syncUrl, syncKey, settingUpdateAt } = await getSyncWithDefault();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setting = await getSettingWithDefault();
|
||||
const res = await apiSyncData(
|
||||
syncUrl,
|
||||
syncKey,
|
||||
{
|
||||
key: KV_SETTING_KEY,
|
||||
value: setting,
|
||||
updateAt: settingUpdateAt,
|
||||
},
|
||||
isBg
|
||||
);
|
||||
|
||||
if (res && res.updateAt > settingUpdateAt) {
|
||||
await updateSync({
|
||||
settingUpdateAt: res.updateAt,
|
||||
settingSyncAt: res.updateAt,
|
||||
});
|
||||
const syncSetting = async () => {
|
||||
const res = await syncData(KV_SETTING_KEY, getSettingWithDefault);
|
||||
if (res?.isNew) {
|
||||
await setSetting(res.value);
|
||||
return res.value;
|
||||
} else {
|
||||
await updateSync({ settingSyncAt: res.updateAt });
|
||||
}
|
||||
};
|
||||
|
||||
export const trySyncSetting = async (isBg = false) => {
|
||||
export const trySyncSetting = async () => {
|
||||
try {
|
||||
return await syncSetting(isBg);
|
||||
await syncSetting();
|
||||
} catch (err) {
|
||||
console.log("[sync setting]", err);
|
||||
}
|
||||
@@ -61,57 +126,77 @@ export const trySyncSetting = async (isBg = false) => {
|
||||
* 同步规则
|
||||
* @returns
|
||||
*/
|
||||
const syncRules = async (isBg = false) => {
|
||||
const { syncUrl, syncKey, rulesUpdateAt } = await getSyncWithDefault();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rules = await getRulesWithDefault();
|
||||
const res = await apiSyncData(
|
||||
syncUrl,
|
||||
syncKey,
|
||||
{
|
||||
key: KV_RULES_KEY,
|
||||
value: rules,
|
||||
updateAt: rulesUpdateAt,
|
||||
},
|
||||
isBg
|
||||
);
|
||||
|
||||
if (res && res.updateAt > rulesUpdateAt) {
|
||||
await updateSync({
|
||||
rulesUpdateAt: res.updateAt,
|
||||
rulesSyncAt: res.updateAt,
|
||||
});
|
||||
const syncRules = async () => {
|
||||
const res = await syncData(KV_RULES_KEY, getRulesWithDefault);
|
||||
if (res?.isNew) {
|
||||
await setRules(res.value);
|
||||
return res.value;
|
||||
} else {
|
||||
await updateSync({ rulesSyncAt: res.updateAt });
|
||||
}
|
||||
};
|
||||
|
||||
export const trySyncRules = async (isBg = false) => {
|
||||
export const trySyncRules = async () => {
|
||||
try {
|
||||
return await syncRules(isBg);
|
||||
await syncRules();
|
||||
} catch (err) {
|
||||
console.log("[sync user rules]", err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步修复规则
|
||||
* @returns
|
||||
*/
|
||||
const syncWebfixRules = async () => {
|
||||
const res = await syncData(KV_WFRULES_KEY, getWebfixRulesWithDefault);
|
||||
if (res?.isNew) {
|
||||
await setWebfixRules(res.value);
|
||||
}
|
||||
};
|
||||
|
||||
export const trySyncWebfixRules = async () => {
|
||||
try {
|
||||
await syncWebfixRules();
|
||||
} catch (err) {
|
||||
console.log("[sync user webfix rules]", err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步词汇
|
||||
* @returns
|
||||
*/
|
||||
const syncWords = async () => {
|
||||
const res = await syncData(KV_WORDS_KEY, getWordsWithDefault);
|
||||
if (res?.isNew) {
|
||||
await setWords(res.value);
|
||||
}
|
||||
};
|
||||
|
||||
export const trySyncWords = async () => {
|
||||
try {
|
||||
await syncWords();
|
||||
} catch (err) {
|
||||
console.log("[sync fav words]", err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步分享规则
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
||||
await apiSyncData(syncUrl, syncKey, {
|
||||
const data = {
|
||||
key: KV_RULES_SHARE_KEY,
|
||||
value: rules,
|
||||
value: JSON.stringify(rules, null, 2),
|
||||
updateAt: Date.now(),
|
||||
});
|
||||
};
|
||||
const args = {
|
||||
syncUrl,
|
||||
syncKey,
|
||||
};
|
||||
await syncByWorker(data, args);
|
||||
const psk = await sha256(syncKey, KV_SALT_SHARE);
|
||||
const shareUrl = `${syncUrl}?psk=${psk}`;
|
||||
const shareUrl = `${syncUrl}/rules?psk=${psk}`;
|
||||
return shareUrl;
|
||||
};
|
||||
|
||||
@@ -119,10 +204,16 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
||||
* 同步个人设置和规则
|
||||
* @returns
|
||||
*/
|
||||
export const syncSettingAndRules = async (isBg = false) => {
|
||||
return [await syncSetting(isBg), await syncRules(isBg)];
|
||||
export const syncSettingAndRules = async () => {
|
||||
await syncSetting();
|
||||
await syncRules();
|
||||
await syncWebfixRules();
|
||||
await syncWords();
|
||||
};
|
||||
|
||||
export const trySyncSettingAndRules = async (isBg = false) => {
|
||||
return [await trySyncSetting(isBg), await trySyncRules(isBg)];
|
||||
export const trySyncSettingAndRules = async () => {
|
||||
await trySyncSetting();
|
||||
await trySyncRules();
|
||||
await trySyncWebfixRules();
|
||||
await trySyncWords();
|
||||
};
|
||||
|
||||
12
src/libs/touch.js
Normal file
12
src/libs/touch.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export function touchTapListener(fn, touchsLength) {
|
||||
const handleTouchend = (e) => {
|
||||
if (e.touches.length === touchsLength) {
|
||||
fn();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("touchstart", handleTouchend);
|
||||
return () => {
|
||||
document.removeEventListener("touchstart", handleTouchend);
|
||||
};
|
||||
}
|
||||
@@ -7,10 +7,16 @@ import {
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_FUZZY,
|
||||
SHADOW_KEY,
|
||||
OPT_MOUSEKEY_DISABLE,
|
||||
OPT_MOUSEKEY_PAGEOPEN,
|
||||
OPT_MOUSEKEY_MOUSEOVER,
|
||||
DEFAULT_TRANS_APIS,
|
||||
} from "../config";
|
||||
import Content from "../views/Content";
|
||||
import { updateFetchPool, clearFetchPool } from "./fetch";
|
||||
import { debounce, genEventName } from "./utils";
|
||||
import { runFixer } from "./webfix";
|
||||
import { apiTranslate } from "../apis";
|
||||
|
||||
/**
|
||||
* 翻译类
|
||||
@@ -18,6 +24,7 @@ import { debounce, genEventName } from "./utils";
|
||||
export class Translator {
|
||||
_rule = {};
|
||||
_setting = {};
|
||||
_fixerSetting = null;
|
||||
_rootNodes = new Set();
|
||||
_tranNodes = new Map();
|
||||
_skipNodeNames = [
|
||||
@@ -37,6 +44,10 @@ export class Translator {
|
||||
"iframe",
|
||||
];
|
||||
_eventName = genEventName();
|
||||
_mouseoverNode = null;
|
||||
_keepSelector = [null, null];
|
||||
_terms = [];
|
||||
_docTitle = "";
|
||||
|
||||
// 显示
|
||||
_interseObserver = new IntersectionObserver(
|
||||
@@ -88,13 +99,22 @@ export class Translator {
|
||||
};
|
||||
};
|
||||
|
||||
constructor(rule, setting) {
|
||||
constructor(rule, setting, fixerSetting) {
|
||||
const { fetchInterval, fetchLimit } = setting;
|
||||
updateFetchPool(fetchInterval, fetchLimit);
|
||||
this._overrideAttachShadow();
|
||||
|
||||
this._setting = setting;
|
||||
this._rule = rule;
|
||||
this._fixerSetting = fixerSetting;
|
||||
|
||||
this._keepSelector = (rule.keepSelector || "")
|
||||
.split(SHADOW_KEY)
|
||||
.map((item) => item.trim());
|
||||
this._terms = (rule.terms || "")
|
||||
.split(/\n|;/)
|
||||
.map((item) => item.split(",").map((item) => item.trim()))
|
||||
.filter(([term]) => Boolean(term));
|
||||
|
||||
if (rule.transOpen === "true") {
|
||||
this._register();
|
||||
@@ -152,6 +172,20 @@ export class Translator {
|
||||
this.rule = { ...this.rule, textStyle };
|
||||
};
|
||||
|
||||
translateText = async (text) => {
|
||||
const { translator, fromLang, toLang } = this._rule;
|
||||
const apiSetting =
|
||||
this._setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
|
||||
const [trText] = await apiTranslate({
|
||||
text,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting,
|
||||
});
|
||||
return trText;
|
||||
};
|
||||
|
||||
_querySelectorAll = (selector, node) => {
|
||||
try {
|
||||
return Array.from(node.querySelectorAll(selector));
|
||||
@@ -167,6 +201,22 @@ export class Translator {
|
||||
);
|
||||
};
|
||||
|
||||
_queryShadowNodes = (selector, rootNode) => {
|
||||
this._rootNodes.add(rootNode);
|
||||
this._queryFilter(selector, rootNode).forEach((item) => {
|
||||
if (!this._tranNodes.has(item)) {
|
||||
this._tranNodes.set(item, "");
|
||||
}
|
||||
});
|
||||
|
||||
Array.from(rootNode.querySelectorAll("*"))
|
||||
.map((item) => item.shadowRoot)
|
||||
.filter(Boolean)
|
||||
.forEach((item) => {
|
||||
this._queryShadowNodes(selector, item);
|
||||
});
|
||||
};
|
||||
|
||||
_queryNodes = (rootNode = document) => {
|
||||
// const childRoots = Array.from(rootNode.querySelectorAll("*"))
|
||||
// .map((item) => item.shadowRoot)
|
||||
@@ -189,14 +239,15 @@ export class Translator {
|
||||
const outNodes = this._querySelectorAll(outSelector, rootNode);
|
||||
outNodes.forEach((outNode) => {
|
||||
if (outNode.shadowRoot) {
|
||||
this._rootNodes.add(outNode.shadowRoot);
|
||||
this._queryFilter(inSelector, outNode.shadowRoot).forEach(
|
||||
(item) => {
|
||||
if (!this._tranNodes.has(item)) {
|
||||
this._tranNodes.set(item, "");
|
||||
}
|
||||
}
|
||||
);
|
||||
// this._rootNodes.add(outNode.shadowRoot);
|
||||
// this._queryFilter(inSelector, outNode.shadowRoot).forEach(
|
||||
// (item) => {
|
||||
// if (!this._tranNodes.has(item)) {
|
||||
// this._tranNodes.set(item, "");
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
this._queryShadowNodes(inSelector, outNode.shadowRoot);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -211,6 +262,15 @@ export class Translator {
|
||||
};
|
||||
|
||||
_register = () => {
|
||||
if (this._rule.fromLang === this._rule.toLang) {
|
||||
return;
|
||||
}
|
||||
|
||||
// webfix
|
||||
if (this._fixerSetting) {
|
||||
runFixer(this._fixerSetting);
|
||||
}
|
||||
|
||||
// 搜索节点
|
||||
this._queryNodes();
|
||||
|
||||
@@ -223,23 +283,120 @@ export class Translator {
|
||||
});
|
||||
});
|
||||
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
if (
|
||||
!this._setting.mouseKey ||
|
||||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
|
||||
) {
|
||||
// 监听节点显示
|
||||
this._interseObserver.observe(node);
|
||||
});
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
this._interseObserver.observe(node);
|
||||
});
|
||||
} else if (this._setting.mouseKey === OPT_MOUSEKEY_PAGEOPEN) {
|
||||
// 全文直接翻译
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
this._render(node);
|
||||
});
|
||||
} else {
|
||||
// 监听鼠标悬停
|
||||
window.addEventListener("keydown", this._handleKeydown);
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
node.addEventListener("mouseenter", this._handleMouseover);
|
||||
node.addEventListener("mouseleave", this._handleMouseout);
|
||||
});
|
||||
}
|
||||
|
||||
// 翻译页面标题
|
||||
if (this._setting.transTitle && !this._docTitle) {
|
||||
const title = document.title;
|
||||
this._docTitle = title;
|
||||
this.translateText(title).then((trText) => {
|
||||
document.title = `${trText} | ${title}`;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_handleMouseover = (e) => {
|
||||
// console.log("mouseenter", e);
|
||||
if (!this._tranNodes.has(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = this._setting.mouseKey.slice(3);
|
||||
if (this._setting.mouseKey === OPT_MOUSEKEY_MOUSEOVER || e[key]) {
|
||||
e.target.removeEventListener("mouseenter", this._handleMouseover);
|
||||
e.target.removeEventListener("mouseleave", this._handleMouseout);
|
||||
this._render(e.target);
|
||||
} else {
|
||||
this._mouseoverNode = e.target;
|
||||
}
|
||||
};
|
||||
|
||||
_handleMouseout = (e) => {
|
||||
// console.log("mouseleave", e);
|
||||
if (!this._tranNodes.has(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._mouseoverNode = null;
|
||||
};
|
||||
|
||||
_handleKeydown = (e) => {
|
||||
// console.log("keydown", e);
|
||||
const key = this._setting.mouseKey.slice(3);
|
||||
if (e[key] && this._mouseoverNode) {
|
||||
this._mouseoverNode.removeEventListener(
|
||||
"mouseenter",
|
||||
this._handleMouseover
|
||||
);
|
||||
this._mouseoverNode.removeEventListener(
|
||||
"mouseleave",
|
||||
this._handleMouseout
|
||||
);
|
||||
|
||||
const node = this._mouseoverNode;
|
||||
this._render(node);
|
||||
this._mouseoverNode = null;
|
||||
}
|
||||
};
|
||||
|
||||
_unRegister = () => {
|
||||
// 恢复页面标题
|
||||
if (this._docTitle) {
|
||||
document.title = this._docTitle;
|
||||
this._docTitle = "";
|
||||
}
|
||||
|
||||
// 解除节点变化监听
|
||||
this._mutaObserver.disconnect();
|
||||
|
||||
// 解除节点显示监听
|
||||
this._interseObserver.disconnect();
|
||||
// this._interseObserver.disconnect();
|
||||
|
||||
// 移除已插入元素
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
node.querySelector(APP_LCNAME)?.remove();
|
||||
});
|
||||
if (
|
||||
!this._setting.mouseKey ||
|
||||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
|
||||
) {
|
||||
// 解除节点显示监听
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
this._interseObserver.unobserve(node);
|
||||
// 移除已插入元素
|
||||
node.querySelector(APP_LCNAME)?.remove();
|
||||
});
|
||||
} else if (this._setting.mouseKey === OPT_MOUSEKEY_PAGEOPEN) {
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
node.querySelector(APP_LCNAME)?.remove();
|
||||
});
|
||||
} else {
|
||||
// 移除鼠标悬停监听
|
||||
window.removeEventListener("keydown", this._handleKeydown);
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
// node.style.pointerEvents = "none";
|
||||
node.removeEventListener("mouseenter", this._handleMouseover);
|
||||
node.removeEventListener("mouseleave", this._handleMouseout);
|
||||
// 移除已插入元素
|
||||
node.querySelector(APP_LCNAME)?.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// 清空节点集合
|
||||
this._rootNodes.clear();
|
||||
@@ -255,6 +412,11 @@ export class Translator {
|
||||
}
|
||||
}, 500);
|
||||
|
||||
_invalidLength = (q) =>
|
||||
!q ||
|
||||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
|
||||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH);
|
||||
|
||||
_render = (el) => {
|
||||
let traEl = el.querySelector(APP_LCNAME);
|
||||
|
||||
@@ -274,19 +436,52 @@ export class Translator {
|
||||
traEl.remove();
|
||||
}
|
||||
|
||||
const q = el.innerText.trim();
|
||||
let q = el.innerText.trim();
|
||||
this._tranNodes.set(el, q);
|
||||
const keeps = [];
|
||||
|
||||
// 保留元素
|
||||
const [matchSelector, subSelector] = this._keepSelector;
|
||||
if (matchSelector || subSelector) {
|
||||
let text = "";
|
||||
el.childNodes.forEach((child) => {
|
||||
if (
|
||||
child.nodeType === 1 &&
|
||||
((matchSelector && child.matches(matchSelector)) ||
|
||||
(subSelector && child.querySelector(subSelector)))
|
||||
) {
|
||||
if (child.nodeName === "IMG") {
|
||||
child.style.cssText += `width: ${child.width}px;`;
|
||||
child.style.cssText += `height: ${child.height}px;`;
|
||||
}
|
||||
text += `[${keeps.length}]`;
|
||||
keeps.push(child.outerHTML);
|
||||
} else {
|
||||
text += child.textContent;
|
||||
}
|
||||
});
|
||||
|
||||
if (keeps.length > 0) {
|
||||
q = text;
|
||||
}
|
||||
}
|
||||
|
||||
// 太长或太短
|
||||
if (
|
||||
!q ||
|
||||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
|
||||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH)
|
||||
) {
|
||||
if (this._invalidLength(q.replace(/\[(\d+)\]/g, "").trim())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("---> ", q);
|
||||
// 专业术语
|
||||
if (this._terms.length > 0) {
|
||||
for (const term of this._terms) {
|
||||
const re = new RegExp(term[0], "g");
|
||||
q = q.replace(re, (t) => {
|
||||
const text = `[${keeps.length}]`;
|
||||
keeps.push(term[1] || t);
|
||||
return text;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
traEl = document.createElement(APP_LCNAME);
|
||||
traEl.style.visibility = "visible";
|
||||
@@ -298,7 +493,9 @@ export class Translator {
|
||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||
}
|
||||
|
||||
// console.log({ q, keeps });
|
||||
|
||||
const root = createRoot(traEl);
|
||||
root.render(<Content q={q} translator={this} />);
|
||||
root.render(<Content q={q} keeps={keeps} translator={this} />);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,15 +48,61 @@ export const sleep = (delay) =>
|
||||
* @returns
|
||||
*/
|
||||
export const debounce = (func, delay = 200) => {
|
||||
let timer;
|
||||
let timer = null;
|
||||
return (...args) => {
|
||||
timer && clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {*} func
|
||||
* @param {*} delay
|
||||
* @returns
|
||||
*/
|
||||
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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断字符串全是某个字符
|
||||
* @param {*} s
|
||||
* @param {*} c
|
||||
* @param {*} i
|
||||
* @returns
|
||||
*/
|
||||
export const isAllchar = (s, c, i = 0) => {
|
||||
while (i < s.length) {
|
||||
if (s[i] !== c) {
|
||||
return false;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 字符串通配符(*)匹配
|
||||
* @param {*} s
|
||||
@@ -68,7 +114,7 @@ export const isMatch = (s, p) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
p = `*${p}*`;
|
||||
p = "*" + p + "*";
|
||||
|
||||
let [sIndex, pIndex] = [0, 0];
|
||||
let [sRecord, pRecord] = [-1, -1];
|
||||
@@ -91,7 +137,7 @@ export const isMatch = (s, p) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
return p.slice(pIndex).replaceAll("*", "") === "";
|
||||
return isAllchar(p, "*", pIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -122,3 +168,68 @@ export const sha256 = async (text, salt) => {
|
||||
* @returns
|
||||
*/
|
||||
export const genEventName = () => btoa(Math.random()).slice(3, 11);
|
||||
|
||||
/**
|
||||
* 判断两个 Set 是否相同
|
||||
* @param {*} a
|
||||
* @param {*} b
|
||||
* @returns
|
||||
*/
|
||||
export const isSameSet = (a, b) => {
|
||||
const s = new Set([...a, ...b]);
|
||||
return s.size === a.size && s.size === b.size;
|
||||
};
|
||||
|
||||
/**
|
||||
* 去掉字符串末尾某个字符
|
||||
* @param {*} s
|
||||
* @param {*} c
|
||||
* @param {*} count
|
||||
* @returns
|
||||
*/
|
||||
export const removeEndchar = (s, c, count = 1) => {
|
||||
let i = s.length;
|
||||
while (i > s.length - count && s[i - 1] === c) {
|
||||
i--;
|
||||
}
|
||||
return s.slice(0, i);
|
||||
};
|
||||
|
||||
/**
|
||||
* 匹配字符串及语言标识
|
||||
* @param {*} str
|
||||
* @param {*} sign
|
||||
* @returns
|
||||
*/
|
||||
export const matchInputStr = (str, sign) => {
|
||||
let reg = /\/([\w-]+)\s+([^]+)/;
|
||||
switch (sign) {
|
||||
case "//":
|
||||
reg = /\/\/([\w-]+)\s+([^]+)/;
|
||||
break;
|
||||
case "\\":
|
||||
reg = /\\([\w-]+)\s+([^]+)/;
|
||||
break;
|
||||
case "\\\\":
|
||||
reg = /\\\\([\w-]+)\s+([^]+)/;
|
||||
break;
|
||||
case ">":
|
||||
reg = />([\w-]+)\s+([^]+)/;
|
||||
break;
|
||||
case ">>":
|
||||
reg = />>([\w-]+)\s+([^]+)/;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return str.match(reg);
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否英文单词
|
||||
* @param {*} str
|
||||
* @returns
|
||||
*/
|
||||
export const isValidWord = (str) => {
|
||||
const regex = /^[a-zA-Z-]+$/;
|
||||
return regex.test(str);
|
||||
};
|
||||
|
||||
256
src/libs/webfix.js
Normal file
256
src/libs/webfix.js
Normal file
@@ -0,0 +1,256 @@
|
||||
import { isMatch } from "./utils";
|
||||
import { getWebfix, setWebfix, getWebfixRulesWithDefault } from "./storage";
|
||||
import { apiFetch } from "../apis";
|
||||
|
||||
/**
|
||||
* 修复程序类型
|
||||
*/
|
||||
const FIXER_BR = "br";
|
||||
const FIXER_BN = "bn";
|
||||
const FIXER_BR_DIV = "brToDiv";
|
||||
const FIXER_BN_DIV = "bnToDiv";
|
||||
const FIXER_FONTSIZE = "fontSize";
|
||||
export const FIXER_ALL = [
|
||||
FIXER_BR,
|
||||
FIXER_BN,
|
||||
FIXER_BR_DIV,
|
||||
FIXER_BN_DIV,
|
||||
// FIXER_FONTSIZE,
|
||||
];
|
||||
|
||||
/**
|
||||
* 需要修复的站点列表
|
||||
* - pattern 匹配网址
|
||||
* - selector 需要修复的选择器
|
||||
* - rootSelector 需要监听的选择器,可留空
|
||||
* - fixer 修复函数,可针对不同网址,选用不同修复函数
|
||||
*/
|
||||
const DEFAULT_SITES = [
|
||||
{
|
||||
pattern: "www.phoronix.com",
|
||||
selector: ".content",
|
||||
rootSelector: "",
|
||||
fixer: FIXER_BR,
|
||||
},
|
||||
{
|
||||
pattern: "t.me/s/",
|
||||
selector: ".tgme_widget_message_text",
|
||||
rootSelector: ".tgme_channel_history",
|
||||
fixer: FIXER_BR,
|
||||
},
|
||||
{
|
||||
pattern: "baidu.com",
|
||||
selector: "html",
|
||||
rootSelector: "",
|
||||
fixer: FIXER_FONTSIZE,
|
||||
},
|
||||
{
|
||||
pattern: "chat.openai.com",
|
||||
selector: "div[data-testid^=conversation-turn] .items-start > div",
|
||||
rootSelector: "",
|
||||
fixer: FIXER_BN,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 修复过的标记
|
||||
*/
|
||||
const fixedSign = "kissfixed";
|
||||
|
||||
/**
|
||||
* 采用 `br` 换行网站的修复函数
|
||||
* 目标是将 `br` 替换成 `p`
|
||||
* @param {*} node
|
||||
* @returns
|
||||
*/
|
||||
function brFixer(node, tag = "p") {
|
||||
if (node.hasAttribute(fixedSign)) {
|
||||
return;
|
||||
}
|
||||
node.setAttribute(fixedSign, "true");
|
||||
|
||||
const gapTags = ["BR", "WBR"];
|
||||
const newlineTags = [
|
||||
"DIV",
|
||||
"UL",
|
||||
"OL",
|
||||
"LI",
|
||||
"H1",
|
||||
"H2",
|
||||
"H3",
|
||||
"H4",
|
||||
"H5",
|
||||
"H6",
|
||||
"P",
|
||||
"HR",
|
||||
"PRE",
|
||||
"TABLE",
|
||||
"BLOCKQUOTE",
|
||||
];
|
||||
|
||||
let html = "";
|
||||
node.childNodes.forEach(function (child, index) {
|
||||
if (index === 0) {
|
||||
html += `<${tag} class="kiss-p">`;
|
||||
}
|
||||
|
||||
if (gapTags.indexOf(child.nodeName) !== -1) {
|
||||
html += `</${tag}><${tag} class="kiss-p">`;
|
||||
} else if (newlineTags.indexOf(child.nodeName) !== -1) {
|
||||
html += `</${tag}>${child.outerHTML}<${tag} class="kiss-p">`;
|
||||
} else if (child.outerHTML) {
|
||||
html += child.outerHTML;
|
||||
} else if (child.textContent) {
|
||||
html += child.textContent;
|
||||
}
|
||||
|
||||
if (index === node.childNodes.length - 1) {
|
||||
html += `</${tag}>`;
|
||||
}
|
||||
});
|
||||
node.innerHTML = html;
|
||||
}
|
||||
|
||||
function brDivFixer(node) {
|
||||
return brFixer(node, "div");
|
||||
}
|
||||
|
||||
/**
|
||||
* 目标是将 `\n` 替换成 `p`
|
||||
* @param {*} node
|
||||
* @returns
|
||||
*/
|
||||
function bnFixer(node, tag = "p") {
|
||||
if (node.hasAttribute(fixedSign)) {
|
||||
return;
|
||||
}
|
||||
node.setAttribute(fixedSign, "true");
|
||||
node.innerHTML = node.innerHTML
|
||||
.split("\n")
|
||||
.map((item) => `<${tag} class="kiss-p">${item || " "}</${tag}>`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function bnDivFixer(node) {
|
||||
return bnFixer(node, "div");
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复字体大小问题,如 baidu.com
|
||||
* @param {*} node
|
||||
*/
|
||||
function fontSizeFixer(node) {
|
||||
node.style.cssText += "font-size:1em;";
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复程序映射
|
||||
*/
|
||||
const fixerMap = {
|
||||
[FIXER_BR]: brFixer,
|
||||
[FIXER_BN]: bnFixer,
|
||||
[FIXER_BR_DIV]: brDivFixer,
|
||||
[FIXER_BN_DIV]: bnDivFixer,
|
||||
[FIXER_FONTSIZE]: fontSizeFixer,
|
||||
};
|
||||
|
||||
/**
|
||||
* 查找、监听节点,并执行修复函数
|
||||
* @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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步远程数据
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const syncWebfix = async (url) => {
|
||||
const sites = await apiFetch(url);
|
||||
await setWebfix(url, sites);
|
||||
return sites;
|
||||
};
|
||||
|
||||
/**
|
||||
* 从缓存或远程加载修复站点
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const loadOrFetchWebfix = async (url) => {
|
||||
try {
|
||||
let sites = await getWebfix(url);
|
||||
if (sites?.length) {
|
||||
return sites;
|
||||
}
|
||||
return syncWebfix(url);
|
||||
} catch (err) {
|
||||
console.log("[load webfix]", err.message);
|
||||
return DEFAULT_SITES;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行fixer
|
||||
* @param {*} param0
|
||||
*/
|
||||
export async function runFixer({ selector, fixer, rootSelector }) {
|
||||
try {
|
||||
run(selector, fixerMap[fixer], rootSelector);
|
||||
} catch (err) {
|
||||
console.error(`[kiss-webfix run]: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配fixer配置
|
||||
*/
|
||||
export async function matchFixer(href, { injectWebfix }) {
|
||||
if (!injectWebfix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const userSites = await getWebfixRulesWithDefault();
|
||||
const subSites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
|
||||
const sites = [...userSites, ...subSites];
|
||||
for (let i = 0; i < sites.length; i++) {
|
||||
const site = sites[i];
|
||||
if (isMatch(href, site.pattern) && fixerMap[site.fixer]) {
|
||||
return site;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[kiss-webfix match]: ${err.message}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,107 +1,3 @@
|
||||
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 {
|
||||
getSettingWithDefault,
|
||||
getRulesWithDefault,
|
||||
getFabWithDefault,
|
||||
} from "./libs/storage";
|
||||
import { Translator } from "./libs/translator";
|
||||
import { trySyncAllSubRules } from "./libs/subRules";
|
||||
import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config";
|
||||
import { isIframe } from "./libs/iframe";
|
||||
import { handlePing, injectScript } from "./libs/gm";
|
||||
import { matchRule } from "./libs/rules";
|
||||
import { genEventName } from "./libs/utils";
|
||||
import { run } from "./common";
|
||||
|
||||
/**
|
||||
* 入口函数
|
||||
*/
|
||||
const init = async () => {
|
||||
// 设置页面
|
||||
if (
|
||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
|
||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE) ||
|
||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE2)
|
||||
) {
|
||||
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
|
||||
unsafeWindow.GM = GM;
|
||||
unsafeWindow.APP_INFO = {
|
||||
name: process.env.REACT_APP_NAME,
|
||||
version: process.env.REACT_APP_VERSION,
|
||||
};
|
||||
} else {
|
||||
const ping = genEventName();
|
||||
window.addEventListener(ping, handlePing);
|
||||
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
|
||||
const script = document.createElement("script");
|
||||
script.textContent = `(${injectScript})("${ping}")`;
|
||||
document.head.append(script);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 翻译页面
|
||||
const href = isIframe ? document.referrer : document.location.href;
|
||||
const setting = await getSettingWithDefault();
|
||||
const rules = await getRulesWithDefault();
|
||||
const rule = await matchRule(rules, href, setting);
|
||||
const translator = new Translator(rule, setting);
|
||||
|
||||
if (isIframe) {
|
||||
// iframe
|
||||
window.addEventListener("message", (e) => {
|
||||
const action = e?.data?.action;
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
translator.toggle();
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
translator.updateRule(e.data.args || {});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 浮球按钮
|
||||
const fab = await getFabWithDefault();
|
||||
const $action = document.createElement("div");
|
||||
$action.setAttribute("id", "kiss-translator");
|
||||
document.body.parentElement.appendChild($action);
|
||||
const shadowContainer = $action.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
const shadowRootElement = document.createElement("div");
|
||||
shadowContainer.appendChild(emotionRoot);
|
||||
shadowContainer.appendChild(shadowRootElement);
|
||||
const cache = createCache({
|
||||
key: "css",
|
||||
prepend: true,
|
||||
container: emotionRoot,
|
||||
});
|
||||
ReactDOM.createRoot(shadowRootElement).render(
|
||||
<React.StrictMode>
|
||||
<CacheProvider value={cache}>
|
||||
<Action translator={translator} fab={fab} />
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// 同步订阅规则
|
||||
trySyncAllSubRules(setting);
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await init();
|
||||
} catch (err) {
|
||||
const $err = document.createElement("div");
|
||||
$err.innerText = `KISS-Translator: ${err.message}`;
|
||||
$err.style.cssText = "background:red; color:#fff; z-index:10000;";
|
||||
document.body.prepend($err);
|
||||
}
|
||||
})();
|
||||
run(true);
|
||||
|
||||
@@ -1,65 +1,51 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { limitNumber } from "../../libs/utils";
|
||||
import { isMobile } from "../../libs/mobile";
|
||||
import { setFab } from "../../libs/storage";
|
||||
import { updateFab } from "../../libs/storage";
|
||||
import { debounce } from "../../libs/utils";
|
||||
import Paper from "@mui/material/Paper";
|
||||
|
||||
const getEdgePosition = (
|
||||
{ x: left, y: top, edge },
|
||||
const getEdgePosition = ({
|
||||
x: left,
|
||||
y: top,
|
||||
width,
|
||||
height,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
width,
|
||||
height
|
||||
) => {
|
||||
hover,
|
||||
}) => {
|
||||
const right = windowWidth - left - width;
|
||||
const bottom = windowHeight - top - height;
|
||||
const min = Math.min(left, top, right, bottom);
|
||||
switch (min) {
|
||||
case right:
|
||||
edge = "right";
|
||||
left = windowWidth - width;
|
||||
left = hover ? windowWidth - width : windowWidth - width / 2;
|
||||
break;
|
||||
case left:
|
||||
edge = "left";
|
||||
left = 0;
|
||||
left = hover ? 0 : -width / 2;
|
||||
break;
|
||||
case bottom:
|
||||
edge = "bottom";
|
||||
top = windowHeight - height;
|
||||
top = hover ? windowHeight - height : windowHeight - height / 2;
|
||||
break;
|
||||
default:
|
||||
edge = "top";
|
||||
top = 0;
|
||||
top = hover ? 0 : -height / 2;
|
||||
}
|
||||
left = limitNumber(left, 0, windowWidth - width);
|
||||
top = limitNumber(top, 0, windowHeight - height);
|
||||
return { x: left, y: top, edge, hide: false };
|
||||
return { x: left, y: top };
|
||||
};
|
||||
|
||||
const getHidePosition = (
|
||||
{ x: left, y: top, edge },
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
width,
|
||||
height
|
||||
) => {
|
||||
switch (edge) {
|
||||
case "right":
|
||||
left = windowWidth - width / 2;
|
||||
break;
|
||||
case "left":
|
||||
left = -width / 2;
|
||||
break;
|
||||
case "bottom":
|
||||
top = windowHeight - height / 2;
|
||||
break;
|
||||
default:
|
||||
top = -height / 2;
|
||||
function DraggableWrapper({ children, usePaper, ...props }) {
|
||||
if (usePaper) {
|
||||
return (
|
||||
<Paper {...props} elevation={4}>
|
||||
{children}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
return { x: left, y: top, edge, hide: true };
|
||||
};
|
||||
return <div {...props}>{children}</div>;
|
||||
}
|
||||
|
||||
export default function Draggable({
|
||||
windowSize,
|
||||
windowSize: { w: windowWidth, h: windowHeight },
|
||||
width,
|
||||
height,
|
||||
left,
|
||||
@@ -70,66 +56,38 @@ export default function Draggable({
|
||||
onMove,
|
||||
handler,
|
||||
children,
|
||||
usePaper,
|
||||
}) {
|
||||
const [origin, setOrigin] = useState({
|
||||
x: left,
|
||||
y: top,
|
||||
px: left,
|
||||
py: top,
|
||||
});
|
||||
const [position, setPosition] = useState({
|
||||
x: left,
|
||||
y: top,
|
||||
edge: null,
|
||||
hide: false,
|
||||
});
|
||||
const [edgeTimer, setEdgeTimer] = useState(null);
|
||||
|
||||
const goEdge = useCallback((w, h, width, height) => {
|
||||
setPosition((pre) => getEdgePosition(pre, w, h, width, height));
|
||||
|
||||
setEdgeTimer(
|
||||
setTimeout(() => {
|
||||
setPosition((pre) => getHidePosition(pre, w, h, width, height));
|
||||
}, 1500)
|
||||
);
|
||||
}, []);
|
||||
const [hover, setHover] = useState(false);
|
||||
const [origin, setOrigin] = useState(null);
|
||||
const [position, setPosition] = useState({ x: left, y: top });
|
||||
const setFabPosition = useMemo(() => debounce(updateFab, 500), []);
|
||||
|
||||
const handlePointerDown = (e) => {
|
||||
!isMobile && e.target.setPointerCapture(e.pointerId);
|
||||
onStart && onStart();
|
||||
edgeTimer && clearTimeout(edgeTimer);
|
||||
const { x, y } = position;
|
||||
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
||||
setOrigin({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
px: clientX,
|
||||
py: clientY,
|
||||
});
|
||||
setOrigin({ x, y, clientX, clientY });
|
||||
};
|
||||
|
||||
const handlePointerMove = (e) => {
|
||||
onMove && onMove();
|
||||
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
||||
if (origin) {
|
||||
const dx = clientX - origin.px;
|
||||
const dy = clientY - origin.py;
|
||||
const dx = clientX - origin.clientX;
|
||||
const dy = clientY - origin.clientY;
|
||||
let x = origin.x + dx;
|
||||
let y = origin.y + dy;
|
||||
const { w, h } = windowSize;
|
||||
x = limitNumber(x, 0, w - width);
|
||||
y = limitNumber(y, 0, h - height);
|
||||
setPosition({ x, y, edge: null, hide: false });
|
||||
x = limitNumber(x, -width / 2, windowWidth - width / 2);
|
||||
y = limitNumber(y, 0, windowHeight - height / 2);
|
||||
setPosition({ x, y });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointerUp = (e) => {
|
||||
e.stopPropagation();
|
||||
setOrigin(null);
|
||||
if (!snapEdge) {
|
||||
return;
|
||||
}
|
||||
goEdge(windowSize.w, windowSize.h, width, height);
|
||||
};
|
||||
|
||||
const handleClick = (e) => {
|
||||
@@ -138,35 +96,48 @@ export default function Draggable({
|
||||
|
||||
const handleMouseEnter = (e) => {
|
||||
e.stopPropagation();
|
||||
if (snapEdge && position.hide) {
|
||||
edgeTimer && clearTimeout(edgeTimer);
|
||||
goEdge(windowSize.w, windowSize.h, width, height);
|
||||
}
|
||||
setHover(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = (e) => {
|
||||
e.stopPropagation();
|
||||
setHover(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setOrigin(null);
|
||||
if (!snapEdge) {
|
||||
if (!snapEdge || !!origin) {
|
||||
return;
|
||||
}
|
||||
goEdge(windowSize.w, windowSize.h, width, height);
|
||||
}, [snapEdge, goEdge, windowSize.w, windowSize.h, width, height]);
|
||||
|
||||
useEffect(() => {
|
||||
if (position.hide) {
|
||||
setFab({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
setPosition((pre) => {
|
||||
const edgePosition = getEdgePosition({
|
||||
...pre,
|
||||
width,
|
||||
height,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
hover,
|
||||
});
|
||||
}
|
||||
}, [position.x, position.y, position.hide]);
|
||||
setFabPosition(edgePosition);
|
||||
return edgePosition;
|
||||
});
|
||||
}, [
|
||||
origin,
|
||||
hover,
|
||||
width,
|
||||
height,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
snapEdge,
|
||||
setFabPosition,
|
||||
]);
|
||||
|
||||
const opacity = useMemo(() => {
|
||||
if (snapEdge) {
|
||||
return position.hide ? 0.2 : 1;
|
||||
return hover || origin ? 1 : 0.2;
|
||||
}
|
||||
return origin ? 0.8 : 1;
|
||||
}, [origin, snapEdge, position.hide]);
|
||||
}, [origin, snapEdge, hover]);
|
||||
|
||||
const touchProps = isMobile
|
||||
? {
|
||||
@@ -181,7 +152,8 @@ export default function Draggable({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<DraggableWrapper
|
||||
usePaper={usePaper}
|
||||
style={{
|
||||
opacity,
|
||||
position: "fixed",
|
||||
@@ -191,6 +163,7 @@ export default function Draggable({
|
||||
display: show ? "block" : "none",
|
||||
}}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div
|
||||
@@ -202,6 +175,6 @@ export default function Draggable({
|
||||
{handler}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</DraggableWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Fab from "@mui/material/Fab";
|
||||
import TranslateIcon from "@mui/icons-material/Translate";
|
||||
import ThemeProvider from "../../hooks/Theme";
|
||||
@@ -7,9 +6,21 @@ import { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { SettingProvider } from "../../hooks/Setting";
|
||||
import Popup from "../Popup";
|
||||
import { debounce } from "../../libs/utils";
|
||||
import * as shortcut from "@violentmonkey/shortcut";
|
||||
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";
|
||||
|
||||
export default function Action({ translator, fab }) {
|
||||
const fabWidth = 40;
|
||||
@@ -44,34 +55,54 @@ export default function Action({ translator, fab }) {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGm) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册快捷键
|
||||
shortcut.register("a-q", () => {
|
||||
translator.toggle();
|
||||
setShowPopup(false);
|
||||
});
|
||||
shortcut.register("a-c", () => {
|
||||
translator.toggleStyle();
|
||||
setShowPopup(false);
|
||||
});
|
||||
shortcut.register("a-k", () => {
|
||||
setShowPopup((pre) => !pre);
|
||||
});
|
||||
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 () => {
|
||||
shortcut.disable();
|
||||
clearShortcuts.forEach((fn) => {
|
||||
fn();
|
||||
});
|
||||
};
|
||||
}, [translator]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGm) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册菜单
|
||||
const menuCommandIds = [];
|
||||
if (isGm) {
|
||||
try {
|
||||
try {
|
||||
const menuCommandIds = [];
|
||||
const { contextMenuType } = translator.setting;
|
||||
contextMenuType !== 0 &&
|
||||
menuCommandIds.push(
|
||||
GM.registerMenuCommand(
|
||||
"Toggle Translate",
|
||||
(event) => {
|
||||
translator.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
setShowPopup(false);
|
||||
},
|
||||
"Q"
|
||||
@@ -80,6 +111,7 @@ export default function Action({ translator, fab }) {
|
||||
"Toggle Style",
|
||||
(event) => {
|
||||
translator.toggleStyle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
setShowPopup(false);
|
||||
},
|
||||
"C"
|
||||
@@ -90,24 +122,24 @@ export default function Action({ translator, fab }) {
|
||||
setShowPopup((pre) => !pre);
|
||||
},
|
||||
"K"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
"Open Setting",
|
||||
(event) => {
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||
},
|
||||
"O"
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
console.log("[registerMenuCommand]", err);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (isGm) {
|
||||
try {
|
||||
menuCommandIds.forEach((id) => {
|
||||
GM.unregisterMenuCommand(id);
|
||||
});
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
}
|
||||
};
|
||||
return () => {
|
||||
menuCommandIds.forEach((id) => {
|
||||
GM.unregisterMenuCommand(id);
|
||||
});
|
||||
};
|
||||
} catch (err) {
|
||||
console.log("[registerMenuCommand]", err);
|
||||
}
|
||||
}, [translator]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -143,7 +175,7 @@ export default function Action({ translator, fab }) {
|
||||
windowSize,
|
||||
width: fabWidth,
|
||||
height: fabWidth,
|
||||
left: fab.x ?? 0,
|
||||
left: fab.x ?? -fabWidth,
|
||||
top: fab.y ?? windowSize.h / 2,
|
||||
};
|
||||
|
||||
@@ -156,23 +188,23 @@ export default function Action({ translator, fab }) {
|
||||
show={showPopup}
|
||||
onStart={handleStart}
|
||||
onMove={handleMove}
|
||||
usePaper
|
||||
handler={
|
||||
<Paper style={{ cursor: "move" }} elevation={3}>
|
||||
<Box style={{ cursor: "move" }}>
|
||||
<Header setShowPopup={setShowPopup} />
|
||||
</Paper>
|
||||
<Divider />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Paper>
|
||||
{showPopup && (
|
||||
<Popup setShowPopup={setShowPopup} translator={translator} />
|
||||
)}
|
||||
</Paper>
|
||||
{showPopup && (
|
||||
<Popup setShowPopup={setShowPopup} translator={translator} />
|
||||
)}
|
||||
</Draggable>
|
||||
<Draggable
|
||||
key="fab"
|
||||
snapEdge
|
||||
{...fabProps}
|
||||
show={!showPopup}
|
||||
show={fab.isHide ? false : !showPopup}
|
||||
onStart={handleStart}
|
||||
onMove={handleMove}
|
||||
handler={
|
||||
@@ -185,7 +217,12 @@ export default function Action({ translator, fab }) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TranslateIcon />
|
||||
<TranslateIcon
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
}}
|
||||
/>
|
||||
</Fab>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,44 +1,14 @@
|
||||
import { DEFAULT_COLOR } from "../../config";
|
||||
import { loadingSvg } from "../../libs/svg";
|
||||
|
||||
export default function LoadingIcon() {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
<div
|
||||
style={{
|
||||
maxWidth: "1.2em",
|
||||
maxHeight: "1.2em",
|
||||
display: "inline-block",
|
||||
width: "1.2em",
|
||||
height: "1em",
|
||||
}}
|
||||
>
|
||||
<circle fill={DEFAULT_COLOR} stroke="none" cx="6" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 15 ; 0 -15; 0 15"
|
||||
repeatCount="indefinite"
|
||||
begin="0.1"
|
||||
/>
|
||||
</circle>
|
||||
<circle fill={DEFAULT_COLOR} stroke="none" cx="30" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 10 ; 0 -10; 0 10"
|
||||
repeatCount="indefinite"
|
||||
begin="0.2"
|
||||
/>
|
||||
</circle>
|
||||
<circle fill={DEFAULT_COLOR} stroke="none" cx="54" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 5 ; 0 -5; 0 5"
|
||||
repeatCount="indefinite"
|
||||
begin="0.3"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
dangerouslySetInnerHTML={{ __html: loadingSvg }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,15 +7,16 @@ import {
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_FUZZY,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_BLOCKQUOTE,
|
||||
OPT_STYLE_DIY,
|
||||
DEFAULT_COLOR,
|
||||
MSG_TRANS_CURRULE,
|
||||
TRANS_NEWLINE_LENGTH,
|
||||
} from "../../config";
|
||||
import { useTranslate } from "../../hooks/Translate";
|
||||
import styled from "styled-components";
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
const LineSpan = styled.span`
|
||||
const LineSpan = styled("span")`
|
||||
opacity: 0.6;
|
||||
-webkit-opacity: 0.6;
|
||||
text-decoration-line: underline;
|
||||
@@ -34,21 +35,33 @@ const LineSpan = styled.span`
|
||||
}
|
||||
`;
|
||||
|
||||
const FuzzySpan = styled.span`
|
||||
filter: blur(5px);
|
||||
-webkit-filter: blur(5px);
|
||||
const BlockquoteSpan = styled("span")`
|
||||
opacity: 0.6;
|
||||
-webkit-opacity: 0.6;
|
||||
display: block;
|
||||
padding: 0 0.75em;
|
||||
border-left: 0.25em solid ${(props) => props.$lineColor};
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
-webkit-opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const FuzzySpan = styled("span")`
|
||||
filter: blur(0.2em);
|
||||
-webkit-filter: blur(0.2em);
|
||||
&:hover {
|
||||
filter: none;
|
||||
-webkit-filter: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const HighlightSpan = styled.span`
|
||||
const HighlightSpan = styled("span")`
|
||||
color: #fff;
|
||||
background-color: ${(props) => props.$bgColor};
|
||||
`;
|
||||
|
||||
const DiySpan = styled.span`
|
||||
const DiySpan = styled("span")`
|
||||
${(props) => props.$diyStyle}
|
||||
`;
|
||||
|
||||
@@ -86,6 +99,12 @@ function StyledSpan({ textStyle, textDiyStyle, bgColor, children }) {
|
||||
{children}
|
||||
</HighlightSpan>
|
||||
);
|
||||
case OPT_STYLE_BLOCKQUOTE: // 引用
|
||||
return (
|
||||
<BlockquoteSpan $lineColor={bgColor || DEFAULT_COLOR}>
|
||||
{children}
|
||||
</BlockquoteSpan>
|
||||
);
|
||||
case OPT_STYLE_DIY: // 自定义
|
||||
return <DiySpan $diyStyle={textDiyStyle}>{children}</DiySpan>;
|
||||
default:
|
||||
@@ -93,7 +112,7 @@ function StyledSpan({ textStyle, textDiyStyle, bgColor, children }) {
|
||||
}
|
||||
}
|
||||
|
||||
export default function Content({ q, translator }) {
|
||||
export default function Content({ q, keeps, translator }) {
|
||||
const [rule, setRule] = useState(translator.rule);
|
||||
const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
|
||||
const { textStyle, bgColor = "", textDiyStyle = "" } = rule;
|
||||
@@ -120,24 +139,34 @@ export default function Content({ q, translator }) {
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
{q.length > newlineLength ? <br /> : " "}
|
||||
{q.length >= newlineLength ? <br /> : " "}
|
||||
<LoadingIcon />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (text && !sameLang) {
|
||||
return (
|
||||
<>
|
||||
{q.length > newlineLength ? <br /> : " "}
|
||||
<StyledSpan
|
||||
textStyle={textStyle}
|
||||
textDiyStyle={textDiyStyle}
|
||||
bgColor={bgColor}
|
||||
>
|
||||
{text}
|
||||
</StyledSpan>
|
||||
</>
|
||||
);
|
||||
if (!text || sameLang) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{q.length >= newlineLength ? <br /> : " "}
|
||||
<StyledSpan
|
||||
textStyle={textStyle}
|
||||
textDiyStyle={textDiyStyle}
|
||||
bgColor={bgColor}
|
||||
>
|
||||
{keeps.length > 0 ? (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: text.replace(/\[(\d+)\]/g, (_, p) => keeps[parseInt(p)]),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</StyledSpan>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
212
src/views/Options/Apis.js
Normal file
212
src/views/Options/Apis.js
Normal file
@@ -0,0 +1,212 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Button from "@mui/material/Button";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import {
|
||||
OPT_TRANS_ALL,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
URL_KISS_PROXY,
|
||||
} from "../../config";
|
||||
import { useState } from "react";
|
||||
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 Alert from "@mui/material/Alert";
|
||||
import { useAlert } from "../../hooks/Alert";
|
||||
import { useApi } from "../../hooks/Api";
|
||||
import { apiTranslate } from "../../apis";
|
||||
import Box from "@mui/material/Box";
|
||||
import Link from "@mui/material/Link";
|
||||
|
||||
function TestButton({ translator, api }) {
|
||||
const i18n = useI18n();
|
||||
const alert = useAlert();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const handleApiTest = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [text] = await apiTranslate({
|
||||
translator,
|
||||
text: "hello world",
|
||||
fromLang: "en",
|
||||
toLang: "zh-CN",
|
||||
apiSetting: api,
|
||||
useCache: false,
|
||||
});
|
||||
if (!text) {
|
||||
throw new Error("empty reault");
|
||||
}
|
||||
alert.success(i18n("test_success"));
|
||||
} catch (err) {
|
||||
// alert.error(`${i18n("test_failed")}: ${err.message}`);
|
||||
alert.error(
|
||||
<>
|
||||
<div>{`${i18n("test_failed")}: ${err.message}`}</div>
|
||||
<pre
|
||||
style={{
|
||||
maxWidth: 400,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(err.cause || {}, null, 2)}
|
||||
</pre>
|
||||
</>
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <CircularProgress size={16} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button size="small" variant="contained" onClick={handleApiTest}>
|
||||
{i18n("click_test")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function ApiFields({ translator }) {
|
||||
const i18n = useI18n();
|
||||
const { api, updateApi, resetApi } = useApi(translator);
|
||||
const { url = "", key = "", model = "", prompt = "" } = api;
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
updateApi({
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const buildinTranslators = [
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
];
|
||||
|
||||
const mulkeysTranslators = [
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
{!buildinTranslators.includes(translator) && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"URL"}
|
||||
name="url"
|
||||
value={url}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"KEY"}
|
||||
name="key"
|
||||
value={key}
|
||||
onChange={handleChange}
|
||||
multiline={mulkeysTranslators.includes(translator)}
|
||||
helperText={
|
||||
mulkeysTranslators.includes(translator) ? i18n("mulkeys_help") : ""
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(translator === OPT_TRANS_OPENAI || translator === OPT_TRANS_GEMINI) && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"MODEL"}
|
||||
name="model"
|
||||
value={model}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"PROMPT"}
|
||||
name="prompt"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TestButton translator={translator} api={api} />
|
||||
{!buildinTranslators.includes(translator) && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
resetApi();
|
||||
}}
|
||||
>
|
||||
{i18n("restore_default")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{translator === OPT_TRANS_CUSTOMIZE && (
|
||||
<pre>{i18n("custom_api_help")}</pre>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function ApiAccordion({ translator }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setExpanded((pre) => !pre);
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion expanded={expanded} onChange={handleChange}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>{translator}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{expanded && <ApiFields translator={translator} />}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Apis() {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Alert severity="info">
|
||||
<Link href={URL_KISS_PROXY} target="_blank">
|
||||
{i18n("about_api_proxy")}
|
||||
</Link>
|
||||
</Alert>
|
||||
|
||||
<Box>
|
||||
{OPT_TRANS_ALL.map((translator) => (
|
||||
<ApiAccordion key={translator} translator={translator} />
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
27
src/views/Options/DownloadButton.js
Normal file
27
src/views/Options/DownloadButton.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import FileDownloadIcon from "@mui/icons-material/FileDownload";
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
export default function DownloadButton({ data, text, fileName }) {
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
if (data) {
|
||||
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();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleClick}
|
||||
startIcon={<FileDownloadIcon />}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
150
src/views/Options/FavWords.js
Normal file
150
src/views/Options/FavWords.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { OPT_TRANS_BAIDU } from "../../config";
|
||||
import { useEffect, useState } from "react";
|
||||
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 CircularProgress from "@mui/material/CircularProgress";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import { apiTranslate } from "../../apis";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useFavWords } from "../../hooks/FavWords";
|
||||
import DictCont from "../Selection/DictCont";
|
||||
import DownloadButton from "./DownloadButton";
|
||||
import UploadButton from "./UploadButton";
|
||||
import Button from "@mui/material/Button";
|
||||
import ClearAllIcon from "@mui/icons-material/ClearAll";
|
||||
import { isValidWord } from "../../libs/utils";
|
||||
|
||||
function DictField({ word }) {
|
||||
const [dictResult, setDictResult] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
const dictRes = await apiTranslate({
|
||||
text: word,
|
||||
translator: OPT_TRANS_BAIDU,
|
||||
fromLang: "en",
|
||||
toLang: "zh-CN",
|
||||
});
|
||||
dictRes[2].type === 1 && setDictResult(JSON.parse(dictRes[2].result));
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [word]);
|
||||
|
||||
if (loading) {
|
||||
return <CircularProgress size={24} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Alert severity="error">{error}</Alert>;
|
||||
}
|
||||
|
||||
return <DictCont dictResult={dictResult} />;
|
||||
}
|
||||
|
||||
function FavAccordion({ word, index }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setExpanded((pre) => !pre);
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion expanded={expanded} onChange={handleChange}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
{/* <Typography>{`[${new Date(
|
||||
createdAt
|
||||
).toLocaleString()}] ${word}`}</Typography> */}
|
||||
<Typography>{`${index + 1}. ${word}`}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{expanded && <DictField word={word} />}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FavWords() {
|
||||
const i18n = useI18n();
|
||||
const { loading, favWords, mergeWords, clearWords } = useFavWords();
|
||||
const favList = Object.entries(favWords).sort((a, b) =>
|
||||
a[0].localeCompare(b[0])
|
||||
);
|
||||
const downloadList = favList.map(([word]) => word);
|
||||
|
||||
const handleImport = async (data) => {
|
||||
try {
|
||||
const newWords = data
|
||||
.split("\n")
|
||||
.map((line) => line.split(",")[0].trim())
|
||||
.filter(isValidWord);
|
||||
await mergeWords(newWords);
|
||||
} catch (err) {
|
||||
console.log("[import rules]", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
useFlexGap
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<UploadButton
|
||||
text={i18n("import")}
|
||||
handleImport={handleImport}
|
||||
fileType="text"
|
||||
fileExts={[".txt", ".csv"]}
|
||||
/>
|
||||
<DownloadButton
|
||||
data={downloadList.join("\n")}
|
||||
text={i18n("export")}
|
||||
fileName={`kiss-words_${Date.now()}.txt`}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
clearWords();
|
||||
}}
|
||||
startIcon={<ClearAllIcon />}
|
||||
>
|
||||
{i18n("clear_all")}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
{loading ? (
|
||||
<CircularProgress size={24} />
|
||||
) : (
|
||||
favList.map(([word, { createdAt }], index) => (
|
||||
<FavAccordion
|
||||
key={word}
|
||||
index={index}
|
||||
word={word}
|
||||
createdAt={createdAt}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import PropTypes from "prop-types";
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
@@ -7,6 +6,7 @@ import Box from "@mui/material/Box";
|
||||
import Link from "@mui/material/Link";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import DarkModeButton from "./DarkModeButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
function Header(props) {
|
||||
const i18n = useI18n();
|
||||
@@ -31,22 +31,18 @@ function Header(props) {
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography component="div" sx={{ flexGrow: 1, fontWeight: "bold" }}>
|
||||
<Link
|
||||
underline="none"
|
||||
color="inherit"
|
||||
href={process.env.REACT_APP_HOMEPAGE}
|
||||
target="_blank"
|
||||
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
|
||||
</Box>
|
||||
</Typography>
|
||||
<DarkModeButton />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
onDrawerToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
||||
19
src/views/Options/HelpButton.js
Normal file
19
src/views/Options/HelpButton.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import Button from "@mui/material/Button";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import HelpIcon from "@mui/icons-material/Help";
|
||||
|
||||
export default function HelpButton({ url }) {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
window.open(url, "_blank");
|
||||
}}
|
||||
startIcon={<HelpIcon />}
|
||||
>
|
||||
{i18n("help")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
178
src/views/Options/InputSetting.js
Normal file
178
src/views/Options/InputSetting.js
Normal file
@@ -0,0 +1,178 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import {
|
||||
OPT_TRANS_ALL,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_INPUT_TRANS_SIGNS,
|
||||
} from "../../config";
|
||||
import ShortcutInput from "./ShortcutInput";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import { useInputRule } from "../../hooks/InputRule";
|
||||
import { useCallback } from "react";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { limitNumber } from "../../libs/utils";
|
||||
|
||||
export default function InputSetting() {
|
||||
const i18n = useI18n();
|
||||
const { inputRule, updateInputRule } = useInputRule();
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
let { name, value } = e.target;
|
||||
switch (name) {
|
||||
case "triggerTime":
|
||||
value = limitNumber(value, 10, 1000);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
updateInputRule({
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleShortcutInput = useCallback(
|
||||
(val) => {
|
||||
updateInputRule({ triggerShortcut: val });
|
||||
},
|
||||
[updateInputRule]
|
||||
);
|
||||
|
||||
const {
|
||||
transOpen,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
triggerShortcut,
|
||||
triggerCount,
|
||||
triggerTime,
|
||||
transSign,
|
||||
} = inputRule;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="transOpen"
|
||||
checked={transOpen}
|
||||
onChange={() => {
|
||||
updateInputRule({ transOpen: !transOpen });
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={i18n("use_input_box_translation")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="translator"
|
||||
value={translator}
|
||||
label={i18n("translate_service")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_TRANS_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="fromLang"
|
||||
value={fromLang}
|
||||
label={i18n("from_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="toLang"
|
||||
value={toLang}
|
||||
label={i18n("to_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="transSign"
|
||||
value={transSign}
|
||||
label={i18n("input_trans_start_sign")}
|
||||
onChange={handleChange}
|
||||
helperText={i18n("input_trans_start_sign_help")}
|
||||
>
|
||||
<MenuItem value={""}>{i18n("style_none")}</MenuItem>
|
||||
{OPT_INPUT_TRANS_SIGNS.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={12}>
|
||||
<Grid item xs={12} sm={12} md={4} lg={4}>
|
||||
<ShortcutInput
|
||||
value={triggerShortcut}
|
||||
onChange={handleShortcutInput}
|
||||
label={i18n("trigger_trans_shortcut")}
|
||||
helperText={i18n("trigger_trans_shortcut_help")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={4} lg={4}>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
size="small"
|
||||
name="triggerCount"
|
||||
value={triggerCount}
|
||||
label={i18n("shortcut_press_count")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((val) => (
|
||||
<MenuItem key={val} value={val}>
|
||||
{val}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={4} lg={4}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label={i18n("combo_timeout")}
|
||||
type="number"
|
||||
name="triggerTime"
|
||||
defaultValue={triggerTime}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,11 @@ import InfoIcon from "@mui/icons-material/Info";
|
||||
import DesignServicesIcon from "@mui/icons-material/DesignServices";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import ApiIcon from "@mui/icons-material/Api";
|
||||
import SendTimeExtensionIcon from "@mui/icons-material/SendTimeExtension";
|
||||
import InputIcon from "@mui/icons-material/Input";
|
||||
import SelectAllIcon from "@mui/icons-material/SelectAll";
|
||||
import EventNoteIcon from "@mui/icons-material/EventNote";
|
||||
|
||||
function LinkItem({ label, url, icon }) {
|
||||
const match = useMatch(url);
|
||||
@@ -36,12 +41,42 @@ export default function Navigator(props) {
|
||||
url: "/rules",
|
||||
icon: <DesignServicesIcon />,
|
||||
},
|
||||
{
|
||||
id: "input_translate",
|
||||
label: i18n("input_translate"),
|
||||
url: "/input",
|
||||
icon: <InputIcon />,
|
||||
},
|
||||
{
|
||||
id: "selection_translate",
|
||||
label: i18n("selection_translate"),
|
||||
url: "/tranbox",
|
||||
icon: <SelectAllIcon />,
|
||||
},
|
||||
{
|
||||
id: "apis_setting",
|
||||
label: i18n("apis_setting"),
|
||||
url: "/apis",
|
||||
icon: <ApiIcon />,
|
||||
},
|
||||
{
|
||||
id: "sync",
|
||||
label: i18n("sync_setting"),
|
||||
url: "/sync",
|
||||
icon: <SyncIcon />,
|
||||
},
|
||||
{
|
||||
id: "webfix",
|
||||
label: i18n("patch_setting"),
|
||||
url: "/webfix",
|
||||
icon: <SendTimeExtensionIcon />,
|
||||
},
|
||||
{
|
||||
id: "words",
|
||||
label: i18n("favorite_words"),
|
||||
url: "/words",
|
||||
icon: <EventNoteIcon />,
|
||||
},
|
||||
{ id: "about", label: i18n("about"), url: "/about", icon: <InfoIcon /> },
|
||||
];
|
||||
return (
|
||||
|
||||
@@ -14,8 +14,9 @@ import {
|
||||
OPT_STYLE_DIY,
|
||||
OPT_STYLE_USE_COLOR,
|
||||
URL_KISS_RULES_NEW_ISSUE,
|
||||
OPT_SYNCTYPE_WORKER,
|
||||
} from "../../config";
|
||||
import { useState, useRef, useEffect, useMemo } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
@@ -25,8 +26,6 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import { useRules } from "../../hooks/Rules";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import FileDownloadIcon from "@mui/icons-material/FileDownload";
|
||||
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
||||
import { useSetting } from "../../hooks/Setting";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Switch from "@mui/material/Switch";
|
||||
@@ -36,7 +35,6 @@ import Radio from "@mui/material/Radio";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import HelpIcon from "@mui/icons-material/Help";
|
||||
import ShareIcon from "@mui/icons-material/Share";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import { useSubRules } from "../../hooks/SubRules";
|
||||
@@ -47,6 +45,11 @@ import { syncShareRules } from "../../libs/sync";
|
||||
import { debounce } from "../../libs/utils";
|
||||
import { delSubRules, getSyncWithDefault } from "../../libs/storage";
|
||||
import OwSubRule from "./OwSubRule";
|
||||
import ClearAllIcon from "@mui/icons-material/ClearAll";
|
||||
import HelpButton from "./HelpButton";
|
||||
import { useSyncCaches } from "../../hooks/Sync";
|
||||
import DownloadButton from "./DownloadButton";
|
||||
import UploadButton from "./UploadButton";
|
||||
|
||||
function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
const initFormValues = rule || {
|
||||
@@ -62,6 +65,8 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
const {
|
||||
pattern,
|
||||
selector,
|
||||
keepSelector = "",
|
||||
terms = "",
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
@@ -176,6 +181,26 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
onFocus={handleFocus}
|
||||
multiline
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("keep_selector")}
|
||||
helperText={i18n("keep_selector_helper")}
|
||||
name="keepSelector"
|
||||
value={keepSelector}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("terms")}
|
||||
helperText={i18n("terms_helper")}
|
||||
name="terms"
|
||||
value={terms}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={12}>
|
||||
@@ -361,6 +386,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
}
|
||||
|
||||
function RuleAccordion({ rule, rules }) {
|
||||
const i18n = useI18n();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const handleChange = (e) => {
|
||||
@@ -371,11 +397,14 @@ function RuleAccordion({ rule, rules }) {
|
||||
<Accordion expanded={expanded} onChange={handleChange}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography
|
||||
style={{
|
||||
sx={{
|
||||
opacity: rules ? 1 : 0.5,
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
>
|
||||
{rule.pattern}
|
||||
{rule.pattern === GLOBAL_KEY
|
||||
? `[${i18n("global_rule")}] ${rule.pattern}`
|
||||
: rule.pattern}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
@@ -385,63 +414,13 @@ function RuleAccordion({ rule, rules }) {
|
||||
);
|
||||
}
|
||||
|
||||
function DownloadButton({ data, text, fileName }) {
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
if (data) {
|
||||
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();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleClick}
|
||||
startIcon={<FileDownloadIcon />}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function UploadButton({ onChange, text }) {
|
||||
const inputRef = useRef(null);
|
||||
const handleClick = () => {
|
||||
inputRef.current && inputRef.current.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleClick}
|
||||
startIcon={<FileUploadIcon />}
|
||||
>
|
||||
{text}
|
||||
<input
|
||||
type="file"
|
||||
accept=".json"
|
||||
ref={inputRef}
|
||||
onChange={onChange}
|
||||
hidden
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function ShareButton({ rules, injectRules, selectedUrl }) {
|
||||
const alert = useAlert();
|
||||
const i18n = useI18n();
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
const { syncUrl, syncKey } = await getSyncWithDefault();
|
||||
if (!syncUrl || !syncKey) {
|
||||
const { syncType, syncUrl, syncKey } = await getSyncWithDefault();
|
||||
if (syncType !== OPT_SYNCTYPE_WORKER || !syncUrl || !syncKey) {
|
||||
alert.warning(i18n("error_sync_setting"));
|
||||
return;
|
||||
}
|
||||
@@ -477,22 +456,6 @@ function ShareButton({ rules, injectRules, selectedUrl }) {
|
||||
);
|
||||
}
|
||||
|
||||
function HelpButton() {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
window.open(URL_KISS_RULES_NEW_ISSUE, "_blank");
|
||||
}}
|
||||
startIcon={<HelpIcon />}
|
||||
>
|
||||
{i18n("help")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function UserRules({ subRules }) {
|
||||
const i18n = useI18n();
|
||||
const rules = useRules();
|
||||
@@ -503,26 +466,12 @@ function UserRules({ subRules }) {
|
||||
const injectRules = !!setting?.injectRules;
|
||||
const { selectedUrl, selectedRules } = subRules;
|
||||
|
||||
const handleImport = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
const handleImport = async (data) => {
|
||||
try {
|
||||
await rules.merge(JSON.parse(data));
|
||||
} catch (err) {
|
||||
console.log("[import rules]", err);
|
||||
}
|
||||
|
||||
if (!file.type.includes("json")) {
|
||||
alert(i18n("error_wrong_file_type"));
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
await rules.merge(JSON.parse(e.target.result));
|
||||
} catch (err) {
|
||||
console.log("[import rules]", err);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
const handleInject = () => {
|
||||
@@ -537,6 +486,10 @@ function UserRules({ subRules }) {
|
||||
}
|
||||
}, [showAdd]);
|
||||
|
||||
if (!rules.list) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<Stack
|
||||
@@ -558,10 +511,11 @@ function UserRules({ subRules }) {
|
||||
{i18n("add")}
|
||||
</Button>
|
||||
|
||||
<UploadButton text={i18n("import")} onChange={handleImport} />
|
||||
<UploadButton text={i18n("import")} handleImport={handleImport} />
|
||||
<DownloadButton
|
||||
data={JSON.stringify([...rules.list].reverse(), null, "\t")}
|
||||
data={JSON.stringify([...rules.list].reverse(), null, 2)}
|
||||
text={i18n("export")}
|
||||
fileName={`kiss-rules_${Date.now()}.json`}
|
||||
/>
|
||||
|
||||
<ShareButton
|
||||
@@ -570,7 +524,18 @@ function UserRules({ subRules }) {
|
||||
selectedUrl={selectedUrl}
|
||||
/>
|
||||
|
||||
<HelpButton />
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
rules.clear();
|
||||
}}
|
||||
startIcon={<ClearAllIcon />}
|
||||
>
|
||||
{i18n("clear_all")}
|
||||
</Button>
|
||||
|
||||
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -619,13 +584,23 @@ function UserRules({ subRules }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
|
||||
function SubRulesItem({
|
||||
index,
|
||||
url,
|
||||
syncAt,
|
||||
selectedUrl,
|
||||
delSub,
|
||||
setSelectedRules,
|
||||
updateDataCache,
|
||||
deleteDataCache,
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleDel = async () => {
|
||||
try {
|
||||
await delSub(url);
|
||||
await delSubRules(url);
|
||||
await deleteDataCache(url);
|
||||
} catch (err) {
|
||||
console.log("[del subrules]", err);
|
||||
}
|
||||
@@ -638,6 +613,7 @@ function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
|
||||
if (rules.length > 0 && url === selectedUrl) {
|
||||
setSelectedRules(rules);
|
||||
}
|
||||
await updateDataCache(url);
|
||||
} catch (err) {
|
||||
console.log("[sync sub rules]", err);
|
||||
} finally {
|
||||
@@ -647,7 +623,20 @@ function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
|
||||
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<FormControlLabel value={url} control={<Radio />} label={url} />
|
||||
<FormControlLabel
|
||||
value={url}
|
||||
control={<Radio />}
|
||||
sx={{
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
label={url}
|
||||
/>
|
||||
|
||||
{syncAt && (
|
||||
<span style={{ marginLeft: "0.5em", opacity: 0.5 }}>
|
||||
[{new Date(syncAt).toLocaleString()}]
|
||||
</span>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<CircularProgress size={16} />
|
||||
@@ -666,7 +655,7 @@ function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SubRulesEdit({ subList, addSub }) {
|
||||
function SubRulesEdit({ subList, addSub, updateDataCache }) {
|
||||
const i18n = useI18n();
|
||||
const [inputText, setInputText] = useState("");
|
||||
const [inputError, setInputError] = useState("");
|
||||
@@ -701,6 +690,7 @@ function SubRulesEdit({ subList, addSub }) {
|
||||
throw new Error("empty rules");
|
||||
}
|
||||
await addSub(url);
|
||||
await updateDataCache(url);
|
||||
setShowInput(false);
|
||||
setInputText("");
|
||||
} catch (err) {
|
||||
@@ -735,7 +725,7 @@ function SubRulesEdit({ subList, addSub }) {
|
||||
>
|
||||
{i18n("add")}
|
||||
</Button>
|
||||
<HelpButton />
|
||||
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
|
||||
</Stack>
|
||||
|
||||
{showInput && (
|
||||
@@ -780,25 +770,38 @@ function SubRules({ subRules }) {
|
||||
setSelectedRules,
|
||||
loading,
|
||||
} = subRules;
|
||||
const { dataCaches, updateDataCache, deleteDataCache, reloadSync } =
|
||||
useSyncCaches();
|
||||
|
||||
const handleSelect = (e) => {
|
||||
const url = e.target.value;
|
||||
selectSub(url);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reloadSync();
|
||||
}, [selectedRules, reloadSync]);
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<SubRulesEdit subList={subList} addSub={addSub} />
|
||||
<SubRulesEdit
|
||||
subList={subList}
|
||||
addSub={addSub}
|
||||
updateDataCache={updateDataCache}
|
||||
/>
|
||||
|
||||
<RadioGroup value={selectedUrl} onChange={handleSelect}>
|
||||
{subList.map((item, index) => (
|
||||
<SubRulesItem
|
||||
key={item.url}
|
||||
url={item.url}
|
||||
syncAt={dataCaches[item.url]}
|
||||
index={index}
|
||||
selectedUrl={selectedUrl}
|
||||
delSub={delSub}
|
||||
setSelectedRules={setSelectedRules}
|
||||
updateDataCache={updateDataCache}
|
||||
deleteDataCache={deleteDataCache}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
|
||||
@@ -10,53 +10,32 @@ import FormHelperText from "@mui/material/FormHelperText";
|
||||
import { useSetting } from "../../hooks/Setting";
|
||||
import { limitNumber } from "../../libs/utils";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import { apiTranslate } from "../../apis";
|
||||
import { useAlert } from "../../hooks/Alert";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { isExt } from "../../libs/client";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import {
|
||||
UI_LANGS,
|
||||
URL_KISS_PROXY,
|
||||
TRANS_NEWLINE_LENGTH,
|
||||
CACHE_NAME,
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_MOUSEKEY_ALL,
|
||||
OPT_MOUSEKEY_DISABLE,
|
||||
OPT_SHORTCUT_TRANSLATE,
|
||||
OPT_SHORTCUT_STYLE,
|
||||
OPT_SHORTCUT_POPUP,
|
||||
OPT_SHORTCUT_SETTING,
|
||||
OPT_LANGS_TO,
|
||||
DEFAULT_BLACKLIST,
|
||||
MSG_CONTEXT_MENUS,
|
||||
} from "../../config";
|
||||
import { useState } from "react";
|
||||
|
||||
function TestLink({ translator, setting }) {
|
||||
const i18n = useI18n();
|
||||
const alert = useAlert();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const handleApiTest = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [text] = await apiTranslate({
|
||||
translator,
|
||||
q: "hello world",
|
||||
fromLang: "en",
|
||||
toLang: "zh-CN",
|
||||
setting,
|
||||
});
|
||||
if (!text) {
|
||||
throw new Error("empty reault");
|
||||
}
|
||||
alert.success(i18n("test_success"));
|
||||
} catch (err) {
|
||||
alert.error(`${i18n("test_failed")}: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <CircularProgress sx={{ marginLeft: "2em" }} size={12} />;
|
||||
}
|
||||
import { useShortcut } from "../../hooks/Shortcut";
|
||||
import ShortcutInput from "./ShortcutInput";
|
||||
import { useFab } from "../../hooks/Fab";
|
||||
import { sendBgMsg } from "../../libs/msg";
|
||||
|
||||
function ShortcutItem({ action, label }) {
|
||||
const { shortcut, setShortcut } = useShortcut(action);
|
||||
return (
|
||||
<Link sx={{ marginLeft: "1em" }} component="button" onClick={handleApiTest}>
|
||||
{i18n("click_test")}
|
||||
</Link>
|
||||
<ShortcutInput value={shortcut} onChange={setShortcut} label={label} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,6 +43,7 @@ export default function Settings() {
|
||||
const i18n = useI18n();
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const alert = useAlert();
|
||||
const { fab, updateFab } = useFab();
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
@@ -84,6 +64,12 @@ export default function Settings() {
|
||||
case "newlineLength":
|
||||
value = limitNumber(value, 1, 1000);
|
||||
break;
|
||||
case "touchTranslate":
|
||||
value = limitNumber(value, 0, 4);
|
||||
break;
|
||||
case "contextMenuType":
|
||||
isExt && sendBgMsg(MSG_CONTEXT_MENUS, { contextMenuType: value });
|
||||
break;
|
||||
default:
|
||||
}
|
||||
updateSetting({
|
||||
@@ -102,20 +88,21 @@ export default function Settings() {
|
||||
|
||||
const {
|
||||
uiLang,
|
||||
googleUrl,
|
||||
fetchLimit,
|
||||
fetchInterval,
|
||||
minLength,
|
||||
maxLength,
|
||||
openaiUrl,
|
||||
deeplUrl = "",
|
||||
deeplKey = "",
|
||||
openaiKey,
|
||||
openaiModel,
|
||||
openaiPrompt,
|
||||
clearCache,
|
||||
newlineLength = TRANS_NEWLINE_LENGTH,
|
||||
mouseKey = OPT_MOUSEKEY_DISABLE,
|
||||
detectRemote = false,
|
||||
contextMenuType = 1,
|
||||
transTitle = false,
|
||||
touchTranslate = 2,
|
||||
blacklist = DEFAULT_BLACKLIST.join(",\n"),
|
||||
disableLangs = [],
|
||||
} = setting;
|
||||
const { isHide = false } = fab || {};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
@@ -141,7 +128,7 @@ export default function Settings() {
|
||||
label={i18n("fetch_limit")}
|
||||
type="number"
|
||||
name="fetchLimit"
|
||||
value={fetchLimit}
|
||||
defaultValue={fetchLimit}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -150,7 +137,7 @@ export default function Settings() {
|
||||
label={i18n("fetch_interval")}
|
||||
type="number"
|
||||
name="fetchInterval"
|
||||
value={fetchInterval}
|
||||
defaultValue={fetchInterval}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -159,7 +146,7 @@ export default function Settings() {
|
||||
label={i18n("min_translate_length")}
|
||||
type="number"
|
||||
name="minLength"
|
||||
value={minLength}
|
||||
defaultValue={minLength}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -168,7 +155,7 @@ export default function Settings() {
|
||||
label={i18n("max_translate_length")}
|
||||
type="number"
|
||||
name="maxLength"
|
||||
value={maxLength}
|
||||
defaultValue={maxLength}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -177,113 +164,175 @@ export default function Settings() {
|
||||
label={i18n("num_of_newline_characters")}
|
||||
type="number"
|
||||
name="newlineLength"
|
||||
value={newlineLength}
|
||||
defaultValue={newlineLength}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("if_clear_cache")}</InputLabel>
|
||||
<InputLabel>{i18n("translate_timing")}</InputLabel>
|
||||
<Select
|
||||
name="clearCache"
|
||||
value={clearCache}
|
||||
label={i18n("if_clear_cache")}
|
||||
name="mouseKey"
|
||||
value={mouseKey}
|
||||
label={i18n("translate_timing")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
|
||||
{OPT_MOUSEKEY_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
<Link component="button" onClick={handleClearCache}>
|
||||
{i18n("clear_all_cache_now")}
|
||||
</Link>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={
|
||||
<>
|
||||
{i18n("google_api")}
|
||||
{googleUrl && (
|
||||
<TestLink translator={OPT_TRANS_GOOGLE} setting={setting} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
name="googleUrl"
|
||||
value={googleUrl}
|
||||
onChange={handleChange}
|
||||
helperText={
|
||||
<Link href={URL_KISS_PROXY} target="_blank">
|
||||
{i18n("about_api_proxy")}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("translate_page_title")}</InputLabel>
|
||||
<Select
|
||||
name="transTitle"
|
||||
value={transTitle}
|
||||
label={i18n("translate_page_title")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("touch_translate_shortcut")}</InputLabel>
|
||||
<Select
|
||||
name="touchTranslate"
|
||||
value={touchTranslate}
|
||||
label={i18n("touch_translate_shortcut")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{[0, 2, 3, 4].map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(`touch_tap_${item}`)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("hide_fab_button")}</InputLabel>
|
||||
<Select
|
||||
name="isHide"
|
||||
value={isHide}
|
||||
label={i18n("hide_fab_button")}
|
||||
onChange={(e) => {
|
||||
updateFab({ isHide: e.target.value });
|
||||
}}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("show")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("hide")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("context_menus")}</InputLabel>
|
||||
<Select
|
||||
name="contextMenuType"
|
||||
value={contextMenuType}
|
||||
label={i18n("context_menus")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={0}>{i18n("hide_context_menus")}</MenuItem>
|
||||
<MenuItem value={1}>{i18n("simple_context_menus")}</MenuItem>
|
||||
<MenuItem value={2}>{i18n("secondary_context_menus")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("detect_lang_remote")}</InputLabel>
|
||||
<Select
|
||||
name="detectRemote"
|
||||
value={detectRemote}
|
||||
label={i18n("detect_lang_remote")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
||||
</Select>
|
||||
<FormHelperText>{i18n("detect_lang_remote_help")}</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("disable_langs")}</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
name="disableLangs"
|
||||
value={disableLangs}
|
||||
label={i18n("disable_langs")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_TO.map(([langKey, langName]) => (
|
||||
<MenuItem key={langKey} value={langKey}>
|
||||
{langName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>{i18n("disable_langs_helper")}</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{isExt ? (
|
||||
<>
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("if_clear_cache")}</InputLabel>
|
||||
<Select
|
||||
name="clearCache"
|
||||
value={clearCache}
|
||||
label={i18n("if_clear_cache")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
<Link component="button" onClick={handleClearCache}>
|
||||
{i18n("clear_all_cache_now")}
|
||||
</Link>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={12}>
|
||||
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||
<ShortcutItem
|
||||
action={OPT_SHORTCUT_TRANSLATE}
|
||||
label={i18n("toggle_translate_shortcut")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||
<ShortcutItem
|
||||
action={OPT_SHORTCUT_STYLE}
|
||||
label={i18n("toggle_style_shortcut")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||
<ShortcutItem
|
||||
action={OPT_SHORTCUT_POPUP}
|
||||
label={i18n("toggle_popup_shortcut")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||
<ShortcutItem
|
||||
action={OPT_SHORTCUT_SETTING}
|
||||
label={i18n("open_setting_shortcut")}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={
|
||||
<>
|
||||
{i18n("deepl_api")}
|
||||
{deeplUrl && (
|
||||
<TestLink translator={OPT_TRANS_DEEPL} setting={setting} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
name="deeplUrl"
|
||||
value={deeplUrl}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("deepl_key")}
|
||||
name="deeplKey"
|
||||
value={deeplKey}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={
|
||||
<>
|
||||
{i18n("openai_api")}
|
||||
{openaiUrl && openaiPrompt && (
|
||||
<TestLink translator={OPT_TRANS_OPENAI} setting={setting} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
name="openaiUrl"
|
||||
value={openaiUrl}
|
||||
onChange={handleChange}
|
||||
helperText={
|
||||
<Link href={URL_KISS_PROXY} target="_blank">
|
||||
{i18n("about_api_proxy")}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
type="password"
|
||||
label={i18n("openai_key")}
|
||||
name="openaiKey"
|
||||
value={openaiKey}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("openai_model")}
|
||||
name="openaiModel"
|
||||
value={openaiModel}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("openai_prompt")}
|
||||
name="openaiPrompt"
|
||||
value={openaiPrompt}
|
||||
label={i18n("translate_blacklist")}
|
||||
helperText={i18n("pattern_helper")}
|
||||
name="blacklist"
|
||||
defaultValue={blacklist}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
/>
|
||||
|
||||
56
src/views/Options/ShortcutInput.js
Normal file
56
src/views/Options/ShortcutInput.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { shortcutListener } from "../../libs/shortcut";
|
||||
|
||||
export default function ShortcutInput({ value, onChange, label, helperText }) {
|
||||
const [disabled, setDisabled] = useState(true);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
inputRef.current.focus();
|
||||
onChange([]);
|
||||
|
||||
const clearShortcut = shortcutListener((curkeys, allkeys) => {
|
||||
onChange(allkeys);
|
||||
if (curkeys.length === 0) {
|
||||
setDisabled(true);
|
||||
}
|
||||
}, inputRef.current);
|
||||
|
||||
return () => {
|
||||
clearShortcut();
|
||||
};
|
||||
}, [disabled, onChange]);
|
||||
|
||||
return (
|
||||
<Stack direction="row" alignItems="flex-start">
|
||||
<TextField
|
||||
size="small"
|
||||
label={label}
|
||||
name={label}
|
||||
value={value.map((item) => (item === " " ? "Space" : item)).join(" + ")}
|
||||
fullWidth
|
||||
inputRef={inputRef}
|
||||
disabled={disabled}
|
||||
onBlur={() => {
|
||||
setDisabled(true);
|
||||
}}
|
||||
helperText={helperText}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setDisabled(false);
|
||||
}}
|
||||
>
|
||||
{<EditIcon />}
|
||||
</IconButton>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,13 @@ import { useI18n } from "../../hooks/I18n";
|
||||
import { useSync } from "../../hooks/Sync";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Link from "@mui/material/Link";
|
||||
import { URL_KISS_WORKER } from "../../config";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import {
|
||||
URL_KISS_WORKER,
|
||||
OPT_SYNCTYPE_ALL,
|
||||
OPT_SYNCTYPE_WORKER,
|
||||
OPT_SYNCTYPE_WEBDAV,
|
||||
} from "../../config";
|
||||
import { useState } from "react";
|
||||
import { syncSettingAndRules } from "../../libs/sync";
|
||||
import Button from "@mui/material/Button";
|
||||
@@ -44,13 +50,37 @@ export default function SyncSetting() {
|
||||
}
|
||||
};
|
||||
|
||||
const { syncUrl, syncKey } = sync;
|
||||
if (!sync) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
syncType = OPT_SYNCTYPE_WORKER,
|
||||
syncUrl = "",
|
||||
syncUser = "",
|
||||
syncKey = "",
|
||||
} = sync;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Alert severity="warning">{i18n("sync_warn")}</Alert>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="syncType"
|
||||
value={syncType}
|
||||
label={i18n("data_sync_type")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_SYNCTYPE_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("data_sync_url")}
|
||||
@@ -58,12 +88,24 @@ export default function SyncSetting() {
|
||||
value={syncUrl}
|
||||
onChange={handleChange}
|
||||
helperText={
|
||||
<Link href={URL_KISS_WORKER} target="_blank">
|
||||
{i18n("about_sync_api")}
|
||||
</Link>
|
||||
syncType === OPT_SYNCTYPE_WORKER && (
|
||||
<Link href={URL_KISS_WORKER} target="_blank">
|
||||
{i18n("about_sync_api")}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{syncType === OPT_SYNCTYPE_WEBDAV && (
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("data_sync_user")}
|
||||
name="syncUser"
|
||||
value={syncUser}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
type="password"
|
||||
@@ -87,7 +129,7 @@ export default function SyncSetting() {
|
||||
onClick={handleSyncTest}
|
||||
startIcon={<SyncIcon />}
|
||||
>
|
||||
{i18n("data_sync_test")}
|
||||
{i18n("sync_now")}
|
||||
</Button>
|
||||
{loading && <CircularProgress size={16} />}
|
||||
</Stack>
|
||||
|
||||
173
src/views/Options/Tranbox.js
Normal file
173
src/views/Options/Tranbox.js
Normal file
@@ -0,0 +1,173 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import { OPT_TRANS_ALL, OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
|
||||
import ShortcutInput from "./ShortcutInput";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import { useCallback } from "react";
|
||||
import { limitNumber } from "../../libs/utils";
|
||||
import { useTranbox } from "../../hooks/Tranbox";
|
||||
import { isExt } from "../../libs/client";
|
||||
|
||||
export default function Tranbox() {
|
||||
const i18n = useI18n();
|
||||
const { tranboxSetting, updateTranbox } = useTranbox();
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
let { name, value } = e.target;
|
||||
switch (name) {
|
||||
case "btnOffsetX":
|
||||
value = limitNumber(value, 0, 100);
|
||||
break;
|
||||
case "btnOffsetY":
|
||||
value = limitNumber(value, 0, 100);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
updateTranbox({
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleShortcutInput = useCallback(
|
||||
(val) => {
|
||||
updateTranbox({ tranboxShortcut: val });
|
||||
},
|
||||
[updateTranbox]
|
||||
);
|
||||
|
||||
const {
|
||||
transOpen,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
toLang2 = "en",
|
||||
tranboxShortcut,
|
||||
btnOffsetX,
|
||||
btnOffsetY,
|
||||
hideTranBtn = false,
|
||||
} = tranboxSetting;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="transOpen"
|
||||
checked={transOpen}
|
||||
onChange={() => {
|
||||
updateTranbox({ transOpen: !transOpen });
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={i18n("toggle_selection_translate")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="translator"
|
||||
value={translator}
|
||||
label={i18n("translate_service")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_TRANS_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="fromLang"
|
||||
value={fromLang}
|
||||
label={i18n("from_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="toLang"
|
||||
value={toLang}
|
||||
label={i18n("to_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="toLang2"
|
||||
value={toLang2}
|
||||
label={i18n("to_lang2")}
|
||||
helperText={i18n("to_lang2_helper")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{[["none", "None"], ...OPT_LANGS_TO].map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("tranbtn_offset_x")}
|
||||
type="number"
|
||||
name="btnOffsetX"
|
||||
defaultValue={btnOffsetX}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("tranbtn_offset_y")}
|
||||
type="number"
|
||||
name="btnOffsetY"
|
||||
defaultValue={btnOffsetY}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="hideTranBtn"
|
||||
value={hideTranBtn}
|
||||
label={i18n("hide_tran_button")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("show")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("hide")}</MenuItem>
|
||||
</TextField>
|
||||
|
||||
{!isExt && (
|
||||
<ShortcutInput
|
||||
value={tranboxShortcut}
|
||||
onChange={handleShortcutInput}
|
||||
label={i18n("trigger_tranbox_shortcut")}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
55
src/views/Options/UploadButton.js
Normal file
55
src/views/Options/UploadButton.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useRef } from "react";
|
||||
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
export default function UploadButton({
|
||||
handleImport,
|
||||
text,
|
||||
fileType = "json",
|
||||
fileExts = [".json"],
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const inputRef = useRef(null);
|
||||
const handleClick = () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.click();
|
||||
inputRef.current.value = null;
|
||||
}
|
||||
};
|
||||
const onChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.type.includes(fileType)) {
|
||||
alert(i18n("error_wrong_file_type"));
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
handleImport(e.target.result);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleClick}
|
||||
startIcon={<FileUploadIcon />}
|
||||
>
|
||||
{text}
|
||||
<input
|
||||
type="file"
|
||||
accept={fileExts.join(", ")}
|
||||
ref={inputRef}
|
||||
onChange={onChange}
|
||||
hidden
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
357
src/views/Options/Webfix.js
Normal file
357
src/views/Options/Webfix.js
Normal file
@@ -0,0 +1,357 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
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 Alert from "@mui/material/Alert";
|
||||
import Box from "@mui/material/Box";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import { useSetting } from "../../hooks/Setting";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { syncWebfix, loadOrFetchWebfix, FIXER_ALL } from "../../libs/webfix";
|
||||
import Button from "@mui/material/Button";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import { useAlert } from "../../hooks/Alert";
|
||||
import HelpButton from "./HelpButton";
|
||||
import { URL_KISS_RULES_NEW_ISSUE } from "../../config";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { useWebfixRules } from "../../hooks/WebfixRules";
|
||||
|
||||
function WebfixFields({ rule, webfix, setShow }) {
|
||||
const editMode = !!rule;
|
||||
const initFormValues = rule || {
|
||||
pattern: "",
|
||||
selector: "",
|
||||
rootSelector: "",
|
||||
fixer: FIXER_ALL[0],
|
||||
};
|
||||
const i18n = useI18n();
|
||||
const [disabled, setDisabled] = useState(editMode);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [formValues, setFormValues] = useState(initFormValues);
|
||||
|
||||
const { pattern, selector, rootSelector, fixer } = formValues;
|
||||
|
||||
const hasSamePattern = (str) => {
|
||||
for (const item of webfix.list || []) {
|
||||
if (item.pattern === str && rule?.pattern !== str) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleFocus = (e) => {
|
||||
e.preventDefault();
|
||||
const { name } = e.target;
|
||||
setErrors((pre) => ({ ...pre, [name]: "" }));
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
const { name, value } = e.target;
|
||||
setFormValues((pre) => ({ ...pre, [name]: value }));
|
||||
};
|
||||
|
||||
const handleCancel = (e) => {
|
||||
e.preventDefault();
|
||||
if (editMode) {
|
||||
setDisabled(true);
|
||||
} else {
|
||||
setShow(false);
|
||||
}
|
||||
setFormValues(initFormValues);
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const errors = {};
|
||||
if (!pattern.trim()) {
|
||||
errors.pattern = i18n("error_cant_be_blank");
|
||||
}
|
||||
if (hasSamePattern(pattern)) {
|
||||
errors.pattern = i18n("error_duplicate_values");
|
||||
}
|
||||
if (!selector.trim()) {
|
||||
errors.selector = i18n("error_cant_be_blank");
|
||||
}
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
if (editMode) {
|
||||
// 编辑
|
||||
setDisabled(true);
|
||||
webfix.put(rule.pattern, formValues);
|
||||
} else {
|
||||
// 添加
|
||||
webfix.add(formValues);
|
||||
setShow(false);
|
||||
setFormValues(initFormValues);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("pattern")}
|
||||
error={!!errors.pattern}
|
||||
helperText={errors.pattern}
|
||||
name="pattern"
|
||||
value={pattern}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
multiline
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("root_selector")}
|
||||
error={!!errors.rootSelector}
|
||||
helperText={errors.rootSelector}
|
||||
name="rootSelector"
|
||||
value={rootSelector}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
multiline
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("selector")}
|
||||
error={!!errors.selector}
|
||||
helperText={errors.selector}
|
||||
name="selector"
|
||||
value={selector}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
multiline
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="fixer"
|
||||
value={fixer}
|
||||
label={i18n("fixer_function")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{FIXER_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{webfix &&
|
||||
(editMode ? (
|
||||
// 编辑
|
||||
<Stack direction="row" spacing={2}>
|
||||
{disabled ? (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setDisabled(false);
|
||||
}}
|
||||
>
|
||||
{i18n("edit")}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
webfix.del(rule.pattern);
|
||||
}}
|
||||
>
|
||||
{i18n("delete")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button size="small" variant="contained" type="submit">
|
||||
{i18n("save")}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{i18n("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
) : (
|
||||
// 添加
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Button size="small" variant="contained" type="submit">
|
||||
{i18n("save")}
|
||||
</Button>
|
||||
<Button size="small" variant="outlined" onClick={handleCancel}>
|
||||
{i18n("cancel")}
|
||||
</Button>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function WebfixAccordion({ rule, webfix }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setExpanded((pre) => !pre);
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion expanded={expanded} onChange={handleChange}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography
|
||||
sx={{
|
||||
opacity: webfix ? 1 : 0.5,
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
>
|
||||
{rule.pattern}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{expanded && <WebfixFields rule={rule} webfix={webfix} />}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Webfix() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sites, setSites] = useState([]);
|
||||
const i18n = useI18n();
|
||||
const alert = useAlert();
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const webfix = useWebfixRules();
|
||||
|
||||
const loadSites = useCallback(async () => {
|
||||
const sites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
|
||||
setSites(sites);
|
||||
}, []);
|
||||
|
||||
const handleSyncTest = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setLoading(true);
|
||||
await syncWebfix(process.env.REACT_APP_WEBFIXURL);
|
||||
await loadSites();
|
||||
alert.success(i18n("sync_success"));
|
||||
} catch (err) {
|
||||
console.log("[sync webfix]", err);
|
||||
alert.error(i18n("sync_failed"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await loadSites();
|
||||
} catch (err) {
|
||||
console.log("[load webfix]", err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [loadSites]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Alert severity="info">{i18n("patch_setting_help")}</Alert>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
useFlexGap
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
disabled={showAdd}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowAdd(true);
|
||||
}}
|
||||
>
|
||||
{i18n("add")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
disabled={loading}
|
||||
onClick={handleSyncTest}
|
||||
startIcon={<SyncIcon />}
|
||||
>
|
||||
{i18n("sync_now")}
|
||||
</Button>
|
||||
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
checked={!!setting.injectWebfix}
|
||||
onChange={() => {
|
||||
updateSetting({
|
||||
injectWebfix: !setting.injectWebfix,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={i18n("inject_webfix")}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{showAdd && <WebfixFields webfix={webfix} setShow={setShowAdd} />}
|
||||
|
||||
{webfix.list?.length > 0 && (
|
||||
<Box>
|
||||
{webfix.list.map((rule) => (
|
||||
<WebfixAccordion key={rule.pattern} rule={rule} webfix={webfix} />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{setting.injectWebfix && (
|
||||
<Box>
|
||||
{loading ? (
|
||||
<center>
|
||||
<CircularProgress size={16} />
|
||||
</center>
|
||||
) : (
|
||||
sites.map((rule) => (
|
||||
<WebfixAccordion key={rule.pattern} rule={rule} />
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,11 @@ import Divider from "@mui/material/Divider";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { adaptScript } from "../../libs/gm";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Apis from "./Apis";
|
||||
import Webfix from "./Webfix";
|
||||
import InputSetting from "./InputSetting";
|
||||
import Tranbox from "./Tranbox";
|
||||
import FavWords from "./FavWords";
|
||||
|
||||
export default function Options() {
|
||||
const [error, setError] = useState("");
|
||||
@@ -34,7 +39,7 @@ export default function Options() {
|
||||
// 检查版本是否一致
|
||||
if (version !== process.env.REACT_APP_VERSION) {
|
||||
setError(
|
||||
`The version of the script(v${version}) and this page(v${process.env.REACT_APP_VERSION}) are inconsistent.`
|
||||
`The version of the local script(v${version}) is not the latest version(v${process.env.REACT_APP_VERSION}). 本地脚本之版本(v${version})非最新版(v${process.env.REACT_APP_VERSION})。`
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -51,7 +56,9 @@ export default function Options() {
|
||||
}
|
||||
|
||||
if (++i > 8) {
|
||||
setError("Time out.");
|
||||
setError(
|
||||
"Time out. Please confirm whether to install or enable KISS Translator GreaseMonkey script? 连接超时,请检查是否安装或启用简约翻译油猴脚本。"
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -68,35 +75,25 @@ export default function Options() {
|
||||
if (error) {
|
||||
return (
|
||||
<center>
|
||||
<Alert severity="error">{error}</Alert>
|
||||
<Divider>
|
||||
<Link
|
||||
href={process.env.REACT_APP_HOMEPAGE}
|
||||
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||
</Divider>
|
||||
<h2>
|
||||
Please confirm whether to install or enable KISS Translator
|
||||
GreaseMonkey script?
|
||||
</h2>
|
||||
<Alert severity="error">{error}</Alert>
|
||||
<Stack spacing={2}>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
||||
Install Userscript 1
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
|
||||
Install Userscript 2
|
||||
Install/Update Userscript for Tampermonkey/Violentmonkey
|
||||
</Link>
|
||||
{/* <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
|
||||
Install/Update Userscript for Tampermonkey/Violentmonkey 2
|
||||
</Link> */}
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
|
||||
Install Userscript Safari 1
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
|
||||
Install Userscript Safari 2
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
|
||||
Open Options Page 1
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
|
||||
Open Options Page 2
|
||||
Install/Update Userscript for iOS Safari
|
||||
</Link>
|
||||
{/* <Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
|
||||
Install/Update Userscript for iOS Safari 2
|
||||
</Link> */}
|
||||
</Stack>
|
||||
</center>
|
||||
);
|
||||
@@ -124,7 +121,12 @@ export default function Options() {
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Setting />} />
|
||||
<Route path="rules" element={<Rules />} />
|
||||
<Route path="input" element={<InputSetting />} />
|
||||
<Route path="tranbox" element={<Tranbox />} />
|
||||
<Route path="apis" element={<Apis />} />
|
||||
<Route path="sync" element={<SyncSetting />} />
|
||||
<Route path="webfix" element={<Webfix />} />
|
||||
<Route path="words" element={<FavWords />} />
|
||||
<Route path="about" element={<About />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import HomeIcon from "@mui/icons-material/Home";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import DarkModeButton from "../Options/DarkModeButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
export default function Header({ setShowPopup }) {
|
||||
const handleHomepage = () => {
|
||||
@@ -21,9 +21,16 @@ export default function Header({ setShowPopup }) {
|
||||
<IconButton onClick={handleHomepage}>
|
||||
<HomeIcon />
|
||||
</IconButton>
|
||||
<Box>
|
||||
<Typography
|
||||
component="div"
|
||||
sx={{
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{setShowPopup ? (
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 { sendTabMsg } from "../../libs/msg";
|
||||
import { sendBgMsg, sendTabMsg, getTabInfo } from "../../libs/msg";
|
||||
import { browser } from "../../libs/browser";
|
||||
import { isExt } from "../../libs/client";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
@@ -16,22 +16,28 @@ import {
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
MSG_OPEN_OPTIONS,
|
||||
MSG_SAVE_RULE,
|
||||
MSG_COMMAND_SHORTCUTS,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_STYLE_USE_COLOR,
|
||||
CACHE_NAME,
|
||||
} from "../../config";
|
||||
import { sendIframeMsg } from "../../libs/iframe";
|
||||
import { saveRule } from "../../libs/rules";
|
||||
import { tryClearCaches } from "../../libs";
|
||||
|
||||
export default function Popup({ setShowPopup, translator: tran }) {
|
||||
const i18n = useI18n();
|
||||
const [rule, setRule] = useState(tran?.rule);
|
||||
const [commands, setCommands] = useState({});
|
||||
|
||||
const handleOpenSetting = () => {
|
||||
if (isExt) {
|
||||
if (!tran) {
|
||||
browser?.runtime.openOptionsPage();
|
||||
} else if (isExt) {
|
||||
sendBgMsg(MSG_OPEN_OPTIONS);
|
||||
} else {
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||
}
|
||||
@@ -42,7 +48,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
try {
|
||||
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
|
||||
|
||||
if (isExt) {
|
||||
if (!tran) {
|
||||
await sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
} else {
|
||||
tran.toggle();
|
||||
@@ -58,7 +64,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
const { name, value } = e.target;
|
||||
setRule((pre) => ({ ...pre, [name]: value }));
|
||||
|
||||
if (isExt) {
|
||||
if (!tran) {
|
||||
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||
} else {
|
||||
tran.updateRule({ [name]: value });
|
||||
@@ -70,15 +76,29 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
};
|
||||
|
||||
const handleClearCache = () => {
|
||||
tryClearCaches();
|
||||
};
|
||||
|
||||
const handleSaveRule = async () => {
|
||||
try {
|
||||
caches.delete(CACHE_NAME);
|
||||
let href = window.location.href;
|
||||
if (!tran) {
|
||||
const tab = await getTabInfo();
|
||||
href = tab.url;
|
||||
}
|
||||
const newRule = { ...rule, pattern: href.split("/")[2] };
|
||||
if (isExt && tran) {
|
||||
sendBgMsg(MSG_SAVE_RULE, newRule);
|
||||
} else {
|
||||
saveRule(newRule);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[clear cache]", err);
|
||||
console.log("[save rule]", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExt) {
|
||||
if (tran) {
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
@@ -91,12 +111,38 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
console.log("[query rule]", err);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
}, [tran]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const commands = {};
|
||||
if (isExt) {
|
||||
const res = await sendBgMsg(MSG_COMMAND_SHORTCUTS);
|
||||
if (!res.error) {
|
||||
res.data.forEach(({ name, shortcut }) => {
|
||||
commands[name] = shortcut;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const shortcuts = tran.setting.shortcuts;
|
||||
if (shortcuts) {
|
||||
Object.entries(shortcuts).forEach(([key, val]) => {
|
||||
commands[key] = val.join("+");
|
||||
});
|
||||
}
|
||||
}
|
||||
setCommands(commands);
|
||||
} catch (err) {
|
||||
console.log("[query cmds]", err);
|
||||
}
|
||||
})();
|
||||
}, [tran]);
|
||||
|
||||
if (!rule) {
|
||||
return (
|
||||
<Box minWidth={300}>
|
||||
{isExt && (
|
||||
{!tran && (
|
||||
<>
|
||||
<Header />
|
||||
<Divider />
|
||||
@@ -111,11 +157,11 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
);
|
||||
}
|
||||
|
||||
const { transOpen, translator, fromLang, toLang, textStyle, bgColor } = rule;
|
||||
const { transOpen, translator, fromLang, toLang, textStyle } = rule;
|
||||
|
||||
return (
|
||||
<Box minWidth={300}>
|
||||
{isExt && (
|
||||
{!tran && (
|
||||
<>
|
||||
<Header />
|
||||
<Divider />
|
||||
@@ -135,13 +181,12 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
onChange={handleTransToggle}
|
||||
/>
|
||||
}
|
||||
label={i18n("translate_alt")}
|
||||
label={
|
||||
commands["toggleTranslate"]
|
||||
? `${i18n("translate_alt")}(${commands["toggleTranslate"]})`
|
||||
: i18n("translate_alt")
|
||||
}
|
||||
/>
|
||||
{!isExt && (
|
||||
<Button variant="text" onClick={handleClearCache}>
|
||||
{i18n("clear_cache")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<TextField
|
||||
@@ -198,7 +243,11 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
size="small"
|
||||
value={textStyle}
|
||||
name="textStyle"
|
||||
label={i18n("text_style_alt")}
|
||||
label={
|
||||
commands["toggleStyle"]
|
||||
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
|
||||
: i18n("text_style_alt")
|
||||
}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
@@ -208,7 +257,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||
{/* {OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||
<TextField
|
||||
size="small"
|
||||
name="bgColor"
|
||||
@@ -216,11 +265,26 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
label={i18n("bg_color")}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
<Button variant="text" onClick={handleOpenSetting}>
|
||||
{i18n("setting")}
|
||||
</Button>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
>
|
||||
<Button variant="text" onClick={handleSaveRule}>
|
||||
{i18n("save_rule")}
|
||||
</Button>
|
||||
{!isExt && (
|
||||
<Button variant="text" onClick={handleClearCache}>
|
||||
{i18n("clear_cache")}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="text" onClick={handleOpenSetting}>
|
||||
{i18n("setting")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
35
src/views/Selection/CopyBtn.js
Normal file
35
src/views/Selection/CopyBtn.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import LibraryAddCheckIcon from "@mui/icons-material/LibraryAddCheck";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function CopyBtn({ text }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const handleClick = async (e) => {
|
||||
e.stopPropagation();
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
const timer = setTimeout(() => {
|
||||
clearTimeout(timer);
|
||||
setCopied(false);
|
||||
}, 500);
|
||||
};
|
||||
return (
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{
|
||||
opacity: 0.5,
|
||||
"&:hover": {
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{copied ? (
|
||||
<LibraryAddCheckIcon fontSize="inherit" />
|
||||
) : (
|
||||
<ContentCopyIcon fontSize="inherit" />
|
||||
)}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
48
src/views/Selection/DictCont.js
Normal file
48
src/views/Selection/DictCont.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import FavBtn from "./FavBtn";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
const phonicMap = {
|
||||
en_phonic: "英",
|
||||
us_phonic: "美",
|
||||
};
|
||||
|
||||
export default function DictCont({ dictResult }) {
|
||||
if (!dictResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
|
||||
{dictResult.src}
|
||||
</Typography>
|
||||
<FavBtn word={dictResult.src} />
|
||||
</Stack>
|
||||
|
||||
<Typography component="div">
|
||||
<Typography>
|
||||
{dictResult.voice
|
||||
.map(Object.entries)
|
||||
.map((item) => item[0])
|
||||
.map(([key, val]) => `${phonicMap[key] || key} ${val}`)
|
||||
.join(" ")}
|
||||
</Typography>
|
||||
<ul style={{ margin: "0.5em 0" }}>
|
||||
{dictResult.content[0].mean.map(({ pre, cont }, idx) => (
|
||||
<li key={idx}>
|
||||
{pre && `[${pre}] `}
|
||||
{Object.keys(cont).join("; ")}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
65
src/views/Selection/DictContV2.js
Normal file
65
src/views/Selection/DictContV2.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Chip from "@mui/material/Chip";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import FavBtn from "./FavBtn";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
const exchangeMap = {
|
||||
word_third: "第三人称单数",
|
||||
word_ing: "现在分词",
|
||||
word_done: "过去式",
|
||||
word_past: "过去分词",
|
||||
word_pl: "复数",
|
||||
word_proto: "词源",
|
||||
};
|
||||
|
||||
export default function DictCont({ dictResult }) {
|
||||
if (!dictResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
|
||||
{dictResult.simple_means?.word_name}
|
||||
</Typography>
|
||||
<FavBtn word={dictResult.simple_means?.word_name} />
|
||||
</Stack>
|
||||
|
||||
{dictResult.simple_means?.symbols?.map(({ ph_en, ph_am, parts }, idx) => (
|
||||
<Typography key={idx} component="div">
|
||||
{(ph_en || ph_am) && (
|
||||
<Typography>{`英 /${ph_en || ""}/ 美 /${ph_am || ""}/`}</Typography>
|
||||
)}
|
||||
<ul style={{ margin: "0.5em 0" }}>
|
||||
{parts.map(({ part, means }, idx) => (
|
||||
<li key={idx}>
|
||||
{part ? `[${part}] ${means.join("; ")}` : means.join("; ")}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Typography>
|
||||
))}
|
||||
|
||||
<Typography>
|
||||
{Object.entries(dictResult.simple_means?.exchange || {})
|
||||
.map(([key, val]) => `${exchangeMap[key] || key}: ${val.join(", ")}`)
|
||||
.join("; ")}
|
||||
</Typography>
|
||||
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
|
||||
{Object.values(dictResult.simple_means?.tags || {})
|
||||
.flat()
|
||||
.filter((item) => item)
|
||||
.map((item) => (
|
||||
<Chip label={item} size="small" />
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
270
src/views/Selection/DraggableResizable.js
Normal file
270
src/views/Selection/DraggableResizable.js
Normal file
@@ -0,0 +1,270 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Box from "@mui/material/Box";
|
||||
import { isMobile } from "../../libs/mobile";
|
||||
|
||||
function Pointer({
|
||||
direction,
|
||||
size,
|
||||
setSize,
|
||||
position,
|
||||
setPosition,
|
||||
children,
|
||||
minSize,
|
||||
maxSize,
|
||||
...props
|
||||
}) {
|
||||
const [origin, setOrigin] = useState(null);
|
||||
|
||||
function handlePointerDown(e) {
|
||||
!isMobile && e.target.setPointerCapture(e.pointerId);
|
||||
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
||||
setOrigin({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
w: size.w,
|
||||
h: size.h,
|
||||
clientX,
|
||||
clientY,
|
||||
});
|
||||
}
|
||||
|
||||
function handlePointerMove(e) {
|
||||
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
||||
if (origin) {
|
||||
const dx = clientX - origin.clientX;
|
||||
const dy = clientY - origin.clientY;
|
||||
let x = position.x;
|
||||
let y = position.y;
|
||||
let w = size.w;
|
||||
let h = size.h;
|
||||
|
||||
switch (direction) {
|
||||
case "Header":
|
||||
x = origin.x + dx;
|
||||
y = origin.y + dy;
|
||||
break;
|
||||
case "TopLeft":
|
||||
x = origin.x + dx;
|
||||
y = origin.y + dy;
|
||||
w = origin.w - dx;
|
||||
h = origin.h - dy;
|
||||
break;
|
||||
case "Top":
|
||||
y = origin.y + dy;
|
||||
h = origin.h - dy;
|
||||
break;
|
||||
case "TopRight":
|
||||
y = origin.y + dy;
|
||||
w = origin.w + dx;
|
||||
h = origin.h - dy;
|
||||
break;
|
||||
case "Left":
|
||||
x = origin.x + dx;
|
||||
w = origin.w - dx;
|
||||
break;
|
||||
case "Right":
|
||||
w = origin.w + dx;
|
||||
break;
|
||||
case "BottomLeft":
|
||||
x = origin.x + dx;
|
||||
w = origin.w - dx;
|
||||
h = origin.h + dy;
|
||||
break;
|
||||
case "Bottom":
|
||||
h = origin.h + dy;
|
||||
break;
|
||||
case "BottomRight":
|
||||
w = origin.w + dx;
|
||||
h = origin.h + dy;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
if (w < minSize.w) {
|
||||
w = minSize.w;
|
||||
x = position.x;
|
||||
}
|
||||
if (w > maxSize.w) {
|
||||
w = maxSize.w;
|
||||
x = position.x;
|
||||
}
|
||||
if (h < minSize.h) {
|
||||
h = minSize.h;
|
||||
y = position.y;
|
||||
}
|
||||
if (h > maxSize.h) {
|
||||
h = maxSize.h;
|
||||
y = position.y;
|
||||
}
|
||||
|
||||
setPosition({ x, y });
|
||||
setSize({ w, h });
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerUp(e) {
|
||||
e.stopPropagation();
|
||||
setOrigin(null);
|
||||
}
|
||||
|
||||
const touchProps = isMobile
|
||||
? {
|
||||
onTouchStart: handlePointerDown,
|
||||
onTouchMove: handlePointerMove,
|
||||
onTouchEnd: handlePointerUp,
|
||||
}
|
||||
: {
|
||||
onPointerDown: handlePointerDown,
|
||||
onPointerMove: handlePointerMove,
|
||||
onPointerUp: handlePointerUp,
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...props} {...touchProps}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DraggableResizable({
|
||||
header,
|
||||
children,
|
||||
defaultPosition = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
defaultSize = {
|
||||
w: 600,
|
||||
h: 400,
|
||||
},
|
||||
minSize = {
|
||||
w: 300,
|
||||
h: 200,
|
||||
},
|
||||
maxSize = {
|
||||
w: 1200,
|
||||
h: 1200,
|
||||
},
|
||||
onChangeSize,
|
||||
onChangePosition,
|
||||
}) {
|
||||
const lineWidth = 4;
|
||||
const [position, setPosition] = useState(defaultPosition);
|
||||
const [size, setSize] = useState(defaultSize);
|
||||
|
||||
const opts = {
|
||||
size,
|
||||
setSize,
|
||||
position,
|
||||
setPosition,
|
||||
minSize,
|
||||
maxSize,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onChangeSize && onChangeSize(size);
|
||||
}, [size, onChangeSize]);
|
||||
|
||||
useEffect(() => {
|
||||
onChangePosition && onChangePosition(position);
|
||||
}, [position, onChangePosition]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
touchAction: "none",
|
||||
position: "fixed",
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
display: "grid",
|
||||
gridTemplateColumns: `${lineWidth * 2}px auto ${lineWidth * 2}px`,
|
||||
gridTemplateRows: `${lineWidth * 2}px auto ${lineWidth * 2}px`,
|
||||
zIndex: 2147483647,
|
||||
}}
|
||||
>
|
||||
<Pointer
|
||||
direction="TopLeft"
|
||||
style={{
|
||||
transform: `translate(${lineWidth}px, ${lineWidth}px)`,
|
||||
cursor: "nw-resize",
|
||||
}}
|
||||
{...opts}
|
||||
/>
|
||||
<Pointer
|
||||
direction="Top"
|
||||
style={{
|
||||
margin: `0 ${lineWidth}px`,
|
||||
transform: `translate(0px, ${lineWidth}px)`,
|
||||
cursor: "row-resize",
|
||||
}}
|
||||
{...opts}
|
||||
/>
|
||||
<Pointer
|
||||
direction="TopRight"
|
||||
style={{
|
||||
transform: `translate(-${lineWidth}px, ${lineWidth}px)`,
|
||||
cursor: "ne-resize",
|
||||
}}
|
||||
{...opts}
|
||||
/>
|
||||
<Pointer
|
||||
direction="Left"
|
||||
style={{
|
||||
margin: `${lineWidth}px 0`,
|
||||
transform: `translate(${lineWidth}px, 0px)`,
|
||||
cursor: "col-resize",
|
||||
}}
|
||||
{...opts}
|
||||
/>
|
||||
<Paper elevation={4}>
|
||||
<Pointer direction="Header" style={{ cursor: "move" }} {...opts}>
|
||||
{header}
|
||||
</Pointer>
|
||||
<div
|
||||
style={{
|
||||
width: size.w,
|
||||
height: size.h,
|
||||
overflow: "hidden auto",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Paper>
|
||||
<Pointer
|
||||
direction="Right"
|
||||
style={{
|
||||
margin: `${lineWidth}px 0`,
|
||||
transform: `translate(-${lineWidth}px, 0px)`,
|
||||
cursor: "col-resize",
|
||||
}}
|
||||
{...opts}
|
||||
/>
|
||||
<Pointer
|
||||
direction="BottomLeft"
|
||||
style={{
|
||||
transform: `translate(${lineWidth}px, -${lineWidth}px)`,
|
||||
cursor: "ne-resize",
|
||||
}}
|
||||
{...opts}
|
||||
/>
|
||||
<Pointer
|
||||
direction="Bottom"
|
||||
style={{
|
||||
margin: `0 ${lineWidth}px`,
|
||||
transform: `translate(0px, -${lineWidth}px)`,
|
||||
cursor: "row-resize",
|
||||
}}
|
||||
{...opts}
|
||||
/>
|
||||
<Pointer
|
||||
direction="BottomRight"
|
||||
style={{
|
||||
transform: `translate(-${lineWidth}px, -${lineWidth}px)`,
|
||||
cursor: "nw-resize",
|
||||
}}
|
||||
{...opts}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
31
src/views/Selection/FavBtn.js
Normal file
31
src/views/Selection/FavBtn.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import FavoriteIcon from "@mui/icons-material/Favorite";
|
||||
import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorder";
|
||||
import { useState } from "react";
|
||||
import { useFavWords } from "../../hooks/FavWords";
|
||||
|
||||
export default function FavBtn({ word }) {
|
||||
const { favWords, toggleFav } = useFavWords();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await toggleFav(word);
|
||||
} catch (err) {
|
||||
console.log("[set fav]", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton disabled={loading} size="small" onClick={handleClick}>
|
||||
{favWords[word] ? (
|
||||
<FavoriteIcon fontSize="inherit" />
|
||||
) : (
|
||||
<FavoriteBorderIcon fontSize="inherit" />
|
||||
)}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
195
src/views/Selection/TranBox.js
Normal file
195
src/views/Selection/TranBox.js
Normal file
@@ -0,0 +1,195 @@
|
||||
import { SettingProvider } from "../../hooks/Setting";
|
||||
import ThemeProvider from "../../hooks/Theme";
|
||||
import DraggableResizable from "./DraggableResizable";
|
||||
import Header from "../Popup/Header";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Box from "@mui/material/Box";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import DoneIcon from "@mui/icons-material/Done";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import { OPT_TRANS_ALL, OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
|
||||
import { useState, useRef } from "react";
|
||||
import TranCont from "./TranCont";
|
||||
import CopyBtn from "./CopyBtn";
|
||||
|
||||
function TranForm({ text, setText, tranboxSetting, transApis }) {
|
||||
const i18n = useI18n();
|
||||
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [editText, setEditText] = useState("");
|
||||
const [translator, setTranslator] = useState(tranboxSetting.translator);
|
||||
const [fromLang, setFromLang] = useState(tranboxSetting.fromLang);
|
||||
const [toLang, setToLang] = useState(tranboxSetting.toLang);
|
||||
const [toLang2, setToLang2] = useState(tranboxSetting.toLang2);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Stack sx={{ p: 2 }} spacing={2}>
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={12}>
|
||||
<Grid item xs={4} sm={4} md={4} lg={4}>
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
fullWidth
|
||||
size="small"
|
||||
name="fromLang"
|
||||
value={fromLang}
|
||||
label={i18n("from_lang")}
|
||||
onChange={(e) => {
|
||||
setFromLang(e.target.value);
|
||||
}}
|
||||
>
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={4} sm={4} md={4} lg={4}>
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
fullWidth
|
||||
size="small"
|
||||
name="toLang"
|
||||
value={toLang}
|
||||
label={i18n("to_lang")}
|
||||
onChange={(e) => {
|
||||
setToLang(e.target.value);
|
||||
}}
|
||||
>
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={4} sm={4} md={4} lg={4}>
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
fullWidth
|
||||
size="small"
|
||||
value={translator}
|
||||
name="translator"
|
||||
label={i18n("translate_service")}
|
||||
onChange={(e) => {
|
||||
setTranslator(e.target.value);
|
||||
}}
|
||||
>
|
||||
{OPT_TRANS_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("original_text")}
|
||||
inputRef={inputRef}
|
||||
fullWidth
|
||||
multiline
|
||||
value={editMode ? editText : text}
|
||||
disabled={!editMode}
|
||||
onChange={(e) => {
|
||||
setEditText(e.target.value);
|
||||
}}
|
||||
onClick={() => {
|
||||
setEditMode(true);
|
||||
setEditText(text);
|
||||
const timer = setTimeout(() => {
|
||||
clearTimeout(timer);
|
||||
inputRef.current?.focus();
|
||||
}, 100);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setEditMode(false);
|
||||
setText(editText.trim());
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
}}
|
||||
>
|
||||
{editMode ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<DoneIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
) : (
|
||||
<CopyBtn text={text} />
|
||||
)}
|
||||
</Stack>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<TranCont
|
||||
text={text}
|
||||
translator={translator}
|
||||
fromLang={fromLang}
|
||||
toLang={toLang}
|
||||
toLang2={toLang2}
|
||||
setToLang={setToLang}
|
||||
setToLang2={setToLang2}
|
||||
transApis={transApis}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TranBox({
|
||||
text,
|
||||
setText,
|
||||
setShowBox,
|
||||
tranboxSetting,
|
||||
transApis,
|
||||
boxSize,
|
||||
setBoxSize,
|
||||
boxPosition,
|
||||
setBoxPosition,
|
||||
}) {
|
||||
return (
|
||||
<SettingProvider>
|
||||
<ThemeProvider>
|
||||
<DraggableResizable
|
||||
defaultPosition={boxPosition}
|
||||
defaultSize={boxSize}
|
||||
header={<Header setShowPopup={setShowBox} />}
|
||||
onChangeSize={setBoxSize}
|
||||
onChangePosition={setBoxPosition}
|
||||
>
|
||||
<Divider />
|
||||
<TranForm
|
||||
text={text}
|
||||
setText={setText}
|
||||
tranboxSetting={tranboxSetting}
|
||||
transApis={transApis}
|
||||
/>
|
||||
</DraggableResizable>
|
||||
</ThemeProvider>
|
||||
</SettingProvider>
|
||||
);
|
||||
}
|
||||
46
src/views/Selection/TranBtn.js
Normal file
46
src/views/Selection/TranBtn.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { isMobile } from "../../libs/mobile";
|
||||
|
||||
export default function TranBtn({ onClick, position, tranboxSetting }) {
|
||||
const left = position.x + tranboxSetting.btnOffsetX;
|
||||
const top = position.y + tranboxSetting.btnOffsetY;
|
||||
|
||||
const touchProps = isMobile
|
||||
? {
|
||||
onTouchEnd: onClick,
|
||||
}
|
||||
: {
|
||||
onMouseUp: onClick,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
position: "absolute",
|
||||
left,
|
||||
top,
|
||||
zIndex: 2147483647,
|
||||
}}
|
||||
{...touchProps}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
>
|
||||
<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"
|
||||
transform="translate(0,0)"
|
||||
/>
|
||||
<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"
|
||||
transform="translate(4,5)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
124
src/views/Selection/TranCont.js
Normal file
124
src/views/Selection/TranCont.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Box from "@mui/material/Box";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import { DEFAULT_TRANS_APIS, OPT_TRANS_BAIDU } from "../../config";
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiTranslate, apiBaiduLangdetect } from "../../apis";
|
||||
import { isValidWord } from "../../libs/utils";
|
||||
import CopyBtn from "./CopyBtn";
|
||||
import DictCont from "./DictCont";
|
||||
|
||||
export default function TranCont({
|
||||
text,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
toLang2 = "en",
|
||||
setToLang,
|
||||
setToLang2,
|
||||
transApis,
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [trText, setTrText] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [dictResult, setDictResult] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setTrText("");
|
||||
setError("");
|
||||
setDictResult(null);
|
||||
|
||||
// 互译
|
||||
if (toLang !== toLang2 && toLang2 !== "none") {
|
||||
const detectLang = await apiBaiduLangdetect(text);
|
||||
if (detectLang === toLang) {
|
||||
setToLang(toLang2);
|
||||
setToLang2(toLang);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const apiSetting =
|
||||
transApis[translator] || DEFAULT_TRANS_APIS[translator];
|
||||
const tranRes = await apiTranslate({
|
||||
text,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting,
|
||||
});
|
||||
setTrText(tranRes[0]);
|
||||
|
||||
// 词典
|
||||
if (isValidWord(text) && toLang.startsWith("zh")) {
|
||||
if (fromLang === "en" && translator === OPT_TRANS_BAIDU) {
|
||||
tranRes[2].type === 1 &&
|
||||
setDictResult(JSON.parse(tranRes[2].result));
|
||||
} else {
|
||||
const dictRes = await apiTranslate({
|
||||
text,
|
||||
translator: OPT_TRANS_BAIDU,
|
||||
fromLang: "en",
|
||||
toLang: "zh-CN",
|
||||
});
|
||||
dictRes[2].type === 1 &&
|
||||
setDictResult(JSON.parse(dictRes[2].result));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [
|
||||
text,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
toLang2,
|
||||
setToLang,
|
||||
setToLang2,
|
||||
transApis,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("translated_text")}
|
||||
// disabled
|
||||
fullWidth
|
||||
multiline
|
||||
value={trText}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
}}
|
||||
>
|
||||
<CopyBtn text={trText} />
|
||||
</Stack>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{loading && <CircularProgress size={24} />}
|
||||
{error && <Alert severity="error">{error}</Alert>}
|
||||
{dictResult && <DictCont dictResult={dictResult} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
159
src/views/Selection/index.js
Normal file
159
src/views/Selection/index.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import TranBtn from "./TranBtn";
|
||||
import TranBox from "./TranBox";
|
||||
import { shortcutRegister } from "../../libs/shortcut";
|
||||
import { sleep, limitNumber } from "../../libs/utils";
|
||||
import { isGm, isExt } from "../../libs/client";
|
||||
import { MSG_OPEN_TRANBOX, DEFAULT_TRANBOX_SHORTCUT } from "../../config";
|
||||
import { isMobile } from "../../libs/mobile";
|
||||
|
||||
export default function Slection({
|
||||
contextMenuType,
|
||||
tranboxSetting,
|
||||
transApis,
|
||||
}) {
|
||||
const boxWidth = limitNumber(window.innerWidth, 300, 600);
|
||||
const boxHeight = limitNumber(window.innerHeight, 200, 400);
|
||||
|
||||
const [showBox, setShowBox] = useState(false);
|
||||
const [showBtn, setShowBtn] = useState(false);
|
||||
const [selectedText, setSelText] = useState("");
|
||||
const [text, setText] = useState("");
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [boxSize, setBoxSize] = useState({
|
||||
w: boxWidth,
|
||||
h: boxHeight,
|
||||
});
|
||||
const [boxPosition, setBoxPosition] = useState({
|
||||
x: (window.innerWidth - boxWidth) / 2,
|
||||
y: (window.innerHeight - boxHeight) / 2,
|
||||
});
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
setShowBtn(false);
|
||||
setText(selectedText);
|
||||
setShowBox(true);
|
||||
};
|
||||
|
||||
const handleTranbox = useCallback(() => {
|
||||
setShowBtn(false);
|
||||
|
||||
const selectedText = window.getSelection()?.toString()?.trim() || "";
|
||||
if (!selectedText) {
|
||||
setShowBox((pre) => !pre);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelText(selectedText);
|
||||
setText(selectedText);
|
||||
setShowBox(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function handleMouseup(e) {
|
||||
e.stopPropagation();
|
||||
await sleep(10);
|
||||
|
||||
const selectedText = window.getSelection()?.toString()?.trim() || "";
|
||||
setSelText(selectedText);
|
||||
if (!selectedText) {
|
||||
setShowBtn(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { pageX, pageY } = isMobile ? e.changedTouches[0] : e;
|
||||
!tranboxSetting.hideTranBtn && setShowBtn(true);
|
||||
// setPosition({ x: e.clientX, y: e.clientY });
|
||||
setPosition({ x: pageX, y: pageY });
|
||||
}
|
||||
|
||||
// todo: mobile support
|
||||
window.addEventListener("mouseup", handleMouseup);
|
||||
// window.addEventListener(isMobile ? "touchend" : "mouseup", handleMouseup);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
isMobile ? "touchend" : "mouseup",
|
||||
handleMouseup
|
||||
);
|
||||
};
|
||||
}, [tranboxSetting.hideTranBtn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clearShortcut = shortcutRegister(
|
||||
tranboxSetting.tranboxShortcut || DEFAULT_TRANBOX_SHORTCUT,
|
||||
handleTranbox
|
||||
);
|
||||
|
||||
return () => {
|
||||
clearShortcut();
|
||||
};
|
||||
}, [tranboxSetting.tranboxShortcut, handleTranbox]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(MSG_OPEN_TRANBOX, handleTranbox);
|
||||
return () => {
|
||||
window.removeEventListener(MSG_OPEN_TRANBOX, handleTranbox);
|
||||
};
|
||||
}, [handleTranbox]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGm) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册菜单
|
||||
try {
|
||||
const menuCommandIds = [];
|
||||
contextMenuType !== 0 &&
|
||||
menuCommandIds.push(
|
||||
GM.registerMenuCommand(
|
||||
"Translate Selected Text",
|
||||
(event) => {
|
||||
handleTranbox();
|
||||
},
|
||||
"S"
|
||||
)
|
||||
);
|
||||
|
||||
return () => {
|
||||
menuCommandIds.forEach((id) => {
|
||||
GM.unregisterMenuCommand(id);
|
||||
});
|
||||
};
|
||||
} catch (err) {
|
||||
console.log("[registerMenuCommand]", err);
|
||||
}
|
||||
}, [handleTranbox, contextMenuType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showBox && (
|
||||
<TranBox
|
||||
text={text}
|
||||
setText={setText}
|
||||
boxSize={boxSize}
|
||||
setBoxSize={setBoxSize}
|
||||
boxPosition={boxPosition}
|
||||
setBoxPosition={setBoxPosition}
|
||||
tranboxSetting={tranboxSetting}
|
||||
transApis={transApis}
|
||||
setShowBox={setShowBox}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showBtn && (
|
||||
<TranBtn
|
||||
position={position}
|
||||
tranboxSetting={tranboxSetting}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user