Compare commits
182 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
c6f8a45027 | ||
|
|
6ec16e1f98 | ||
|
|
40adf85b20 | ||
|
|
4c78f469c1 | ||
|
|
55af58faac | ||
|
|
4200caa641 | ||
|
|
0ac06f8e3d | ||
|
|
966c78fb16 | ||
|
|
5c5a35d3bb | ||
|
|
2c24214f48 | ||
|
|
67d9e70b3c | ||
|
|
000a55f43b | ||
|
|
4096a6976c | ||
|
|
df4c4ebd50 | ||
|
|
b43bd4e0e2 | ||
|
|
2660dbf866 | ||
|
|
e0b7c60099 | ||
|
|
536b58bf67 | ||
|
|
6bb742f828 | ||
|
|
72742e5e12 | ||
|
|
3667e0a509 | ||
|
|
c2d7668ba7 | ||
|
|
aa830f5e20 | ||
|
|
b593fa4146 | ||
|
|
b00b906484 | ||
|
|
c1bd6a1be6 | ||
|
|
36739f04b3 | ||
|
|
23eb92853e | ||
|
|
5ab2910dc7 | ||
|
|
40d07f6764 | ||
|
|
5c8e216169 | ||
|
|
5ba061deda | ||
|
|
935c83185d | ||
|
|
6327391e65 | ||
|
|
3d656cf5b0 | ||
|
|
d570a0f1a2 | ||
|
|
503a71302c | ||
|
|
3e36ceb5b9 | ||
|
|
cde7a1d49f | ||
|
|
b14a38e4fb | ||
|
|
732a526a8e | ||
|
|
2da5ffef44 | ||
|
|
2e6e52004f | ||
|
|
4486ad353c | ||
|
|
aa795e2731 | ||
|
|
c46fe7d1c6 | ||
|
|
d7cee8cca6 | ||
|
|
11f790ace5 | ||
|
|
13e7c1b754 | ||
|
|
d314d5515f | ||
|
|
09b19e3ca0 | ||
|
|
687bd11fd1 | ||
|
|
56cb1cd30d | ||
|
|
7a3df25521 | ||
|
|
ea8919ba07 | ||
|
|
3dece4fcdb | ||
|
|
df950a1bd2 | ||
|
|
74b9ee31fa | ||
|
|
64cd55fe58 | ||
|
|
e80ede14fb | ||
|
|
45ba9d3320 | ||
|
|
47c7048538 | ||
|
|
f9bfa8101f | ||
|
|
620ac464eb | ||
|
|
62289f8ab8 | ||
|
|
d84594da96 | ||
|
|
e1d74aae6a | ||
|
|
c4980d9eb7 | ||
|
|
882d83c6b7 | ||
|
|
c4a7fd81f8 | ||
|
|
0e55799109 | ||
|
|
a3cdcb2a1a | ||
|
|
e0ccc298f9 | ||
|
|
36b49bb577 | ||
|
|
2636c24e84 | ||
|
|
6bcf294635 | ||
|
|
c5fa6689a4 | ||
|
|
3bf0cb2485 | ||
|
|
19c9335527 | ||
|
|
20da2e1b97 | ||
|
|
9eceb8641d | ||
|
|
86bc915d74 | ||
|
|
6b35525207 | ||
|
|
4633bf4fc6 | ||
|
|
2665f31d94 | ||
|
|
6c4d3149eb | ||
|
|
a2762e6ce6 | ||
|
|
792a1bfcad | ||
|
|
a0eba9d60e | ||
|
|
c2e0064253 | ||
|
|
f246efc84b | ||
|
|
4a3bf7e96c | ||
|
|
523b81090d | ||
|
|
d706c405d9 | ||
|
|
1191791447 | ||
|
|
5c510f2df2 | ||
|
|
7c0aa23177 | ||
|
|
4bc1c26653 | ||
|
|
ca1e1148d6 | ||
|
|
2224455a7f |
29
.env
29
.env
@@ -2,11 +2,28 @@ GENERATE_SOURCEMAP=false
|
||||
|
||||
REACT_APP_NAME=KISS Translator
|
||||
REACT_APP_NAME_CN=简约翻译
|
||||
REACT_APP_VERSION=1.5.3
|
||||
REACT_APP_VERSION=1.7.1
|
||||
|
||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||
REACT_APP_OPTIONSPAGE=https://kiss-translator.rayjar.com/options
|
||||
REACT_APP_OPTIONSPAGE2=https://fishjar.github.io/kiss-translator/options.html
|
||||
|
||||
REACT_APP_OPTIONSPAGE=https://fishjar.github.io/kiss-translator/options.html
|
||||
REACT_APP_OPTIONSPAGE2=https://kiss-translator.rayjar.com/options
|
||||
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
|
||||
REACT_APP_LOGOURL=https://kiss-translator.rayjar.com/images/logo192.png
|
||||
REACT_APP_USERSCRIPT_DOWNLOADURL=https://kiss-translator.rayjar.com/kiss-translator.user.js
|
||||
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
|
||||
|
||||
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
|
||||
REACT_APP_LOGOURL2=https://kiss-translator.rayjar.com/images/logo192.png
|
||||
|
||||
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
|
||||
|
||||
REACT_APP_USERSCRIPT_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
|
||||
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator.user.js
|
||||
|
||||
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js
|
||||
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator-ios-safari.user.js
|
||||
|
||||
90
README.en.md
90
README.en.md
@@ -1,10 +1,10 @@
|
||||
## KISS Translator
|
||||
# KISS Translator
|
||||
|
||||
A minimalist [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
|
||||
## 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.
|
||||
|
||||
@@ -14,37 +14,61 @@ It just so happens that I am obsessed with translation tools. Based on the conce
|
||||
|
||||
If you also like a little more simplicity, welcome to pick it up.
|
||||
|
||||
### Features
|
||||
## Features
|
||||
|
||||
- Keep it simple, smart
|
||||
|
||||
### 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] OpenAI
|
||||
- [ ] DeepL
|
||||
- [ ] Upload to app Store
|
||||
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
|
||||
- [ ] Edge
|
||||
- [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] Keep it simple, smart
|
||||
- [x] Open source
|
||||
- [x] Data Synchronization Function
|
||||
- [x] Greasemonkey Script ([link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js))
|
||||
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox)
|
||||
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (need test)
|
||||
- [x] Adapt to common browsers
|
||||
- [x] Chrome/Edge/Firefox/Kiwi
|
||||
- [ ] Safari
|
||||
- [x] Supports multiple translation services
|
||||
- [x] Google/Microsoft/DeepL/OpenAI
|
||||
- [x] Custom translation interface
|
||||
- [x] Support input box translation
|
||||
- [x] Support YouTube subtitle translation
|
||||
- [x] Data synchronization function
|
||||
- [x] Custom rules + rule subscription
|
||||
- [x] Custom style
|
||||
- [x] Custom shortcut keys
|
||||
- `Alt+Q` Toggle Translation
|
||||
- `Alt+C` Toggle Styles
|
||||
- `Alt+K` Open Popup
|
||||
- `Alt+O` Open Options
|
||||
- `Alt+I` Input Box Translation
|
||||
|
||||
### Guide
|
||||
## Download
|
||||
|
||||
- [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 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、 [Installation link 2](https://kiss-translator.rayjar.com/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 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、 [Installation link 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.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.
|
||||
- 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.
|
||||
- Help with rules-related issues.
|
||||
- Web page correction script: [https://github.com/fishjar/kiss-webfixer](https://github.com/fishjar/kiss-webfixer)
|
||||
- Fixed scripts for some special sites.
|
||||
- So that the translation software can get better display effect.
|
||||
- Translation interface agent: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
|
||||
- If you encounter network problems when accessing a certain translation interface, this proxy service may help you.
|
||||
- Deploy and manage by yourself.
|
||||
- Minimalistic Dictionary Plugin: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
|
||||
- A word-marking translation plug-in used with this project.
|
||||
- Supports query of English words, sentences and Chinese characters.
|
||||
- Supports history records and word collections.
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
```sh
|
||||
git clone https://github.com/fishjar/kiss-translator.git
|
||||
@@ -53,10 +77,6 @@ yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Data Sync
|
||||
|
||||
Goto: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||
|
||||
### Discussion
|
||||
## Discussion
|
||||
|
||||
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||
|
||||
88
README.md
88
README.md
@@ -1,10 +1,10 @@
|
||||
## 简约翻译
|
||||
# 简约翻译
|
||||
|
||||
一个简约的 [双语网页翻译扩展 & 油猴脚本](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) 一起使用,刚好形成很好补充。
|
||||
|
||||
@@ -14,37 +14,61 @@
|
||||
|
||||
如果你也喜欢简约一点的,欢迎自取。
|
||||
|
||||
### 特点
|
||||
## 特点
|
||||
|
||||
- 保持简约
|
||||
|
||||
### 进度
|
||||
|
||||
- [x] 提供试用安装包
|
||||
- [x] 适配浏览器
|
||||
- [x] Chrome
|
||||
- [x] Edge
|
||||
- [x] Firefox
|
||||
- [ ] Safari
|
||||
- [x] Kiwi
|
||||
- [x] 支持翻译服务
|
||||
- [x] Google
|
||||
- [x] Microsoft
|
||||
- [x] OpenAI
|
||||
- [ ] DeepL
|
||||
- [ ] 上架应用市场
|
||||
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
||||
- [ ] Edge
|
||||
- [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] 适配常见浏览器
|
||||
- [x] Chrome/Edge/Firefox/Kiwi
|
||||
- [ ] Safari
|
||||
- [x] 支持多种翻译服务
|
||||
- [x] Google/Microsoft/DeepL/OpenAI
|
||||
- [x] 自定义翻译接口
|
||||
- [x] 支持输入框翻译
|
||||
- [x] 支持 YouTube 字幕翻译
|
||||
- [x] 数据同步功能
|
||||
- [x] 油猴脚本([链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js))
|
||||
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox)
|
||||
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (待测)
|
||||
- [x] 自定义规则 + 规则订阅
|
||||
- [x] 自定义样式
|
||||
- [x] 自定义快捷键
|
||||
- `Alt+Q` 开启翻译
|
||||
- `Alt+C` 切换样式
|
||||
- `Alt+K` 打开弹窗
|
||||
- `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/)) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、 [安装链接 2](https://kiss-translator.rayjar.com/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)) [安装链接 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)
|
||||
|
||||
## 关联项目
|
||||
|
||||
- 数据同步服务: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||
- 可用于本项目的数据同步服务。
|
||||
- 亦可用于分享个人的私有规则列表。
|
||||
- 自己部署,自己管理,数据私有。
|
||||
- 社区订阅规则: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
||||
- 提供社区维护的,最新最全的订阅规则列表。
|
||||
- 求助规则相关的问题。
|
||||
- 网页修正脚本: [https://github.com/fishjar/kiss-webfixer](https://github.com/fishjar/kiss-webfixer)
|
||||
- 针对一些特殊网站的修正脚本。
|
||||
- 以便翻译软件得到更好的展示效果。
|
||||
- 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
|
||||
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮你到你。
|
||||
- 自己部署,自己管理。
|
||||
- 简约词典插件: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
|
||||
- 搭配本项目一起使用的划词翻译插件。
|
||||
- 支持英文单词、句子、汉字的查询。
|
||||
- 支持历史记录、单词收藏。
|
||||
|
||||
## 开发指引
|
||||
|
||||
```sh
|
||||
git clone https://github.com/fishjar/kiss-translator.git
|
||||
@@ -53,10 +77,6 @@ yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 数据同步
|
||||
|
||||
移步: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||
|
||||
### 交流
|
||||
## 交流
|
||||
|
||||
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||
|
||||
@@ -93,6 +93,8 @@ const userscriptWebpack = (config, env) => {
|
||||
// @connect translate.googleapis.com
|
||||
// @connect api-edge.cognitive.microsofttranslator.com
|
||||
// @connect edge.microsoft.com
|
||||
// @connect api-free.deepl.com
|
||||
// @connect api.deepl.com
|
||||
// @connect api.openai.com
|
||||
// @connect openai.azure.com
|
||||
// @connect workers.dev
|
||||
@@ -100,6 +102,7 @@ const userscriptWebpack = (config, env) => {
|
||||
// @connect githubusercontent.com
|
||||
// @connect kiss-translator.rayjar.com
|
||||
// @connect ghproxy.com
|
||||
// @connect localhost:3000
|
||||
// @run-at document-end
|
||||
// ==/UserScript==
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kiss-translator",
|
||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||
"version": "1.5.3",
|
||||
"version": "1.7.1",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@@ -24,9 +24,10 @@
|
||||
"build:edge": "rm -rf build/edge && cp -r build/chrome build/edge",
|
||||
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json",
|
||||
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
|
||||
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/kiss-translator.user.js build/userscript/kiss-translator.user.js",
|
||||
"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 && yarn build:rules",
|
||||
"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",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject"
|
||||
|
||||
@@ -10,5 +10,8 @@
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "Toggle Style"
|
||||
},
|
||||
"open_options": {
|
||||
"message": "Open Options"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
"message": "一个简约的双语网页翻译扩展 & 油猴脚本"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "切换翻译"
|
||||
"message": "开启翻译"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "切换样式"
|
||||
},
|
||||
"open_options": {
|
||||
"message": "打开设置"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,85 @@
|
||||
max-height: 1.2em;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// (() => {
|
||||
// var shadow = document.querySelector("#shadow1");
|
||||
// var root = shadow.attachShadow({ mode: "open" });
|
||||
// var newLine = document.createElement("p");
|
||||
// newLine.innerText = "new line";
|
||||
// root.appendChild(newLine);
|
||||
// })();
|
||||
|
||||
// setTimeout(function () {
|
||||
// var shadow = document.querySelector("#shadow2");
|
||||
// var root = shadow.attachShadow({ mode: "open" });
|
||||
// }, 1000);
|
||||
|
||||
// setTimeout(() => {
|
||||
// var newLine = document.createElement("p");
|
||||
// newLine.innerText = "new line";
|
||||
// var shadow = document.querySelector("#shadow2");
|
||||
// shadow.shadowRoot.appendChild(newLine);
|
||||
// }, 2000);
|
||||
|
||||
// setTimeout(() => {
|
||||
// var newLine = document.createElement("div");
|
||||
// newLine.innerHTML = "<p>second line</p><p>third line</p>";
|
||||
// var shadow = document.querySelector("#shadow2");
|
||||
// shadow.shadowRoot.appendChild(newLine);
|
||||
// }, 3000);
|
||||
|
||||
// setTimeout(function () {
|
||||
// var el = document.querySelector("h2");
|
||||
// el.innerText = "hello world";
|
||||
|
||||
// var title = document.querySelector("#addtitle");
|
||||
// title.innerHTML =
|
||||
// "<div><p>second title</p><ul><li>second title</li><li><p>second title</p></li></ul></div>";
|
||||
// }, 1000);
|
||||
|
||||
setTimeout(function () {
|
||||
var el = document.querySelector("h2>p>span");
|
||||
el.innerText = "hello world";
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<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
|
||||
coming to Google Cloud with the C3A instances.
|
||||
<br />
|
||||
But these upcoming instances for now are only in private preview form.
|
||||
<br />
|
||||
<br />
|
||||
Needless to say I also haven't had any AmpereOne access to check out the
|
||||
performance and power efficiency of these new Arm server processors from
|
||||
Ampere Computing.
|
||||
<br />
|
||||
</div>
|
||||
<h2>
|
||||
<p>
|
||||
<span
|
||||
>React is a JavaScript library for building user interfaces.</span
|
||||
>
|
||||
</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>
|
||||
<h2>Shadow 2</h2>
|
||||
<div id="shadow2"></div>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
@@ -53,7 +126,16 @@
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<h2>
|
||||
React Server Components (or RSC) is a new application architecture
|
||||
designed by the React team.
|
||||
</h2>
|
||||
<iframe
|
||||
id="iframe1"
|
||||
width="800px"
|
||||
height="600px"
|
||||
src="http://localhost:3000/index.html"
|
||||
></iframe>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
@@ -86,7 +168,10 @@
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<h2>
|
||||
We’ve first shared our research on RSC in an introductory talk and an
|
||||
RFC.
|
||||
</h2>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
@@ -119,7 +204,17 @@
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<h2>
|
||||
To recap them, we are introducing a new kind of component—Server
|
||||
Components—that run ahead of time and are excluded from your JavaScript
|
||||
bundle.
|
||||
</h2>
|
||||
<iframe
|
||||
id="iframe2"
|
||||
width="800px"
|
||||
height="600px"
|
||||
src="https://react.dev/"
|
||||
></iframe>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
@@ -153,175 +248,42 @@
|
||||
<br />
|
||||
<br />
|
||||
<div class="cont cont1">
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Declarative: React makes it painless to create interactive UIs.
|
||||
Design simple views for each state in your application, and React
|
||||
will efficiently update and render just the right components when
|
||||
your data changes. Declarative views make your code more
|
||||
predictable, simpler to understand, and easier to debug.
|
||||
</li>
|
||||
<li>
|
||||
Component-Based: Build encapsulated components that manage their own
|
||||
state, then compose them to make complex UIs. Since component logic
|
||||
is written in JavaScript instead of templates, you can easily pass
|
||||
rich data through your app and keep the state out of the DOM.
|
||||
</li>
|
||||
<li>
|
||||
React 使创建交互式 UI
|
||||
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
|
||||
能高效更新并渲染合适的组件。
|
||||
</li>
|
||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<div class="cont cont2">
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Declarative: React makes it painless to create interactive UIs.
|
||||
Design simple views for each state in your application, and React
|
||||
will efficiently update and render just the right components when
|
||||
your data changes. Declarative views make your code more
|
||||
predictable, simpler to understand, and easier to debug.
|
||||
</li>
|
||||
<li>
|
||||
Component-Based: Build encapsulated components that manage their own
|
||||
state, then compose them to make complex UIs. Since component logic
|
||||
is written in JavaScript instead of templates, you can easily pass
|
||||
rich data through your app and keep the state out of the DOM.
|
||||
</li>
|
||||
<li>
|
||||
React 使创建交互式 UI
|
||||
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
|
||||
能高效更新并渲染合适的组件。
|
||||
</li>
|
||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<div class="cont cont3">
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Declarative: React makes it painless to create interactive UIs.
|
||||
Design simple views for each state in your application, and React
|
||||
will efficiently update and render just the right components when
|
||||
your data changes. Declarative views make your code more
|
||||
predictable, simpler to understand, and easier to debug.
|
||||
</li>
|
||||
<li>
|
||||
Component-Based: Build encapsulated components that manage their own
|
||||
state, then compose them to make complex UIs. Since component logic
|
||||
is written in JavaScript instead of templates, you can easily pass
|
||||
rich data through your app and keep the state out of the DOM.
|
||||
</li>
|
||||
<li>
|
||||
React 使创建交互式 UI
|
||||
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
|
||||
能高效更新并渲染合适的组件。
|
||||
</li>
|
||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<div class="cont cont4">
|
||||
<h2>
|
||||
React is a <code>JavaScript</code> <a href="#">library</a> for
|
||||
building user interfaces.
|
||||
Server Components can run during the build, letting you read from the
|
||||
filesystem or fetch static content.
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Declarative: React makes it painless to create interactive UIs.
|
||||
Design simple views for each state in your application, and React
|
||||
will efficiently update and render just the right components when
|
||||
your data changes. Declarative views make your code more
|
||||
predictable, simpler to understand, and easier to debug.
|
||||
</li>
|
||||
<li>
|
||||
Component-Based: Build encapsulated components that manage their own
|
||||
state, then compose them to make complex UIs. Since component logic
|
||||
is written in JavaScript instead of templates, you can easily pass
|
||||
rich data through your app and keep the state out of the DOM.
|
||||
</li>
|
||||
<li>
|
||||
React 使创建交互式 UI
|
||||
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
|
||||
能高效更新并渲染合适的组件。
|
||||
They can also run on the server, letting you access your data layer
|
||||
without having to build an API. You can pass data by props from
|
||||
Server Components to the interactive Client Components in the
|
||||
browser.
|
||||
</li>
|
||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<div class="cont cont5">
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<div class="cont cont2">
|
||||
<h2>
|
||||
Since our last update, we have merged the React Server Components RFC
|
||||
to ratify the proposal.
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Declarative: React makes it painless to create interactive UIs.
|
||||
Design simple views for each state in your application, and React
|
||||
will efficiently update and render just the right components when
|
||||
your data changes. Declarative views make your code more
|
||||
predictable, simpler to understand, and easier to debug.
|
||||
</li>
|
||||
<li>
|
||||
Component-Based: Build encapsulated components that manage their own
|
||||
state, then compose them to make complex UIs. Since component logic
|
||||
is written in JavaScript instead of templates, you can easily pass
|
||||
rich data through your app and keep the state out of the DOM.
|
||||
RSC combines the simple “request/response” mental model of
|
||||
server-centric Multi-Page Apps with the seamless interactivity of
|
||||
client-centric Single-Page Apps, giving you the best of both worlds.
|
||||
</li>
|
||||
<li>
|
||||
React 使创建交互式 UI
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.5.3",
|
||||
"version": "1.7.1",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -12,10 +12,16 @@
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": ["content.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
"matches": ["<all_urls>"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+K"
|
||||
}
|
||||
},
|
||||
"toggleTranslate": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+Q"
|
||||
@@ -27,6 +33,12 @@
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
},
|
||||
"openOptions": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+O"
|
||||
},
|
||||
"description": "__MSG_open_options__"
|
||||
}
|
||||
},
|
||||
"permissions": ["<all_urls>", "storage"],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.5.3",
|
||||
"version": "1.7.1",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -13,10 +13,16 @@
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": ["content.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
"matches": ["<all_urls>"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"_execute_action": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+K"
|
||||
}
|
||||
},
|
||||
"toggleTranslate": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+Q"
|
||||
@@ -28,6 +34,12 @@
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
},
|
||||
"openOptions": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+O"
|
||||
},
|
||||
"description": "__MSG_open_options__"
|
||||
}
|
||||
},
|
||||
"permissions": ["storage"],
|
||||
|
||||
@@ -3,14 +3,15 @@ import { fetchPolyfill } from "../libs/fetch";
|
||||
import {
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
URL_MICROSOFT_TRANS,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_LANGS_SPECIAL,
|
||||
PROMPT_PLACE_FROM,
|
||||
PROMPT_PLACE_TO,
|
||||
KV_SALT_SYNC,
|
||||
} from "../config";
|
||||
import { getSetting, detectLang } from "../libs";
|
||||
import { tryDetectLang } from "../libs";
|
||||
import { sha256 } from "../libs/utils";
|
||||
|
||||
/**
|
||||
@@ -20,7 +21,7 @@ import { sha256 } from "../libs/utils";
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
export const apiSyncData = async (url, key, data) =>
|
||||
export const apiSyncData = async (url, key, data, isBg = false) =>
|
||||
fetchPolyfill(url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
@@ -28,8 +29,17 @@ export const apiSyncData = async (url, key, data) =>
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
isBg,
|
||||
});
|
||||
|
||||
/**
|
||||
* 下载数据
|
||||
* @param {*} url
|
||||
* @param {*} isBg
|
||||
* @returns
|
||||
*/
|
||||
export const apiFetch = (url, isBg = false) => fetchPolyfill(url, { isBg });
|
||||
|
||||
/**
|
||||
* 谷歌翻译
|
||||
* @param {*} text
|
||||
@@ -37,7 +47,13 @@ export const apiSyncData = async (url, key, data) =>
|
||||
* @param {*} from
|
||||
* @returns
|
||||
*/
|
||||
const apiGoogleTranslate = async (translator, text, to, from) => {
|
||||
const apiGoogleTranslate = async (
|
||||
translator,
|
||||
text,
|
||||
to,
|
||||
from,
|
||||
{ url, key, useCache = true }
|
||||
) => {
|
||||
const params = {
|
||||
client: "gtx",
|
||||
dt: "t",
|
||||
@@ -47,17 +63,20 @@ const apiGoogleTranslate = async (translator, text, to, from) => {
|
||||
tl: to,
|
||||
q: text,
|
||||
};
|
||||
const { googleUrl } = await getSetting();
|
||||
const input = `${googleUrl}?${queryString.stringify(params)}`;
|
||||
return fetchPolyfill(
|
||||
input,
|
||||
{
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
const input = `${url}?${queryString.stringify(params)}`;
|
||||
const res = await fetchPolyfill(input, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
{ useCache: true, usePool: true, translator }
|
||||
);
|
||||
useCache,
|
||||
usePool: true,
|
||||
translator,
|
||||
token: key,
|
||||
});
|
||||
const trText = res.sentences.map((item) => item.trans).join(" ");
|
||||
const isSame = to === res.src;
|
||||
|
||||
return [trText, isSame];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -67,24 +86,72 @@ const apiGoogleTranslate = async (translator, text, to, from) => {
|
||||
* @param {*} from
|
||||
* @returns
|
||||
*/
|
||||
const apiMicrosoftTranslate = (translator, text, to, from) => {
|
||||
const apiMicrosoftTranslate = async (
|
||||
translator,
|
||||
text,
|
||||
to,
|
||||
from,
|
||||
{ url, useCache = true }
|
||||
) => {
|
||||
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 }]),
|
||||
const input = `${url}?${queryString.stringify(params)}`;
|
||||
const res = await fetchPolyfill(input, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
{ useCache: true, usePool: true, translator }
|
||||
);
|
||||
method: "POST",
|
||||
body: JSON.stringify([{ Text: text }]),
|
||||
useCache,
|
||||
usePool: true,
|
||||
translator,
|
||||
});
|
||||
const trText = res[0].translations[0].text;
|
||||
const isSame = to === res[0].detectedLanguage?.language;
|
||||
|
||||
return [trText, isSame];
|
||||
};
|
||||
|
||||
/**
|
||||
* DeepL翻译
|
||||
* @param {*} text
|
||||
* @param {*} to
|
||||
* @param {*} from
|
||||
* @returns
|
||||
*/
|
||||
const apiDeepLTranslate = async (
|
||||
translator,
|
||||
text,
|
||||
to,
|
||||
from,
|
||||
{ url, key, useCache = true }
|
||||
) => {
|
||||
const data = {
|
||||
text: [text],
|
||||
target_lang: to,
|
||||
split_sentences: "0",
|
||||
};
|
||||
if (from) {
|
||||
data.source_lang = from;
|
||||
}
|
||||
const res = await fetchPolyfill(url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
useCache,
|
||||
usePool: true,
|
||||
translator,
|
||||
token: key,
|
||||
});
|
||||
const trText = res.translations.map((item) => item.text).join(" ");
|
||||
const isSame = to === res.translations[0].detected_source_language;
|
||||
|
||||
return [trText, isSame];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -94,37 +161,82 @@ const apiMicrosoftTranslate = (translator, text, to, from) => {
|
||||
* @param {*} from
|
||||
* @returns
|
||||
*/
|
||||
const apiOpenaiTranslate = async (translator, text, to, from) => {
|
||||
const { openaiUrl, openaiKey, openaiModel, openaiPrompt } =
|
||||
await getSetting();
|
||||
let prompt = openaiPrompt
|
||||
const apiOpenaiTranslate = async (
|
||||
translator,
|
||||
text,
|
||||
to,
|
||||
from,
|
||||
{ url, key, model, prompt, useCache = true }
|
||||
) => {
|
||||
prompt = prompt
|
||||
.replaceAll(PROMPT_PLACE_FROM, from)
|
||||
.replaceAll(PROMPT_PLACE_TO, to);
|
||||
return fetchPolyfill(
|
||||
openaiUrl,
|
||||
{
|
||||
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,
|
||||
}),
|
||||
const res = await fetchPolyfill(url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
{ useCache: true, usePool: true, translator, token: openaiKey }
|
||||
);
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: prompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: text,
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 256,
|
||||
}),
|
||||
useCache,
|
||||
usePool: true,
|
||||
translator,
|
||||
token: key,
|
||||
});
|
||||
const trText = res?.choices?.[0].message.content;
|
||||
const sLang = await tryDetectLang(text);
|
||||
const tLang = await tryDetectLang(trText);
|
||||
const isSame = text === trText || (sLang && tLang && sLang === tLang);
|
||||
|
||||
return [trText, isSame];
|
||||
};
|
||||
|
||||
/**
|
||||
* 自定义接口 翻译
|
||||
* @param {*} text
|
||||
* @param {*} to
|
||||
* @param {*} from
|
||||
* @returns
|
||||
*/
|
||||
const apiCustomTranslate = async (
|
||||
translator,
|
||||
text,
|
||||
to,
|
||||
from,
|
||||
{ url, key, useCache = true }
|
||||
) => {
|
||||
const res = await fetchPolyfill(url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
}),
|
||||
useCache,
|
||||
usePool: true,
|
||||
translator,
|
||||
token: key,
|
||||
});
|
||||
const trText = res.text;
|
||||
const isSame = to === res.from;
|
||||
|
||||
return [trText, isSame];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -132,26 +244,29 @@ const apiOpenaiTranslate = async (translator, text, to, from) => {
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const apiTranslate = async ({ translator, q, fromLang, toLang }) => {
|
||||
let trText = "";
|
||||
let isSame = false;
|
||||
export const apiTranslate = ({
|
||||
translator,
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting,
|
||||
}) => {
|
||||
const from = OPT_LANGS_SPECIAL[translator]?.get(fromLang) ?? fromLang;
|
||||
const to = OPT_LANGS_SPECIAL[translator]?.get(toLang) ?? toLang;
|
||||
const callApi = (api) => api(translator, text, to, from, apiSetting);
|
||||
|
||||
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);
|
||||
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_OPENAI) {
|
||||
const res = await apiOpenaiTranslate(translator, q, to, from);
|
||||
trText = res?.choices?.[0].message.content;
|
||||
isSame = (await detectLang(q)) === (await detectLang(trText));
|
||||
switch (translator) {
|
||||
case OPT_TRANS_GOOGLE:
|
||||
return callApi(apiGoogleTranslate);
|
||||
case OPT_TRANS_MICROSOFT:
|
||||
return callApi(apiMicrosoftTranslate);
|
||||
case OPT_TRANS_DEEPL:
|
||||
return callApi(apiDeepLTranslate);
|
||||
case OPT_TRANS_OPENAI:
|
||||
return callApi(apiOpenaiTranslate);
|
||||
case OPT_TRANS_CUSTOMIZE:
|
||||
return callApi(apiCustomTranslate);
|
||||
default:
|
||||
return ["", false];
|
||||
}
|
||||
|
||||
return [trText, isSame];
|
||||
};
|
||||
|
||||
@@ -7,29 +7,20 @@ import {
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
CMD_TOGGLE_TRANSLATE,
|
||||
CMD_TOGGLE_STYLE,
|
||||
DEFAULT_SETTING,
|
||||
DEFAULT_RULES,
|
||||
DEFAULT_SYNC,
|
||||
STOKEY_SETTING,
|
||||
STOKEY_RULES,
|
||||
STOKEY_SYNC,
|
||||
CACHE_NAME,
|
||||
CMD_OPEN_OPTIONS,
|
||||
} from "./config";
|
||||
import storage from "./libs/storage";
|
||||
import { getSetting } from "./libs";
|
||||
import { syncAll } from "./libs/sync";
|
||||
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
|
||||
import { trySyncSettingAndRules } from "./libs/sync";
|
||||
import { fetchData, fetchPool } from "./libs/fetch";
|
||||
import { sendTabMsg } from "./libs/msg";
|
||||
import { trySyncAllSubRules } from "./libs/subRules";
|
||||
import { tryClearCaches } from "./libs";
|
||||
|
||||
/**
|
||||
* 插件安装
|
||||
*/
|
||||
browser.runtime.onInstalled.addListener(() => {
|
||||
console.log("KISS Translator onInstalled");
|
||||
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
|
||||
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES);
|
||||
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
|
||||
// todo:缓存内置rules
|
||||
tryInitDefaultData();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -39,13 +30,16 @@ browser.runtime.onStartup.addListener(async () => {
|
||||
console.log("browser onStartup");
|
||||
|
||||
// 同步数据
|
||||
await syncAll();
|
||||
await trySyncSettingAndRules(true);
|
||||
|
||||
// 清除缓存
|
||||
const { clearCache } = await getSetting();
|
||||
if (clearCache) {
|
||||
caches.delete(CACHE_NAME);
|
||||
const setting = await getSettingWithDefault();
|
||||
if (setting.clearCache) {
|
||||
tryClearCaches();
|
||||
}
|
||||
|
||||
// 同步订阅规则
|
||||
trySyncAllSubRules(setting, true);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -55,8 +49,8 @@ browser.runtime.onMessage.addListener(
|
||||
({ action, args }, sender, sendResponse) => {
|
||||
switch (action) {
|
||||
case MSG_FETCH:
|
||||
const { input, init, opts } = args;
|
||||
fetchData(input, init, opts)
|
||||
const { input, opts } = args;
|
||||
fetchData(input, opts)
|
||||
.then((data) => {
|
||||
sendResponse({ data });
|
||||
})
|
||||
@@ -92,6 +86,9 @@ browser.commands.onCommand.addListener((command) => {
|
||||
case CMD_TOGGLE_STYLE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
case CMD_OPEN_OPTIONS:
|
||||
browser.runtime.openOptionsPage();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
4
src/config/app.js
Normal file
4
src/config/app.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const APP_NAME = process.env.REACT_APP_NAME.trim()
|
||||
.split(/\s+/)
|
||||
.join("-");
|
||||
export const APP_LCNAME = APP_NAME.toLowerCase();
|
||||
@@ -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,6 +107,14 @@ export const I18N = {
|
||||
zh: `翻译`,
|
||||
en: `Translate`,
|
||||
},
|
||||
custom_api_help: {
|
||||
zh: customApiHelpZH,
|
||||
en: customApiHelpEN,
|
||||
},
|
||||
translate_alt: {
|
||||
zh: `翻译 (Alt+Q)`,
|
||||
en: `Translate (Alt+Q)`,
|
||||
},
|
||||
basic_setting: {
|
||||
zh: `基本设置`,
|
||||
en: `Basic Setting`,
|
||||
@@ -20,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`,
|
||||
@@ -41,17 +160,53 @@ export const I18N = {
|
||||
en: `Interface Language`,
|
||||
},
|
||||
fetch_limit: {
|
||||
zh: `最大请求数量`,
|
||||
en: `Maximum Number Of Request`,
|
||||
zh: `最大请求数量 (1-100)`,
|
||||
en: `Maximum Number Of Request (1-100)`,
|
||||
},
|
||||
fetch_interval: {
|
||||
zh: `请求间隔时间(ms)`,
|
||||
en: `Request Interval(ms)`,
|
||||
zh: `请求间隔时间 (0-5000ms)`,
|
||||
en: `Request Interval (0-5000ms)`,
|
||||
},
|
||||
min_translate_length: {
|
||||
zh: `最小翻译长度 (1-100)`,
|
||||
en: `Min Translate Length (1-100)`,
|
||||
},
|
||||
max_translate_length: {
|
||||
zh: `最大翻译长度 (100-10000)`,
|
||||
en: `Max Translate Length (100-10000)`,
|
||||
},
|
||||
num_of_newline_characters: {
|
||||
zh: `换行字符数 (1-1000)`,
|
||||
en: `Number of Newline Characters (1-1000)`,
|
||||
},
|
||||
translate_service: {
|
||||
zh: `翻译服务`,
|
||||
en: `Translate Service`,
|
||||
},
|
||||
mouseover_translation: {
|
||||
zh: `鼠标悬停翻译`,
|
||||
en: `Mouseover translation`,
|
||||
},
|
||||
mk_disable: {
|
||||
zh: `禁用`,
|
||||
en: `Disable`,
|
||||
},
|
||||
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`,
|
||||
@@ -64,10 +219,18 @@ export const I18N = {
|
||||
zh: `文字样式`,
|
||||
en: `Text Style`,
|
||||
},
|
||||
text_style_alt: {
|
||||
zh: `文字样式 (Alt+C)`,
|
||||
en: `Text Style (Alt+C)`,
|
||||
},
|
||||
bg_color: {
|
||||
zh: `样式颜色`,
|
||||
en: `Style Color`,
|
||||
},
|
||||
remain_unchanged: {
|
||||
zh: `保留不变`,
|
||||
en: `Remain Unchanged`,
|
||||
},
|
||||
google_api: {
|
||||
zh: `谷歌翻译接口`,
|
||||
en: `Google Translate API`,
|
||||
@@ -108,18 +271,30 @@ export const I18N = {
|
||||
zh: `注入订阅规则`,
|
||||
en: `Inject Subscribe Rules`,
|
||||
},
|
||||
edit_rules: {
|
||||
zh: `编辑规则`,
|
||||
en: `Edit Rules`,
|
||||
personal_rules: {
|
||||
zh: `个人规则`,
|
||||
en: `Rules`,
|
||||
},
|
||||
subscribe_rules: {
|
||||
zh: `订阅规则`,
|
||||
en: `Subscribe Rules`,
|
||||
en: `Subscribe`,
|
||||
},
|
||||
overwrite_subscribe_rules: {
|
||||
zh: `覆写订阅规则`,
|
||||
en: `Overwrite`,
|
||||
},
|
||||
subscribe_url: {
|
||||
zh: `订阅地址`,
|
||||
en: `Subscribe URL`,
|
||||
},
|
||||
rules_warn_1: {
|
||||
zh: `1、“个人规则”一直生效,选择“注入订阅规则”后,“订阅规则”才会生效。`,
|
||||
en: `1. The "Personal Rules" are always in effect. After selecting "Inject Subscription Rules", the "Subscription Rules" will take effect.`,
|
||||
},
|
||||
rules_warn_2: {
|
||||
zh: `2、“订阅规则”的注入位置是倒数第二的位置,因此除全局规则(*)外,“个人规则”优先级比“订阅规则”高,“个人规则”填写同样的网址会覆盖”订阅规则“的条目。`,
|
||||
en: `2. The injection position of "Subscription Rules" is the penultimate position. Therefore, except for the global rules (*), the priority of "Personal Rules" is higher than that of "Subscription Rules". Filling in the same url in "Personal Rules" will overwrite "Subscription Rules" entry.`,
|
||||
},
|
||||
sync_warn: {
|
||||
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
|
||||
en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`,
|
||||
@@ -128,6 +303,10 @@ export const I18N = {
|
||||
zh: `查看关于数据同步接口部署`,
|
||||
en: `View About Data Synchronization Interface Deployment`,
|
||||
},
|
||||
about_api_proxy: {
|
||||
zh: `查看自建一个翻译接口代理`,
|
||||
en: `Check out the self-built translation interface proxy`,
|
||||
},
|
||||
style_none: {
|
||||
zh: `无`,
|
||||
en: `None`,
|
||||
@@ -156,9 +335,17 @@ export const I18N = {
|
||||
zh: `高亮`,
|
||||
en: `Highlight`,
|
||||
},
|
||||
diy_style: {
|
||||
zh: `自定义样式`,
|
||||
en: `Custom Style`,
|
||||
},
|
||||
diy_style_helper: {
|
||||
zh: `遵循“CSS”的语法`,
|
||||
en: `Follow the syntax of "CSS"`,
|
||||
},
|
||||
setting: {
|
||||
zh: `设置`,
|
||||
en: `Setting`,
|
||||
zh: `设置 (Alt+O)`,
|
||||
en: `Setting (Alt+O)`,
|
||||
},
|
||||
pattern: {
|
||||
zh: `匹配网址`,
|
||||
@@ -169,8 +356,8 @@ export const I18N = {
|
||||
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`,
|
||||
},
|
||||
selector_helper: {
|
||||
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。`,
|
||||
en: `1. Follow CSS selector rules. 2. Leave blank to adopt the global setting.`,
|
||||
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: `开启翻译`,
|
||||
@@ -212,6 +399,14 @@ export const I18N = {
|
||||
zh: `请检查url地址是否正确或稍后再试。`,
|
||||
en: `Please check if the url address is correct or try again later.`,
|
||||
},
|
||||
deepl_api: {
|
||||
zh: `DeepL 接口`,
|
||||
en: `DeepL API`,
|
||||
},
|
||||
deepl_key: {
|
||||
zh: `DeepL 密钥`,
|
||||
en: `DeepL Key`,
|
||||
},
|
||||
openai_api: {
|
||||
zh: `OpenAI 接口`,
|
||||
en: `OpenAI API`,
|
||||
@@ -228,7 +423,7 @@ export const I18N = {
|
||||
zh: `OpenAI 提示词`,
|
||||
en: `OpenAI Prompt`,
|
||||
},
|
||||
clear_cache: {
|
||||
if_clear_cache: {
|
||||
zh: `是否清除缓存`,
|
||||
en: `Whether clear cache`,
|
||||
},
|
||||
@@ -248,12 +443,148 @@ export const I18N = {
|
||||
zh: `数据同步密钥`,
|
||||
en: `Data Sync Key`,
|
||||
},
|
||||
sync_now: {
|
||||
zh: `立即同步`,
|
||||
en: `Sync Now`,
|
||||
},
|
||||
sync_success: {
|
||||
zh: `同步成功!`,
|
||||
en: `Sync Success`,
|
||||
},
|
||||
sync_failed: {
|
||||
zh: `同步失败!`,
|
||||
en: `Sync Error`,
|
||||
},
|
||||
error_got_some_wrong: {
|
||||
zh: "抱歉,出错了!",
|
||||
en: "Sorry, something went wrong!",
|
||||
zh: `抱歉,出错了!`,
|
||||
en: `Sorry, something went wrong!`,
|
||||
},
|
||||
error_sync_setting: {
|
||||
zh: "您的同步设置未填写,无法在线分享。",
|
||||
en: "Your sync settings are missing and cannot be shared online.",
|
||||
zh: `您的同步设置未填写,无法在线分享。`,
|
||||
en: `Your sync settings are missing and cannot be shared online.`,
|
||||
},
|
||||
click_test: {
|
||||
zh: `点击测试`,
|
||||
en: `Click Test`,
|
||||
},
|
||||
test_success: {
|
||||
zh: `测试成功`,
|
||||
en: `Test success`,
|
||||
},
|
||||
test_failed: {
|
||||
zh: `测试失败`,
|
||||
en: `Test failed`,
|
||||
},
|
||||
clear_all_cache_now: {
|
||||
zh: `立即清除全部缓存`,
|
||||
en: `Clear all cache now`,
|
||||
},
|
||||
clear_cache: {
|
||||
zh: `清除缓存`,
|
||||
en: `Clear Cache`,
|
||||
},
|
||||
clear_success: {
|
||||
zh: `清除成功`,
|
||||
en: `Clear success`,
|
||||
},
|
||||
clear_failed: {
|
||||
zh: `清除失败`,
|
||||
en: `Clear failed`,
|
||||
},
|
||||
share: {
|
||||
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`,
|
||||
},
|
||||
show: {
|
||||
zh: `显示`,
|
||||
en: `Show`,
|
||||
},
|
||||
hide: {
|
||||
zh: `隐藏`,
|
||||
en: `Hide`,
|
||||
},
|
||||
save_rule: {
|
||||
zh: `保存规则`,
|
||||
en: `Save Rule`,
|
||||
},
|
||||
global_rule: {
|
||||
zh: `全局规则`,
|
||||
en: `Global Rule`,
|
||||
},
|
||||
input_setting: {
|
||||
zh: `输入框设置`,
|
||||
en: `Input Box Setting`,
|
||||
},
|
||||
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: `默认为单击“Alt+i”`,
|
||||
en: `Default is "Alt+i"`,
|
||||
},
|
||||
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"`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import {
|
||||
DEFAULT_SELECTOR,
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
SHADOW_KEY,
|
||||
DEFAULT_RULE,
|
||||
DEFAULT_OW_RULE,
|
||||
BUILTIN_RULES,
|
||||
} from "./rules";
|
||||
import { APP_NAME, APP_LCNAME } from "./app";
|
||||
export { I18N, UI_LANGS } from "./i18n";
|
||||
export { GLOBAL_KEY, DEFAULT_RULE, BUILTIN_RULES };
|
||||
|
||||
const APP_NAME = process.env.REACT_APP_NAME.trim().split(/\s+/).join("-");
|
||||
|
||||
export const APP_LCNAME = APP_NAME.toLowerCase();
|
||||
export {
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
SHADOW_KEY,
|
||||
DEFAULT_RULE,
|
||||
DEFAULT_OW_RULE,
|
||||
BUILTIN_RULES,
|
||||
APP_LCNAME,
|
||||
};
|
||||
|
||||
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
|
||||
export const STOKEY_SETTING = `${APP_NAME}_setting`;
|
||||
@@ -17,9 +25,11 @@ export const STOKEY_RULES = `${APP_NAME}_rules`;
|
||||
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 CLIENT_WEB = "web";
|
||||
export const CLIENT_CHROME = "chrome";
|
||||
@@ -45,25 +55,29 @@ export const MSG_TRANS_GETRULE = "trans_getrule";
|
||||
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
||||
export const MSG_TRANS_CURRULE = "trans_currule";
|
||||
|
||||
export const EVENT_KISS = "kissEvent";
|
||||
|
||||
export const THEME_LIGHT = "light";
|
||||
export const THEME_DARK = "dark";
|
||||
|
||||
export const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker";
|
||||
export const URL_KISS_PROXY = "https://github.com/fishjar/kiss-proxy";
|
||||
export const URL_KISS_RULES = "https://github.com/fishjar/kiss-rules";
|
||||
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 =
|
||||
"https://api-edge.cognitive.microsofttranslator.com/translate";
|
||||
|
||||
export const OPT_TRANS_GOOGLE = "Google";
|
||||
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
||||
export const OPT_TRANS_DEEPL = "DeepL";
|
||||
export const OPT_TRANS_OPENAI = "OpenAI";
|
||||
export const OPT_TRANS_CUSTOMIZE = "Custom";
|
||||
export const OPT_TRANS_ALL = [
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
];
|
||||
|
||||
export const OPT_LANGS_TO = [
|
||||
@@ -112,10 +126,18 @@ export const OPT_LANGS_SPECIAL = {
|
||||
["zh-CN", "zh-Hans"],
|
||||
["zh-TW", "zh-Hant"],
|
||||
]),
|
||||
[OPT_TRANS_DEEPL]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
|
||||
["auto", ""],
|
||||
["zh-CN", "ZH"],
|
||||
["zh-TW", "ZH"],
|
||||
]),
|
||||
[OPT_TRANS_OPENAI]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_CUSTOMIZE]: new Map([["auto", ""]]),
|
||||
};
|
||||
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
|
||||
|
||||
export const OPT_STYLE_NONE = "style_none"; // 无
|
||||
export const OPT_STYLE_LINE = "under_line"; // 下划线
|
||||
@@ -123,7 +145,8 @@ export const OPT_STYLE_DOTLINE = "dot_line"; // 点状线
|
||||
export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
|
||||
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
|
||||
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
|
||||
export const OPT_STYLE_HIGHTLIGHT = "highlight"; // 高亮
|
||||
export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
|
||||
export const OPT_STYLE_DIY = "diy_style"; // 自定义样式
|
||||
export const OPT_STYLE_ALL = [
|
||||
OPT_STYLE_NONE,
|
||||
OPT_STYLE_LINE,
|
||||
@@ -131,7 +154,28 @@ export const OPT_STYLE_ALL = [
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_FUZZY,
|
||||
OPT_STYLE_HIGHTLIGHT,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_DIY,
|
||||
];
|
||||
export const OPT_STYLE_USE_COLOR = [
|
||||
OPT_STYLE_LINE,
|
||||
OPT_STYLE_DOTLINE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
];
|
||||
|
||||
export const OPT_MOUSEKEY_DISABLE = "mk_disable";
|
||||
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_MOUSEOVER,
|
||||
OPT_MOUSEKEY_CONTROL,
|
||||
OPT_MOUSEKEY_SHIFT,
|
||||
OPT_MOUSEKEY_ALT,
|
||||
];
|
||||
|
||||
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
|
||||
@@ -152,39 +196,103 @@ export const GLOBLA_RULE = {
|
||||
textStyle: OPT_STYLE_DASHLINE,
|
||||
transOpen: "false",
|
||||
bgColor: "",
|
||||
textDiyStyle: "",
|
||||
};
|
||||
|
||||
// 输入框翻译
|
||||
export const OPT_INPUT_TRANS_SIGNS = ["/", "//", "\\", "\\\\", ">", ">>"];
|
||||
export const DEFAULT_INPUT_SHORTCUT = ["Alt", "i"];
|
||||
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_SUBRULES_LIST = [
|
||||
{
|
||||
url: "https://kiss-translator.rayjar.com/kiss-translator-rules.json",
|
||||
url: process.env.REACT_APP_RULESURL,
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
url: process.env.REACT_APP_RULESURL_ON,
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
url: "https://fishjar.github.io/kiss-translator/kiss-translator-rules.json",
|
||||
url: process.env.REACT_APP_RULESURL_OFF,
|
||||
selected: false,
|
||||
},
|
||||
];
|
||||
|
||||
// 翻译接口
|
||||
export const DEFAULT_TRANS_APIS = {
|
||||
[OPT_TRANS_GOOGLE]: {
|
||||
url: "https://translate.googleapis.com/translate_a/single",
|
||||
key: "",
|
||||
},
|
||||
[OPT_TRANS_MICROSOFT]: {
|
||||
url: "https://api-edge.cognitive.microsofttranslator.com/translate",
|
||||
authUrl: "https://edge.microsoft.com/translate/auth",
|
||||
},
|
||||
[OPT_TRANS_DEEPL]: {
|
||||
url: "https://api-free.deepl.com/v2/translate",
|
||||
key: "",
|
||||
},
|
||||
[OPT_TRANS_OPENAI]: {
|
||||
url: "https://api.openai.com/v1/chat/completion",
|
||||
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_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]: ["Alt", "q"],
|
||||
[OPT_SHORTCUT_STYLE]: ["Alt", "c"],
|
||||
[OPT_SHORTCUT_POPUP]: ["Alt", "k"],
|
||||
[OPT_SHORTCUT_SETTING]: ["Alt", "o"],
|
||||
};
|
||||
|
||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
||||
export const TRANS_NEWLINE_LENGTH = 40; // 换行字符数
|
||||
|
||||
export const DEFAULT_SETTING = {
|
||||
darkMode: false, // 深色模式
|
||||
uiLang: "en", // 界面语言
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
||||
minLength: TRANS_MIN_LENGTH,
|
||||
maxLength: TRANS_MAX_LENGTH,
|
||||
newlineLength: TRANS_NEWLINE_LENGTH,
|
||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||
injectRules: true, // 是否注入订阅规则
|
||||
injectWebfix: true, // 是否注入修复补丁
|
||||
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
||||
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口
|
||||
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}.`,
|
||||
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
|
||||
transApis: DEFAULT_TRANS_APIS, // 翻译接口
|
||||
mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译
|
||||
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
||||
hideFab: false, // 是否隐藏按钮
|
||||
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
||||
};
|
||||
|
||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||
|
||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
||||
|
||||
export const DEFAULT_SYNC = {
|
||||
syncUrl: "", // 数据同步接口
|
||||
syncKey: "", // 数据同步密钥
|
||||
@@ -192,4 +300,6 @@ export const DEFAULT_SYNC = {
|
||||
settingSyncAt: 0,
|
||||
rulesUpdateAt: 0,
|
||||
rulesSyncAt: 0,
|
||||
subRulesSyncAt: 0, // 订阅规则同步时间
|
||||
dataCaches: {}, // 缓存同步时间
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
const els = `li, p, h1, h2, h3, h4, h5, h6, dd`;
|
||||
const els = `li, p, h1, h2, h3, h4, h5, h6, dd, blockquote`;
|
||||
|
||||
export const DEFAULT_SELECTOR = `:is(${els})`;
|
||||
|
||||
export const GLOBAL_KEY = "*";
|
||||
export const REMAIN_KEY = "-";
|
||||
|
||||
export const SHADOW_KEY = ">>>";
|
||||
|
||||
export const DEFAULT_RULE = {
|
||||
pattern: "",
|
||||
@@ -13,6 +16,30 @@ export const DEFAULT_RULE = {
|
||||
textStyle: GLOBAL_KEY,
|
||||
transOpen: GLOBAL_KEY,
|
||||
bgColor: "",
|
||||
textDiyStyle: "",
|
||||
};
|
||||
|
||||
const DEFAULT_DIY_STYLE = `color: #666;
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
LightGreen 20%,
|
||||
LightPink 20% 40%,
|
||||
LightSalmon 40% 60%,
|
||||
LightSeaGreen 60% 80%,
|
||||
LightSkyBlue 80%
|
||||
);
|
||||
&:hover {
|
||||
color: #333;
|
||||
};`;
|
||||
|
||||
export const DEFAULT_OW_RULE = {
|
||||
translator: REMAIN_KEY,
|
||||
fromLang: REMAIN_KEY,
|
||||
toLang: REMAIN_KEY,
|
||||
textStyle: REMAIN_KEY,
|
||||
transOpen: REMAIN_KEY,
|
||||
bgColor: "",
|
||||
textDiyStyle: DEFAULT_DIY_STYLE,
|
||||
};
|
||||
|
||||
const RULES = [
|
||||
@@ -21,9 +48,13 @@ const RULES = [
|
||||
selector: `h3, .IsZvec, .VwiC3b`,
|
||||
},
|
||||
{
|
||||
pattern: `https://news.google.com/`,
|
||||
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,
|
||||
@@ -146,7 +177,9 @@ const RULES = [
|
||||
},
|
||||
];
|
||||
|
||||
export const BUILTIN_RULES = RULES.map((item) => ({
|
||||
export const BUILTIN_RULES = RULES.sort((a, b) =>
|
||||
a.pattern.localeCompare(b.pattern)
|
||||
).map((item) => ({
|
||||
...DEFAULT_RULE,
|
||||
...item,
|
||||
transOpen: "true",
|
||||
|
||||
@@ -5,17 +5,22 @@ import {
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
} from "./config";
|
||||
import { getSetting, getRules, matchRule } from "./libs";
|
||||
import { getSettingWithDefault, getRulesWithDefault } from "./libs/storage";
|
||||
import { Translator } from "./libs/translator";
|
||||
import { isIframe } from "./libs/iframe";
|
||||
import { matchRule } from "./libs/rules";
|
||||
import { webfix } from "./libs/webfix";
|
||||
|
||||
/**
|
||||
* 入口函数
|
||||
*/
|
||||
(async () => {
|
||||
const setting = await getSetting();
|
||||
const rules = await getRules();
|
||||
const rule = await matchRule(rules, document.location.href, setting);
|
||||
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);
|
||||
webfix(href, setting);
|
||||
|
||||
// 监听消息
|
||||
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||
@@ -36,4 +41,16 @@ import { Translator } from "./libs/translator";
|
||||
}
|
||||
return { data: translator.rule };
|
||||
});
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await init();
|
||||
} catch (err) {
|
||||
console.error("[KISS-Translator]", err);
|
||||
const $err = document.createElement("div");
|
||||
$err.innerText = `KISS-Translator: ${err.message}`;
|
||||
$err.style.cssText = "background:red; color:#fff;";
|
||||
document.body.prepend($err);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -20,11 +20,6 @@ export function AlertProvider({ children }) {
|
||||
const [severity, setSeverity] = useState("info");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const error = (msg) => showAlert(msg, "error");
|
||||
const warning = (msg) => showAlert(msg, "warning");
|
||||
const info = (msg) => showAlert(msg, "info");
|
||||
const success = (msg) => showAlert(msg, "success");
|
||||
|
||||
const showAlert = (msg, type) => {
|
||||
setOpen(true);
|
||||
setMessage(msg);
|
||||
@@ -38,6 +33,11 @@ export function AlertProvider({ children }) {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const error = (msg) => showAlert(msg, "error");
|
||||
const warning = (msg) => showAlert(msg, "warning");
|
||||
const info = (msg) => showAlert(msg, "info");
|
||||
const success = (msg) => showAlert(msg, "success");
|
||||
|
||||
return (
|
||||
<AlertContext.Provider value={{ error, warning, info, success }}>
|
||||
{children}
|
||||
|
||||
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 };
|
||||
}
|
||||
@@ -1,22 +1,19 @@
|
||||
import { useSetting, useSettingUpdate } from "./Setting";
|
||||
import { useCallback } from "react";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
/**
|
||||
* 深色模式hook
|
||||
* @returns
|
||||
*/
|
||||
export function useDarkMode() {
|
||||
const setting = useSetting();
|
||||
return !!setting?.darkMode;
|
||||
}
|
||||
const {
|
||||
setting: { darkMode },
|
||||
updateSetting,
|
||||
} = useSetting();
|
||||
|
||||
/**
|
||||
* 切换深色模式
|
||||
* @returns
|
||||
*/
|
||||
export function useDarkModeSwitch() {
|
||||
const darkMode = useDarkMode();
|
||||
const updateSetting = useSettingUpdate();
|
||||
return async () => {
|
||||
const toggleDarkMode = useCallback(async () => {
|
||||
await updateSetting({ darkMode: !darkMode });
|
||||
};
|
||||
}, [darkMode, updateSetting]);
|
||||
|
||||
return { darkMode, toggleDarkMode };
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import { useFetch } from "./Fetch";
|
||||
* @returns
|
||||
*/
|
||||
export const useI18n = () => {
|
||||
const { uiLang } = useSetting() ?? {};
|
||||
const {
|
||||
setting: { uiLang },
|
||||
} = useSetting();
|
||||
return (key, defaultText = "") => I18N?.[key]?.[uiLang] ?? defaultText;
|
||||
};
|
||||
|
||||
|
||||
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,107 +1,88 @@
|
||||
import { STOKEY_RULES, DEFAULT_SUBRULES_LIST } from "../config";
|
||||
import storage from "../libs/storage";
|
||||
import { useStorages } from "./Storage";
|
||||
import { syncRules } from "../libs/sync";
|
||||
import { useSync } from "./Sync";
|
||||
import { useSetting, useSettingUpdate } from "./Setting";
|
||||
import { STOKEY_RULES, DEFAULT_RULES } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
import { trySyncRules } from "../libs/sync";
|
||||
import { checkRules } from "../libs/rules";
|
||||
import { useCallback } from "react";
|
||||
|
||||
/**
|
||||
* 匹配规则增删改查 hook
|
||||
* 规则 hook
|
||||
* @returns
|
||||
*/
|
||||
export function useRules() {
|
||||
const storages = useStorages();
|
||||
const list = storages?.[STOKEY_RULES] || [];
|
||||
const sync = useSync();
|
||||
const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
|
||||
|
||||
const update = async (rules) => {
|
||||
const updateAt = sync.opt?.rulesUpdateAt ? Date.now() : 0;
|
||||
await storage.setObj(STOKEY_RULES, rules);
|
||||
await sync.update({ rulesUpdateAt: updateAt });
|
||||
syncRules();
|
||||
};
|
||||
const updateRules = useCallback(
|
||||
async (rules) => {
|
||||
await save(rules);
|
||||
trySyncRules(false, true);
|
||||
},
|
||||
[save]
|
||||
);
|
||||
|
||||
const add = async (rule) => {
|
||||
const rules = [...list];
|
||||
if (rule.pattern === "*") {
|
||||
return;
|
||||
}
|
||||
if (rules.map((item) => item.pattern).includes(rule.pattern)) {
|
||||
return;
|
||||
}
|
||||
rules.unshift(rule);
|
||||
await update(rules);
|
||||
};
|
||||
const add = useCallback(
|
||||
async (rule) => {
|
||||
const rules = [...list];
|
||||
if (rule.pattern === "*") {
|
||||
return;
|
||||
}
|
||||
if (rules.map((item) => item.pattern).includes(rule.pattern)) {
|
||||
return;
|
||||
}
|
||||
rules.unshift(rule);
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
const del = async (pattern) => {
|
||||
const del = useCallback(
|
||||
async (pattern) => {
|
||||
let rules = [...list];
|
||||
if (pattern === "*") {
|
||||
return;
|
||||
}
|
||||
rules = rules.filter((item) => item.pattern !== pattern);
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
const clear = useCallback(async () => {
|
||||
let rules = [...list];
|
||||
if (pattern === "*") {
|
||||
return;
|
||||
}
|
||||
rules = rules.filter((item) => item.pattern !== pattern);
|
||||
await update(rules);
|
||||
};
|
||||
rules = rules.filter((item) => item.pattern === "*");
|
||||
await updateRules(rules);
|
||||
}, [list, updateRules]);
|
||||
|
||||
const put = async (pattern, obj) => {
|
||||
const rules = [...list];
|
||||
if (pattern === "*") {
|
||||
obj.pattern = "*";
|
||||
}
|
||||
const rule = rules.find((r) => r.pattern === pattern);
|
||||
rule && Object.assign(rule, obj);
|
||||
await update(rules);
|
||||
};
|
||||
|
||||
const merge = async (newRules) => {
|
||||
const rules = [...list];
|
||||
newRules = checkRules(newRules);
|
||||
newRules.forEach((newRule) => {
|
||||
const rule = rules.find((oldRule) => oldRule.pattern === newRule.pattern);
|
||||
if (rule) {
|
||||
Object.assign(rule, newRule);
|
||||
} else {
|
||||
rules.unshift(newRule);
|
||||
const put = useCallback(
|
||||
async (pattern, obj) => {
|
||||
const rules = [...list];
|
||||
if (pattern === "*") {
|
||||
obj.pattern = "*";
|
||||
}
|
||||
});
|
||||
await update(rules);
|
||||
};
|
||||
const rule = rules.find((r) => r.pattern === pattern);
|
||||
rule && Object.assign(rule, obj);
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
return { list, add, del, put, merge };
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅规则
|
||||
* @returns
|
||||
*/
|
||||
export function useSubrules() {
|
||||
const setting = useSetting();
|
||||
const updateSetting = useSettingUpdate();
|
||||
const list = setting?.subrulesList || DEFAULT_SUBRULES_LIST;
|
||||
|
||||
const select = async (url) => {
|
||||
const subrulesList = [...list];
|
||||
subrulesList.forEach((item) => {
|
||||
if (item.url === url) {
|
||||
item.selected = true;
|
||||
} else {
|
||||
item.selected = false;
|
||||
}
|
||||
});
|
||||
await updateSetting({ subrulesList });
|
||||
};
|
||||
|
||||
const add = async (url) => {
|
||||
const subrulesList = [...list];
|
||||
subrulesList.push({ url });
|
||||
await updateSetting({ subrulesList });
|
||||
};
|
||||
|
||||
const del = async (url) => {
|
||||
let subrulesList = [...list];
|
||||
subrulesList = subrulesList.filter((item) => item.url !== url);
|
||||
await updateSetting({ subrulesList });
|
||||
};
|
||||
|
||||
return { list, select, add, del };
|
||||
const merge = useCallback(
|
||||
async (newRules) => {
|
||||
const rules = [...list];
|
||||
newRules = checkRules(newRules);
|
||||
newRules.forEach((newRule) => {
|
||||
const rule = rules.find(
|
||||
(oldRule) => oldRule.pattern === newRule.pattern
|
||||
);
|
||||
if (rule) {
|
||||
Object.assign(rule, newRule);
|
||||
} else {
|
||||
rules.unshift(newRule);
|
||||
}
|
||||
});
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
return { list, add, del, clear, put, merge };
|
||||
}
|
||||
|
||||
@@ -1,28 +1,58 @@
|
||||
import { STOKEY_SETTING } from "../config";
|
||||
import storage from "../libs/storage";
|
||||
import { useStorages } from "./Storage";
|
||||
import { useSync } from "./Sync";
|
||||
import { syncSetting } from "../libs/sync";
|
||||
import { STOKEY_SETTING, DEFAULT_SETTING } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
import { trySyncSetting } from "../libs/sync";
|
||||
import { createContext, useCallback, useContext, useMemo } from "react";
|
||||
import { debounce } from "../libs/utils";
|
||||
|
||||
const SettingContext = createContext({
|
||||
setting: {},
|
||||
updateSetting: async () => {},
|
||||
reloadSetting: async () => {},
|
||||
});
|
||||
|
||||
export function SettingProvider({ children }) {
|
||||
const { data, update, reload, loading } = useStorage(
|
||||
STOKEY_SETTING,
|
||||
DEFAULT_SETTING
|
||||
);
|
||||
|
||||
const syncSetting = useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
trySyncSetting(false, true);
|
||||
}, [2000]),
|
||||
[]
|
||||
);
|
||||
|
||||
const updateSetting = useCallback(
|
||||
async (obj) => {
|
||||
await update(obj);
|
||||
syncSetting();
|
||||
},
|
||||
[update, syncSetting]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContext.Provider
|
||||
value={{
|
||||
setting: data,
|
||||
updateSetting,
|
||||
reloadSetting: reload,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SettingContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置hook
|
||||
* 设置 hook
|
||||
* @returns
|
||||
*/
|
||||
export function useSetting() {
|
||||
const storages = useStorages();
|
||||
return storages?.[STOKEY_SETTING];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设置
|
||||
* @returns
|
||||
*/
|
||||
export function useSettingUpdate() {
|
||||
const sync = useSync();
|
||||
return async (obj) => {
|
||||
const updateAt = sync.opt?.settingUpdateAt ? Date.now() : 0;
|
||||
await storage.putObj(STOKEY_SETTING, obj);
|
||||
await sync.update({ settingUpdateAt: updateAt });
|
||||
syncSetting();
|
||||
};
|
||||
return useContext(SettingContext);
|
||||
}
|
||||
|
||||
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,91 +1,53 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import { browser, isExt, isGm, isWeb } from "../libs/browser";
|
||||
import {
|
||||
STOKEY_SETTING,
|
||||
STOKEY_RULES,
|
||||
STOKEY_SYNC,
|
||||
DEFAULT_SETTING,
|
||||
DEFAULT_RULES,
|
||||
DEFAULT_SYNC,
|
||||
} from "../config";
|
||||
import storage from "../libs/storage";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { storage } from "../libs/storage";
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
export const defaultStorage = {
|
||||
[STOKEY_SETTING]: DEFAULT_SETTING,
|
||||
[STOKEY_RULES]: DEFAULT_RULES,
|
||||
[STOKEY_SYNC]: DEFAULT_SYNC,
|
||||
};
|
||||
export function useStorage(key, defaultVal = null) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [data, setData] = useState(defaultVal);
|
||||
|
||||
const activeKeys = Object.keys(defaultStorage);
|
||||
const save = useCallback(
|
||||
async (val) => {
|
||||
setData(val);
|
||||
await storage.setObj(key, val);
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
||||
const StoragesContext = createContext(null);
|
||||
const update = useCallback(
|
||||
async (obj) => {
|
||||
setData((pre) => ({ ...pre, ...obj }));
|
||||
await storage.putObj(key, obj);
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
||||
export function StoragesProvider({ children }) {
|
||||
const [storages, setStorages] = useState(null);
|
||||
const remove = useCallback(async () => {
|
||||
setData(null);
|
||||
await storage.del(key);
|
||||
}, [key]);
|
||||
|
||||
const handleChanged = (changes) => {
|
||||
if (isWeb || isGm) {
|
||||
const { key, oldValue, newValue } = changes;
|
||||
changes = {
|
||||
[key]: {
|
||||
oldValue,
|
||||
newValue,
|
||||
},
|
||||
};
|
||||
const reload = useCallback(async () => {
|
||||
const val = await storage.getObj(key);
|
||||
if (val) {
|
||||
setData(val);
|
||||
} else if (defaultVal) {
|
||||
setData(defaultVal);
|
||||
await storage.setObj(key, defaultVal);
|
||||
}
|
||||
const newStorages = {};
|
||||
Object.entries(changes)
|
||||
.filter(
|
||||
([key, { oldValue, newValue }]) =>
|
||||
activeKeys.includes(key) && oldValue !== newValue
|
||||
)
|
||||
.forEach(([key, { newValue }]) => {
|
||||
newStorages[key] = JSON.parse(newValue);
|
||||
});
|
||||
if (Object.keys(newStorages).length !== 0) {
|
||||
setStorages((pre) => ({ ...pre, ...newStorages }));
|
||||
}
|
||||
};
|
||||
}, [key, defaultVal]);
|
||||
|
||||
useEffect(() => {
|
||||
// 首次从storage同步配置到内存
|
||||
(async () => {
|
||||
const curStorages = {};
|
||||
for (const key of activeKeys) {
|
||||
const val = await storage.get(key);
|
||||
if (val) {
|
||||
curStorages[key] = JSON.parse(val);
|
||||
} else {
|
||||
await storage.setObj(key, defaultStorage[key]);
|
||||
curStorages[key] = defaultStorage[key];
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
await reload();
|
||||
} catch (err) {
|
||||
//
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
setStorages(curStorages);
|
||||
})();
|
||||
}, [reload]);
|
||||
|
||||
// 监听storage,并同步到内存中
|
||||
storage.onChanged(handleChanged);
|
||||
|
||||
// 解除监听
|
||||
return () => {
|
||||
if (isExt) {
|
||||
browser.storage.onChanged.removeListener(handleChanged);
|
||||
} else {
|
||||
window.removeEventListener("storage", handleChanged);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StoragesContext.Provider value={storages}>
|
||||
{children}
|
||||
</StoragesContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useStorages() {
|
||||
return useContext(StoragesContext);
|
||||
return { data, save, update, remove, reload, loading };
|
||||
}
|
||||
|
||||
113
src/hooks/SubRules.js
Normal file
113
src/hooks/SubRules.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import { DEFAULT_SUBRULES_LIST, DEFAULT_OW_RULE } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { loadOrFetchSubRules } from "../libs/subRules";
|
||||
import { delSubRules } from "../libs/storage";
|
||||
|
||||
/**
|
||||
* 订阅规则
|
||||
* @returns
|
||||
*/
|
||||
export function useSubRules() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRules, setSelectedRules] = useState([]);
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const list = setting?.subrulesList || DEFAULT_SUBRULES_LIST;
|
||||
|
||||
const selectedSub = useMemo(() => list.find((item) => item.selected), [list]);
|
||||
const selectedUrl = selectedSub.url;
|
||||
|
||||
const selectSub = useCallback(
|
||||
async (url) => {
|
||||
const subrulesList = [...list];
|
||||
subrulesList.forEach((item) => {
|
||||
if (item.url === url) {
|
||||
item.selected = true;
|
||||
} else {
|
||||
item.selected = false;
|
||||
}
|
||||
});
|
||||
await updateSetting({ subrulesList });
|
||||
},
|
||||
[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];
|
||||
subrulesList.push({ url, selected: false });
|
||||
await updateSetting({ subrulesList });
|
||||
},
|
||||
[list, updateSetting]
|
||||
);
|
||||
|
||||
const delSub = useCallback(
|
||||
async (url) => {
|
||||
let subrulesList = [...list];
|
||||
subrulesList = subrulesList.filter((item) => item.url !== url);
|
||||
await updateSetting({ subrulesList });
|
||||
await delSubRules(url);
|
||||
},
|
||||
[list, updateSetting]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (selectedUrl) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const rules = await loadOrFetchSubRules(selectedUrl);
|
||||
setSelectedRules(rules);
|
||||
} catch (err) {
|
||||
console.log("[loadOrFetchSubRules]", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [selectedUrl]);
|
||||
|
||||
return {
|
||||
subList: list,
|
||||
selectSub,
|
||||
updateSub,
|
||||
addSub,
|
||||
delSub,
|
||||
selectedSub,
|
||||
selectedUrl,
|
||||
selectedRules,
|
||||
setSelectedRules,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆写订阅规则
|
||||
* @returns
|
||||
*/
|
||||
export function useOwSubRule() {
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const { owSubrule = DEFAULT_OW_RULE } = setting;
|
||||
|
||||
const updateOwSubrule = useCallback(
|
||||
async (obj) => {
|
||||
await updateSetting({ owSubrule: { ...owSubrule, ...obj } });
|
||||
},
|
||||
[owSubrule, updateSetting]
|
||||
);
|
||||
|
||||
return { owSubrule, updateOwSubrule };
|
||||
}
|
||||
@@ -1,20 +1,46 @@
|
||||
import { useCallback } from "react";
|
||||
import { STOKEY_SYNC } from "../config";
|
||||
import storage from "../libs/storage";
|
||||
import { useStorages } from "./Storage";
|
||||
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
|
||||
/**
|
||||
* sync hook
|
||||
* @returns
|
||||
*/
|
||||
export function useSync() {
|
||||
const storages = useStorages();
|
||||
const opt = storages?.[STOKEY_SYNC];
|
||||
const update = useCallback(async (obj) => {
|
||||
await storage.putObj(STOKEY_SYNC, obj);
|
||||
}, []);
|
||||
const { data, update, reload } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
|
||||
return { sync: data, updateSync: update, reloadSync: reload };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
opt,
|
||||
update,
|
||||
dataCaches: sync.dataCaches || {},
|
||||
updateDataCache,
|
||||
deleteDataCache,
|
||||
reloadSync,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import { THEME_DARK, THEME_LIGHT } from "../config";
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function MuiThemeProvider({ children, options }) {
|
||||
const darkMode = useDarkMode();
|
||||
export default function Theme({ children, options }) {
|
||||
const { darkMode } = useDarkMode();
|
||||
const theme = useMemo(() => {
|
||||
return createTheme({
|
||||
palette: {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { detectLang } from "../libs";
|
||||
import { tryDetectLang } from "../libs";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { DEFAULT_TRANS_APIS } from "../config";
|
||||
|
||||
/**
|
||||
* 翻译hook
|
||||
* @param {*} q
|
||||
* @param {*} rule
|
||||
* @param {*} setting
|
||||
* @returns
|
||||
*/
|
||||
export function useTranslate(q, rule) {
|
||||
export function useTranslate(q, rule, setting) {
|
||||
const [text, setText] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sameLang, setSamelang] = useState(false);
|
||||
@@ -21,15 +23,16 @@ export function useTranslate(q, rule) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const deLang = await detectLang(q);
|
||||
if (toLang.includes(deLang)) {
|
||||
const deLang = await tryDetectLang(q);
|
||||
if (deLang && toLang.includes(deLang)) {
|
||||
setSamelang(true);
|
||||
} else {
|
||||
const [trText, isSame] = await apiTranslate({
|
||||
translator,
|
||||
q,
|
||||
text: q,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting: (setting.transApis || DEFAULT_TRANS_APIS)[translator],
|
||||
});
|
||||
setText(trText);
|
||||
setSamelang(isSame);
|
||||
@@ -40,7 +43,7 @@ export function useTranslate(q, rule) {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [q, translator, fromLang, toLang]);
|
||||
}, [q, translator, fromLang, toLang, setting]);
|
||||
|
||||
return { text, sameLang, loading };
|
||||
}
|
||||
|
||||
45
src/index.js
45
src/index.js
@@ -1,19 +1,58 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Button from "@mui/material/Button";
|
||||
import Link from "@mui/material/Link";
|
||||
import { useFetch } from "./hooks/Fetch";
|
||||
import { I18N, URL_RAW_PREFIX } from "./config";
|
||||
|
||||
function App() {
|
||||
const [lang, setLang] = useState("zh");
|
||||
const [data, loading, error] = useFetch(
|
||||
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.["zh"]}`
|
||||
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}`
|
||||
);
|
||||
return (
|
||||
<Paper sx={{ padding: 2, margin: 2 }}>
|
||||
<Divider>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Divider>
|
||||
<Stack spacing={2} direction="row" justifyContent="flex-end">
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setLang((pre) => (pre === "zh" ? "en" : "zh"));
|
||||
}}
|
||||
>
|
||||
{lang === "zh" ? "ENGLISH" : "中文"}
|
||||
</Button>
|
||||
</Stack>
|
||||
<Divider>
|
||||
<Link
|
||||
href={process.env.REACT_APP_HOMEPAGE}
|
||||
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||
</Divider>
|
||||
<Stack spacing={2} direction="row" useFlexGap flexWrap="wrap">
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
||||
Install Userscript 1
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
|
||||
Install Userscript 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
|
||||
</Link>
|
||||
</Stack>
|
||||
|
||||
{loading ? (
|
||||
<center>
|
||||
<CircularProgress />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import storage from "./storage";
|
||||
import { STOKEY_MSAUTH, URL_MICROSOFT_AUTH } from "../config";
|
||||
import { getMsauth, setMsauth } from "./storage";
|
||||
import { URL_MICROSOFT_AUTH } from "../config";
|
||||
import { fetchData } from "./fetch";
|
||||
|
||||
const parseMSToken = (token) => {
|
||||
@@ -26,9 +26,9 @@ const _msAuth = () => {
|
||||
}
|
||||
|
||||
// 查询storage缓存
|
||||
const res = (await storage.getObj(STOKEY_MSAUTH)) || {};
|
||||
token = res.token;
|
||||
exp = res.exp;
|
||||
const res = await getMsauth();
|
||||
token = res?.token;
|
||||
exp = res?.exp;
|
||||
if (token && exp * 1000 > now + 1000) {
|
||||
return [token, exp];
|
||||
}
|
||||
@@ -36,7 +36,7 @@ const _msAuth = () => {
|
||||
// 缓存没有或失效,查询接口
|
||||
token = await fetchData(URL_MICROSOFT_AUTH);
|
||||
exp = parseMSToken(token);
|
||||
await storage.setObj(STOKEY_MSAUTH, { token, exp });
|
||||
await setMsauth({ token, exp });
|
||||
return [token, exp];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
|
||||
// import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
|
||||
|
||||
/**
|
||||
* 浏览器兼容插件,另可用于判断是插件模式还是网页模式,方便开发
|
||||
@@ -13,7 +13,3 @@ function _browser() {
|
||||
}
|
||||
|
||||
export const browser = _browser();
|
||||
export const client = process.env.REACT_APP_CLIENT;
|
||||
export const isExt = CLIENT_EXTS.includes(client);
|
||||
export const isGm = client === CLIENT_USERSCRIPT;
|
||||
export const isWeb = client === CLIENT_WEB;
|
||||
|
||||
6
src/libs/client.js
Normal file
6
src/libs/client.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
|
||||
|
||||
export const client = process.env.REACT_APP_CLIENT;
|
||||
export const isExt = CLIENT_EXTS.includes(client);
|
||||
export const isGm = client === CLIENT_USERSCRIPT;
|
||||
export const isWeb = client === CLIENT_WEB;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isExt, isGm } from "./browser";
|
||||
import { sendMsg } from "./msg";
|
||||
import { isExt, isGm } from "./client";
|
||||
import { sendBgMsg } from "./msg";
|
||||
import { taskPool } from "./pool";
|
||||
import {
|
||||
MSG_FETCH,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MSG_FETCH_CLEAR,
|
||||
CACHE_NAME,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
DEFAULT_FETCH_INTERVAL,
|
||||
DEFAULT_FETCH_LIMIT,
|
||||
@@ -19,7 +20,7 @@ import { msAuth } from "./auth";
|
||||
* @param {*} init
|
||||
* @returns
|
||||
*/
|
||||
const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
||||
export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
GM.xmlHttpRequest({
|
||||
method,
|
||||
@@ -65,20 +66,36 @@ const newCacheReq = async (request) => {
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const fetchApi = async ({ input, init, translator, token }) => {
|
||||
if (translator === OPT_TRANS_MICROSOFT) {
|
||||
init.headers["Authorization"] = `Bearer ${token}`;
|
||||
} else if (translator === OPT_TRANS_OPENAI) {
|
||||
init.headers["Authorization"] = `Bearer ${token}`; // // OpenAI
|
||||
init.headers["api-key"] = token; // Azure OpenAI
|
||||
const fetchApi = async ({ input, init = {}, translator, token }) => {
|
||||
if (token) {
|
||||
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
|
||||
} else {
|
||||
init.headers["Authorization"] = `Bearer ${token}`; // Microsoft & others
|
||||
}
|
||||
}
|
||||
|
||||
if (isGm) {
|
||||
const connects = GM?.info?.script?.connects || [];
|
||||
let info;
|
||||
if (window.KISS_GM) {
|
||||
info = await window.KISS_GM.getInfo();
|
||||
} 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) {
|
||||
return fetchGM(input, init);
|
||||
if (window.KISS_GM) {
|
||||
return window.KISS_GM.fetch(input, init);
|
||||
} else {
|
||||
return fetchGM(input, init);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fetch(input, init);
|
||||
@@ -103,25 +120,23 @@ export const fetchPool = taskPool(
|
||||
/**
|
||||
* 请求数据统一接口
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @param {*} opts
|
||||
* @returns
|
||||
*/
|
||||
export const fetchData = async (
|
||||
input,
|
||||
init,
|
||||
{ useCache, usePool, translator, token } = {}
|
||||
{ useCache, usePool, translator, token, ...init } = {}
|
||||
) => {
|
||||
const cacheReq = await newCacheReq(new Request(input, init));
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
let res;
|
||||
|
||||
// 查询缓存
|
||||
if (useCache) {
|
||||
try {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
res = await cache.match(cacheReq);
|
||||
} catch (err) {
|
||||
console.log("[cache match]", err);
|
||||
console.log("[cache match]", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,9 +155,10 @@ export const fetchData = async (
|
||||
// 插入缓存
|
||||
if (useCache) {
|
||||
try {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
await cache.put(cacheReq, res.clone());
|
||||
} catch (err) {
|
||||
console.log("[cache put]", err);
|
||||
console.log("[cache put]", err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,22 +173,25 @@ export const fetchData = async (
|
||||
/**
|
||||
* fetch 兼容性封装
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @param {*} opts
|
||||
* @returns
|
||||
*/
|
||||
export const fetchPolyfill = async (input, init, opts) => {
|
||||
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
|
||||
if (!input.trim()) {
|
||||
throw new Error("URL is empty");
|
||||
}
|
||||
|
||||
// 插件
|
||||
if (isExt) {
|
||||
const res = await sendMsg(MSG_FETCH, { input, init, opts });
|
||||
if (isExt && !isBg) {
|
||||
const res = await sendBgMsg(MSG_FETCH, { input, opts });
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// 油猴/网页
|
||||
return await fetchData(input, init, opts);
|
||||
// 油猴/网页/BackgroundPage
|
||||
return await fetchData(input, opts);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -180,9 +199,9 @@ export const fetchPolyfill = async (input, init, opts) => {
|
||||
* @param {*} interval
|
||||
* @param {*} limit
|
||||
*/
|
||||
export const fetchUpdate = async (interval, limit) => {
|
||||
export const updateFetchPool = async (interval, limit) => {
|
||||
if (isExt) {
|
||||
const res = await sendMsg(MSG_FETCH_LIMIT, { interval, limit });
|
||||
const res = await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit });
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
@@ -194,9 +213,9 @@ export const fetchUpdate = async (interval, limit) => {
|
||||
/**
|
||||
* 清空任务池
|
||||
*/
|
||||
export const fetchClear = async () => {
|
||||
export const clearFetchPool = async () => {
|
||||
if (isExt) {
|
||||
const res = await sendMsg(MSG_FETCH_CLEAR);
|
||||
const res = await sendBgMsg(MSG_FETCH_CLEAR);
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
102
src/libs/gm.js
Normal file
102
src/libs/gm.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { fetchGM } from "./fetch";
|
||||
import { genEventName } from "./utils";
|
||||
|
||||
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
|
||||
const MSG_GM_setValue = "setValue";
|
||||
const MSG_GM_getValue = "getValue";
|
||||
const MSG_GM_deleteValue = "deleteValue";
|
||||
const MSG_GM_info = "info";
|
||||
|
||||
/**
|
||||
* 注入页面的脚本,请求并接受GM接口信息
|
||||
* @param {*} param0
|
||||
*/
|
||||
export const injectScript = (ping) => {
|
||||
window.APP_INFO = {
|
||||
name: process.env.REACT_APP_NAME,
|
||||
version: process.env.REACT_APP_VERSION,
|
||||
eventName: ping,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 适配GM脚本
|
||||
*/
|
||||
export const adaptScript = (ping) => {
|
||||
const promiseGM = (action, args, timeout = 5000) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const pong = genEventName();
|
||||
const handleEvent = (e) => {
|
||||
window.removeEventListener(pong, handleEvent);
|
||||
const { data, error } = e.detail;
|
||||
if (error) {
|
||||
reject(new Error(error));
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(pong, handleEvent);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(ping, { detail: { action, args, pong } })
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
window.removeEventListener(pong, handleEvent);
|
||||
reject(new Error("timeout"));
|
||||
}, timeout);
|
||||
});
|
||||
|
||||
window.KISS_GM = {
|
||||
fetch: (input, init) => promiseGM(MSG_GM_xmlHttpRequest, { input, init }),
|
||||
setValue: (key, val) => promiseGM(MSG_GM_setValue, { key, val }),
|
||||
getValue: (key) => promiseGM(MSG_GM_getValue, { key }),
|
||||
deleteValue: (key) => promiseGM(MSG_GM_deleteValue, { key }),
|
||||
getInfo: async () => {
|
||||
if (!window.GM_info) {
|
||||
window.GM_info = await promiseGM(MSG_GM_info);
|
||||
}
|
||||
return window.GM_info;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听并回应页面对GM接口的请求
|
||||
* @param {*} param0
|
||||
*/
|
||||
export const handlePing = async (e) => {
|
||||
const { action, args, pong } = e.detail;
|
||||
let res;
|
||||
try {
|
||||
switch (action) {
|
||||
case MSG_GM_xmlHttpRequest:
|
||||
const { input, init } = args;
|
||||
res = await fetchGM(input, init);
|
||||
break;
|
||||
case MSG_GM_setValue:
|
||||
const { key, val } = args;
|
||||
await GM.setValue(key, val);
|
||||
res = val;
|
||||
break;
|
||||
case MSG_GM_getValue:
|
||||
res = await GM.getValue(args.key);
|
||||
break;
|
||||
case MSG_GM_deleteValue:
|
||||
await GM.deleteValue(args.key);
|
||||
res = "ok";
|
||||
break;
|
||||
case MSG_GM_info:
|
||||
res = GM.info;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`message action is unavailable: ${action}`);
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent(pong, { detail: { data: res } }));
|
||||
} catch (err) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(pong, { detail: { error: err.message } })
|
||||
);
|
||||
}
|
||||
};
|
||||
7
src/libs/iframe.js
Normal file
7
src/libs/iframe.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export const isIframe = window.self !== window.top;
|
||||
|
||||
export const sendIframeMsg = (action, args) => {
|
||||
document.querySelectorAll("iframe").forEach((iframe) => {
|
||||
iframe.contentWindow.postMessage({ action, args }, "*");
|
||||
});
|
||||
};
|
||||
@@ -1,104 +1,15 @@
|
||||
import storage from "./storage";
|
||||
import {
|
||||
DEFAULT_SETTING,
|
||||
STOKEY_SETTING,
|
||||
STOKEY_RULES,
|
||||
STOKEY_FAB,
|
||||
GLOBLA_RULE,
|
||||
GLOBAL_KEY,
|
||||
DEFAULT_SUBRULES_LIST,
|
||||
} from "../config";
|
||||
import { CACHE_NAME } from "../config";
|
||||
import { browser } from "./browser";
|
||||
import { isMatch } from "./utils";
|
||||
import { tryLoadRules } from "./rules";
|
||||
|
||||
/**
|
||||
* 获取节点列表并转为数组
|
||||
* @param {*} selector
|
||||
* @param {*} el
|
||||
* @returns
|
||||
* 清除缓存数据
|
||||
*/
|
||||
export const queryEls = (selector, el = document) =>
|
||||
Array.from(el.querySelectorAll(selector));
|
||||
|
||||
/**
|
||||
* 查询storage中的设置
|
||||
* @returns
|
||||
*/
|
||||
export const getSetting = async () => ({
|
||||
...DEFAULT_SETTING,
|
||||
...((await storage.getObj(STOKEY_SETTING)) || {}),
|
||||
});
|
||||
|
||||
/**
|
||||
* 查询规则列表
|
||||
* @returns
|
||||
*/
|
||||
export const getRules = async () => (await storage.getObj(STOKEY_RULES)) || [];
|
||||
|
||||
/**
|
||||
* 查询fab位置信息
|
||||
* @returns
|
||||
*/
|
||||
export const getFab = async () => (await storage.getObj(STOKEY_FAB)) || {};
|
||||
|
||||
/**
|
||||
* 设置fab位置信息
|
||||
* @returns
|
||||
*/
|
||||
export const setFab = async (obj) => await storage.setObj(STOKEY_FAB, obj);
|
||||
|
||||
/**
|
||||
* 根据href匹配规则
|
||||
* @param {*} rules
|
||||
* @param {string} href
|
||||
* @returns
|
||||
*/
|
||||
export const matchRule = async (
|
||||
rules,
|
||||
href,
|
||||
{ injectRules, subrulesList = DEFAULT_SUBRULES_LIST }
|
||||
) => {
|
||||
if (injectRules) {
|
||||
try {
|
||||
const selectedSub = subrulesList.find((item) => item.selected);
|
||||
if (selectedSub?.url) {
|
||||
const subRules = await tryLoadRules(selectedSub.url);
|
||||
rules.splice(-1, 0, ...subRules);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[load injectRules]", err);
|
||||
}
|
||||
export const tryClearCaches = async () => {
|
||||
try {
|
||||
caches.delete(CACHE_NAME);
|
||||
} catch (err) {
|
||||
console.log("[clean caches]", err.message);
|
||||
}
|
||||
|
||||
const rule = rules.find((rule) =>
|
||||
rule.pattern.split(",").some((p) => isMatch(href, p.trim()))
|
||||
);
|
||||
const globalRule =
|
||||
rules.find((rule) =>
|
||||
rule.pattern.split(",").some((p) => p.trim() === "*")
|
||||
) || 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();
|
||||
|
||||
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach(
|
||||
(key) => {
|
||||
if (rule[key] === GLOBAL_KEY) {
|
||||
rule[key] = globalRule[key];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return rule;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -106,7 +17,11 @@ export const matchRule = async (
|
||||
* @param {*} q
|
||||
* @returns
|
||||
*/
|
||||
export const detectLang = async (q) => {
|
||||
const res = await browser?.i18n.detectLanguage(q);
|
||||
return res?.languages?.[0]?.language;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,8 +6,8 @@ import { browser } from "./browser";
|
||||
* @param {*} args
|
||||
* @returns
|
||||
*/
|
||||
export const sendMsg = (action, args) =>
|
||||
browser?.runtime?.sendMessage({ action, args });
|
||||
export const sendBgMsg = (action, args) =>
|
||||
browser.runtime.sendMessage({ action, args });
|
||||
|
||||
/**
|
||||
* 发送消息给当前页面
|
||||
@@ -16,6 +16,15 @@ export const sendMsg = (action, args) =>
|
||||
* @returns
|
||||
*/
|
||||
export const sendTabMsg = async (action, args) => {
|
||||
const tabs = await browser?.tabs.query({ active: true, currentWindow: true });
|
||||
return await browser?.tabs.sendMessage(tabs[0].id, { 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];
|
||||
};
|
||||
|
||||
@@ -1,17 +1,88 @@
|
||||
import storage from "./storage";
|
||||
import { fetchPolyfill } from "./fetch";
|
||||
import { matchValue, type } from "./utils";
|
||||
import { matchValue, type, isMatch } from "./utils";
|
||||
import {
|
||||
STOKEY_RULESCACHE_PREFIX,
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
GLOBLA_RULE,
|
||||
DEFAULT_SUBRULES_LIST,
|
||||
DEFAULT_OW_RULE,
|
||||
} from "../config";
|
||||
import { loadOrFetchSubRules } from "./subRules";
|
||||
import { getRulesWithDefault, setRules } from "./storage";
|
||||
import { trySyncRules } from "./sync";
|
||||
|
||||
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
|
||||
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
|
||||
/**
|
||||
* 根据href匹配规则
|
||||
* @param {*} rules
|
||||
* @param {string} href
|
||||
* @returns
|
||||
*/
|
||||
export const matchRule = async (
|
||||
rules,
|
||||
href,
|
||||
{
|
||||
injectRules = true,
|
||||
subrulesList = DEFAULT_SUBRULES_LIST,
|
||||
owSubrule = DEFAULT_OW_RULE,
|
||||
}
|
||||
) => {
|
||||
rules = [...rules];
|
||||
if (injectRules) {
|
||||
try {
|
||||
const selectedSub = subrulesList.find((item) => item.selected);
|
||||
if (selectedSub?.url) {
|
||||
const mixRule = {};
|
||||
Object.entries(owSubrule)
|
||||
.filter(([key, val]) => {
|
||||
if (
|
||||
owSubrule.textStyle === REMAIN_KEY &&
|
||||
(key === "bgColor" || key === "textDiyStyle")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return val !== REMAIN_KEY;
|
||||
})
|
||||
.forEach(([key, val]) => {
|
||||
mixRule[key] = val;
|
||||
});
|
||||
|
||||
let subRules = await loadOrFetchSubRules(selectedSub.url);
|
||||
subRules = subRules.map((item) => ({ ...item, ...mixRule }));
|
||||
rules.splice(-1, 0, ...subRules);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[load injectRules]", err);
|
||||
}
|
||||
}
|
||||
|
||||
const rule = rules.find((r) =>
|
||||
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
|
||||
);
|
||||
const globalRule = rules.find((r) => r.pattern === GLOBAL_KEY) || GLOBLA_RULE;
|
||||
if (!rule) {
|
||||
return globalRule;
|
||||
}
|
||||
|
||||
rule.selector = rule.selector?.trim() || globalRule.selector;
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查过滤rules
|
||||
@@ -26,6 +97,8 @@ export const checkRules = (rules) => {
|
||||
throw new Error("data error");
|
||||
}
|
||||
|
||||
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
|
||||
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
|
||||
const patternSet = new Set();
|
||||
rules = rules
|
||||
.filter((rule) => type(rule) === "object")
|
||||
@@ -46,10 +119,12 @@ export const checkRules = (rules) => {
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
}) => ({
|
||||
pattern: pattern.trim(),
|
||||
selector: type(selector) === "string" ? selector : "",
|
||||
bgColor: type(bgColor) === "string" ? bgColor : "",
|
||||
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
|
||||
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
|
||||
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
|
||||
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
|
||||
@@ -62,38 +137,17 @@ export const checkRules = (rules) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 本地rules缓存
|
||||
* 保存或更新rule
|
||||
* @param {*} newRule
|
||||
*/
|
||||
export const rulesCache = {
|
||||
fetch: async (url) => {
|
||||
const res = await fetchPolyfill(url);
|
||||
const rules = checkRules(res).filter(
|
||||
(rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== ""
|
||||
);
|
||||
return rules;
|
||||
},
|
||||
set: async (url, rules) => {
|
||||
await storage.setObj(`${STOKEY_RULESCACHE_PREFIX}${url}`, rules);
|
||||
},
|
||||
get: async (url) => {
|
||||
return await storage.getObj(`${STOKEY_RULESCACHE_PREFIX}${url}`);
|
||||
},
|
||||
del: async (url) => {
|
||||
await storage.del(`${STOKEY_RULESCACHE_PREFIX}${url}`);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 从缓存或远程加载订阅的rules
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const tryLoadRules = async (url) => {
|
||||
let rules = await rulesCache.get(url);
|
||||
if (rules?.length) {
|
||||
return rules;
|
||||
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);
|
||||
}
|
||||
rules = await rulesCache.fetch(url);
|
||||
await rulesCache.set(url, rules);
|
||||
return rules;
|
||||
await setRules(rules);
|
||||
trySyncRules(false, true);
|
||||
};
|
||||
|
||||
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.key);
|
||||
curkeys.add(e.key);
|
||||
fn([...curkeys], [...allkeys]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyup = (e) => {
|
||||
curkeys.delete(e.key);
|
||||
if (curkeys.size === 0) {
|
||||
fn([...curkeys], [...allkeys]);
|
||||
allkeys.clear();
|
||||
}
|
||||
};
|
||||
|
||||
target.addEventListener("keydown", handleKeydown);
|
||||
target.addEventListener("keyup", handleKeyup);
|
||||
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,28 +1,26 @@
|
||||
import { browser, isExt, isGm } from "./browser";
|
||||
import {
|
||||
STOKEY_SETTING,
|
||||
STOKEY_RULES,
|
||||
STOKEY_FAB,
|
||||
STOKEY_SYNC,
|
||||
STOKEY_MSAUTH,
|
||||
STOKEY_RULESCACHE_PREFIX,
|
||||
STOKEY_WEBFIXCACHE_PREFIX,
|
||||
DEFAULT_SETTING,
|
||||
DEFAULT_RULES,
|
||||
DEFAULT_SYNC,
|
||||
BUILTIN_RULES,
|
||||
} from "../config";
|
||||
import { isExt, isGm } from "./client";
|
||||
import { browser } from "./browser";
|
||||
|
||||
async function set(key, val) {
|
||||
if (isExt) {
|
||||
await browser.storage.local.set({ [key]: val });
|
||||
} else if (isGm) {
|
||||
const oldValue = await GM.getValue(key);
|
||||
await GM.setValue(key, val);
|
||||
window.dispatchEvent(
|
||||
new StorageEvent("storage", {
|
||||
key,
|
||||
oldValue,
|
||||
newValue: val,
|
||||
})
|
||||
);
|
||||
await (window.KISS_GM || GM).setValue(key, val);
|
||||
} else {
|
||||
const oldValue = window.localStorage.getItem(key);
|
||||
window.localStorage.setItem(key, val);
|
||||
window.dispatchEvent(
|
||||
new StorageEvent("storage", {
|
||||
key,
|
||||
oldValue,
|
||||
newValue: val,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +29,7 @@ async function get(key) {
|
||||
const val = await browser.storage.local.get([key]);
|
||||
return val[key];
|
||||
} else if (isGm) {
|
||||
const val = await GM.getValue(key);
|
||||
const val = await (window.KISS_GM || GM).getValue(key);
|
||||
return val;
|
||||
}
|
||||
return window.localStorage.getItem(key);
|
||||
@@ -41,25 +39,9 @@ async function del(key) {
|
||||
if (isExt) {
|
||||
await browser.storage.local.remove([key]);
|
||||
} else if (isGm) {
|
||||
const oldValue = await GM.getValue(key);
|
||||
await GM.deleteValue(key);
|
||||
window.dispatchEvent(
|
||||
new StorageEvent("storage", {
|
||||
key,
|
||||
oldValue,
|
||||
newValue: null,
|
||||
})
|
||||
);
|
||||
await (window.KISS_GM || GM).deleteValue(key);
|
||||
} else {
|
||||
const oldValue = window.localStorage.getItem(key);
|
||||
window.localStorage.removeItem(key);
|
||||
window.dispatchEvent(
|
||||
new StorageEvent("storage", {
|
||||
key,
|
||||
oldValue,
|
||||
newValue: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,22 +65,10 @@ async function putObj(key, obj) {
|
||||
await setObj(key, { ...cur, ...obj });
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听storage事件
|
||||
* @param {*} handleChanged
|
||||
*/
|
||||
function onChanged(handleChanged) {
|
||||
if (isExt) {
|
||||
browser.storage.onChanged.addListener(handleChanged);
|
||||
} else {
|
||||
window.addEventListener("storage", handleChanged);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对storage的封装
|
||||
*/
|
||||
const storage = {
|
||||
export const storage = {
|
||||
get,
|
||||
set,
|
||||
del,
|
||||
@@ -106,7 +76,76 @@ const storage = {
|
||||
trySetObj,
|
||||
getObj,
|
||||
putObj,
|
||||
onChanged,
|
||||
// onChanged,
|
||||
};
|
||||
|
||||
export default storage;
|
||||
/**
|
||||
* 设置信息
|
||||
*/
|
||||
export const getSetting = () => getObj(STOKEY_SETTING);
|
||||
export const getSettingWithDefault = async () =>
|
||||
(await getSetting()) || DEFAULT_SETTING;
|
||||
export const setSetting = (val) => setObj(STOKEY_SETTING, val);
|
||||
export const updateSetting = (obj) => putObj(STOKEY_SETTING, obj);
|
||||
|
||||
/**
|
||||
* 规则列表
|
||||
*/
|
||||
export const getRules = () => getObj(STOKEY_RULES);
|
||||
export const getRulesWithDefault = async () =>
|
||||
(await getRules()) || DEFAULT_RULES;
|
||||
export const setRules = (val) => setObj(STOKEY_RULES, val);
|
||||
|
||||
/**
|
||||
* 订阅规则
|
||||
*/
|
||||
export const getSubRules = (url) => getObj(STOKEY_RULESCACHE_PREFIX + url);
|
||||
export const getSubRulesWithDefault = async () => (await getSubRules()) || [];
|
||||
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 getSync = () => getObj(STOKEY_SYNC);
|
||||
export const getSyncWithDefault = async () => (await getSync()) || DEFAULT_SYNC;
|
||||
export const updateSync = (obj) => putObj(STOKEY_SYNC, obj);
|
||||
|
||||
/**
|
||||
* ms auth
|
||||
*/
|
||||
export const getMsauth = () => getObj(STOKEY_MSAUTH);
|
||||
export const setMsauth = (val) => setObj(STOKEY_MSAUTH, val);
|
||||
|
||||
/**
|
||||
* 存入默认数据
|
||||
*/
|
||||
export const tryInitDefaultData = async () => {
|
||||
try {
|
||||
await trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
|
||||
await trySetObj(STOKEY_RULES, DEFAULT_RULES);
|
||||
await trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
|
||||
await trySetObj(
|
||||
`${STOKEY_RULESCACHE_PREFIX}${process.env.REACT_APP_RULESURL}`,
|
||||
BUILTIN_RULES
|
||||
);
|
||||
} catch (err) {
|
||||
console.log("[init default]", err);
|
||||
}
|
||||
};
|
||||
|
||||
90
src/libs/subRules.js
Normal file
90
src/libs/subRules.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { GLOBAL_KEY } from "../config";
|
||||
import {
|
||||
getSyncWithDefault,
|
||||
updateSync,
|
||||
setSubRules,
|
||||
getSubRules,
|
||||
} from "./storage";
|
||||
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 apiFetch(url, isBg);
|
||||
const rules = checkRules(res).filter(
|
||||
({ pattern }) => !isAllchar(pattern, GLOBAL_KEY)
|
||||
);
|
||||
if (rules.length > 0) {
|
||||
await setSubRules(url, rules);
|
||||
}
|
||||
return rules;
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步所有订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const syncAllSubRules = async (subrulesList, isBg = false) => {
|
||||
for (let subrules of subrulesList) {
|
||||
try {
|
||||
await syncSubRules(subrules.url, isBg);
|
||||
await updateSyncDataCache(subrules.url);
|
||||
} catch (err) {
|
||||
console.log(`[sync subrule error]: ${subrules.url}`, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据时间同步所有订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
|
||||
try {
|
||||
const { subRulesSyncAt } = await getSyncWithDefault();
|
||||
const now = Date.now();
|
||||
const interval = 24 * 60 * 60 * 1000; // 间隔一天
|
||||
if (now - subRulesSyncAt > interval) {
|
||||
// 同步订阅规则
|
||||
await syncAllSubRules(subrulesList, isBg);
|
||||
await updateSync({ subRulesSyncAt: now });
|
||||
|
||||
// 同步修复规则
|
||||
await syncWebfix(process.env.REACT_APP_WEBFIXURL);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[try sync all subrules]", err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从缓存或远程加载订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const loadOrFetchSubRules = async (url) => {
|
||||
let rules = await getSubRules(url);
|
||||
if (!rules || rules.length === 0) {
|
||||
rules = await syncSubRules(url);
|
||||
await updateSyncDataCache(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>
|
||||
`;
|
||||
157
src/libs/sync.js
157
src/libs/sync.js
@@ -1,81 +1,125 @@
|
||||
import {
|
||||
STOKEY_SYNC,
|
||||
DEFAULT_SYNC,
|
||||
KV_SETTING_KEY,
|
||||
KV_RULES_KEY,
|
||||
KV_RULES_SHARE_KEY,
|
||||
STOKEY_SETTING,
|
||||
STOKEY_RULES,
|
||||
KV_SALT_SHARE,
|
||||
} from "../config";
|
||||
import storage from "../libs/storage";
|
||||
import { getSetting, getRules } from ".";
|
||||
import {
|
||||
getSyncWithDefault,
|
||||
updateSync,
|
||||
getSettingWithDefault,
|
||||
getRulesWithDefault,
|
||||
setSetting,
|
||||
setRules,
|
||||
} from "./storage";
|
||||
import { apiSyncData } from "../apis";
|
||||
import { sha256 } from "./utils";
|
||||
|
||||
export const loadSyncOpt = async () =>
|
||||
(await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC;
|
||||
/**
|
||||
* 同步设置
|
||||
* @returns
|
||||
*/
|
||||
const syncSetting = async (isBg = false, isForce = false) => {
|
||||
let {
|
||||
syncUrl,
|
||||
syncKey,
|
||||
settingUpdateAt = 0,
|
||||
settingSyncAt = 0,
|
||||
} = await getSyncWithDefault();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
export const syncSetting = async () => {
|
||||
try {
|
||||
const { syncUrl, syncKey, settingUpdateAt } = await loadSyncOpt();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
if (isForce) {
|
||||
settingUpdateAt = Date.now();
|
||||
}
|
||||
|
||||
const setting = await getSetting();
|
||||
const res = await apiSyncData(syncUrl, syncKey, {
|
||||
const setting = await getSettingWithDefault();
|
||||
const res = await apiSyncData(
|
||||
syncUrl,
|
||||
syncKey,
|
||||
{
|
||||
key: KV_SETTING_KEY,
|
||||
value: setting,
|
||||
updateAt: settingUpdateAt,
|
||||
});
|
||||
updateAt: settingSyncAt === 0 ? 0 : settingUpdateAt,
|
||||
},
|
||||
isBg
|
||||
);
|
||||
|
||||
if (res && res.updateAt > settingUpdateAt) {
|
||||
await storage.putObj(STOKEY_SYNC, {
|
||||
settingUpdateAt: res.updateAt,
|
||||
settingSyncAt: res.updateAt,
|
||||
});
|
||||
await storage.setObj(STOKEY_SETTING, res.value);
|
||||
} else {
|
||||
await storage.putObj(STOKEY_SYNC, {
|
||||
settingSyncAt: res.updateAt,
|
||||
});
|
||||
}
|
||||
if (res.updateAt > settingUpdateAt) {
|
||||
await setSetting(res.value);
|
||||
}
|
||||
await updateSync({
|
||||
settingUpdateAt: res.updateAt,
|
||||
settingSyncAt: Date.now(),
|
||||
});
|
||||
|
||||
return res.value;
|
||||
};
|
||||
|
||||
export const trySyncSetting = async (isBg = false, isForce = false) => {
|
||||
try {
|
||||
return await syncSetting(isBg, isForce);
|
||||
} catch (err) {
|
||||
console.log("[sync setting]", err);
|
||||
}
|
||||
};
|
||||
|
||||
export const syncRules = async () => {
|
||||
try {
|
||||
const { syncUrl, syncKey, rulesUpdateAt } = await loadSyncOpt();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 同步规则
|
||||
* @returns
|
||||
*/
|
||||
const syncRules = async (isBg = false, isForce = false) => {
|
||||
let {
|
||||
syncUrl,
|
||||
syncKey,
|
||||
rulesUpdateAt = 0,
|
||||
rulesSyncAt = 0,
|
||||
} = await getSyncWithDefault();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rules = await getRules();
|
||||
const res = await apiSyncData(syncUrl, syncKey, {
|
||||
if (isForce) {
|
||||
rulesUpdateAt = Date.now();
|
||||
}
|
||||
|
||||
const rules = await getRulesWithDefault();
|
||||
const res = await apiSyncData(
|
||||
syncUrl,
|
||||
syncKey,
|
||||
{
|
||||
key: KV_RULES_KEY,
|
||||
value: rules,
|
||||
updateAt: rulesUpdateAt,
|
||||
});
|
||||
updateAt: rulesSyncAt === 0 ? 0 : rulesUpdateAt,
|
||||
},
|
||||
isBg
|
||||
);
|
||||
|
||||
if (res && res.updateAt > rulesUpdateAt) {
|
||||
await storage.putObj(STOKEY_SYNC, {
|
||||
rulesUpdateAt: res.updateAt,
|
||||
rulesSyncAt: res.updateAt,
|
||||
});
|
||||
await storage.setObj(STOKEY_RULES, res.value);
|
||||
} else {
|
||||
await storage.putObj(STOKEY_SYNC, {
|
||||
rulesSyncAt: res.updateAt,
|
||||
});
|
||||
}
|
||||
if (res.updateAt > rulesUpdateAt) {
|
||||
await setRules(res.value);
|
||||
}
|
||||
await updateSync({
|
||||
rulesUpdateAt: res.updateAt,
|
||||
rulesSyncAt: Date.now(),
|
||||
});
|
||||
|
||||
return res.value;
|
||||
};
|
||||
|
||||
export const trySyncRules = async (isBg = false, isForce = false) => {
|
||||
try {
|
||||
return await syncRules(isBg, isForce);
|
||||
} catch (err) {
|
||||
console.log("[sync user rules]", err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步分享规则
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
||||
await apiSyncData(syncUrl, syncKey, {
|
||||
key: KV_RULES_SHARE_KEY,
|
||||
@@ -87,7 +131,14 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
||||
return shareUrl;
|
||||
};
|
||||
|
||||
export const syncAll = async () => {
|
||||
await syncSetting();
|
||||
await syncRules();
|
||||
/**
|
||||
* 同步个人设置和规则
|
||||
* @returns
|
||||
*/
|
||||
export const syncSettingAndRules = async (isBg = false) => {
|
||||
return [await syncSetting(isBg), await syncRules(isBg)];
|
||||
};
|
||||
|
||||
export const trySyncSettingAndRules = async (isBg = false) => {
|
||||
return [await trySyncSetting(isBg), await trySyncRules(isBg)];
|
||||
};
|
||||
|
||||
@@ -3,21 +3,129 @@ import {
|
||||
APP_LCNAME,
|
||||
TRANS_MIN_LENGTH,
|
||||
TRANS_MAX_LENGTH,
|
||||
EVENT_KISS,
|
||||
MSG_TRANS_CURRULE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_FUZZY,
|
||||
SHADOW_KEY,
|
||||
OPT_MOUSEKEY_DISABLE,
|
||||
OPT_MOUSEKEY_MOUSEOVER,
|
||||
DEFAULT_INPUT_RULE,
|
||||
DEFAULT_TRANS_APIS,
|
||||
DEFAULT_INPUT_SHORTCUT,
|
||||
OPT_LANGS_LIST,
|
||||
} from "../config";
|
||||
import { queryEls } from ".";
|
||||
import Content from "../views/Content";
|
||||
import { fetchUpdate, fetchClear } from "./fetch";
|
||||
import { updateFetchPool, clearFetchPool } from "./fetch";
|
||||
import {
|
||||
debounce,
|
||||
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(loadingId) {
|
||||
const div = document.getElementById(loadingId);
|
||||
if (div) {
|
||||
div.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译类
|
||||
*/
|
||||
export class Translator {
|
||||
_rule = {};
|
||||
_inputRule = {};
|
||||
_setting = {};
|
||||
_rootNodes = new Set();
|
||||
_tranNodes = new Map();
|
||||
_skipNodeNames = [
|
||||
APP_LCNAME,
|
||||
"style",
|
||||
"svg",
|
||||
"img",
|
||||
"audio",
|
||||
"video",
|
||||
"textarea",
|
||||
"input",
|
||||
"button",
|
||||
"select",
|
||||
"option",
|
||||
"head",
|
||||
"script",
|
||||
"iframe",
|
||||
];
|
||||
_eventName = genEventName();
|
||||
|
||||
// 显示
|
||||
_interseObserver = new IntersectionObserver(
|
||||
(intersections) => {
|
||||
intersections.forEach((intersection) => {
|
||||
@@ -32,26 +140,65 @@ export class Translator {
|
||||
}
|
||||
);
|
||||
|
||||
// 变化
|
||||
_mutaObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
try {
|
||||
queryEls(this.rule.selector, node).forEach((el) => {
|
||||
this._interseObserver.observe(el);
|
||||
});
|
||||
} catch (err) {
|
||||
//
|
||||
if (
|
||||
!this._skipNodeNames.includes(mutation.target.localName) &&
|
||||
mutation.addedNodes.length > 0
|
||||
) {
|
||||
const nodes = Array.from(mutation.addedNodes).filter((node) => {
|
||||
if (
|
||||
this._skipNodeNames.includes(node.localName) ||
|
||||
node.id === APP_LCNAME
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (nodes.length > 0) {
|
||||
// const rootNode = mutation.target.getRootNode();
|
||||
// todo
|
||||
this._reTranslate();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
constructor(rule, { fetchInterval, fetchLimit }) {
|
||||
fetchUpdate(fetchInterval, fetchLimit);
|
||||
this.rule = rule;
|
||||
// 插入 shadowroot
|
||||
_overrideAttachShadow = () => {
|
||||
const _this = this;
|
||||
const _attachShadow = HTMLElement.prototype.attachShadow;
|
||||
HTMLElement.prototype.attachShadow = function () {
|
||||
_this._reTranslate();
|
||||
return _attachShadow.apply(this, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
constructor(rule, setting) {
|
||||
const { fetchInterval, fetchLimit } = setting;
|
||||
updateFetchPool(fetchInterval, fetchLimit);
|
||||
this._overrideAttachShadow();
|
||||
|
||||
this._setting = setting;
|
||||
this._rule = rule;
|
||||
|
||||
if (rule.transOpen === "true") {
|
||||
this._register();
|
||||
}
|
||||
|
||||
this._inputRule = setting.inputRule || DEFAULT_INPUT_RULE;
|
||||
if (this._inputRule.transOpen) {
|
||||
this._registerInput();
|
||||
}
|
||||
}
|
||||
|
||||
get setting() {
|
||||
return this._setting;
|
||||
}
|
||||
|
||||
get eventName() {
|
||||
return this._eventName;
|
||||
}
|
||||
|
||||
get rule() {
|
||||
@@ -64,8 +211,9 @@ export class Translator {
|
||||
this._rule = rule;
|
||||
|
||||
// 广播消息
|
||||
const eventName = this._eventName;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(EVENT_KISS, {
|
||||
new CustomEvent(eventName, {
|
||||
detail: {
|
||||
action: MSG_TRANS_CURRULE,
|
||||
args: rule,
|
||||
@@ -96,63 +244,294 @@ export class Translator {
|
||||
this.rule = { ...this.rule, textStyle };
|
||||
};
|
||||
|
||||
_querySelectorAll = (selector, node) => {
|
||||
try {
|
||||
return Array.from(node.querySelectorAll(selector));
|
||||
} catch (err) {
|
||||
console.log(`[querySelectorAll err]: ${selector}`);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
_queryFilter = (selector, rootNode) => {
|
||||
return this._querySelectorAll(selector, rootNode).filter(
|
||||
(node) => this._queryFilter(selector, node).length === 0
|
||||
);
|
||||
};
|
||||
|
||||
_queryNodes = (rootNode = document) => {
|
||||
// const childRoots = Array.from(rootNode.querySelectorAll("*"))
|
||||
// .map((item) => item.shadowRoot)
|
||||
// .filter(Boolean);
|
||||
// const childNodes = childRoots.map((item) => this._queryNodes(item));
|
||||
// const nodes = Array.from(rootNode.querySelectorAll(this.rule.selector));
|
||||
// return nodes.concat(childNodes).flat();
|
||||
|
||||
this._rootNodes.add(rootNode);
|
||||
this._rule.selector
|
||||
.split(";")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((selector) => {
|
||||
if (selector.includes(SHADOW_KEY)) {
|
||||
const [outSelector, inSelector] = selector
|
||||
.split(SHADOW_KEY)
|
||||
.map((item) => item.trim());
|
||||
if (outSelector && inSelector) {
|
||||
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, "");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this._queryFilter(selector, rootNode).forEach((item) => {
|
||||
if (!this._tranNodes.has(item)) {
|
||||
this._tranNodes.set(item, "");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
_register = () => {
|
||||
// 监听节点变化
|
||||
this._mutaObserver.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
if (this._rule.fromLang === this._rule.toLang) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 搜索节点
|
||||
this._queryNodes();
|
||||
|
||||
this._rootNodes.forEach((node) => {
|
||||
// 监听节点变化;
|
||||
this._mutaObserver.observe(node, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
// characterData: true,
|
||||
});
|
||||
});
|
||||
|
||||
// 监听节点显示
|
||||
queryEls(this.rule.selector).forEach((el) => {
|
||||
this._interseObserver.observe(el);
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
if (
|
||||
!this._setting.mouseKey ||
|
||||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
|
||||
) {
|
||||
// 监听节点显示
|
||||
this._interseObserver.observe(node);
|
||||
} else {
|
||||
// 监听鼠标悬停
|
||||
node.addEventListener("mouseover", this._handleMouseover);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
_registerInput = () => {
|
||||
const {
|
||||
triggerShortcut: initTriggerShortcut,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang: initToLang,
|
||||
triggerCount: initTriggerCount,
|
||||
triggerTime,
|
||||
transSign,
|
||||
} = this._inputRule;
|
||||
const apiSetting = (this._setting.transApis || DEFAULT_TRANS_APIS)[
|
||||
translator
|
||||
];
|
||||
|
||||
let triggerShortcut = initTriggerShortcut;
|
||||
let triggerCount = initTriggerCount;
|
||||
if (triggerShortcut.length === 0) {
|
||||
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
||||
triggerCount = 1;
|
||||
}
|
||||
|
||||
stepShortcutRegister(
|
||||
triggerShortcut,
|
||||
async () => {
|
||||
const node = document.activeElement;
|
||||
if (!node || !(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;
|
||||
let toLang = initToLang;
|
||||
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);
|
||||
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(loadingId);
|
||||
}
|
||||
},
|
||||
triggerCount,
|
||||
triggerTime
|
||||
);
|
||||
};
|
||||
|
||||
_handleMouseover = (e) => {
|
||||
const key = this._setting.mouseKey.slice(3);
|
||||
if (this._setting.mouseKey === OPT_MOUSEKEY_MOUSEOVER || e[key]) {
|
||||
e.target.removeEventListener("mouseover", this._handleMouseover);
|
||||
this._render(e.target);
|
||||
}
|
||||
};
|
||||
|
||||
_unRegister = () => {
|
||||
// 解除节点变化监听
|
||||
this._mutaObserver.disconnect();
|
||||
|
||||
// 解除节点显示监听
|
||||
queryEls(this.rule.selector).forEach((el) =>
|
||||
this._interseObserver.unobserve(el)
|
||||
);
|
||||
// this._interseObserver.disconnect();
|
||||
|
||||
// 移除已插入元素
|
||||
queryEls(APP_LCNAME).forEach((el) => el.remove());
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
if (
|
||||
!this._setting.mouseKey ||
|
||||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
|
||||
) {
|
||||
// 解除节点显示监听
|
||||
this._interseObserver.unobserve(node);
|
||||
} else {
|
||||
// 移除鼠标悬停监听
|
||||
node.removeEventListener("mouseover", this._handleMouseover);
|
||||
}
|
||||
|
||||
// 移除已插入元素
|
||||
node.querySelector(APP_LCNAME)?.remove();
|
||||
});
|
||||
|
||||
// 清空节点集合
|
||||
this._rootNodes.clear();
|
||||
this._tranNodes.clear();
|
||||
|
||||
// 清空任务池
|
||||
fetchClear();
|
||||
clearFetchPool();
|
||||
};
|
||||
|
||||
_render = (el) => {
|
||||
// 含子元素
|
||||
if (el.querySelector(this.rule.selector)) {
|
||||
return;
|
||||
_reTranslate = debounce(() => {
|
||||
if (this._rule.transOpen === "true") {
|
||||
this._register();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
_render = (el) => {
|
||||
let traEl = el.querySelector(APP_LCNAME);
|
||||
|
||||
// 已翻译
|
||||
if (el.querySelector(APP_LCNAME)) {
|
||||
return;
|
||||
if (traEl) {
|
||||
const preText = this._tranNodes.get(el);
|
||||
const curText = el.innerText.trim();
|
||||
// const traText = traEl.innerText.trim();
|
||||
|
||||
// todo
|
||||
// 1. traText when loading
|
||||
// 2. replace startsWith
|
||||
if (curText.startsWith(preText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
traEl.remove();
|
||||
}
|
||||
|
||||
// 太长或太短
|
||||
const q = el.innerText.trim();
|
||||
if (!q || q.length < TRANS_MIN_LENGTH || q.length > TRANS_MAX_LENGTH) {
|
||||
this._tranNodes.set(el, q);
|
||||
|
||||
// 太长或太短
|
||||
if (
|
||||
!q ||
|
||||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
|
||||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("---> ", q);
|
||||
|
||||
const span = document.createElement(APP_LCNAME);
|
||||
span.style.visibility = "visible";
|
||||
el.appendChild(span);
|
||||
traEl = document.createElement(APP_LCNAME);
|
||||
traEl.style.visibility = "visible";
|
||||
el.appendChild(traEl);
|
||||
el.style.cssText +=
|
||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||
el.parentElement.style.cssText +=
|
||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||
if (el.parentElement) {
|
||||
el.parentElement.style.cssText +=
|
||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||
}
|
||||
|
||||
const root = createRoot(span);
|
||||
const root = createRoot(traEl);
|
||||
root.render(<Content q={q} translator={this} />);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,7 +34,12 @@ export const matchValue = (arr, val) => {
|
||||
* @returns
|
||||
*/
|
||||
export const sleep = (delay) =>
|
||||
new Promise((resolve) => setTimeout(resolve, delay));
|
||||
new Promise((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
@@ -43,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
|
||||
@@ -63,7 +114,7 @@ export const isMatch = (s, p) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
p = `*${p}*`;
|
||||
p = "*" + p + "*";
|
||||
|
||||
let [sIndex, pIndex] = [0, 0];
|
||||
let [sRecord, pRecord] = [-1, -1];
|
||||
@@ -86,7 +137,7 @@ export const isMatch = (s, p) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
return p.slice(pIndex).replaceAll("*", "") === "";
|
||||
return isAllchar(p, "*", pIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -111,3 +162,64 @@ export const sha256 = async (text, salt) => {
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机事件名称
|
||||
* @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);
|
||||
};
|
||||
|
||||
192
src/libs/webfix.js
Normal file
192
src/libs/webfix.js
Normal file
@@ -0,0 +1,192 @@
|
||||
import { isMatch } from "./utils";
|
||||
import { getWebfix, setWebfix } from "./storage";
|
||||
import { apiFetch } from "../apis";
|
||||
|
||||
/**
|
||||
* 修复程序类型
|
||||
*/
|
||||
const FIXER_BR = "br";
|
||||
const FIXER_FONTSIZE = "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,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 修复过的标记
|
||||
*/
|
||||
const fixedSign = "kissfixed";
|
||||
|
||||
/**
|
||||
* 采用 `br` 换行网站的修复函数
|
||||
* 目标是将 `br` 替换成 `p`
|
||||
* @param {*} node
|
||||
* @returns
|
||||
*/
|
||||
function brFixer(node) {
|
||||
if (node.hasAttribute(fixedSign)) {
|
||||
return;
|
||||
}
|
||||
node.setAttribute(fixedSign, "true");
|
||||
|
||||
var gapTags = ["BR", "WBR"];
|
||||
var newlineTags = [
|
||||
"DIV",
|
||||
"UL",
|
||||
"OL",
|
||||
"LI",
|
||||
"H1",
|
||||
"H2",
|
||||
"H3",
|
||||
"H4",
|
||||
"H5",
|
||||
"H6",
|
||||
"P",
|
||||
"HR",
|
||||
"PRE",
|
||||
"TABLE",
|
||||
];
|
||||
|
||||
var html = "";
|
||||
node.childNodes.forEach(function (child, index) {
|
||||
if (index === 0) {
|
||||
html += "<p>";
|
||||
}
|
||||
|
||||
if (gapTags.indexOf(child.nodeName) !== -1) {
|
||||
html += "</p><p>";
|
||||
} else if (newlineTags.indexOf(child.nodeName) !== -1) {
|
||||
html += "</p>" + child.outerHTML + "<p>";
|
||||
} else if (child.outerHTML) {
|
||||
html += child.outerHTML;
|
||||
} else if (child.nodeValue) {
|
||||
html += child.nodeValue;
|
||||
}
|
||||
|
||||
if (index === node.childNodes.length - 1) {
|
||||
html += "</p>";
|
||||
}
|
||||
});
|
||||
node.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复字体大小问题,如 baidu.com
|
||||
* @param {*} node
|
||||
*/
|
||||
function fontSizeFixer(node) {
|
||||
node.style.cssText += "font-size:1em;";
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复程序映射
|
||||
*/
|
||||
const fixerMap = {
|
||||
[FIXER_BR]: brFixer,
|
||||
[FIXER_FONTSIZE]: fontSizeFixer,
|
||||
};
|
||||
|
||||
/**
|
||||
* 查找、监听节点,并执行修复函数
|
||||
* @param {*} selector
|
||||
* @param {*} fixer
|
||||
* @param {*} rootSelector
|
||||
*/
|
||||
function run(selector, fixer, rootSelector) {
|
||||
var mutaObserver = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
mutation.addedNodes.forEach(function (addNode) {
|
||||
addNode.querySelectorAll(selector).forEach(fixer);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var rootNodes = [document];
|
||||
if (rootSelector) {
|
||||
rootNodes = document.querySelectorAll(rootSelector);
|
||||
}
|
||||
|
||||
rootNodes.forEach(function (rootNode) {
|
||||
rootNode.querySelectorAll(selector).forEach(fixer);
|
||||
mutaObserver.observe(rootNode, {
|
||||
childList: 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;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 匹配站点
|
||||
*/
|
||||
export async function webfix(href, { injectWebfix }) {
|
||||
try {
|
||||
if (!injectWebfix) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
|
||||
for (var i = 0; i < sites.length; i++) {
|
||||
var site = sites[i];
|
||||
if (isMatch(href, site.pattern)) {
|
||||
if (fixerMap[site.fixer]) {
|
||||
run(site.selector, fixerMap[site.fixer], site.rootSelector);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[kiss-webfix]: ${err.message}`);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { StoragesProvider } from "./hooks/Storage";
|
||||
import { SettingProvider } from "./hooks/Setting";
|
||||
import ThemeProvider from "./hooks/Theme";
|
||||
import Popup from "./views/Popup";
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<StoragesProvider>
|
||||
<SettingProvider>
|
||||
<ThemeProvider>
|
||||
<Popup />
|
||||
</ThemeProvider>
|
||||
</StoragesProvider>
|
||||
</SettingProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
11
src/rules.js
11
src/rules.js
@@ -3,6 +3,7 @@ import path from "path";
|
||||
import { BUILTIN_RULES } from "./config/rules";
|
||||
|
||||
(() => {
|
||||
// rules
|
||||
try {
|
||||
const data = JSON.stringify(BUILTIN_RULES, null, " ");
|
||||
const file = path.resolve(
|
||||
@@ -14,4 +15,14 @@ import { BUILTIN_RULES } from "./config/rules";
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
// version
|
||||
try {
|
||||
var pjson = require("../package.json");
|
||||
const file = path.resolve(__dirname, "../build/web/version.txt");
|
||||
fs.writeFileSync(file, pjson.version);
|
||||
console.info(`Version file generated: ${file}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -3,41 +3,79 @@ import ReactDOM from "react-dom/client";
|
||||
import Action from "./views/Action";
|
||||
import createCache from "@emotion/cache";
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import { getSetting, getRules, matchRule, getFab } from "./libs";
|
||||
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 { webfix } from "./libs/webfix";
|
||||
|
||||
/**
|
||||
* 入口函数
|
||||
*/
|
||||
(async () => {
|
||||
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)
|
||||
) {
|
||||
unsafeWindow.GM = GM;
|
||||
unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// skip iframe
|
||||
if (window.self !== window.top) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 翻译页面
|
||||
const setting = await getSetting();
|
||||
const rules = await getRules();
|
||||
const rule = await matchRule(rules, document.location.href, setting);
|
||||
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);
|
||||
webfix(href, 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 getFab();
|
||||
const fab = await getFabWithDefault();
|
||||
const $action = document.createElement("div");
|
||||
$action.setAttribute("id", "kiss-translator");
|
||||
document.body.parentElement.appendChild($action);
|
||||
const shadowContainer = $action.attachShadow({ mode: "open" });
|
||||
const shadowContainer = $action.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
const shadowRootElement = document.createElement("div");
|
||||
shadowContainer.appendChild(emotionRoot);
|
||||
@@ -55,19 +93,18 @@ import { Translator } from "./libs/translator";
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// 注册菜单
|
||||
GM.registerMenuCommand(
|
||||
"Toggle Translate",
|
||||
(event) => {
|
||||
translator.toggle();
|
||||
},
|
||||
"Q"
|
||||
);
|
||||
GM.registerMenuCommand(
|
||||
"Toggle Style",
|
||||
(event) => {
|
||||
translator.toggleStyle();
|
||||
},
|
||||
"C"
|
||||
);
|
||||
// 同步订阅规则
|
||||
trySyncAllSubRules(setting);
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await init();
|
||||
} catch (err) {
|
||||
console.error("[KISS-Translator]", err);
|
||||
const $err = document.createElement("div");
|
||||
$err.innerText = `KISS-Translator: ${err.message}`;
|
||||
$err.style.cssText = "background:red; color:#fff;";
|
||||
document.body.prepend($err);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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";
|
||||
import { setFab } 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(setFab, 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]);
|
||||
setFabPosition(edgePosition);
|
||||
return edgePosition;
|
||||
});
|
||||
}, [
|
||||
origin,
|
||||
hover,
|
||||
width,
|
||||
height,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
snapEdge,
|
||||
setFabPosition,
|
||||
]);
|
||||
|
||||
const opacity = useMemo(() => {
|
||||
if (snapEdge) {
|
||||
return position.hide ? 0.1 : 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,23 +1,30 @@
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Box from "@mui/material/Box";
|
||||
import Fab from "@mui/material/Fab";
|
||||
import TranslateIcon from "@mui/icons-material/Translate";
|
||||
import ThemeProvider from "../../hooks/Theme";
|
||||
import Draggable from "./Draggable";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { StoragesProvider } from "../../hooks/Storage";
|
||||
import { SettingProvider } from "../../hooks/Setting";
|
||||
import Popup from "../Popup";
|
||||
import { debounce } from "../../libs/utils";
|
||||
import { isGm } from "../../libs/client";
|
||||
import Header from "../Popup/Header";
|
||||
import Box from "@mui/material/Box";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import {
|
||||
DEFAULT_SHORTCUTS,
|
||||
OPT_SHORTCUT_TRANSLATE,
|
||||
OPT_SHORTCUT_STYLE,
|
||||
OPT_SHORTCUT_POPUP,
|
||||
OPT_SHORTCUT_SETTING,
|
||||
} from "../../config";
|
||||
import { shortcutRegister } from "../../libs/shortcut";
|
||||
|
||||
export default function Action({ translator, fab }) {
|
||||
const fabWidth = 40;
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
w: document.documentElement.clientWidth,
|
||||
h: document.documentElement.clientHeight,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
});
|
||||
const [moved, setMoved] = useState(false);
|
||||
|
||||
@@ -25,8 +32,8 @@ export default function Action({ translator, fab }) {
|
||||
() =>
|
||||
debounce(() => {
|
||||
setWindowSize({
|
||||
w: document.documentElement.clientWidth,
|
||||
h: document.documentElement.clientHeight,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
});
|
||||
}),
|
||||
[]
|
||||
@@ -44,6 +51,84 @@ export default function Action({ translator, fab }) {
|
||||
setMoved(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 注册快捷键
|
||||
const shortcuts = translator.setting.shortcuts || DEFAULT_SHORTCUTS;
|
||||
const clearShortcuts = [
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () => {
|
||||
translator.toggle();
|
||||
setShowPopup(false);
|
||||
}),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () => {
|
||||
translator.toggleStyle();
|
||||
setShowPopup(false);
|
||||
}),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () => {
|
||||
setShowPopup((pre) => !pre);
|
||||
}),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () => {
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||
}),
|
||||
];
|
||||
|
||||
return () => {
|
||||
clearShortcuts.forEach((fn) => {
|
||||
fn();
|
||||
});
|
||||
};
|
||||
}, [translator]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGm) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册菜单
|
||||
try {
|
||||
const menuCommandIds = [];
|
||||
menuCommandIds.push(
|
||||
GM.registerMenuCommand(
|
||||
"Toggle Translate (Alt+q)",
|
||||
(event) => {
|
||||
translator.toggle();
|
||||
setShowPopup(false);
|
||||
},
|
||||
"Q"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
"Toggle Style (Alt+c)",
|
||||
(event) => {
|
||||
translator.toggleStyle();
|
||||
setShowPopup(false);
|
||||
},
|
||||
"C"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
"Open Menu (Alt+k)",
|
||||
(event) => {
|
||||
setShowPopup((pre) => !pre);
|
||||
},
|
||||
"K"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
"Open Setting (Alt+o)",
|
||||
(event) => {
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||
},
|
||||
"O"
|
||||
)
|
||||
);
|
||||
|
||||
return () => {
|
||||
menuCommandIds.forEach((id) => {
|
||||
GM.unregisterMenuCommand(id);
|
||||
});
|
||||
};
|
||||
} catch (err) {
|
||||
console.log("[registerMenuCommand]", err);
|
||||
}
|
||||
}, [translator]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
return () => {
|
||||
@@ -53,6 +138,7 @@ export default function Action({ translator, fab }) {
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("click", handleWindowClick);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("click", handleWindowClick);
|
||||
};
|
||||
@@ -76,12 +162,12 @@ 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,
|
||||
};
|
||||
|
||||
return (
|
||||
<StoragesProvider>
|
||||
<SettingProvider>
|
||||
<ThemeProvider>
|
||||
<Draggable
|
||||
key="pop"
|
||||
@@ -89,39 +175,23 @@ export default function Action({ translator, fab }) {
|
||||
show={showPopup}
|
||||
onStart={handleStart}
|
||||
onMove={handleMove}
|
||||
usePaper
|
||||
handler={
|
||||
<Paper style={{ cursor: "move" }} elevation={3}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
>
|
||||
<Box style={{ marginLeft: 16 }}>
|
||||
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
|
||||
</Box>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setShowPopup(false);
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Box style={{ cursor: "move" }}>
|
||||
<Header setShowPopup={setShowPopup} />
|
||||
<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={translator.setting.hideFab ? false : !showPopup}
|
||||
onStart={handleStart}
|
||||
onMove={handleMove}
|
||||
handler={
|
||||
@@ -139,6 +209,6 @@ export default function Action({ translator, fab }) {
|
||||
}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</StoragesProvider>
|
||||
</SettingProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import LoadingIcon from "./LoadingIcon";
|
||||
import {
|
||||
OPT_STYLE_LINE,
|
||||
@@ -6,26 +6,99 @@ import {
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_FUZZY,
|
||||
OPT_STYLE_HIGHTLIGHT,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_DIY,
|
||||
DEFAULT_COLOR,
|
||||
EVENT_KISS,
|
||||
MSG_TRANS_CURRULE,
|
||||
TRANS_NEWLINE_LENGTH,
|
||||
} from "../../config";
|
||||
import { useTranslate } from "../../hooks/Translate";
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
const LineSpan = styled("span")`
|
||||
opacity: 0.6;
|
||||
-webkit-opacity: 0.6;
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: ${(props) => props.$lineStyle};
|
||||
text-decoration-color: ${(props) => props.$lineColor};
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 0.3em;
|
||||
-webkit-text-decoration-line: underline;
|
||||
-webkit-text-decoration-style: ${(props) => props.$lineStyle};
|
||||
-webkit-text-decoration-color: ${(props) => props.$lineColor};
|
||||
-webkit-text-decoration-thickness: 2px;
|
||||
-webkit-text-underline-offset: 0.3em;
|
||||
&: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")`
|
||||
color: #fff;
|
||||
background-color: ${(props) => props.$bgColor};
|
||||
`;
|
||||
|
||||
const DiySpan = styled("span")`
|
||||
${(props) => props.$diyStyle}
|
||||
`;
|
||||
|
||||
function StyledSpan({ textStyle, textDiyStyle, bgColor, children }) {
|
||||
switch (textStyle) {
|
||||
case OPT_STYLE_LINE: // 下划线
|
||||
return (
|
||||
<LineSpan $lineStyle="solid" $lineColor={bgColor}>
|
||||
{children}
|
||||
</LineSpan>
|
||||
);
|
||||
case OPT_STYLE_DOTLINE: // 点状线
|
||||
return (
|
||||
<LineSpan $lineStyle="dotted" $lineColor={bgColor}>
|
||||
{children}
|
||||
</LineSpan>
|
||||
);
|
||||
case OPT_STYLE_DASHLINE: // 虚线
|
||||
return (
|
||||
<LineSpan $lineStyle="dashed" $lineColor={bgColor}>
|
||||
{children}
|
||||
</LineSpan>
|
||||
);
|
||||
case OPT_STYLE_WAVYLINE: // 波浪线
|
||||
return (
|
||||
<LineSpan $lineStyle="wavy" $lineColor={bgColor}>
|
||||
{children}
|
||||
</LineSpan>
|
||||
);
|
||||
case OPT_STYLE_FUZZY: // 模糊
|
||||
return <FuzzySpan>{children}</FuzzySpan>;
|
||||
case OPT_STYLE_HIGHLIGHT: // 高亮
|
||||
return (
|
||||
<HighlightSpan $bgColor={bgColor || DEFAULT_COLOR}>
|
||||
{children}
|
||||
</HighlightSpan>
|
||||
);
|
||||
case OPT_STYLE_DIY: // 自定义
|
||||
return <DiySpan $diyStyle={textDiyStyle}>{children}</DiySpan>;
|
||||
default:
|
||||
return <span>{children}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
export default function Content({ q, translator }) {
|
||||
const [rule, setRule] = useState(translator.rule);
|
||||
const [hover, setHover] = useState(false);
|
||||
const { text, sameLang, loading } = useTranslate(q, rule);
|
||||
const { textStyle, bgColor } = rule;
|
||||
const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
|
||||
const { textStyle, bgColor = "", textDiyStyle = "" } = rule;
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setHover(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setHover(false);
|
||||
};
|
||||
const { newlineLength = TRANS_NEWLINE_LENGTH } = translator.setting;
|
||||
|
||||
const handleKissEvent = (e) => {
|
||||
const { action, args } = e.detail;
|
||||
@@ -34,63 +107,20 @@ export default function Content({ q, translator }) {
|
||||
setRule(args);
|
||||
break;
|
||||
default:
|
||||
// console.log(`[popup] kissEvent action skip: ${action}`);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(EVENT_KISS, handleKissEvent);
|
||||
window.addEventListener(translator.eventName, handleKissEvent);
|
||||
return () => {
|
||||
window.removeEventListener(EVENT_KISS, handleKissEvent);
|
||||
window.removeEventListener(translator.eventName, handleKissEvent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const style = useMemo(() => {
|
||||
const lineColor = bgColor || "";
|
||||
switch (textStyle) {
|
||||
case OPT_STYLE_LINE: // 下划线
|
||||
return {
|
||||
opacity: hover ? 1 : 0.6,
|
||||
textDecoration: `underline 2px ${lineColor}`,
|
||||
textUnderlineOffset: "0.3em",
|
||||
};
|
||||
case OPT_STYLE_DOTLINE: // 点状线
|
||||
return {
|
||||
opacity: hover ? 1 : 0.6,
|
||||
textDecoration: `dotted underline 2px ${lineColor}`,
|
||||
textUnderlineOffset: "0.3em",
|
||||
};
|
||||
case OPT_STYLE_DASHLINE: // 虚线
|
||||
return {
|
||||
opacity: hover ? 1 : 0.6,
|
||||
textDecoration: `dashed underline 2px ${lineColor}`,
|
||||
textUnderlineOffset: "0.3em",
|
||||
};
|
||||
case OPT_STYLE_WAVYLINE: // 波浪线
|
||||
return {
|
||||
opacity: hover ? 1 : 0.6,
|
||||
textDecoration: `wavy underline 2px ${lineColor}`,
|
||||
textUnderlineOffset: "0.3em",
|
||||
};
|
||||
case OPT_STYLE_FUZZY: // 模糊
|
||||
return {
|
||||
filter: hover ? "none" : "blur(5px)",
|
||||
transition: "filter 0.2s ease-in-out",
|
||||
};
|
||||
case OPT_STYLE_HIGHTLIGHT: // 高亮
|
||||
return {
|
||||
color: "#FFF",
|
||||
backgroundColor: bgColor || DEFAULT_COLOR,
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}, [textStyle, hover, bgColor]);
|
||||
}, [translator.eventName]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
{q.length > 40 ? <br /> : " "}
|
||||
{q.length > newlineLength ? <br /> : " "}
|
||||
<LoadingIcon />
|
||||
</>
|
||||
);
|
||||
@@ -99,14 +129,14 @@ export default function Content({ q, translator }) {
|
||||
if (text && !sameLang) {
|
||||
return (
|
||||
<>
|
||||
{q.length > 40 ? <br /> : " "}
|
||||
<span
|
||||
style={style}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
{q.length > newlineLength ? <br /> : " "}
|
||||
<StyledSpan
|
||||
textStyle={textStyle}
|
||||
textDiyStyle={textDiyStyle}
|
||||
bgColor={bgColor}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</StyledSpan>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
174
src/views/Options/Apis.js
Normal file
174
src/views/Options/Apis.js
Normal file
@@ -0,0 +1,174 @@
|
||||
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_OPENAI,
|
||||
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}`);
|
||||
} 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,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
{translator !== OPT_TRANS_MICROSOFT && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"URL"}
|
||||
name="url"
|
||||
value={url}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"KEY"}
|
||||
name="key"
|
||||
value={key}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{translator === OPT_TRANS_OPENAI && (
|
||||
<>
|
||||
<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} />
|
||||
{translator !== OPT_TRANS_MICROSOFT && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
13
src/views/Options/DarkModeButton.js
Normal file
13
src/views/Options/DarkModeButton.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import { useDarkMode } from "../../hooks/ColorMode";
|
||||
import LightModeIcon from "@mui/icons-material/LightMode";
|
||||
import DarkModeIcon from "@mui/icons-material/DarkMode";
|
||||
|
||||
export default function DarkModeButton() {
|
||||
const { darkMode, toggleDarkMode } = useDarkMode();
|
||||
return (
|
||||
<IconButton onClick={toggleDarkMode} color="inherit">
|
||||
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
@@ -4,17 +4,13 @@ import IconButton from "@mui/material/IconButton";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useDarkModeSwitch } from "../../hooks/ColorMode";
|
||||
import { useDarkMode } from "../../hooks/ColorMode";
|
||||
import LightModeIcon from "@mui/icons-material/LightMode";
|
||||
import DarkModeIcon from "@mui/icons-material/DarkMode";
|
||||
import Link from "@mui/material/Link";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import DarkModeButton from "./DarkModeButton";
|
||||
|
||||
function Header(props) {
|
||||
const i18n = useI18n();
|
||||
const { onDrawerToggle } = props;
|
||||
const switchColorMode = useDarkModeSwitch();
|
||||
const darkMode = useDarkMode();
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
@@ -35,12 +31,15 @@ function Header(props) {
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>{`${i18n("app_name")} v${
|
||||
process.env.REACT_APP_VERSION
|
||||
}`}</Box>
|
||||
<IconButton onClick={switchColorMode} color="inherit">
|
||||
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
|
||||
</IconButton>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Link
|
||||
underline="none"
|
||||
color="inherit"
|
||||
href={process.env.REACT_APP_HOMEPAGE}
|
||||
target="_blank"
|
||||
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
|
||||
</Box>
|
||||
<DarkModeButton />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
|
||||
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("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,9 @@ 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";
|
||||
|
||||
function LinkItem({ label, url, icon }) {
|
||||
const match = useMatch(url);
|
||||
@@ -36,12 +39,30 @@ export default function Navigator(props) {
|
||||
url: "/rules",
|
||||
icon: <DesignServicesIcon />,
|
||||
},
|
||||
{
|
||||
id: "input_setting",
|
||||
label: i18n("input_setting"),
|
||||
url: "/input",
|
||||
icon: <InputIcon />,
|
||||
},
|
||||
{
|
||||
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: "about", label: i18n("about"), url: "/about", icon: <InfoIcon /> },
|
||||
];
|
||||
return (
|
||||
|
||||
175
src/views/Options/OwSubRule.js
Normal file
175
src/views/Options/OwSubRule.js
Normal file
@@ -0,0 +1,175 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_STYLE_DIY,
|
||||
OPT_STYLE_USE_COLOR,
|
||||
} from "../../config";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { useOwSubRule } from "../../hooks/SubRules";
|
||||
|
||||
export default function OwSubRule() {
|
||||
const i18n = useI18n();
|
||||
const { owSubrule, updateOwSubrule } = useOwSubRule();
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
const { name, value } = e.target;
|
||||
updateOwSubrule({ [name]: value });
|
||||
};
|
||||
|
||||
const {
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
} = owSubrule;
|
||||
|
||||
const RemainItem = (
|
||||
<MenuItem key={REMAIN_KEY} value={REMAIN_KEY}>
|
||||
{i18n("remain_unchanged")}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const GlobalItem = (
|
||||
<MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}>
|
||||
{GLOBAL_KEY}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={12}>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transOpen"
|
||||
value={transOpen}
|
||||
label={i18n("translate_switch")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
<MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem>
|
||||
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="translator"
|
||||
value={translator}
|
||||
label={i18n("translate_service")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_TRANS_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="fromLang"
|
||||
value={fromLang}
|
||||
label={i18n("from_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="toLang"
|
||||
value={toLang}
|
||||
label={i18n("to_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="textStyle"
|
||||
value={textStyle}
|
||||
label={i18n("text_style")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
name="bgColor"
|
||||
value={bgColor}
|
||||
label={i18n("bg_color")}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{textStyle === OPT_STYLE_DIY && (
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("diy_style")}
|
||||
helperText={i18n("diy_style_helper")}
|
||||
name="textDiyStyle"
|
||||
value={textDiyStyle}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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 Alert from "@mui/material/Alert";
|
||||
import {
|
||||
GLOBAL_KEY,
|
||||
DEFAULT_RULE,
|
||||
@@ -10,8 +11,11 @@ import {
|
||||
OPT_LANGS_TO,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_STYLE_DIY,
|
||||
OPT_STYLE_USE_COLOR,
|
||||
URL_KISS_RULES_NEW_ISSUE,
|
||||
} from "../../config";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useState, useRef, useEffect, useMemo } from "react";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
@@ -23,7 +27,7 @@ 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, useSettingUpdate } from "../../hooks/Setting";
|
||||
import { useSetting } from "../../hooks/Setting";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
@@ -34,13 +38,23 @@ import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ShareIcon from "@mui/icons-material/Share";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import { useSubrules } from "../../hooks/Rules";
|
||||
import { rulesCache, tryLoadRules } from "../../libs/rules";
|
||||
import { useSubRules } from "../../hooks/SubRules";
|
||||
import { syncSubRules } from "../../libs/subRules";
|
||||
import { loadOrFetchSubRules } from "../../libs/subRules";
|
||||
import { useAlert } from "../../hooks/Alert";
|
||||
import { loadSyncOpt, syncShareRules } from "../../libs/sync";
|
||||
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";
|
||||
|
||||
function RuleFields({ rule, rules, setShow }) {
|
||||
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
|
||||
function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
const initFormValues = rule || {
|
||||
...DEFAULT_RULE,
|
||||
transOpen: "true",
|
||||
};
|
||||
const editMode = !!rule;
|
||||
|
||||
const i18n = useI18n();
|
||||
@@ -56,6 +70,7 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
} = formValues;
|
||||
|
||||
const hasSamePattern = (str) => {
|
||||
@@ -73,10 +88,21 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
setErrors((pre) => ({ ...pre, [name]: "" }));
|
||||
};
|
||||
|
||||
const handlePatternChange = useMemo(
|
||||
() =>
|
||||
debounce(async (patterns) => {
|
||||
setKeyword(patterns.trim());
|
||||
}, 500),
|
||||
[setKeyword]
|
||||
);
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
const { name, value } = e.target;
|
||||
setFormValues((pre) => ({ ...pre, [name]: value }));
|
||||
if (name === "pattern" && !editMode) {
|
||||
handlePatternChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = (e) => {
|
||||
@@ -119,7 +145,7 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
}
|
||||
};
|
||||
|
||||
const globalItem = rule?.pattern !== "*" && (
|
||||
const GlobalItem = rule?.pattern !== "*" && (
|
||||
<MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}>
|
||||
{GLOBAL_KEY}
|
||||
</MenuItem>
|
||||
@@ -166,7 +192,7 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{globalItem}
|
||||
{GlobalItem}
|
||||
<MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem>
|
||||
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
|
||||
</TextField>
|
||||
@@ -182,7 +208,7 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{globalItem}
|
||||
{GlobalItem}
|
||||
{OPT_TRANS_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
@@ -201,7 +227,7 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{globalItem}
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
@@ -220,7 +246,7 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{globalItem}
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
@@ -239,7 +265,7 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{globalItem}
|
||||
{GlobalItem}
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
@@ -247,20 +273,35 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
name="bgColor"
|
||||
value={bgColor}
|
||||
label={i18n("bg_color")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Grid>
|
||||
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
name="bgColor"
|
||||
value={bgColor}
|
||||
label={i18n("bg_color")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{textStyle === OPT_STYLE_DIY && (
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("diy_style")}
|
||||
helperText={i18n("diy_style_helper")}
|
||||
name="textDiyStyle"
|
||||
value={textDiyStyle}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
/>
|
||||
)}
|
||||
|
||||
{rules &&
|
||||
(editMode ? (
|
||||
// 编辑
|
||||
@@ -322,6 +363,7 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
}
|
||||
|
||||
function RuleAccordion({ rule, rules }) {
|
||||
const i18n = useI18n();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const handleChange = (e) => {
|
||||
@@ -336,7 +378,9 @@ function RuleAccordion({ rule, rules }) {
|
||||
opacity: rules ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
{rule.pattern}
|
||||
{rule.pattern === GLOBAL_KEY
|
||||
? `[${i18n("global_rule")}] ${rule.pattern}`
|
||||
: rule.pattern}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
@@ -396,12 +440,12 @@ function UploadButton({ onChange, text }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ShareButton({ rules, injectRules, selectedSub }) {
|
||||
function ShareButton({ rules, injectRules, selectedUrl }) {
|
||||
const alert = useAlert();
|
||||
const i18n = useI18n();
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
const { syncUrl, syncKey } = await loadSyncOpt();
|
||||
const { syncUrl, syncKey } = await getSyncWithDefault();
|
||||
if (!syncUrl || !syncKey) {
|
||||
alert.warning(i18n("error_sync_setting"));
|
||||
return;
|
||||
@@ -409,7 +453,7 @@ function ShareButton({ rules, injectRules, selectedSub }) {
|
||||
|
||||
const shareRules = [...rules.list];
|
||||
if (injectRules) {
|
||||
const subRules = await tryLoadRules(selectedSub?.url);
|
||||
const subRules = await loadOrFetchSubRules(selectedUrl);
|
||||
shareRules.splice(-1, 0, ...subRules);
|
||||
}
|
||||
|
||||
@@ -433,21 +477,20 @@ function ShareButton({ rules, injectRules, selectedSub }) {
|
||||
onClick={handleClick}
|
||||
startIcon={<ShareIcon />}
|
||||
>
|
||||
{"分享"}
|
||||
{i18n("share")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function UserRules() {
|
||||
function UserRules({ subRules }) {
|
||||
const i18n = useI18n();
|
||||
const rules = useRules();
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const setting = useSetting();
|
||||
const updateSetting = useSettingUpdate();
|
||||
const subrules = useSubrules();
|
||||
const selectedSub = subrules.list.find((item) => item.selected);
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const [keyword, setKeyword] = useState("");
|
||||
|
||||
const injectRules = !!setting?.injectRules;
|
||||
const { selectedUrl, selectedRules } = subRules;
|
||||
|
||||
const handleImport = (e) => {
|
||||
const file = e.target.files[0];
|
||||
@@ -477,9 +520,21 @@ function UserRules() {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!showAdd) {
|
||||
setKeyword("");
|
||||
}
|
||||
}, [showAdd]);
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<Stack direction="row" spacing={2} useFlexGap flexWrap="wrap">
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
useFlexGap
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
@@ -501,9 +556,22 @@ function UserRules() {
|
||||
<ShareButton
|
||||
rules={rules}
|
||||
injectRules={injectRules}
|
||||
selectedSub={selectedSub}
|
||||
selectedUrl={selectedUrl}
|
||||
/>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
rules.clear();
|
||||
}}
|
||||
startIcon={<ClearAllIcon />}
|
||||
>
|
||||
{i18n("clear_all")}
|
||||
</Button>
|
||||
|
||||
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
@@ -516,24 +584,58 @@ function UserRules() {
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{showAdd && <RuleFields rules={rules} setShow={setShowAdd} />}
|
||||
{showAdd && (
|
||||
<RuleFields
|
||||
rules={rules}
|
||||
setShow={setShowAdd}
|
||||
setKeyword={setKeyword}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
{rules.list.map((rule) => (
|
||||
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} />
|
||||
))}
|
||||
{rules.list
|
||||
.filter(
|
||||
(rule) =>
|
||||
rule.pattern.includes(keyword) || keyword.includes(rule.pattern)
|
||||
)
|
||||
.map((rule) => (
|
||||
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} />
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{injectRules && (
|
||||
<Box>
|
||||
{selectedRules
|
||||
.filter(
|
||||
(rule) =>
|
||||
rule.pattern.includes(keyword) || keyword.includes(rule.pattern)
|
||||
)
|
||||
.map((rule) => (
|
||||
<RuleAccordion key={rule.pattern} rule={rule} />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
|
||||
function SubRulesItem({
|
||||
index,
|
||||
url,
|
||||
syncAt,
|
||||
selectedUrl,
|
||||
delSub,
|
||||
setSelectedRules,
|
||||
updateDataCache,
|
||||
deleteDataCache,
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleDel = async () => {
|
||||
try {
|
||||
await subrules.del(url);
|
||||
await rulesCache.del(url);
|
||||
await delSub(url);
|
||||
await delSubRules(url);
|
||||
await deleteDataCache(url);
|
||||
} catch (err) {
|
||||
console.log("[del subrules]", err);
|
||||
}
|
||||
@@ -542,11 +644,11 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const rules = await rulesCache.fetch(url);
|
||||
await rulesCache.set(url, rules);
|
||||
if (url === selectedUrl) {
|
||||
setRules(rules);
|
||||
const rules = await syncSubRules(url);
|
||||
if (rules.length > 0 && url === selectedUrl) {
|
||||
setSelectedRules(rules);
|
||||
}
|
||||
await updateDataCache(url);
|
||||
} catch (err) {
|
||||
console.log("[sync sub rules]", err);
|
||||
} finally {
|
||||
@@ -558,6 +660,12 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<FormControlLabel value={url} control={<Radio />} label={url} />
|
||||
|
||||
{syncAt && (
|
||||
<span style={{ marginLeft: "0.5em", opacity: 0.5 }}>
|
||||
[{new Date(syncAt).toLocaleString()}]
|
||||
</span>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<CircularProgress size={16} />
|
||||
) : (
|
||||
@@ -575,11 +683,12 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SubRulesEdit({ subrules }) {
|
||||
function SubRulesEdit({ subList, addSub, updateDataCache }) {
|
||||
const i18n = useI18n();
|
||||
const [inputText, setInputText] = useState("");
|
||||
const [inputError, setInputError] = useState("");
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleCancel = (e) => {
|
||||
e.preventDefault();
|
||||
@@ -597,23 +706,26 @@ function SubRulesEdit({ subrules }) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (subrules.list.find((item) => item.url === url)) {
|
||||
if (subList.find((item) => item.url === url)) {
|
||||
setInputError(i18n("error_duplicate_values"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const rules = await rulesCache.fetch(url);
|
||||
setLoading(true);
|
||||
const rules = await syncSubRules(url);
|
||||
if (rules.length === 0) {
|
||||
throw new Error("empty rules");
|
||||
}
|
||||
await rulesCache.set(url, rules);
|
||||
await subrules.add(url);
|
||||
await addSub(url);
|
||||
await updateDataCache(url);
|
||||
setShowInput(false);
|
||||
setInputText("");
|
||||
} catch (err) {
|
||||
console.log("[fetch rules]", err);
|
||||
setInputError(i18n("error_fetch_url"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -641,6 +753,7 @@ function SubRulesEdit({ subrules }) {
|
||||
>
|
||||
{i18n("add")}
|
||||
</Button>
|
||||
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
|
||||
</Stack>
|
||||
|
||||
{showInput && (
|
||||
@@ -656,7 +769,12 @@ function SubRulesEdit({ subrules }) {
|
||||
/>
|
||||
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<Button size="small" variant="contained" onClick={handleSave}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
>
|
||||
{i18n("save")}
|
||||
</Button>
|
||||
<Button size="small" variant="outlined" onClick={handleCancel}>
|
||||
@@ -669,47 +787,49 @@ function SubRulesEdit({ subrules }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SubRules() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [rules, setRules] = useState([]);
|
||||
const subrules = useSubrules();
|
||||
const selectedSub = subrules.list.find((item) => item.selected);
|
||||
function SubRules({ subRules }) {
|
||||
const {
|
||||
subList,
|
||||
selectSub,
|
||||
addSub,
|
||||
delSub,
|
||||
selectedUrl,
|
||||
selectedRules,
|
||||
setSelectedRules,
|
||||
loading,
|
||||
} = subRules;
|
||||
const { dataCaches, updateDataCache, deleteDataCache, reloadSync } =
|
||||
useSyncCaches();
|
||||
|
||||
const handleSelect = (e) => {
|
||||
const url = e.target.value;
|
||||
subrules.select(url);
|
||||
selectSub(url);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (selectedSub?.url) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const rules = await tryLoadRules(selectedSub?.url);
|
||||
setRules(rules);
|
||||
} catch (err) {
|
||||
console.log("[load rules]", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [selectedSub?.url]);
|
||||
reloadSync();
|
||||
}, [selectedRules, reloadSync]);
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<SubRulesEdit subrules={subrules} />
|
||||
<SubRulesEdit
|
||||
subList={subList}
|
||||
addSub={addSub}
|
||||
updateDataCache={updateDataCache}
|
||||
/>
|
||||
|
||||
<RadioGroup value={selectedSub?.url} onChange={handleSelect}>
|
||||
{subrules.list.map((item, index) => (
|
||||
<RadioGroup value={selectedUrl} onChange={handleSelect}>
|
||||
{subList.map((item, index) => (
|
||||
<SubRulesItem
|
||||
key={item.url}
|
||||
url={item.url}
|
||||
syncAt={dataCaches[item.url]}
|
||||
index={index}
|
||||
selectedUrl={selectedSub?.url}
|
||||
subrules={subrules}
|
||||
setRules={setRules}
|
||||
selectedUrl={selectedUrl}
|
||||
delSub={delSub}
|
||||
setSelectedRules={setSelectedRules}
|
||||
updateDataCache={updateDataCache}
|
||||
deleteDataCache={deleteDataCache}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
@@ -720,7 +840,9 @@ function SubRules() {
|
||||
<CircularProgress />
|
||||
</center>
|
||||
) : (
|
||||
rules.map((rule) => <RuleAccordion key={rule.pattern} rule={rule} />)
|
||||
selectedRules.map((rule) => (
|
||||
<RuleAccordion key={rule.pattern} rule={rule} />
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
@@ -730,6 +852,7 @@ function SubRules() {
|
||||
export default function Rules() {
|
||||
const i18n = useI18n();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const subRules = useSubRules();
|
||||
|
||||
const handleTabChange = (e, newValue) => {
|
||||
setActiveTab(newValue);
|
||||
@@ -738,14 +861,26 @@ export default function Rules() {
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Alert severity="info">
|
||||
{i18n("rules_warn_1")}
|
||||
<br />
|
||||
{i18n("rules_warn_2")}
|
||||
</Alert>
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<Tab label={i18n("edit_rules")} />
|
||||
<Tab label={i18n("personal_rules")} />
|
||||
<Tab label={i18n("subscribe_rules")} />
|
||||
<Tab label={i18n("overwrite_subscribe_rules")} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<div hidden={activeTab !== 0}>{activeTab === 0 && <UserRules />}</div>
|
||||
<div hidden={activeTab !== 1}>{activeTab === 1 && <SubRules />}</div>
|
||||
<div hidden={activeTab !== 0}>
|
||||
{activeTab === 0 && <UserRules subRules={subRules} />}
|
||||
</div>
|
||||
<div hidden={activeTab !== 1}>
|
||||
{activeTab === 1 && <SubRules subRules={subRules} />}
|
||||
</div>
|
||||
<div hidden={activeTab !== 2}>{activeTab === 2 && <OwSubRule />}</div>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -5,52 +5,85 @@ import TextField from "@mui/material/TextField";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import Select from "@mui/material/Select";
|
||||
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
|
||||
import { limitNumber, debounce } from "../../libs/utils";
|
||||
import Link from "@mui/material/Link";
|
||||
import FormHelperText from "@mui/material/FormHelperText";
|
||||
import { useSetting } from "../../hooks/Setting";
|
||||
import { limitNumber } from "../../libs/utils";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import { UI_LANGS } from "../../config";
|
||||
import { useMemo } from "react";
|
||||
import { useAlert } from "../../hooks/Alert";
|
||||
import { isExt } from "../../libs/client";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import {
|
||||
UI_LANGS,
|
||||
TRANS_NEWLINE_LENGTH,
|
||||
CACHE_NAME,
|
||||
OPT_MOUSEKEY_ALL,
|
||||
OPT_MOUSEKEY_DISABLE,
|
||||
OPT_SHORTCUT_TRANSLATE,
|
||||
OPT_SHORTCUT_STYLE,
|
||||
OPT_SHORTCUT_POPUP,
|
||||
OPT_SHORTCUT_SETTING,
|
||||
} from "../../config";
|
||||
import { useShortcut } from "../../hooks/Shortcut";
|
||||
import ShortcutInput from "./ShortcutInput";
|
||||
|
||||
function ShortcutItem({ action, label }) {
|
||||
const { shortcut, setShortcut } = useShortcut(action);
|
||||
return (
|
||||
<ShortcutInput value={shortcut} onChange={setShortcut} label={label} />
|
||||
);
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
const i18n = useI18n();
|
||||
const setting = useSetting();
|
||||
const updateSetting = useSettingUpdate();
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const alert = useAlert();
|
||||
|
||||
const handleChange = useMemo(
|
||||
() =>
|
||||
debounce((e) => {
|
||||
e.preventDefault();
|
||||
let { name, value } = e.target;
|
||||
switch (name) {
|
||||
case "fetchLimit":
|
||||
value = limitNumber(value, 1, 100);
|
||||
break;
|
||||
case "fetchInterval":
|
||||
value = limitNumber(value, 0, 5000);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
updateSetting({
|
||||
[name]: value,
|
||||
});
|
||||
}, 500),
|
||||
[updateSetting]
|
||||
);
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
let { name, value } = e.target;
|
||||
switch (name) {
|
||||
case "fetchLimit":
|
||||
value = limitNumber(value, 1, 100);
|
||||
break;
|
||||
case "fetchInterval":
|
||||
value = limitNumber(value, 0, 5000);
|
||||
break;
|
||||
case "minLength":
|
||||
value = limitNumber(value, 1, 100);
|
||||
break;
|
||||
case "maxLength":
|
||||
value = limitNumber(value, 100, 10000);
|
||||
break;
|
||||
case "newlineLength":
|
||||
value = limitNumber(value, 1, 1000);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
updateSetting({
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
const handleClearCache = () => {
|
||||
try {
|
||||
caches.delete(CACHE_NAME);
|
||||
alert.success(i18n("clear_success"));
|
||||
} catch (err) {
|
||||
console.log("[clear cache]", err);
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
uiLang,
|
||||
googleUrl,
|
||||
fetchLimit,
|
||||
fetchInterval,
|
||||
openaiUrl,
|
||||
openaiKey,
|
||||
openaiModel,
|
||||
openaiPrompt,
|
||||
minLength,
|
||||
maxLength,
|
||||
clearCache,
|
||||
newlineLength = TRANS_NEWLINE_LENGTH,
|
||||
mouseKey = OPT_MOUSEKEY_DISABLE,
|
||||
hideFab = false,
|
||||
} = setting;
|
||||
|
||||
return (
|
||||
@@ -90,60 +123,112 @@ export default function Settings() {
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("min_translate_length")}
|
||||
type="number"
|
||||
name="minLength"
|
||||
defaultValue={minLength}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("max_translate_length")}
|
||||
type="number"
|
||||
name="maxLength"
|
||||
defaultValue={maxLength}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("num_of_newline_characters")}
|
||||
type="number"
|
||||
name="newlineLength"
|
||||
defaultValue={newlineLength}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("clear_cache")}</InputLabel>
|
||||
<InputLabel>{i18n("mouseover_translation")}</InputLabel>
|
||||
<Select
|
||||
name="clearCache"
|
||||
value={clearCache}
|
||||
label={i18n("clear_cache")}
|
||||
name="mouseKey"
|
||||
value={mouseKey}
|
||||
label={i18n("mouseover_translation")}
|
||||
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>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("google_api")}
|
||||
name="googleUrl"
|
||||
defaultValue={googleUrl}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{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>
|
||||
) : (
|
||||
<>
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("hide_fab_button")}</InputLabel>
|
||||
<Select
|
||||
name="hideFab"
|
||||
value={hideFab}
|
||||
label={i18n("hide_fab_button")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("show")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("hide")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("openai_api")}
|
||||
name="openaiUrl"
|
||||
defaultValue={openaiUrl}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
type="password"
|
||||
label={i18n("openai_key")}
|
||||
name="openaiKey"
|
||||
defaultValue={openaiKey}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("openai_model")}
|
||||
name="openaiModel"
|
||||
defaultValue={openaiModel}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("openai_prompt")}
|
||||
name="openaiPrompt"
|
||||
defaultValue={openaiPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -6,32 +6,45 @@ import { useSync } from "../../hooks/Sync";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Link from "@mui/material/Link";
|
||||
import { URL_KISS_WORKER } from "../../config";
|
||||
import { debounce } from "../../libs/utils";
|
||||
import { useMemo } from "react";
|
||||
import { syncAll } from "../../libs/sync";
|
||||
import { useState } from "react";
|
||||
import { syncSettingAndRules } from "../../libs/sync";
|
||||
import Button from "@mui/material/Button";
|
||||
import { useAlert } from "../../hooks/Alert";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { useSetting } from "../../hooks/Setting";
|
||||
|
||||
export default function SyncSetting() {
|
||||
const i18n = useI18n();
|
||||
const sync = useSync();
|
||||
const { sync, updateSync } = useSync();
|
||||
const alert = useAlert();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { reloadSetting } = useSetting();
|
||||
|
||||
const handleChange = useMemo(
|
||||
() =>
|
||||
debounce(async (e) => {
|
||||
e.preventDefault();
|
||||
const { name, value } = e.target;
|
||||
await sync.update({
|
||||
[name]: value,
|
||||
});
|
||||
await syncAll();
|
||||
}, 1000),
|
||||
[sync]
|
||||
);
|
||||
const handleChange = async (e) => {
|
||||
e.preventDefault();
|
||||
const { name, value } = e.target;
|
||||
await updateSync({
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
if (!sync.opt) {
|
||||
return;
|
||||
}
|
||||
const handleSyncTest = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setLoading(true);
|
||||
await syncSettingAndRules();
|
||||
await reloadSetting();
|
||||
alert.success(i18n("sync_success"));
|
||||
} catch (err) {
|
||||
console.log("[sync all]", err);
|
||||
alert.error(i18n("sync_failed"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const { syncUrl, syncKey } = sync.opt;
|
||||
const { syncUrl = "", syncKey = "" } = sync;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
@@ -42,10 +55,12 @@ export default function SyncSetting() {
|
||||
size="small"
|
||||
label={i18n("data_sync_url")}
|
||||
name="syncUrl"
|
||||
defaultValue={syncUrl}
|
||||
value={syncUrl}
|
||||
onChange={handleChange}
|
||||
helperText={
|
||||
<Link href={URL_KISS_WORKER}>{i18n("about_sync_api")}</Link>
|
||||
<Link href={URL_KISS_WORKER} target="_blank">
|
||||
{i18n("about_sync_api")}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -54,9 +69,28 @@ export default function SyncSetting() {
|
||||
type="password"
|
||||
label={i18n("data_sync_key")}
|
||||
name="syncKey"
|
||||
defaultValue={syncKey}
|
||||
value={syncKey}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
useFlexGap
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
disabled={!syncUrl || !syncKey || loading}
|
||||
onClick={handleSyncTest}
|
||||
startIcon={<SyncIcon />}
|
||||
>
|
||||
{i18n("sync_now")}
|
||||
</Button>
|
||||
{loading && <CircularProgress size={16} />}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
165
src/views/Options/Webfix.js
Normal file
165
src/views/Options/Webfix.js
Normal file
@@ -0,0 +1,165 @@
|
||||
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 } 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";
|
||||
|
||||
function ApiFields({ site }) {
|
||||
const { selector, rootSelector, fixer } = site;
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"rootSelector"}
|
||||
name="rootSelector"
|
||||
value={rootSelector || "document"}
|
||||
disabled
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"selector"}
|
||||
name="selector"
|
||||
value={selector}
|
||||
disabled
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"fixer"}
|
||||
name="fixer"
|
||||
value={fixer}
|
||||
disabled
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function ApiAccordion({ site }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setExpanded((pre) => !pre);
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion expanded={expanded} onChange={handleChange}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>{site.pattern}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{expanded && <ApiFields site={site} />}
|
||||
</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 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="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>
|
||||
|
||||
{setting.injectWebfix && (
|
||||
<Box>
|
||||
{loading ? (
|
||||
<center>
|
||||
<CircularProgress size={16} />
|
||||
</center>
|
||||
) : (
|
||||
sites.map((site) => (
|
||||
<ApiAccordion key={site.pattern} site={site} />
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -4,17 +4,25 @@ import Rules from "./Rules";
|
||||
import Setting from "./Setting";
|
||||
import Layout from "./Layout";
|
||||
import SyncSetting from "./SyncSetting";
|
||||
import { StoragesProvider } from "../../hooks/Storage";
|
||||
import { SettingProvider } from "../../hooks/Setting";
|
||||
import ThemeProvider from "../../hooks/Theme";
|
||||
import { useEffect, useState } from "react";
|
||||
import { isGm } from "../../libs/browser";
|
||||
import { isGm } from "../../libs/client";
|
||||
import { sleep } from "../../libs/utils";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { syncAll } from "../../libs/sync";
|
||||
import { trySyncSettingAndRules } from "../../libs/sync";
|
||||
import { AlertProvider } from "../../hooks/Alert";
|
||||
import Link from "@mui/material/Link";
|
||||
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";
|
||||
|
||||
export default function Options() {
|
||||
const [error, setError] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -23,52 +31,99 @@ export default function Options() {
|
||||
// 等待GM注入
|
||||
let i = 0;
|
||||
for (;;) {
|
||||
if (window.APP_NAME === process.env.REACT_APP_NAME) {
|
||||
if (window?.APP_INFO?.name === process.env.REACT_APP_NAME) {
|
||||
const { version, eventName } = window.APP_INFO;
|
||||
|
||||
// 检查版本是否一致
|
||||
if (version !== process.env.REACT_APP_VERSION) {
|
||||
setError(
|
||||
`The version is inconsistent, please check whether the script(v${version}) is the latest version(v${process.env.REACT_APP_VERSION}). (版本不一致,请检查脚本(v${version})是否为最新版(v${process.env.REACT_APP_VERSION}))`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (eventName) {
|
||||
// 注入GM接口
|
||||
adaptScript(eventName);
|
||||
}
|
||||
|
||||
// 同步数据
|
||||
await trySyncSettingAndRules();
|
||||
setReady(true);
|
||||
break;
|
||||
}
|
||||
|
||||
if (++i > 8) {
|
||||
setError(true);
|
||||
setError("Time out. (连接超时)");
|
||||
break;
|
||||
}
|
||||
|
||||
await sleep(1000);
|
||||
}
|
||||
} else {
|
||||
// 同步数据
|
||||
await trySyncSettingAndRules();
|
||||
setReady(true);
|
||||
}
|
||||
|
||||
// 同步数据
|
||||
syncAll();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
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{" "}
|
||||
<a href={process.env.REACT_APP_HOMEPAGE}>KISS Translator</a>{" "}
|
||||
GreaseMonkey script?
|
||||
</h2>
|
||||
<h2>
|
||||
<a href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>Click here</a>{" "}
|
||||
to install, or <a href={process.env.REACT_APP_HOMEPAGE}>click here</a>{" "}
|
||||
for help.
|
||||
Please confirm whether to install or enable KISS Translator
|
||||
GreaseMonkey script? (请检查是否安装或启用简约翻译油猴脚本)
|
||||
</h2>
|
||||
<Stack spacing={2}>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
||||
Install Userscript for Tampermonkey/Violentmonkey 1 (油猴脚本
|
||||
安装地址 1)
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
|
||||
Install Userscript for Tampermonkey/Violentmonkey 2 (油猴脚本
|
||||
安装地址 2)
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
|
||||
Install Userscript for iOS Safari 1 (油猴脚本 iOS Safari专用
|
||||
安装地址 1)
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
|
||||
Install Userscript for iOS Safari 2 (油猴脚本 iOS Safari专用
|
||||
安装地址 2)
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
|
||||
Open Options Page 1 (打开设置页面 1)
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
|
||||
Open Options Page 2 (打开设置页面 2)
|
||||
</Link>
|
||||
</Stack>
|
||||
</center>
|
||||
);
|
||||
}
|
||||
|
||||
if (isGm && !ready) {
|
||||
if (!ready) {
|
||||
return (
|
||||
<center>
|
||||
<Divider>
|
||||
<Link
|
||||
href={process.env.REACT_APP_HOMEPAGE}
|
||||
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||
</Divider>
|
||||
<CircularProgress />
|
||||
</center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StoragesProvider>
|
||||
<SettingProvider>
|
||||
<ThemeProvider>
|
||||
<AlertProvider>
|
||||
<HashRouter>
|
||||
@@ -76,13 +131,16 @@ export default function Options() {
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Setting />} />
|
||||
<Route path="rules" element={<Rules />} />
|
||||
<Route path="input" element={<InputSetting />} />
|
||||
<Route path="apis" element={<Apis />} />
|
||||
<Route path="sync" element={<SyncSetting />} />
|
||||
<Route path="webfix" element={<Webfix />} />
|
||||
<Route path="about" element={<About />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</AlertProvider>
|
||||
</ThemeProvider>
|
||||
</StoragesProvider>
|
||||
</SettingProvider>
|
||||
);
|
||||
}
|
||||
|
||||
47
src/views/Popup/Header.js
Normal file
47
src/views/Popup/Header.js
Normal file
@@ -0,0 +1,47 @@
|
||||
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";
|
||||
|
||||
export default function Header({ setShowPopup }) {
|
||||
const handleHomepage = () => {
|
||||
window.open(process.env.REACT_APP_HOMEPAGE, "_blank");
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
>
|
||||
<Stack direction="row" justifyContent="flex-start" alignItems="center">
|
||||
<IconButton onClick={handleHomepage}>
|
||||
<HomeIcon />
|
||||
</IconButton>
|
||||
<Box
|
||||
sx={{
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
}}
|
||||
>
|
||||
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{setShowPopup ? (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setShowPopup(false);
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<DarkModeButton />
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -5,10 +5,13 @@ 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 { browser, isExt } from "../../libs/browser";
|
||||
import { sendTabMsg, getTabInfo } from "../../libs/msg";
|
||||
import { browser } from "../../libs/browser";
|
||||
import { isExt } from "../../libs/client";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Header from "./Header";
|
||||
import {
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
@@ -17,7 +20,11 @@ import {
|
||||
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";
|
||||
|
||||
export default function Popup({ setShowPopup, translator: tran }) {
|
||||
const i18n = useI18n();
|
||||
@@ -40,6 +47,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
await sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
} else {
|
||||
tran.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[toggle trans]", err);
|
||||
@@ -55,12 +63,34 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||
} else {
|
||||
tran.updateRule({ [name]: value });
|
||||
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[update rule]", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearCache = () => {
|
||||
try {
|
||||
caches.delete(CACHE_NAME);
|
||||
} catch (err) {
|
||||
console.log("[clear cache]", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveRule = async () => {
|
||||
try {
|
||||
let href = window.location.href;
|
||||
if (isExt) {
|
||||
const tab = await getTabInfo();
|
||||
href = tab.url;
|
||||
}
|
||||
saveRule({ ...rule, pattern: href });
|
||||
} catch (err) {
|
||||
console.log("[save rule]", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExt) {
|
||||
return;
|
||||
@@ -79,8 +109,14 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
|
||||
if (!rule) {
|
||||
return (
|
||||
<Box minWidth={300} sx={{ p: 2 }}>
|
||||
<Stack spacing={3}>
|
||||
<Box minWidth={300}>
|
||||
{isExt && (
|
||||
<>
|
||||
<Header />
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<Stack sx={{ p: 2 }} spacing={3}>
|
||||
<Button variant="text" onClick={handleOpenSetting}>
|
||||
{i18n("setting")}
|
||||
</Button>
|
||||
@@ -92,17 +128,35 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
const { transOpen, translator, fromLang, toLang, textStyle, bgColor } = rule;
|
||||
|
||||
return (
|
||||
<Box minWidth={300} sx={{ p: 2 }}>
|
||||
<Stack spacing={2}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={transOpen === "true"}
|
||||
onChange={handleTransToggle}
|
||||
/>
|
||||
}
|
||||
label={i18n("translate")}
|
||||
/>
|
||||
<Box minWidth={300}>
|
||||
{isExt && (
|
||||
<>
|
||||
<Header />
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<Stack sx={{ p: 2 }} spacing={2}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={transOpen === "true"}
|
||||
onChange={handleTransToggle}
|
||||
/>
|
||||
}
|
||||
label={i18n("translate_alt")}
|
||||
/>
|
||||
{!isExt && (
|
||||
<Button variant="text" onClick={handleClearCache}>
|
||||
{i18n("clear_cache")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<TextField
|
||||
select
|
||||
@@ -158,7 +212,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
size="small"
|
||||
value={textStyle}
|
||||
name="textStyle"
|
||||
label={i18n("text_style")}
|
||||
label={i18n("text_style_alt")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
@@ -168,17 +222,29 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
name="bgColor"
|
||||
value={bgColor}
|
||||
label={i18n("bg_color")}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||
<TextField
|
||||
size="small"
|
||||
name="bgColor"
|
||||
value={bgColor}
|
||||
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>
|
||||
<Button variant="text" onClick={handleOpenSetting}>
|
||||
{i18n("setting")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user