Compare commits
227 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
471a4a3159 | ||
|
|
a2f99da3b4 | ||
|
|
accab22d56 | ||
|
|
6ea5228a5f | ||
|
|
a07d2cafb6 | ||
|
|
1b38f19cc1 | ||
|
|
aa5b286e0b | ||
|
|
6b6bbed330 | ||
|
|
489bc9534b | ||
|
|
01ebc184ad | ||
|
|
f591d66365 | ||
|
|
80782287d8 | ||
|
|
3494bb1297 | ||
|
|
92ffda5220 | ||
|
|
fbaeff6b7b | ||
|
|
248d3726dd | ||
|
|
1553559b1a | ||
|
|
8935ced75a | ||
|
|
a865d6d74f | ||
|
|
6d976554fd | ||
|
|
189b7f480a | ||
|
|
5e3aa7e2d1 | ||
|
|
730be678ef | ||
|
|
9293f422f3 | ||
|
|
6e8158bb34 | ||
|
|
3078d3ca91 | ||
|
|
947e1c7f08 | ||
|
|
938c123412 | ||
|
|
e7a57ad3b2 | ||
|
|
1e40f81bf7 | ||
|
|
72b2f44e32 | ||
|
|
76f54461e7 | ||
|
|
14ca13e31d | ||
|
|
556fd71275 | ||
|
|
a8002bba9f | ||
|
|
ddd9371fbd | ||
|
|
0ea97b73e3 | ||
|
|
f8c8a4ebeb | ||
|
|
5f613ab558 | ||
|
|
56281f9e82 | ||
|
|
5e8743dbb7 | ||
|
|
f4e4c84712 | ||
|
|
c57a0a11fa | ||
|
|
fa244b2097 | ||
|
|
79612f8a1b | ||
|
|
2bf79dbc51 | ||
|
|
c2658d5dd0 | ||
|
|
13684884c7 | ||
|
|
f216a9254e | ||
|
|
dbdbcbba2d | ||
|
|
2ee4609192 | ||
|
|
0d93cf78f7 | ||
|
|
3398ca0dd7 | ||
|
|
c1778fbcbb | ||
|
|
1ef9974c05 | ||
|
|
399c6b6fed | ||
|
|
62a60eee44 | ||
|
|
54339af885 | ||
|
|
06cfd33e60 | ||
|
|
08c9d78d2a | ||
|
|
e7a5e5dce1 | ||
|
|
3a59a127d1 | ||
|
|
26f213cad2 | ||
|
|
7b6148302d | ||
|
|
38c781b8f3 | ||
|
|
64d827fdcd | ||
|
|
74ad812f37 | ||
|
|
364c829119 | ||
|
|
1ac2c5b61e | ||
|
|
0766199353 | ||
|
|
878bccf151 | ||
|
|
acbd258296 | ||
|
|
54a14e6e5a | ||
|
|
298e4b52f0 | ||
|
|
bee1fbcf88 | ||
|
|
345a34287e | ||
|
|
441a2ca2da | ||
|
|
1ff1b21355 | ||
|
|
117ca4e05b | ||
|
|
07d457be4e | ||
|
|
d48296046e | ||
|
|
56350de2cf | ||
|
|
850dc0e83b | ||
|
|
35f01478b1 | ||
|
|
f9a3ec012f | ||
|
|
3b9b404482 | ||
|
|
d8b0cc4834 | ||
|
|
da13f5e218 | ||
|
|
08e14ae11c | ||
|
|
c2902dff28 | ||
|
|
c4fb39f02f | ||
|
|
b7df44c35a | ||
|
|
9a2b21eee5 | ||
|
|
bdac67df88 | ||
|
|
0b8f19bfad | ||
|
|
c7c5866131 | ||
|
|
f772fa000c | ||
|
|
93fd82fcd9 | ||
|
|
3ae10bfd04 | ||
|
|
a44747ccad | ||
|
|
87ab45f936 | ||
|
|
37b046eb46 | ||
|
|
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 | ||
|
|
f463f3ce08 | ||
|
|
c0872db98c | ||
|
|
d3a5d91f01 | ||
|
|
3e9338be0e | ||
|
|
ef7f1ad638 | ||
|
|
1f10ebe404 | ||
|
|
f4a8251c61 | ||
|
|
f585a43480 | ||
|
|
3a11465c24 | ||
|
|
3c3ebdf96c | ||
|
|
6b30f443e1 | ||
|
|
232e9a47a2 | ||
|
|
7ec43a1d3f | ||
|
|
a8caa34bbe | ||
|
|
c2fd1fe9e0 | ||
|
|
2773a76af8 | ||
|
|
1dc7026e8f | ||
|
|
b36ede7393 | ||
|
|
b18721a4e5 | ||
|
|
01676bc682 | ||
|
|
53c32f2bd8 | ||
|
|
0b9fe65833 | ||
|
|
bd45947d68 | ||
|
|
5d2e767e74 | ||
|
|
30af4c11d0 |
29
.env
29
.env
@@ -2,11 +2,28 @@ GENERATE_SOURCEMAP=false
|
|||||||
|
|
||||||
REACT_APP_NAME=KISS Translator
|
REACT_APP_NAME=KISS Translator
|
||||||
REACT_APP_NAME_CN=简约翻译
|
REACT_APP_NAME_CN=简约翻译
|
||||||
REACT_APP_VERSION=1.4.4
|
REACT_APP_VERSION=1.7.2
|
||||||
|
|
||||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
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_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_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
|
||||||
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
|
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
|
||||||
|
|||||||
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@@ -10,12 +10,15 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8.7.6
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: "18.17.0"
|
node-version: "18.17.0"
|
||||||
cache: "yarn"
|
cache: "pnpm"
|
||||||
- run: yarn install
|
- run: pnpm install
|
||||||
- run: yarn build
|
- run: pnpm build
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
nodeLinker: node-modules
|
|
||||||
105
README.en.md
105
README.en.md
@@ -1,10 +1,10 @@
|
|||||||
## KISS Translator
|
# KISS Translator
|
||||||
|
|
||||||
A minimalist [bilingual translation Extension & Greasemonkey Script](https://github.com/fishjar/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)
|
[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.
|
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,49 +14,80 @@ 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.
|
If you also like a little more simplicity, welcome to pick it up.
|
||||||
|
|
||||||
### Features
|
## Features
|
||||||
|
|
||||||
- Keep it simple, smart
|
- [x] 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] Open source
|
- [x] Open source
|
||||||
- [x] Data Synchronization Function
|
- [x] Adapt to common browsers
|
||||||
- [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] Chrome/Edge/Firefox/Kiwi
|
||||||
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox)
|
- [ ] Safari
|
||||||
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (need test)
|
- [x] Supports multiple translation services
|
||||||
|
- [x] Google/Microsoft/DeepL/OpenAI
|
||||||
|
- [x] Custom translation interface
|
||||||
|
- [x] Covers common translation scenarios
|
||||||
|
- [x] Web bilingual translation
|
||||||
|
- [x] Input box translation
|
||||||
|
- [x] Mouseover translation
|
||||||
|
- [x] YouTube subtitle translation
|
||||||
|
- [x] Cross-client data synchronization
|
||||||
|
- [x] KISS-Worker(cloudflare/docker)
|
||||||
|
- [x] WebDAV
|
||||||
|
- [x] Custom translation rules
|
||||||
|
- [x] Rule subscription/rule sharing
|
||||||
|
- [x] Custom translation 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
|
## Install
|
||||||
|
|
||||||
|
> Note: For the following reasons, it is recommended to use browser extensions first
|
||||||
|
>
|
||||||
|
> - Browser extension can use local language recognition
|
||||||
|
> - Grease Monkey script will encounter more usage problems
|
||||||
|
|
||||||
|
- [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
|
```sh
|
||||||
git clone https://github.com/fishjar/kiss-translator.git
|
git clone https://github.com/fishjar/kiss-translator.git
|
||||||
cd kiss-translator
|
cd kiss-translator
|
||||||
yarn install
|
pnpm install
|
||||||
yarn build
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Data Sync
|
## Discussion
|
||||||
|
|
||||||
Goto: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
|
||||||
|
|
||||||
### Discussion
|
|
||||||
|
|
||||||
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
|
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||||
|
|||||||
107
README.md
107
README.md
@@ -1,10 +1,10 @@
|
|||||||
## 简约翻译
|
# 简约翻译
|
||||||
|
|
||||||
一个简约的 [双语网页翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
一个简约的 [网页双语翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
||||||
|
|
||||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
[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) 一起使用,刚好形成很好补充。
|
本项目灵感来源于 [Immersive Translate](https://github.com/immersive-translate/immersive-translate),在试用了后,发现搭配本人早前开发的 [网页划词翻译扩展](https://github.com/fishjar/kiss-dictionary) 一起使用,刚好形成很好补充。
|
||||||
|
|
||||||
@@ -14,49 +14,80 @@
|
|||||||
|
|
||||||
如果你也喜欢简约一点的,欢迎自取。
|
如果你也喜欢简约一点的,欢迎自取。
|
||||||
|
|
||||||
### 特点
|
## 特性
|
||||||
|
|
||||||
- 保持简约
|
- [x] 保持简约
|
||||||
|
|
||||||
### 进度
|
|
||||||
|
|
||||||
- [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] 适配常见浏览器
|
||||||
- [x] 油猴脚本([链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js))
|
- [x] Chrome/Edge/Firefox/Kiwi
|
||||||
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox)
|
- [ ] Safari
|
||||||
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (待测)
|
- [x] 支持多种翻译服务
|
||||||
|
- [x] Google/Microsoft/DeepL/OpenAI
|
||||||
|
- [x] 自定义翻译接口
|
||||||
|
- [x] 覆盖常见翻译场景
|
||||||
|
- [x] 网页双语翻译
|
||||||
|
- [x] 输入框翻译
|
||||||
|
- [x] 鼠标悬停翻译
|
||||||
|
- [x] YouTube 字幕翻译
|
||||||
|
- [x] 跨客户端数据同步
|
||||||
|
- [x] KISS-Worker(cloudflare/docker)
|
||||||
|
- [x] WebDAV
|
||||||
|
- [x] 自定义翻译规则
|
||||||
|
- [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
|
```sh
|
||||||
git clone https://github.com/fishjar/kiss-translator.git
|
git clone https://github.com/fishjar/kiss-translator.git
|
||||||
cd kiss-translator
|
cd kiss-translator
|
||||||
yarn install
|
pnpm install
|
||||||
yarn build
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
### 数据同步
|
## 交流
|
||||||
|
|
||||||
移步: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
|
||||||
|
|
||||||
### 交流
|
|
||||||
|
|
||||||
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
|
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const userscriptWebpack = (config, env) => {
|
|||||||
// @name ${process.env.REACT_APP_NAME}
|
// @name ${process.env.REACT_APP_NAME}
|
||||||
// @namespace ${process.env.REACT_APP_HOMEPAGE}
|
// @namespace ${process.env.REACT_APP_HOMEPAGE}
|
||||||
// @version ${process.env.REACT_APP_VERSION}
|
// @version ${process.env.REACT_APP_VERSION}
|
||||||
// @description A minimalist bilingual translation Extension & Greasemonkey Script (一个简约的双语网页翻译扩展 & 油猴脚本)
|
// @description A minimalist bilingual translation Extension & Greasemonkey Script (一个简约的网页双语翻译扩展 & 油猴脚本)
|
||||||
// @author Gabe<yugang2002@gmail.com>
|
// @author Gabe<yugang2002@gmail.com>
|
||||||
// @homepageURL ${process.env.REACT_APP_HOMEPAGE}
|
// @homepageURL ${process.env.REACT_APP_HOMEPAGE}
|
||||||
// @license GPL-3.0
|
// @license GPL-3.0
|
||||||
@@ -83,21 +83,27 @@ const userscriptWebpack = (config, env) => {
|
|||||||
// @icon ${process.env.REACT_APP_LOGOURL}
|
// @icon ${process.env.REACT_APP_LOGOURL}
|
||||||
// @downloadURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
// @downloadURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||||
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||||
// @grant GM_xmlhttpRequest
|
// @grant GM.xmlHttpRequest
|
||||||
// @grant GM.xmlhttpRequest
|
// @grant GM.registerMenuCommand
|
||||||
// @grant GM_setValue
|
|
||||||
// @grant GM.setValue
|
// @grant GM.setValue
|
||||||
// @grant GM_getValue
|
|
||||||
// @grant GM.getValue
|
// @grant GM.getValue
|
||||||
// @grant GM_deleteValue
|
|
||||||
// @grant GM.deleteValue
|
// @grant GM.deleteValue
|
||||||
|
// @grant GM.info
|
||||||
// @grant unsafeWindow
|
// @grant unsafeWindow
|
||||||
// @connect translate.googleapis.com
|
// @connect translate.googleapis.com
|
||||||
// @connect api-edge.cognitive.microsofttranslator.com
|
// @connect api-edge.cognitive.microsofttranslator.com
|
||||||
// @connect edge.microsoft.com
|
// @connect edge.microsoft.com
|
||||||
|
// @connect api-free.deepl.com
|
||||||
|
// @connect api.deepl.com
|
||||||
// @connect api.openai.com
|
// @connect api.openai.com
|
||||||
// @connect openai.azure.com
|
// @connect openai.azure.com
|
||||||
// @connect workers.dev
|
// @connect workers.dev
|
||||||
|
// @connect github.io
|
||||||
|
// @connect githubusercontent.com
|
||||||
|
// @connect kiss-translator.rayjar.com
|
||||||
|
// @connect ghproxy.com
|
||||||
|
// @connect dav.jianguoyun.com
|
||||||
|
// @connect localhost:3000
|
||||||
// @run-at document-end
|
// @run-at document-end
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
|
|||||||
20
package.json
20
package.json
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "kiss-translator",
|
"name": "kiss-translator",
|
||||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||||
"version": "1.4.4",
|
"version": "1.7.2",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/cache": "^11.11.0",
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.10.8",
|
"@emotion/styled": "^11.10.8",
|
||||||
"@mui/icons-material": "^5.11.11",
|
"@mui/icons-material": "^5.11.11",
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-router-dom": "^6.10.0",
|
"react-router-dom": "^6.10.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"webdav": "^5.3.0",
|
||||||
"webextension-polyfill": "^0.10.0"
|
"webextension-polyfill": "^0.10.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -24,8 +26,10 @@
|
|||||||
"build:edge": "rm -rf build/edge && cp -r build/chrome build/edge",
|
"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: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: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": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript",
|
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
|
||||||
|
"build:rules": "babel-node src/rules.js",
|
||||||
|
"build": "pnpm build:chrome && pnpm build:edge && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
|
||||||
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
|
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
|
||||||
"test": "react-app-rewired test",
|
"test": "react-app-rewired test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
@@ -37,7 +41,8 @@
|
|||||||
],
|
],
|
||||||
"globals": {
|
"globals": {
|
||||||
"GM": true,
|
"GM": true,
|
||||||
"unsafeWindow": true
|
"unsafeWindow": true,
|
||||||
|
"globalThis": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
@@ -53,7 +58,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.22.10",
|
||||||
|
"@babel/node": "^7.22.10",
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
|
"@babel/preset-env": "^7.22.10",
|
||||||
"react-app-rewired": "^2.2.1",
|
"react-app-rewired": "^2.2.1",
|
||||||
"wrangler": "^3.4.0"
|
"wrangler": "^3.4.0"
|
||||||
}
|
},
|
||||||
|
"packageManager": "yarn@3.6.3"
|
||||||
}
|
}
|
||||||
|
|||||||
10738
pnpm-lock.yaml
generated
Normal file
10738
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,5 +4,14 @@
|
|||||||
},
|
},
|
||||||
"app_description": {
|
"app_description": {
|
||||||
"message": "A minimalist bilingual translation Extension & Greasemonkey Script"
|
"message": "A minimalist bilingual translation Extension & Greasemonkey Script"
|
||||||
|
},
|
||||||
|
"toggle_translate": {
|
||||||
|
"message": "Toggle Translate"
|
||||||
|
},
|
||||||
|
"toggle_style": {
|
||||||
|
"message": "Toggle Style"
|
||||||
|
},
|
||||||
|
"open_options": {
|
||||||
|
"message": "Open Options"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
"message": "简约翻译"
|
"message": "简约翻译"
|
||||||
},
|
},
|
||||||
"app_description": {
|
"app_description": {
|
||||||
"message": "一个简约的双语网页翻译扩展 & 油猴脚本"
|
"message": "一个简约的网页双语翻译扩展 & 油猴脚本"
|
||||||
|
},
|
||||||
|
"toggle_translate": {
|
||||||
|
"message": "开启翻译"
|
||||||
|
},
|
||||||
|
"toggle_style": {
|
||||||
|
"message": "切换样式"
|
||||||
|
},
|
||||||
|
"open_options": {
|
||||||
|
"message": "打开设置"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,85 @@
|
|||||||
max-height: 1.2em;
|
max-height: 1.2em;
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root">
|
<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 />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@@ -53,7 +126,16 @@
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<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 />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@@ -86,7 +168,10 @@
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<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 />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@@ -119,7 +204,17 @@
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<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 />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@@ -153,175 +248,42 @@
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<div class="cont cont1">
|
<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>
|
<h2>
|
||||||
React is a <code>JavaScript</code> <a href="#">library</a> for
|
Server Components can run during the build, letting you read from the
|
||||||
building user interfaces.
|
filesystem or fetch static content.
|
||||||
</h2>
|
</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Declarative: React makes it painless to create interactive UIs.
|
They can also run on the server, letting you access your data layer
|
||||||
Design simple views for each state in your application, and React
|
without having to build an API. You can pass data by props from
|
||||||
will efficiently update and render just the right components when
|
Server Components to the interactive Client Components in the
|
||||||
your data changes. Declarative views make your code more
|
browser.
|
||||||
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>
|
||||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<div class="cont cont5">
|
<div class="cont cont2">
|
||||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
<h2>
|
||||||
|
Since our last update, we have merged the React Server Components RFC
|
||||||
|
to ratify the proposal.
|
||||||
|
</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Declarative: React makes it painless to create interactive UIs.
|
RSC combines the simple “request/response” mental model of
|
||||||
Design simple views for each state in your application, and React
|
server-centric Multi-Page Apps with the seamless interactivity of
|
||||||
will efficiently update and render just the right components when
|
client-centric Single-Page Apps, giving you the best of both worlds.
|
||||||
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>
|
||||||
<li>
|
<li>
|
||||||
React 使创建交互式 UI
|
React 使创建交互式 UI
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "1.4.4",
|
"version": "1.7.2",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
@@ -12,9 +12,35 @@
|
|||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"js": ["content.js"],
|
"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"
|
||||||
|
},
|
||||||
|
"description": "__MSG_toggle_translate__"
|
||||||
|
},
|
||||||
|
"toggleStyle": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+C"
|
||||||
|
},
|
||||||
|
"description": "__MSG_toggle_style__"
|
||||||
|
},
|
||||||
|
"openOptions": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+O"
|
||||||
|
},
|
||||||
|
"description": "__MSG_open_options__"
|
||||||
|
}
|
||||||
|
},
|
||||||
"permissions": ["<all_urls>", "storage"],
|
"permissions": ["<all_urls>", "storage"],
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "images/logo16.png",
|
"16": "images/logo16.png",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "1.4.4",
|
"version": "1.7.2",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
@@ -13,9 +13,35 @@
|
|||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"js": ["content.js"],
|
"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"
|
||||||
|
},
|
||||||
|
"description": "__MSG_toggle_translate__"
|
||||||
|
},
|
||||||
|
"toggleStyle": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+C"
|
||||||
|
},
|
||||||
|
"description": "__MSG_toggle_style__"
|
||||||
|
},
|
||||||
|
"openOptions": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+O"
|
||||||
|
},
|
||||||
|
"description": "__MSG_open_options__"
|
||||||
|
}
|
||||||
|
},
|
||||||
"permissions": ["storage"],
|
"permissions": ["storage"],
|
||||||
"host_permissions": ["<all_urls>"],
|
"host_permissions": ["<all_urls>"],
|
||||||
"icons": {
|
"icons": {
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ import { fetchPolyfill } from "../libs/fetch";
|
|||||||
import {
|
import {
|
||||||
OPT_TRANS_GOOGLE,
|
OPT_TRANS_GOOGLE,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_DEEPL,
|
||||||
OPT_TRANS_OPENAI,
|
OPT_TRANS_OPENAI,
|
||||||
URL_MICROSOFT_TRANS,
|
OPT_TRANS_CUSTOMIZE,
|
||||||
OPT_LANGS_SPECIAL,
|
OPT_LANGS_SPECIAL,
|
||||||
PROMPT_PLACE_FROM,
|
PROMPT_PLACE_FROM,
|
||||||
PROMPT_PLACE_TO,
|
PROMPT_PLACE_TO,
|
||||||
KV_HEADER_KEY,
|
KV_SALT_SYNC,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { getSetting, detectLang } from "../libs";
|
import { tryDetectLang } from "../libs";
|
||||||
|
import { sha256 } from "../libs/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步数据
|
* 同步数据
|
||||||
@@ -20,18 +22,21 @@ import { getSetting, detectLang } from "../libs";
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiSyncData = async (url, key, data) =>
|
export const apiSyncData = async (url, key, data) =>
|
||||||
fetchPolyfill(
|
fetchPolyfill(url, {
|
||||||
url,
|
headers: {
|
||||||
{
|
"Content-type": "application/json",
|
||||||
headers: {
|
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
||||||
"Content-type": "application/json",
|
|
||||||
[KV_HEADER_KEY]: key,
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
},
|
},
|
||||||
{ useUnsafe: true }
|
method: "POST",
|
||||||
);
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载数据
|
||||||
|
* @param {*} url
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiFetch = (url) => fetchPolyfill(url);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 谷歌翻译
|
* 谷歌翻译
|
||||||
@@ -40,7 +45,13 @@ export const apiSyncData = async (url, key, data) =>
|
|||||||
* @param {*} from
|
* @param {*} from
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const apiGoogleTranslate = async (translator, text, to, from) => {
|
const apiGoogleTranslate = async (
|
||||||
|
translator,
|
||||||
|
text,
|
||||||
|
to,
|
||||||
|
from,
|
||||||
|
{ url, key, useCache = true }
|
||||||
|
) => {
|
||||||
const params = {
|
const params = {
|
||||||
client: "gtx",
|
client: "gtx",
|
||||||
dt: "t",
|
dt: "t",
|
||||||
@@ -50,17 +61,20 @@ const apiGoogleTranslate = async (translator, text, to, from) => {
|
|||||||
tl: to,
|
tl: to,
|
||||||
q: text,
|
q: text,
|
||||||
};
|
};
|
||||||
const { googleUrl } = await getSetting();
|
const input = `${url}?${queryString.stringify(params)}`;
|
||||||
const input = `${googleUrl}?${queryString.stringify(params)}`;
|
const res = await fetchPolyfill(input, {
|
||||||
return fetchPolyfill(
|
headers: {
|
||||||
input,
|
"Content-type": "application/json",
|
||||||
{
|
|
||||||
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];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,24 +84,72 @@ const apiGoogleTranslate = async (translator, text, to, from) => {
|
|||||||
* @param {*} from
|
* @param {*} from
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const apiMicrosoftTranslate = (translator, text, to, from) => {
|
const apiMicrosoftTranslate = async (
|
||||||
|
translator,
|
||||||
|
text,
|
||||||
|
to,
|
||||||
|
from,
|
||||||
|
{ url, useCache = true }
|
||||||
|
) => {
|
||||||
const params = {
|
const params = {
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
"api-version": "3.0",
|
"api-version": "3.0",
|
||||||
};
|
};
|
||||||
const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`;
|
const input = `${url}?${queryString.stringify(params)}`;
|
||||||
return fetchPolyfill(
|
const res = await fetchPolyfill(input, {
|
||||||
input,
|
headers: {
|
||||||
{
|
"Content-type": "application/json",
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify([{ Text: text }]),
|
|
||||||
},
|
},
|
||||||
{ 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];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,37 +159,82 @@ const apiMicrosoftTranslate = (translator, text, to, from) => {
|
|||||||
* @param {*} from
|
* @param {*} from
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const apiOpenaiTranslate = async (translator, text, to, from) => {
|
const apiOpenaiTranslate = async (
|
||||||
const { openaiUrl, openaiKey, openaiModel, openaiPrompt } =
|
translator,
|
||||||
await getSetting();
|
text,
|
||||||
let prompt = openaiPrompt
|
to,
|
||||||
|
from,
|
||||||
|
{ url, key, model, prompt, useCache = true }
|
||||||
|
) => {
|
||||||
|
prompt = prompt
|
||||||
.replaceAll(PROMPT_PLACE_FROM, from)
|
.replaceAll(PROMPT_PLACE_FROM, from)
|
||||||
.replaceAll(PROMPT_PLACE_TO, to);
|
.replaceAll(PROMPT_PLACE_TO, to);
|
||||||
return fetchPolyfill(
|
const res = await fetchPolyfill(url, {
|
||||||
openaiUrl,
|
headers: {
|
||||||
{
|
"Content-type": "application/json",
|
||||||
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,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{ 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];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,26 +242,29 @@ const apiOpenaiTranslate = async (translator, text, to, from) => {
|
|||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiTranslate = async ({ translator, q, fromLang, toLang }) => {
|
export const apiTranslate = ({
|
||||||
let trText = "";
|
translator,
|
||||||
let isSame = false;
|
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;
|
switch (translator) {
|
||||||
let to = OPT_LANGS_SPECIAL?.[translator]?.get(toLang) ?? toLang;
|
case OPT_TRANS_GOOGLE:
|
||||||
|
return callApi(apiGoogleTranslate);
|
||||||
if (translator === OPT_TRANS_GOOGLE) {
|
case OPT_TRANS_MICROSOFT:
|
||||||
const res = await apiGoogleTranslate(translator, q, to, from);
|
return callApi(apiMicrosoftTranslate);
|
||||||
trText = res.sentences.map((item) => item.trans).join(" ");
|
case OPT_TRANS_DEEPL:
|
||||||
isSame = to === res.src;
|
return callApi(apiDeepLTranslate);
|
||||||
} else if (translator === OPT_TRANS_MICROSOFT) {
|
case OPT_TRANS_OPENAI:
|
||||||
const res = await apiMicrosoftTranslate(translator, q, to, from);
|
return callApi(apiOpenaiTranslate);
|
||||||
trText = res[0].translations[0].text;
|
case OPT_TRANS_CUSTOMIZE:
|
||||||
isSame = to === res[0].detectedLanguage.language;
|
return callApi(apiCustomTranslate);
|
||||||
} else if (translator === OPT_TRANS_OPENAI) {
|
default:
|
||||||
const res = await apiOpenaiTranslate(translator, q, to, from);
|
return ["", false];
|
||||||
trText = res?.choices?.[0].message.content;
|
|
||||||
isSame = (await detectLang(q)) === (await detectLang(trText));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [trText, isSame];
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,43 +3,45 @@ import {
|
|||||||
MSG_FETCH,
|
MSG_FETCH,
|
||||||
MSG_FETCH_LIMIT,
|
MSG_FETCH_LIMIT,
|
||||||
MSG_FETCH_CLEAR,
|
MSG_FETCH_CLEAR,
|
||||||
DEFAULT_SETTING,
|
MSG_TRANS_TOGGLE,
|
||||||
DEFAULT_RULES,
|
MSG_TRANS_TOGGLE_STYLE,
|
||||||
DEFAULT_SYNC,
|
CMD_TOGGLE_TRANSLATE,
|
||||||
STOKEY_SETTING,
|
CMD_TOGGLE_STYLE,
|
||||||
STOKEY_RULES,
|
CMD_OPEN_OPTIONS,
|
||||||
STOKEY_SYNC,
|
|
||||||
CACHE_NAME,
|
|
||||||
} from "./config";
|
} from "./config";
|
||||||
import storage from "./libs/storage";
|
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
|
||||||
import { getSetting } from "./libs";
|
import { trySyncSettingAndRules } from "./libs/sync";
|
||||||
import { syncAll } from "./libs/sync";
|
|
||||||
import { fetchData, fetchPool } from "./libs/fetch";
|
import { fetchData, fetchPool } from "./libs/fetch";
|
||||||
|
import { sendTabMsg } from "./libs/msg";
|
||||||
|
import { trySyncAllSubRules } from "./libs/subRules";
|
||||||
|
import { tryClearCaches } from "./libs";
|
||||||
|
|
||||||
|
globalThis.ContextType = "BACKGROUND";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件安装
|
* 插件安装
|
||||||
*/
|
*/
|
||||||
browser.runtime.onInstalled.addListener(() => {
|
browser.runtime.onInstalled.addListener(() => {
|
||||||
console.log("onInstalled");
|
tryInitDefaultData();
|
||||||
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
|
|
||||||
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES);
|
|
||||||
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 浏览器启动
|
* 浏览器启动
|
||||||
*/
|
*/
|
||||||
browser.runtime.onStartup.addListener(async () => {
|
browser.runtime.onStartup.addListener(async () => {
|
||||||
console.log("onStartup");
|
console.log("browser onStartup");
|
||||||
|
|
||||||
// 同步数据
|
// 同步数据
|
||||||
await syncAll();
|
await trySyncSettingAndRules();
|
||||||
|
|
||||||
// 清除缓存
|
// 清除缓存
|
||||||
const { clearCache } = await getSetting();
|
const setting = await getSettingWithDefault();
|
||||||
if (clearCache) {
|
if (setting.clearCache) {
|
||||||
caches.delete(CACHE_NAME);
|
tryClearCaches();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步订阅规则
|
||||||
|
trySyncAllSubRules(setting);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,8 +51,8 @@ browser.runtime.onMessage.addListener(
|
|||||||
({ action, args }, sender, sendResponse) => {
|
({ action, args }, sender, sendResponse) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MSG_FETCH:
|
case MSG_FETCH:
|
||||||
const { input, init, opts } = args;
|
const { input, opts } = args;
|
||||||
fetchData(input, init, opts)
|
fetchData(input, opts)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
sendResponse({ data });
|
sendResponse({ data });
|
||||||
})
|
})
|
||||||
@@ -73,3 +75,22 @@ browser.runtime.onMessage.addListener(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听快捷键
|
||||||
|
*/
|
||||||
|
browser.commands.onCommand.addListener((command) => {
|
||||||
|
// console.log(`Command: ${command}`);
|
||||||
|
switch (command) {
|
||||||
|
case CMD_TOGGLE_TRANSLATE:
|
||||||
|
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||||
|
break;
|
||||||
|
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", "中文"],
|
["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 = {
|
export const I18N = {
|
||||||
app_name: {
|
app_name: {
|
||||||
zh: `简约翻译`,
|
zh: `简约翻译`,
|
||||||
@@ -12,6 +107,14 @@ export const I18N = {
|
|||||||
zh: `翻译`,
|
zh: `翻译`,
|
||||||
en: `Translate`,
|
en: `Translate`,
|
||||||
},
|
},
|
||||||
|
custom_api_help: {
|
||||||
|
zh: customApiHelpZH,
|
||||||
|
en: customApiHelpEN,
|
||||||
|
},
|
||||||
|
translate_alt: {
|
||||||
|
zh: `翻译 (Alt+Q)`,
|
||||||
|
en: `Translate (Alt+Q)`,
|
||||||
|
},
|
||||||
basic_setting: {
|
basic_setting: {
|
||||||
zh: `基本设置`,
|
zh: `基本设置`,
|
||||||
en: `Basic Setting`,
|
en: `Basic Setting`,
|
||||||
@@ -20,10 +123,26 @@ export const I18N = {
|
|||||||
zh: `规则设置`,
|
zh: `规则设置`,
|
||||||
en: `Rules Setting`,
|
en: `Rules Setting`,
|
||||||
},
|
},
|
||||||
|
apis_setting: {
|
||||||
|
zh: `接口设置`,
|
||||||
|
en: `Apis Setting`,
|
||||||
|
},
|
||||||
sync_setting: {
|
sync_setting: {
|
||||||
zh: `同步设置`,
|
zh: `同步设置`,
|
||||||
en: `Sync Setting`,
|
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: {
|
about: {
|
||||||
zh: `关于`,
|
zh: `关于`,
|
||||||
en: `About`,
|
en: `About`,
|
||||||
@@ -41,17 +160,53 @@ export const I18N = {
|
|||||||
en: `Interface Language`,
|
en: `Interface Language`,
|
||||||
},
|
},
|
||||||
fetch_limit: {
|
fetch_limit: {
|
||||||
zh: `最大请求数量`,
|
zh: `最大请求数量 (1-100)`,
|
||||||
en: `Maximum Number Of Request`,
|
en: `Maximum Number Of Request (1-100)`,
|
||||||
},
|
},
|
||||||
fetch_interval: {
|
fetch_interval: {
|
||||||
zh: `请求间隔时间(ms)`,
|
zh: `请求间隔时间 (0-5000ms)`,
|
||||||
en: `Request Interval(ms)`,
|
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: {
|
translate_service: {
|
||||||
zh: `翻译服务`,
|
zh: `翻译服务`,
|
||||||
en: `Translate Service`,
|
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: {
|
from_lang: {
|
||||||
zh: `原文语言`,
|
zh: `原文语言`,
|
||||||
en: `Source Language`,
|
en: `Source Language`,
|
||||||
@@ -64,10 +219,18 @@ export const I18N = {
|
|||||||
zh: `文字样式`,
|
zh: `文字样式`,
|
||||||
en: `Text Style`,
|
en: `Text Style`,
|
||||||
},
|
},
|
||||||
|
text_style_alt: {
|
||||||
|
zh: `文字样式 (Alt+C)`,
|
||||||
|
en: `Text Style (Alt+C)`,
|
||||||
|
},
|
||||||
bg_color: {
|
bg_color: {
|
||||||
zh: `样式颜色`,
|
zh: `样式颜色`,
|
||||||
en: `Style Color`,
|
en: `Style Color`,
|
||||||
},
|
},
|
||||||
|
remain_unchanged: {
|
||||||
|
zh: `保留不变`,
|
||||||
|
en: `Remain Unchanged`,
|
||||||
|
},
|
||||||
google_api: {
|
google_api: {
|
||||||
zh: `谷歌翻译接口`,
|
zh: `谷歌翻译接口`,
|
||||||
en: `Google Translate API`,
|
en: `Google Translate API`,
|
||||||
@@ -104,6 +267,34 @@ export const I18N = {
|
|||||||
zh: `添加`,
|
zh: `添加`,
|
||||||
en: `Add`,
|
en: `Add`,
|
||||||
},
|
},
|
||||||
|
inject_rules: {
|
||||||
|
zh: `注入订阅规则`,
|
||||||
|
en: `Inject Subscribe Rules`,
|
||||||
|
},
|
||||||
|
personal_rules: {
|
||||||
|
zh: `个人规则`,
|
||||||
|
en: `Rules`,
|
||||||
|
},
|
||||||
|
subscribe_rules: {
|
||||||
|
zh: `订阅规则`,
|
||||||
|
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: {
|
sync_warn: {
|
||||||
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
|
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.`,
|
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.`,
|
||||||
@@ -112,6 +303,10 @@ export const I18N = {
|
|||||||
zh: `查看关于数据同步接口部署`,
|
zh: `查看关于数据同步接口部署`,
|
||||||
en: `View About Data Synchronization Interface Deployment`,
|
en: `View About Data Synchronization Interface Deployment`,
|
||||||
},
|
},
|
||||||
|
about_api_proxy: {
|
||||||
|
zh: `查看自建一个翻译接口代理`,
|
||||||
|
en: `Check out the self-built translation interface proxy`,
|
||||||
|
},
|
||||||
style_none: {
|
style_none: {
|
||||||
zh: `无`,
|
zh: `无`,
|
||||||
en: `None`,
|
en: `None`,
|
||||||
@@ -140,21 +335,29 @@ export const I18N = {
|
|||||||
zh: `高亮`,
|
zh: `高亮`,
|
||||||
en: `Highlight`,
|
en: `Highlight`,
|
||||||
},
|
},
|
||||||
|
diy_style: {
|
||||||
|
zh: `自定义样式`,
|
||||||
|
en: `Custom Style`,
|
||||||
|
},
|
||||||
|
diy_style_helper: {
|
||||||
|
zh: `遵循“CSS”的语法`,
|
||||||
|
en: `Follow the syntax of "CSS"`,
|
||||||
|
},
|
||||||
setting: {
|
setting: {
|
||||||
zh: `设置`,
|
zh: `设置 (Alt+O)`,
|
||||||
en: `Setting`,
|
en: `Setting (Alt+O)`,
|
||||||
},
|
},
|
||||||
pattern: {
|
pattern: {
|
||||||
zh: `匹配网址`,
|
zh: `匹配网址`,
|
||||||
en: `URL pattern`,
|
en: `URL pattern`,
|
||||||
},
|
},
|
||||||
pattern_helper: {
|
pattern_helper: {
|
||||||
zh: `多个URL支持英文逗号“,”分隔`,
|
zh: `1、支持星号(*)通配符。2、多个URL支持英文逗号“,”分隔。`,
|
||||||
en: `Multiple URLs can be separated by English commas ","`,
|
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`,
|
||||||
},
|
},
|
||||||
selector_helper: {
|
selector_helper: {
|
||||||
zh: `1、遵循CSS选择器规则,但不同浏览器,支持写法不尽相同。2、留空表示采用全局设置。`,
|
zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`,
|
||||||
en: `1. Follow CSS selector rules, but different browsers support different writing methods. 2. Leave blank to adopt the global setting.`,
|
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: {
|
translate_switch: {
|
||||||
zh: `开启翻译`,
|
zh: `开启翻译`,
|
||||||
@@ -192,6 +395,18 @@ export const I18N = {
|
|||||||
zh: `错误的文件类型`,
|
zh: `错误的文件类型`,
|
||||||
en: `Wrong file type`,
|
en: `Wrong file type`,
|
||||||
},
|
},
|
||||||
|
error_fetch_url: {
|
||||||
|
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: {
|
openai_api: {
|
||||||
zh: `OpenAI 接口`,
|
zh: `OpenAI 接口`,
|
||||||
en: `OpenAI API`,
|
en: `OpenAI API`,
|
||||||
@@ -208,7 +423,7 @@ export const I18N = {
|
|||||||
zh: `OpenAI 提示词`,
|
zh: `OpenAI 提示词`,
|
||||||
en: `OpenAI Prompt`,
|
en: `OpenAI Prompt`,
|
||||||
},
|
},
|
||||||
clear_cache: {
|
if_clear_cache: {
|
||||||
zh: `是否清除缓存`,
|
zh: `是否清除缓存`,
|
||||||
en: `Whether clear cache`,
|
en: `Whether clear cache`,
|
||||||
},
|
},
|
||||||
@@ -220,12 +435,164 @@ export const I18N = {
|
|||||||
zh: `重启浏览器时清除缓存`,
|
zh: `重启浏览器时清除缓存`,
|
||||||
en: `Clear cache when restarting browser`,
|
en: `Clear cache when restarting browser`,
|
||||||
},
|
},
|
||||||
|
data_sync_type: {
|
||||||
|
zh: `数据同步方式`,
|
||||||
|
en: `Data Sync Type`,
|
||||||
|
},
|
||||||
data_sync_url: {
|
data_sync_url: {
|
||||||
zh: `数据同步接口`,
|
zh: `数据同步接口`,
|
||||||
en: `Data Sync API`,
|
en: `Data Sync API`,
|
||||||
},
|
},
|
||||||
|
data_sync_user: {
|
||||||
|
zh: `数据同步账户`,
|
||||||
|
en: `Data Sync User`,
|
||||||
|
},
|
||||||
data_sync_key: {
|
data_sync_key: {
|
||||||
zh: `数据同步密钥`,
|
zh: `数据同步密钥`,
|
||||||
en: `Data Sync Key`,
|
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!`,
|
||||||
|
},
|
||||||
|
error_sync_setting: {
|
||||||
|
zh: `您的同步类型必须为“KISS-Worker”,且需填写完整`,
|
||||||
|
en: `Your sync type must be "KISS-Worker" and must be filled in completely`,
|
||||||
|
},
|
||||||
|
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,17 +1,35 @@
|
|||||||
import { DEFAULT_SELECTOR, RULES } from "./rules";
|
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 { I18N, UI_LANGS } from "./i18n";
|
||||||
|
export {
|
||||||
const APP_NAME = process.env.REACT_APP_NAME.trim().split(/\s+/).join("-");
|
GLOBAL_KEY,
|
||||||
|
REMAIN_KEY,
|
||||||
export const APP_LCNAME = APP_NAME.toLowerCase();
|
SHADOW_KEY,
|
||||||
|
DEFAULT_RULE,
|
||||||
|
DEFAULT_OW_RULE,
|
||||||
|
BUILTIN_RULES,
|
||||||
|
APP_LCNAME,
|
||||||
|
};
|
||||||
|
|
||||||
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
|
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
|
||||||
export const STOKEY_SETTING = `${APP_NAME}_setting`;
|
export const STOKEY_SETTING = `${APP_NAME}_setting`;
|
||||||
export const STOKEY_RULES = `${APP_NAME}_rules`;
|
export const STOKEY_RULES = `${APP_NAME}_rules`;
|
||||||
export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
||||||
export const STOKEY_FAB = `${APP_NAME}_fab`;
|
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 GLOBAL_KEY = "*";
|
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_WEB = "web";
|
||||||
export const CLIENT_CHROME = "chrome";
|
export const CLIENT_CHROME = "chrome";
|
||||||
@@ -20,9 +38,11 @@ export const CLIENT_FIREFOX = "firefox";
|
|||||||
export const CLIENT_USERSCRIPT = "userscript";
|
export const CLIENT_USERSCRIPT = "userscript";
|
||||||
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
|
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
|
||||||
|
|
||||||
export const KV_HEADER_KEY = "X-KISS-PSK";
|
export const KV_RULES_KEY = "kiss-rules.json";
|
||||||
export const KV_RULES_KEY = "KT_RULES";
|
export const KV_RULES_SHARE_KEY = "kiss-rules-share.json";
|
||||||
export const KV_SETTING_KEY = "KT_SETTING";
|
export const KV_SETTING_KEY = "kiss-setting.json";
|
||||||
|
export const KV_SALT_SYNC = "KISS-Translator-SYNC";
|
||||||
|
export const KV_SALT_SHARE = "KISS-Translator-SHARE";
|
||||||
|
|
||||||
export const CACHE_NAME = `${APP_NAME}_cache`;
|
export const CACHE_NAME = `${APP_NAME}_cache`;
|
||||||
|
|
||||||
@@ -30,29 +50,34 @@ export const MSG_FETCH = "fetch";
|
|||||||
export const MSG_FETCH_LIMIT = "fetch_limit";
|
export const MSG_FETCH_LIMIT = "fetch_limit";
|
||||||
export const MSG_FETCH_CLEAR = "fetch_clear";
|
export const MSG_FETCH_CLEAR = "fetch_clear";
|
||||||
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
||||||
|
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
|
||||||
export const MSG_TRANS_GETRULE = "trans_getrule";
|
export const MSG_TRANS_GETRULE = "trans_getrule";
|
||||||
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
||||||
export const MSG_TRANS_CURRULE = "trans_currule";
|
export const MSG_TRANS_CURRULE = "trans_currule";
|
||||||
|
|
||||||
export const EVENT_KISS = "kissEvent";
|
|
||||||
|
|
||||||
export const THEME_LIGHT = "light";
|
export const THEME_LIGHT = "light";
|
||||||
export const THEME_DARK = "dark";
|
export const THEME_DARK = "dark";
|
||||||
|
|
||||||
export const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker";
|
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 =
|
export const URL_RAW_PREFIX =
|
||||||
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
|
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
|
||||||
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
|
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_GOOGLE = "Google";
|
||||||
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
||||||
|
export const OPT_TRANS_DEEPL = "DeepL";
|
||||||
export const OPT_TRANS_OPENAI = "OpenAI";
|
export const OPT_TRANS_OPENAI = "OpenAI";
|
||||||
|
export const OPT_TRANS_CUSTOMIZE = "Custom";
|
||||||
export const OPT_TRANS_ALL = [
|
export const OPT_TRANS_ALL = [
|
||||||
OPT_TRANS_GOOGLE,
|
OPT_TRANS_GOOGLE,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_DEEPL,
|
||||||
OPT_TRANS_OPENAI,
|
OPT_TRANS_OPENAI,
|
||||||
|
OPT_TRANS_CUSTOMIZE,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const OPT_LANGS_TO = [
|
export const OPT_LANGS_TO = [
|
||||||
@@ -101,10 +126,18 @@ export const OPT_LANGS_SPECIAL = {
|
|||||||
["zh-CN", "zh-Hans"],
|
["zh-CN", "zh-Hans"],
|
||||||
["zh-TW", "zh-Hant"],
|
["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_TRANS_OPENAI]: new Map(
|
||||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
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_NONE = "style_none"; // 无
|
||||||
export const OPT_STYLE_LINE = "under_line"; // 下划线
|
export const OPT_STYLE_LINE = "under_line"; // 下划线
|
||||||
@@ -112,7 +145,8 @@ export const OPT_STYLE_DOTLINE = "dot_line"; // 点状线
|
|||||||
export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
|
export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
|
||||||
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
|
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
|
||||||
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
|
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 = [
|
export const OPT_STYLE_ALL = [
|
||||||
OPT_STYLE_NONE,
|
OPT_STYLE_NONE,
|
||||||
OPT_STYLE_LINE,
|
OPT_STYLE_LINE,
|
||||||
@@ -120,7 +154,28 @@ export const OPT_STYLE_ALL = [
|
|||||||
OPT_STYLE_DASHLINE,
|
OPT_STYLE_DASHLINE,
|
||||||
OPT_STYLE_WAVYLINE,
|
OPT_STYLE_WAVYLINE,
|
||||||
OPT_STYLE_FUZZY,
|
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; // 默认最大任务数量
|
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
|
||||||
@@ -141,50 +196,117 @@ export const GLOBLA_RULE = {
|
|||||||
textStyle: OPT_STYLE_DASHLINE,
|
textStyle: OPT_STYLE_DASHLINE,
|
||||||
transOpen: "false",
|
transOpen: "false",
|
||||||
bgColor: "",
|
bgColor: "",
|
||||||
|
textDiyStyle: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 默认规则
|
// 输入框翻译
|
||||||
export const DEFAULT_RULE = {
|
export const OPT_INPUT_TRANS_SIGNS = ["/", "//", "\\", "\\\\", ">", ">>"];
|
||||||
pattern: "",
|
export const DEFAULT_INPUT_SHORTCUT = ["Alt", "i"];
|
||||||
selector: "",
|
export const DEFAULT_INPUT_RULE = {
|
||||||
translator: GLOBAL_KEY,
|
transOpen: true,
|
||||||
fromLang: GLOBAL_KEY,
|
translator: OPT_TRANS_MICROSOFT,
|
||||||
toLang: GLOBAL_KEY,
|
fromLang: "auto",
|
||||||
textStyle: GLOBAL_KEY,
|
toLang: "en",
|
||||||
transOpen: GLOBAL_KEY,
|
triggerShortcut: DEFAULT_INPUT_SHORTCUT,
|
||||||
bgColor: "",
|
triggerCount: 1,
|
||||||
|
triggerTime: 200,
|
||||||
|
transSign: OPT_INPUT_TRANS_SIGNS[0],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 订阅列表
|
||||||
|
export const DEFAULT_SUBRULES_LIST = [
|
||||||
|
{
|
||||||
|
url: process.env.REACT_APP_RULESURL,
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: process.env.REACT_APP_RULESURL_ON,
|
||||||
|
selected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 = {
|
export const DEFAULT_SETTING = {
|
||||||
darkMode: false, // 深色模式
|
darkMode: false, // 深色模式
|
||||||
uiLang: "en", // 界面语言
|
uiLang: "en", // 界面语言
|
||||||
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
||||||
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
||||||
|
minLength: TRANS_MIN_LENGTH,
|
||||||
|
maxLength: TRANS_MAX_LENGTH,
|
||||||
|
newlineLength: TRANS_NEWLINE_LENGTH,
|
||||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||||
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口
|
injectRules: true, // 是否注入订阅规则
|
||||||
openaiUrl: "https://api.openai.com/v1/chat/completions",
|
injectWebfix: true, // 是否注入修复补丁
|
||||||
openaiKey: "",
|
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
||||||
openaiModel: "gpt-4",
|
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
|
||||||
openaiPrompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
|
transApis: DEFAULT_TRANS_APIS, // 翻译接口
|
||||||
|
mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译
|
||||||
|
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
||||||
|
hideFab: false, // 是否隐藏按钮
|
||||||
|
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_RULES = [
|
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||||
...RULES.map((item) => ({
|
|
||||||
...DEFAULT_RULE,
|
|
||||||
...item,
|
|
||||||
transOpen: "true",
|
|
||||||
})),
|
|
||||||
GLOBLA_RULE,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
export const OPT_SYNCTYPE_WORKER = "KISS-Worker";
|
||||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
export const OPT_SYNCTYPE_WEBDAV = "WebDAV";
|
||||||
|
export const OPT_SYNCTYPE_ALL = [OPT_SYNCTYPE_WORKER, OPT_SYNCTYPE_WEBDAV];
|
||||||
|
|
||||||
export const DEFAULT_SYNC = {
|
export const DEFAULT_SYNC = {
|
||||||
|
syncType: OPT_SYNCTYPE_WORKER, // 同步方式
|
||||||
syncUrl: "", // 数据同步接口
|
syncUrl: "", // 数据同步接口
|
||||||
|
syncUser: "", // 数据同步用户名
|
||||||
syncKey: "", // 数据同步密钥
|
syncKey: "", // 数据同步密钥
|
||||||
settingUpdateAt: 0,
|
syncMeta: {}, // 数据更新及同步信息
|
||||||
settingSyncAt: 0,
|
// settingUpdateAt: 0,
|
||||||
rulesUpdateAt: 0,
|
// settingSyncAt: 0,
|
||||||
rulesSyncAt: 0,
|
// rulesUpdateAt: 0,
|
||||||
|
// rulesSyncAt: 0,
|
||||||
|
subRulesSyncAt: 0, // 订阅规则同步时间
|
||||||
|
dataCaches: {}, // 缓存同步时间
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,64 @@
|
|||||||
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 DEFAULT_SELECTOR = `:is(${els})`;
|
||||||
|
|
||||||
export const RULES = [
|
export const GLOBAL_KEY = "*";
|
||||||
|
export const REMAIN_KEY = "-";
|
||||||
|
|
||||||
|
export const SHADOW_KEY = ">>>";
|
||||||
|
|
||||||
|
export const DEFAULT_RULE = {
|
||||||
|
pattern: "",
|
||||||
|
selector: "",
|
||||||
|
translator: GLOBAL_KEY,
|
||||||
|
fromLang: GLOBAL_KEY,
|
||||||
|
toLang: GLOBAL_KEY,
|
||||||
|
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 = [
|
||||||
|
{
|
||||||
|
pattern: `www.google.com/search`,
|
||||||
|
selector: `h3, .IsZvec, .VwiC3b`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: `news.google.com`,
|
||||||
|
selector: `h4`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: `www.foxnews.com`,
|
||||||
|
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,
|
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,
|
||||||
selector: DEFAULT_SELECTOR,
|
selector: DEFAULT_SELECTOR,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
pattern: `https://news.google.com/`,
|
|
||||||
selector: `h4`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
pattern: `themessenger.com`,
|
pattern: `themessenger.com`,
|
||||||
selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
|
selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
|
||||||
@@ -117,7 +165,7 @@ export const RULES = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: `https://github.com/`,
|
pattern: `https://github.com/`,
|
||||||
selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop='description']`,
|
selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop='description'], .markdown-title, bdi`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: `twitter.com`,
|
pattern: `twitter.com`,
|
||||||
@@ -127,8 +175,12 @@ export const RULES = [
|
|||||||
pattern: `youtube.com`,
|
pattern: `youtube.com`,
|
||||||
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`,
|
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
pattern: `www.google.com/search`,
|
|
||||||
selector: `h3, .IsZvec, .VwiC3b`,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const BUILTIN_RULES = RULES.sort((a, b) =>
|
||||||
|
a.pattern.localeCompare(b.pattern)
|
||||||
|
).map((item) => ({
|
||||||
|
...DEFAULT_RULE,
|
||||||
|
...item,
|
||||||
|
transOpen: "true",
|
||||||
|
}));
|
||||||
|
|||||||
@@ -1,35 +1,94 @@
|
|||||||
import { browser } from "./libs/browser";
|
import { browser } from "./libs/browser";
|
||||||
import {
|
import {
|
||||||
MSG_TRANS_TOGGLE,
|
MSG_TRANS_TOGGLE,
|
||||||
|
MSG_TRANS_TOGGLE_STYLE,
|
||||||
MSG_TRANS_GETRULE,
|
MSG_TRANS_GETRULE,
|
||||||
MSG_TRANS_PUTRULE,
|
MSG_TRANS_PUTRULE,
|
||||||
} from "./config";
|
} from "./config";
|
||||||
import { getSetting, getRules, matchRule } from "./libs";
|
import { getSettingWithDefault, getRulesWithDefault } from "./libs/storage";
|
||||||
import { Translator } from "./libs/translator";
|
import { Translator } from "./libs/translator";
|
||||||
|
import { isIframe, sendIframeMsg, sendPrentMsg } from "./libs/iframe";
|
||||||
|
import { matchRule } from "./libs/rules";
|
||||||
|
import { webfix } from "./libs/webfix";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 入口函数
|
* 入口函数
|
||||||
*/
|
*/
|
||||||
(async () => {
|
const init = async () => {
|
||||||
const setting = await getSetting();
|
const setting = await getSettingWithDefault();
|
||||||
const rules = await getRules();
|
|
||||||
const rule = matchRule(rules, document.location.href);
|
if (isIframe) {
|
||||||
|
let translator;
|
||||||
|
window.addEventListener("message", (e) => {
|
||||||
|
const { action, args } = e.data || {};
|
||||||
|
switch (action) {
|
||||||
|
case MSG_TRANS_TOGGLE:
|
||||||
|
translator?.toggle();
|
||||||
|
break;
|
||||||
|
case MSG_TRANS_TOGGLE_STYLE:
|
||||||
|
translator?.toggleStyle();
|
||||||
|
break;
|
||||||
|
case MSG_TRANS_PUTRULE:
|
||||||
|
if (!translator) {
|
||||||
|
translator = new Translator(args, setting);
|
||||||
|
} else {
|
||||||
|
translator.updateRule(args || {});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sendPrentMsg(MSG_TRANS_GETRULE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const href = document.location.href;
|
||||||
|
const rules = await getRulesWithDefault();
|
||||||
|
const rule = await matchRule(rules, href, setting);
|
||||||
const translator = new Translator(rule, setting);
|
const translator = new Translator(rule, setting);
|
||||||
|
webfix(href, setting);
|
||||||
|
|
||||||
// 监听消息
|
// 监听消息
|
||||||
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MSG_TRANS_TOGGLE:
|
case MSG_TRANS_TOGGLE:
|
||||||
translator.toggle();
|
translator.toggle();
|
||||||
|
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||||
|
break;
|
||||||
|
case MSG_TRANS_TOGGLE_STYLE:
|
||||||
|
translator.toggleStyle();
|
||||||
|
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||||
break;
|
break;
|
||||||
case MSG_TRANS_GETRULE:
|
case MSG_TRANS_GETRULE:
|
||||||
break;
|
break;
|
||||||
case MSG_TRANS_PUTRULE:
|
case MSG_TRANS_PUTRULE:
|
||||||
translator.updateRule(args);
|
translator.updateRule(args);
|
||||||
|
sendIframeMsg(MSG_TRANS_PUTRULE, args);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return { error: `message action is unavailable: ${action}` };
|
return { error: `message action is unavailable: ${action}` };
|
||||||
}
|
}
|
||||||
return { data: translator.rule };
|
return { data: translator.rule };
|
||||||
});
|
});
|
||||||
|
window.addEventListener("message", (e) => {
|
||||||
|
const { action } = e.data || {};
|
||||||
|
switch (action) {
|
||||||
|
case MSG_TRANS_GETRULE:
|
||||||
|
sendIframeMsg(MSG_TRANS_PUTRULE, rule);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
(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);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
60
src/hooks/Alert.js
Normal file
60
src/hooks/Alert.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { createContext, useContext, useState, forwardRef } from "react";
|
||||||
|
import Snackbar from "@mui/material/Snackbar";
|
||||||
|
import MuiAlert from "@mui/material/Alert";
|
||||||
|
|
||||||
|
const Alert = forwardRef(function Alert(props, ref) {
|
||||||
|
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertContext = createContext(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 左下角提示,注入context后,方便全局调用
|
||||||
|
* @param {*} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function AlertProvider({ children }) {
|
||||||
|
const vertical = "top";
|
||||||
|
const horizontal = "center";
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [severity, setSeverity] = useState("info");
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
|
||||||
|
const showAlert = (msg, type) => {
|
||||||
|
setOpen(true);
|
||||||
|
setMessage(msg);
|
||||||
|
setSeverity(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (_, reason) => {
|
||||||
|
if (reason === "clickaway") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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}
|
||||||
|
<Snackbar
|
||||||
|
open={open}
|
||||||
|
autoHideDuration={3000}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorOrigin={{ vertical, horizontal }}
|
||||||
|
>
|
||||||
|
<Alert onClose={handleClose} severity={severity} sx={{ width: "100%" }}>
|
||||||
|
{message}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</AlertContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAlert() {
|
||||||
|
return useContext(AlertContext);
|
||||||
|
}
|
||||||
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
|
* 深色模式hook
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useDarkMode() {
|
export function useDarkMode() {
|
||||||
const setting = useSetting();
|
const {
|
||||||
return !!setting?.darkMode;
|
setting: { darkMode },
|
||||||
}
|
updateSetting,
|
||||||
|
} = useSetting();
|
||||||
|
|
||||||
/**
|
const toggleDarkMode = useCallback(async () => {
|
||||||
* 切换深色模式
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function useDarkModeSwitch() {
|
|
||||||
const darkMode = useDarkMode();
|
|
||||||
const updateSetting = useSettingUpdate();
|
|
||||||
return async () => {
|
|
||||||
await updateSetting({ darkMode: !darkMode });
|
await updateSetting({ darkMode: !darkMode });
|
||||||
};
|
}, [darkMode, updateSetting]);
|
||||||
|
|
||||||
|
return { darkMode, toggleDarkMode };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { useFetch } from "./Fetch";
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const useI18n = () => {
|
export const useI18n = () => {
|
||||||
const { uiLang } = useSetting() ?? {};
|
const {
|
||||||
|
setting: { uiLang },
|
||||||
|
} = useSetting();
|
||||||
return (key, defaultText = "") => I18N?.[key]?.[uiLang] ?? defaultText;
|
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,96 +1,78 @@
|
|||||||
import {
|
import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
|
||||||
STOKEY_RULES,
|
import { useStorage } from "./Storage";
|
||||||
OPT_TRANS_ALL,
|
import { trySyncRules } from "../libs/sync";
|
||||||
OPT_STYLE_ALL,
|
import { checkRules } from "../libs/rules";
|
||||||
OPT_LANGS_FROM,
|
import { useCallback } from "react";
|
||||||
OPT_LANGS_TO,
|
import { useSyncMeta } from "./Sync";
|
||||||
GLOBAL_KEY,
|
|
||||||
} from "../config";
|
|
||||||
import storage from "../libs/storage";
|
|
||||||
import { useStorages } from "./Storage";
|
|
||||||
import { matchValue } from "../libs/utils";
|
|
||||||
import { syncRules } from "../libs/sync";
|
|
||||||
import { useSync } from "./Sync";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 匹配规则增删改查 hook
|
* 规则 hook
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useRules() {
|
export function useRules() {
|
||||||
const storages = useStorages();
|
const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
|
||||||
const list = storages?.[STOKEY_RULES] || [];
|
const { updateSyncMeta } = useSyncMeta();
|
||||||
const sync = useSync();
|
|
||||||
|
|
||||||
const update = async (rules) => {
|
const updateRules = useCallback(
|
||||||
const updateAt = sync.opt?.rulesUpdateAt ? Date.now() : 0;
|
async (rules) => {
|
||||||
await storage.setObj(STOKEY_RULES, rules);
|
await save(rules);
|
||||||
await sync.update({ rulesUpdateAt: updateAt });
|
await updateSyncMeta(KV_RULES_KEY);
|
||||||
try {
|
trySyncRules();
|
||||||
await syncRules();
|
},
|
||||||
} catch (err) {
|
[save, updateSyncMeta]
|
||||||
console.log("[sync rules]", err);
|
);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const add = async (rule) => {
|
const add = useCallback(
|
||||||
const rules = [...list];
|
async (rule) => {
|
||||||
if (rule.pattern === "*") {
|
const rules = [...list];
|
||||||
return;
|
if (rule.pattern === "*") {
|
||||||
}
|
return;
|
||||||
if (rules.map((item) => item.pattern).includes(rule.pattern)) {
|
}
|
||||||
return;
|
if (rules.map((item) => item.pattern).includes(rule.pattern)) {
|
||||||
}
|
return;
|
||||||
rules.unshift(rule);
|
}
|
||||||
await update(rules);
|
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];
|
let rules = [...list];
|
||||||
if (pattern === "*") {
|
rules = rules.filter((item) => item.pattern === "*");
|
||||||
return;
|
await updateRules(rules);
|
||||||
}
|
}, [list, updateRules]);
|
||||||
rules = rules.filter((item) => item.pattern !== pattern);
|
|
||||||
await update(rules);
|
|
||||||
};
|
|
||||||
|
|
||||||
const put = async (pattern, obj) => {
|
const put = useCallback(
|
||||||
const rules = [...list];
|
async (pattern, obj) => {
|
||||||
if (pattern === "*") {
|
const rules = [...list];
|
||||||
obj.pattern = "*";
|
if (pattern === "*") {
|
||||||
}
|
obj.pattern = "*";
|
||||||
const rule = rules.find((r) => r.pattern === pattern);
|
}
|
||||||
rule && Object.assign(rule, obj);
|
const rule = rules.find((r) => r.pattern === pattern);
|
||||||
await update(rules);
|
rule && Object.assign(rule, obj);
|
||||||
};
|
await updateRules(rules);
|
||||||
|
},
|
||||||
|
[list, updateRules]
|
||||||
|
);
|
||||||
|
|
||||||
const merge = async (newRules) => {
|
const merge = useCallback(
|
||||||
const rules = [...list];
|
async (newRules) => {
|
||||||
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
|
const rules = [...list];
|
||||||
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
|
newRules = checkRules(newRules);
|
||||||
newRules
|
newRules.forEach((newRule) => {
|
||||||
.filter(({ pattern }) => pattern && typeof pattern === "string")
|
|
||||||
.map(
|
|
||||||
({
|
|
||||||
pattern,
|
|
||||||
selector,
|
|
||||||
translator,
|
|
||||||
fromLang,
|
|
||||||
toLang,
|
|
||||||
textStyle,
|
|
||||||
transOpen,
|
|
||||||
bgColor,
|
|
||||||
}) => ({
|
|
||||||
pattern,
|
|
||||||
selector: typeof selector === "string" ? selector : "",
|
|
||||||
bgColor: typeof bgColor === "string" ? bgColor : "",
|
|
||||||
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
|
|
||||||
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
|
|
||||||
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
|
|
||||||
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
|
|
||||||
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.forEach((newRule) => {
|
|
||||||
const rule = rules.find(
|
const rule = rules.find(
|
||||||
(oldRule) => oldRule.pattern === newRule.pattern
|
(oldRule) => oldRule.pattern === newRule.pattern
|
||||||
);
|
);
|
||||||
@@ -100,8 +82,10 @@ export function useRules() {
|
|||||||
rules.unshift(newRule);
|
rules.unshift(newRule);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await update(rules);
|
await updateRules(rules);
|
||||||
};
|
},
|
||||||
|
[list, updateRules]
|
||||||
|
);
|
||||||
|
|
||||||
return { list, add, del, put, merge };
|
return { list, add, del, clear, put, merge };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,58 @@
|
|||||||
import { STOKEY_SETTING } from "../config";
|
import { STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY } from "../config";
|
||||||
import storage from "../libs/storage";
|
import { useStorage } from "./Storage";
|
||||||
import { useStorages } from "./Storage";
|
import { trySyncSetting } from "../libs/sync";
|
||||||
import { useSync } from "./Sync";
|
import { createContext, useCallback, useContext, useMemo } from "react";
|
||||||
|
import { debounce } from "../libs/utils";
|
||||||
|
import { useSyncMeta } from "./Sync";
|
||||||
|
|
||||||
|
const SettingContext = createContext({
|
||||||
|
setting: null,
|
||||||
|
updateSetting: async () => {},
|
||||||
|
reloadSetting: async () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function SettingProvider({ children }) {
|
||||||
|
const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
|
||||||
|
const { updateSyncMeta } = useSyncMeta();
|
||||||
|
|
||||||
|
const syncSetting = useMemo(
|
||||||
|
() =>
|
||||||
|
debounce(() => {
|
||||||
|
trySyncSetting();
|
||||||
|
}, [2000]),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateSetting = useCallback(
|
||||||
|
async (obj) => {
|
||||||
|
await update(obj);
|
||||||
|
await updateSyncMeta(KV_SETTING_KEY);
|
||||||
|
syncSetting();
|
||||||
|
},
|
||||||
|
[update, syncSetting, updateSyncMeta]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingContext.Provider
|
||||||
|
value={{
|
||||||
|
setting: data,
|
||||||
|
updateSetting,
|
||||||
|
reloadSetting: reload,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SettingContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置hook
|
* 设置 hook
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useSetting() {
|
export function useSetting() {
|
||||||
const storages = useStorages();
|
return useContext(SettingContext);
|
||||||
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 });
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
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,89 +1,63 @@
|
|||||||
import { createContext, useContext, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { browser, isExt, isGm, isWeb } from "../libs/browser";
|
import { storage } from "../libs/storage";
|
||||||
import {
|
|
||||||
STOKEY_SETTING,
|
|
||||||
STOKEY_RULES,
|
|
||||||
STOKEY_MSAUTH,
|
|
||||||
STOKEY_SYNC,
|
|
||||||
DEFAULT_SETTING,
|
|
||||||
DEFAULT_RULES,
|
|
||||||
DEFAULT_SYNC,
|
|
||||||
} from "../config";
|
|
||||||
import storage from "../libs/storage";
|
|
||||||
|
|
||||||
/**
|
export function useStorage(key, defaultVal) {
|
||||||
* 默认配置
|
const [loading, setLoading] = useState(false);
|
||||||
*/
|
const [data, setData] = useState(null);
|
||||||
export const defaultStorage = {
|
|
||||||
[STOKEY_MSAUTH]: null,
|
|
||||||
[STOKEY_SETTING]: DEFAULT_SETTING,
|
|
||||||
[STOKEY_RULES]: DEFAULT_RULES,
|
|
||||||
[STOKEY_SYNC]: DEFAULT_SYNC,
|
|
||||||
};
|
|
||||||
|
|
||||||
const StoragesContext = createContext(null);
|
const save = useCallback(
|
||||||
|
async (val) => {
|
||||||
|
setData(val);
|
||||||
|
await storage.setObj(key, val);
|
||||||
|
},
|
||||||
|
[key]
|
||||||
|
);
|
||||||
|
|
||||||
export function StoragesProvider({ children }) {
|
const update = useCallback(
|
||||||
const [storages, setStorages] = useState(null);
|
async (obj) => {
|
||||||
|
setData((pre = {}) => ({ ...pre, ...obj }));
|
||||||
|
await storage.putObj(key, obj);
|
||||||
|
},
|
||||||
|
[key]
|
||||||
|
);
|
||||||
|
|
||||||
const handleChanged = (changes) => {
|
const remove = useCallback(async () => {
|
||||||
if (isWeb || isGm) {
|
setData(null);
|
||||||
const { key, oldValue, newValue } = changes;
|
await storage.del(key);
|
||||||
changes = {
|
}, [key]);
|
||||||
[key]: {
|
|
||||||
oldValue,
|
const reload = useCallback(async () => {
|
||||||
newValue,
|
try {
|
||||||
},
|
setLoading(true);
|
||||||
};
|
const val = await storage.getObj(key);
|
||||||
|
if (val) {
|
||||||
|
setData(val);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[storage reload]", err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
const newStorages = {};
|
}, [key]);
|
||||||
Object.entries(changes)
|
|
||||||
.filter(([_, { oldValue, newValue }]) => oldValue !== newValue)
|
|
||||||
.forEach(([key, { newValue }]) => {
|
|
||||||
newStorages[key] = JSON.parse(newValue);
|
|
||||||
});
|
|
||||||
if (Object.keys(newStorages).length !== 0) {
|
|
||||||
setStorages((pre) => ({ ...pre, ...newStorages }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 首次从storage同步配置到内存
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const curStorages = {};
|
try {
|
||||||
const keys = Object.keys(defaultStorage);
|
setLoading(true);
|
||||||
for (const key of keys) {
|
const val = await storage.getObj(key);
|
||||||
const val = await storage.get(key);
|
|
||||||
if (val) {
|
if (val) {
|
||||||
curStorages[key] = JSON.parse(val);
|
setData(val);
|
||||||
} else {
|
} else if (defaultVal) {
|
||||||
await storage.setObj(key, defaultStorage[key]);
|
setData(defaultVal);
|
||||||
curStorages[key] = defaultStorage[key];
|
await storage.setObj(key, defaultVal);
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[storage load]", err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
setStorages(curStorages);
|
|
||||||
})();
|
})();
|
||||||
|
}, [key, defaultVal]);
|
||||||
|
|
||||||
// 监听storage,并同步到内存中
|
return { data, save, update, remove, reload, loading };
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
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,63 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { STOKEY_SYNC } from "../config";
|
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
|
||||||
import storage from "../libs/storage";
|
import { useStorage } from "./Storage";
|
||||||
import { useStorages } from "./Storage";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* sync hook
|
* sync hook
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useSync() {
|
export function useSync() {
|
||||||
const storages = useStorages();
|
const { data, update, reload } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
|
||||||
const opt = storages?.[STOKEY_SYNC];
|
return { sync: data, updateSync: update, reloadSync: reload };
|
||||||
const update = useCallback(async (obj) => {
|
}
|
||||||
await storage.putObj(STOKEY_SYNC, obj);
|
|
||||||
}, []);
|
/**
|
||||||
|
* update syncmeta hook
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useSyncMeta() {
|
||||||
|
const { sync, updateSync } = useSync();
|
||||||
|
const updateSyncMeta = useCallback(
|
||||||
|
async (key) => {
|
||||||
|
const syncMeta = sync?.syncMeta || {};
|
||||||
|
syncMeta[key] = { ...(syncMeta[key] || {}), updateAt: Date.now() };
|
||||||
|
await updateSync({ syncMeta });
|
||||||
|
},
|
||||||
|
[sync, updateSync]
|
||||||
|
);
|
||||||
|
return { updateSyncMeta };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* caches sync hook
|
||||||
|
* @param {*} url
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useSyncCaches() {
|
||||||
|
const { sync, updateSync, reloadSync } = useSync();
|
||||||
|
|
||||||
|
const updateDataCache = useCallback(
|
||||||
|
async (url) => {
|
||||||
|
const dataCaches = sync?.dataCaches || {};
|
||||||
|
dataCaches[url] = Date.now();
|
||||||
|
await updateSync({ dataCaches });
|
||||||
|
},
|
||||||
|
[sync, updateSync]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteDataCache = useCallback(
|
||||||
|
async (url) => {
|
||||||
|
const dataCaches = sync?.dataCaches || {};
|
||||||
|
delete dataCaches[url];
|
||||||
|
await updateSync({ dataCaches });
|
||||||
|
},
|
||||||
|
[sync, updateSync]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
opt,
|
dataCaches: sync?.dataCaches || {},
|
||||||
update,
|
updateDataCache,
|
||||||
|
deleteDataCache,
|
||||||
|
reloadSync,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { THEME_DARK, THEME_LIGHT } from "../config";
|
|||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function MuiThemeProvider({ children, options }) {
|
export default function Theme({ children, options }) {
|
||||||
const darkMode = useDarkMode();
|
const { darkMode } = useDarkMode();
|
||||||
const theme = useMemo(() => {
|
const theme = useMemo(() => {
|
||||||
return createTheme({
|
return createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { detectLang } from "../libs";
|
import { tryDetectLang } from "../libs";
|
||||||
import { apiTranslate } from "../apis";
|
import { apiTranslate } from "../apis";
|
||||||
|
import { DEFAULT_TRANS_APIS } from "../config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 翻译hook
|
* 翻译hook
|
||||||
* @param {*} q
|
* @param {*} q
|
||||||
* @param {*} rule
|
* @param {*} rule
|
||||||
|
* @param {*} setting
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useTranslate(q, rule) {
|
export function useTranslate(q, rule, setting) {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [sameLang, setSamelang] = useState(false);
|
const [sameLang, setSamelang] = useState(false);
|
||||||
@@ -21,15 +23,16 @@ export function useTranslate(q, rule) {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const deLang = await detectLang(q);
|
const deLang = await tryDetectLang(q);
|
||||||
if (toLang.includes(deLang)) {
|
if (deLang && toLang.includes(deLang)) {
|
||||||
setSamelang(true);
|
setSamelang(true);
|
||||||
} else {
|
} else {
|
||||||
const [trText, isSame] = await apiTranslate({
|
const [trText, isSame] = await apiTranslate({
|
||||||
translator,
|
translator,
|
||||||
q,
|
text: q,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
|
apiSetting: (setting.transApis || DEFAULT_TRANS_APIS)[translator],
|
||||||
});
|
});
|
||||||
setText(trText);
|
setText(trText);
|
||||||
setSamelang(isSame);
|
setSamelang(isSame);
|
||||||
@@ -40,7 +43,7 @@ export function useTranslate(q, rule) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [q, translator, fromLang, toLang]);
|
}, [q, translator, fromLang, toLang, setting]);
|
||||||
|
|
||||||
return { text, sameLang, loading };
|
return { text, sameLang, loading };
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/index.js
45
src/index.js
@@ -1,17 +1,58 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import Paper from "@mui/material/Paper";
|
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 { useFetch } from "./hooks/Fetch";
|
||||||
import { I18N, URL_RAW_PREFIX } from "./config";
|
import { I18N, URL_RAW_PREFIX } from "./config";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [lang, setLang] = useState("zh");
|
||||||
const [data, loading, error] = useFetch(
|
const [data, loading, error] = useFetch(
|
||||||
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.["zh"]}`
|
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}`
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ padding: 2, margin: 2 }}>
|
<Paper sx={{ padding: 2, margin: 2 }}>
|
||||||
|
<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 ? (
|
{loading ? (
|
||||||
<center>
|
<center>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import storage from "./storage";
|
import { getMsauth, setMsauth } from "./storage";
|
||||||
import { STOKEY_MSAUTH, URL_MICROSOFT_AUTH } from "../config";
|
import { URL_MICROSOFT_AUTH } from "../config";
|
||||||
import { fetchData } from "./fetch";
|
import { fetchData } from "./fetch";
|
||||||
|
|
||||||
const parseMSToken = (token) => {
|
const parseMSToken = (token) => {
|
||||||
@@ -26,9 +26,9 @@ const _msAuth = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查询storage缓存
|
// 查询storage缓存
|
||||||
const res = (await storage.getObj(STOKEY_MSAUTH)) || {};
|
const res = await getMsauth();
|
||||||
token = res.token;
|
token = res?.token;
|
||||||
exp = res.exp;
|
exp = res?.exp;
|
||||||
if (token && exp * 1000 > now + 1000) {
|
if (token && exp * 1000 > now + 1000) {
|
||||||
return [token, exp];
|
return [token, exp];
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ const _msAuth = () => {
|
|||||||
// 缓存没有或失效,查询接口
|
// 缓存没有或失效,查询接口
|
||||||
token = await fetchData(URL_MICROSOFT_AUTH);
|
token = await fetchData(URL_MICROSOFT_AUTH);
|
||||||
exp = parseMSToken(token);
|
exp = parseMSToken(token);
|
||||||
await storage.setObj(STOKEY_MSAUTH, { token, exp });
|
await setMsauth({ token, exp });
|
||||||
return [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,5 @@ function _browser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const browser = _browser();
|
export const browser = _browser();
|
||||||
export const client = process.env.REACT_APP_CLIENT;
|
|
||||||
export const isExt = CLIENT_EXTS.includes(client);
|
export const isBg = () => globalThis?.ContextType === "BACKGROUND";
|
||||||
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 { isExt, isGm } from "./client";
|
||||||
import { sendMsg } from "./msg";
|
import { sendBgMsg } from "./msg";
|
||||||
import { taskPool } from "./pool";
|
import { taskPool } from "./pool";
|
||||||
import {
|
import {
|
||||||
MSG_FETCH,
|
MSG_FETCH,
|
||||||
@@ -7,11 +7,13 @@ import {
|
|||||||
MSG_FETCH_CLEAR,
|
MSG_FETCH_CLEAR,
|
||||||
CACHE_NAME,
|
CACHE_NAME,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_DEEPL,
|
||||||
OPT_TRANS_OPENAI,
|
OPT_TRANS_OPENAI,
|
||||||
DEFAULT_FETCH_INTERVAL,
|
DEFAULT_FETCH_INTERVAL,
|
||||||
DEFAULT_FETCH_LIMIT,
|
DEFAULT_FETCH_LIMIT,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { msAuth } from "./auth";
|
import { msAuth } from "./auth";
|
||||||
|
import { isBg } from "./browser";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 油猴脚本的请求封装
|
* 油猴脚本的请求封装
|
||||||
@@ -19,7 +21,7 @@ import { msAuth } from "./auth";
|
|||||||
* @param {*} init
|
* @param {*} init
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
GM.xmlHttpRequest({
|
GM.xmlHttpRequest({
|
||||||
method,
|
method,
|
||||||
@@ -27,7 +29,7 @@ const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
|||||||
headers,
|
headers,
|
||||||
data: body,
|
data: body,
|
||||||
onload: (response) => {
|
onload: (response) => {
|
||||||
if (response.status === 200) {
|
if (response.status < 300) {
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
response.responseHeaders.split("\n").forEach((line) => {
|
response.responseHeaders.split("\n").forEach((line) => {
|
||||||
const [name, value] = line.split(":").map((item) => item.trim());
|
const [name, value] = line.split(":").map((item) => item.trim());
|
||||||
@@ -65,16 +67,37 @@ const newCacheReq = async (request) => {
|
|||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const fetchApi = async ({ input, init, useUnsafe, translator, token }) => {
|
export const fetchApi = async ({ input, init = {}, translator, token }) => {
|
||||||
if (translator === OPT_TRANS_MICROSOFT) {
|
if (token) {
|
||||||
init.headers["Authorization"] = `Bearer ${token}`;
|
if (translator === OPT_TRANS_DEEPL) {
|
||||||
} else if (translator === OPT_TRANS_OPENAI) {
|
init.headers["Authorization"] = `DeepL-Auth-Key ${token}`; // DeepL
|
||||||
init.headers["Authorization"] = `Bearer ${token}`; // // OpenAI
|
} else if (translator === OPT_TRANS_OPENAI) {
|
||||||
init.headers["api-key"] = token; // Azure OpenAI
|
init.headers["Authorization"] = `Bearer ${token}`; // OpenAI
|
||||||
|
init.headers["api-key"] = token; // Azure OpenAI
|
||||||
|
} else {
|
||||||
|
init.headers["Authorization"] = `Bearer ${token}`; // Microsoft & others
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGm && !useUnsafe) {
|
if (isGm) {
|
||||||
return fetchGM(input, init);
|
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) {
|
||||||
|
if (window.KISS_GM) {
|
||||||
|
return window.KISS_GM.fetch(input, init);
|
||||||
|
} else {
|
||||||
|
return fetchGM(input, init);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return fetch(input, init);
|
return fetch(input, init);
|
||||||
};
|
};
|
||||||
@@ -98,34 +121,32 @@ export const fetchPool = taskPool(
|
|||||||
/**
|
/**
|
||||||
* 请求数据统一接口
|
* 请求数据统一接口
|
||||||
* @param {*} input
|
* @param {*} input
|
||||||
* @param {*} init
|
|
||||||
* @param {*} opts
|
* @param {*} opts
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const fetchData = async (
|
export const fetchData = async (
|
||||||
input,
|
input,
|
||||||
init,
|
{ useCache, usePool, translator, token, ...init } = {}
|
||||||
{ useCache, usePool, translator, useUnsafe, token } = {}
|
|
||||||
) => {
|
) => {
|
||||||
const cacheReq = await newCacheReq(new Request(input, init));
|
const cacheReq = await newCacheReq(new Request(input, init));
|
||||||
const cache = await caches.open(CACHE_NAME);
|
|
||||||
let res;
|
let res;
|
||||||
|
|
||||||
// 查询缓存
|
// 查询缓存
|
||||||
if (useCache) {
|
if (useCache) {
|
||||||
try {
|
try {
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
res = await cache.match(cacheReq);
|
res = await cache.match(cacheReq);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[cache match]", err);
|
console.log("[cache match]", err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
// 发送请求
|
// 发送请求
|
||||||
if (usePool) {
|
if (usePool) {
|
||||||
res = await fetchPool.push({ input, init, useUnsafe, translator, token });
|
res = await fetchPool.push({ input, init, translator, token });
|
||||||
} else {
|
} else {
|
||||||
res = await fetchApi({ input, init, useUnsafe, translator, token });
|
res = await fetchApi({ input, init, translator, token });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res?.ok) {
|
if (!res?.ok) {
|
||||||
@@ -135,9 +156,10 @@ export const fetchData = async (
|
|||||||
// 插入缓存
|
// 插入缓存
|
||||||
if (useCache) {
|
if (useCache) {
|
||||||
try {
|
try {
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
await cache.put(cacheReq, res.clone());
|
await cache.put(cacheReq, res.clone());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[cache put]", err);
|
console.log("[cache put]", err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,22 +174,25 @@ export const fetchData = async (
|
|||||||
/**
|
/**
|
||||||
* fetch 兼容性封装
|
* fetch 兼容性封装
|
||||||
* @param {*} input
|
* @param {*} input
|
||||||
* @param {*} init
|
|
||||||
* @param {*} opts
|
* @param {*} opts
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const fetchPolyfill = async (input, init, opts) => {
|
export const fetchPolyfill = async (input, opts) => {
|
||||||
|
if (!input.trim()) {
|
||||||
|
throw new Error("URL is empty");
|
||||||
|
}
|
||||||
|
|
||||||
// 插件
|
// 插件
|
||||||
if (isExt) {
|
if (isExt && !isBg()) {
|
||||||
const res = await sendMsg(MSG_FETCH, { input, init, opts });
|
const res = await sendBgMsg(MSG_FETCH, { input, opts });
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
throw new Error(res.error);
|
throw new Error(res.error);
|
||||||
}
|
}
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 油猴/网页
|
// 油猴/网页/BackgroundPage
|
||||||
return await fetchData(input, init, opts);
|
return await fetchData(input, opts);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,9 +200,9 @@ export const fetchPolyfill = async (input, init, opts) => {
|
|||||||
* @param {*} interval
|
* @param {*} interval
|
||||||
* @param {*} limit
|
* @param {*} limit
|
||||||
*/
|
*/
|
||||||
export const fetchUpdate = async (interval, limit) => {
|
export const updateFetchPool = async (interval, limit) => {
|
||||||
if (isExt) {
|
if (isExt) {
|
||||||
const res = await sendMsg(MSG_FETCH_LIMIT, { interval, limit });
|
const res = await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit });
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
throw new Error(res.error);
|
throw new Error(res.error);
|
||||||
}
|
}
|
||||||
@@ -189,9 +214,9 @@ export const fetchUpdate = async (interval, limit) => {
|
|||||||
/**
|
/**
|
||||||
* 清空任务池
|
* 清空任务池
|
||||||
*/
|
*/
|
||||||
export const fetchClear = async () => {
|
export const clearFetchPool = async () => {
|
||||||
if (isExt) {
|
if (isExt) {
|
||||||
const res = await sendMsg(MSG_FETCH_CLEAR);
|
const res = await sendBgMsg(MSG_FETCH_CLEAR);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
throw new Error(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 } })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
11
src/libs/iframe.js
Normal file
11
src/libs/iframe.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export const isIframe = window.self !== window.top;
|
||||||
|
|
||||||
|
export const sendIframeMsg = (action, args) => {
|
||||||
|
document.querySelectorAll("iframe").forEach((iframe) => {
|
||||||
|
iframe.contentWindow.postMessage({ action, args }, "*");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendPrentMsg = (action, args) => {
|
||||||
|
window.parent.postMessage({ action, args }, "*");
|
||||||
|
};
|
||||||
@@ -1,86 +1,15 @@
|
|||||||
import storage from "./storage";
|
import { CACHE_NAME } from "../config";
|
||||||
import {
|
|
||||||
DEFAULT_SETTING,
|
|
||||||
STOKEY_SETTING,
|
|
||||||
STOKEY_RULES,
|
|
||||||
STOKEY_FAB,
|
|
||||||
GLOBLA_RULE,
|
|
||||||
GLOBAL_KEY,
|
|
||||||
} from "../config";
|
|
||||||
import { browser } from "./browser";
|
import { browser } from "./browser";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取节点列表并转为数组
|
* 清除缓存数据
|
||||||
* @param {*} selector
|
|
||||||
* @param {*} el
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export const queryEls = (selector, el = document) =>
|
export const tryClearCaches = async () => {
|
||||||
Array.from(el.querySelectorAll(selector));
|
try {
|
||||||
|
caches.delete(CACHE_NAME);
|
||||||
/**
|
} catch (err) {
|
||||||
* 查询storage中的设置
|
console.log("[clean caches]", err.message);
|
||||||
* @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匹配规则
|
|
||||||
* TODO: 支持通配符(*)匹配
|
|
||||||
* @param {*} rules
|
|
||||||
* @param {string} href
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const matchRule = (rules, href) => {
|
|
||||||
const rule = rules.find((rule) =>
|
|
||||||
rule.pattern.split(",").some((p) => href.includes(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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,7 +17,11 @@ export const matchRule = (rules, href) => {
|
|||||||
* @param {*} q
|
* @param {*} q
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const detectLang = async (q) => {
|
export const tryDetectLang = async (q) => {
|
||||||
const res = await browser?.i18n.detectLanguage(q);
|
try {
|
||||||
return res?.languages?.[0]?.language;
|
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
|
* @param {*} args
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const sendMsg = (action, args) =>
|
export const sendBgMsg = (action, args) =>
|
||||||
browser?.runtime?.sendMessage({ action, args });
|
browser.runtime.sendMessage({ action, args });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送消息给当前页面
|
* 发送消息给当前页面
|
||||||
@@ -16,6 +16,15 @@ export const sendMsg = (action, args) =>
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const sendTabMsg = async (action, args) => {
|
export const sendTabMsg = async (action, args) => {
|
||||||
const tabs = await browser?.tabs.query({ active: true, currentWindow: true });
|
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||||
return await browser?.tabs.sendMessage(tabs[0].id, { action, args });
|
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,3 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* 任务池
|
||||||
|
* @param {*} fn
|
||||||
|
* @param {*} preFn
|
||||||
|
* @param {*} _interval
|
||||||
|
* @param {*} _limit
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
||||||
const pool = [];
|
const pool = [];
|
||||||
const maxRetry = 2; // 最大重试次数
|
const maxRetry = 2; // 最大重试次数
|
||||||
@@ -6,11 +14,6 @@ export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
|||||||
let interval = _interval; // 间隔时间
|
let interval = _interval; // 间隔时间
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务池
|
|
||||||
* @param {*} item
|
|
||||||
* @param {*} preArgs
|
|
||||||
*/
|
|
||||||
const handleTask = async (item, preArgs) => {
|
const handleTask = async (item, preArgs) => {
|
||||||
curCount++;
|
curCount++;
|
||||||
const { args, resolve, reject, retry } = item;
|
const { args, resolve, reject, retry } = item;
|
||||||
|
|||||||
153
src/libs/rules.js
Normal file
153
src/libs/rules.js
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { matchValue, type, isMatch } from "./utils";
|
||||||
|
import {
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据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
|
||||||
|
* @param {*} rules
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const checkRules = (rules) => {
|
||||||
|
if (type(rules) === "string") {
|
||||||
|
rules = JSON.parse(rules);
|
||||||
|
}
|
||||||
|
if (type(rules) !== "array") {
|
||||||
|
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")
|
||||||
|
.filter(({ pattern }) => {
|
||||||
|
if (type(pattern) !== "string" || patternSet.has(pattern.trim())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
patternSet.add(pattern.trim());
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(
|
||||||
|
({
|
||||||
|
pattern,
|
||||||
|
selector,
|
||||||
|
translator,
|
||||||
|
fromLang,
|
||||||
|
toLang,
|
||||||
|
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),
|
||||||
|
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
|
||||||
|
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存或更新rule
|
||||||
|
* @param {*} newRule
|
||||||
|
*/
|
||||||
|
export const saveRule = async (newRule) => {
|
||||||
|
const rules = await getRulesWithDefault();
|
||||||
|
const rule = rules.find((item) => isMatch(newRule.pattern, item.pattern));
|
||||||
|
if (rule && rule.pattern !== GLOBAL_KEY) {
|
||||||
|
Object.assign(rule, { ...newRule, pattern: rule.pattern });
|
||||||
|
} else {
|
||||||
|
rules.unshift(newRule);
|
||||||
|
}
|
||||||
|
await setRules(rules);
|
||||||
|
trySyncRules();
|
||||||
|
};
|
||||||
112
src/libs/shortcut.js
Normal file
112
src/libs/shortcut.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { isSameSet } from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 键盘快捷键监听
|
||||||
|
* @param {*} fn
|
||||||
|
* @param {*} target
|
||||||
|
* @param {*} timeout
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const shortcutListener = (fn, target = document, timeout = 3000) => {
|
||||||
|
const allkeys = new Set();
|
||||||
|
const curkeys = new Set();
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
const handleKeydown = (e) => {
|
||||||
|
timer && clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
allkeys.clear();
|
||||||
|
curkeys.clear();
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
if (e.code) {
|
||||||
|
allkeys.add(e.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) {
|
async function set(key, val) {
|
||||||
if (isExt) {
|
if (isExt) {
|
||||||
await browser.storage.local.set({ [key]: val });
|
await browser.storage.local.set({ [key]: val });
|
||||||
} else if (isGm) {
|
} else if (isGm) {
|
||||||
const oldValue = await GM.getValue(key);
|
await (window.KISS_GM || GM).setValue(key, val);
|
||||||
await GM.setValue(key, val);
|
|
||||||
window.dispatchEvent(
|
|
||||||
new StorageEvent("storage", {
|
|
||||||
key,
|
|
||||||
oldValue,
|
|
||||||
newValue: val,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const oldValue = window.localStorage.getItem(key);
|
|
||||||
window.localStorage.setItem(key, val);
|
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]);
|
const val = await browser.storage.local.get([key]);
|
||||||
return val[key];
|
return val[key];
|
||||||
} else if (isGm) {
|
} else if (isGm) {
|
||||||
const val = await GM.getValue(key);
|
const val = await (window.KISS_GM || GM).getValue(key);
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
return window.localStorage.getItem(key);
|
return window.localStorage.getItem(key);
|
||||||
@@ -41,25 +39,9 @@ async function del(key) {
|
|||||||
if (isExt) {
|
if (isExt) {
|
||||||
await browser.storage.local.remove([key]);
|
await browser.storage.local.remove([key]);
|
||||||
} else if (isGm) {
|
} else if (isGm) {
|
||||||
const oldValue = await GM.getValue(key);
|
await (window.KISS_GM || GM).deleteValue(key);
|
||||||
await GM.deleteValue(key);
|
|
||||||
window.dispatchEvent(
|
|
||||||
new StorageEvent("storage", {
|
|
||||||
key,
|
|
||||||
oldValue,
|
|
||||||
newValue: null,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const oldValue = window.localStorage.getItem(key);
|
|
||||||
window.localStorage.removeItem(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 });
|
await setObj(key, { ...cur, ...obj });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听storage事件
|
|
||||||
* @param {*} handleChanged
|
|
||||||
*/
|
|
||||||
function onChanged(handleChanged) {
|
|
||||||
if (isExt) {
|
|
||||||
browser.storage.onChanged.addListener(handleChanged);
|
|
||||||
} else {
|
|
||||||
window.addEventListener("storage", handleChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对storage的封装
|
* 对storage的封装
|
||||||
*/
|
*/
|
||||||
const storage = {
|
export const storage = {
|
||||||
get,
|
get,
|
||||||
set,
|
set,
|
||||||
del,
|
del,
|
||||||
@@ -106,7 +76,76 @@ const storage = {
|
|||||||
trySetObj,
|
trySetObj,
|
||||||
getObj,
|
getObj,
|
||||||
putObj,
|
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) => {
|
||||||
|
const res = await apiFetch(url);
|
||||||
|
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) => {
|
||||||
|
for (let subrules of subrulesList) {
|
||||||
|
try {
|
||||||
|
await syncSubRules(subrules.url);
|
||||||
|
await updateSyncDataCache(subrules.url);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[sync subrule error]: ${subrules.url}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据时间同步所有订阅规则
|
||||||
|
* @param {*} url
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const trySyncAllSubRules = async ({ subrulesList }) => {
|
||||||
|
try {
|
||||||
|
const { subRulesSyncAt } = await getSyncWithDefault();
|
||||||
|
const now = Date.now();
|
||||||
|
const interval = 24 * 60 * 60 * 1000; // 间隔一天
|
||||||
|
if (now - subRulesSyncAt > interval) {
|
||||||
|
// 同步订阅规则
|
||||||
|
await syncAllSubRules(subrulesList);
|
||||||
|
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>
|
||||||
|
`;
|
||||||
207
src/libs/sync.js
207
src/libs/sync.js
@@ -1,74 +1,171 @@
|
|||||||
import {
|
import {
|
||||||
STOKEY_SYNC,
|
APP_LCNAME,
|
||||||
DEFAULT_SYNC,
|
|
||||||
KV_SETTING_KEY,
|
KV_SETTING_KEY,
|
||||||
KV_RULES_KEY,
|
KV_RULES_KEY,
|
||||||
STOKEY_SETTING,
|
KV_RULES_SHARE_KEY,
|
||||||
STOKEY_RULES,
|
KV_SALT_SHARE,
|
||||||
|
OPT_SYNCTYPE_WEBDAV,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import storage from "../libs/storage";
|
import {
|
||||||
import { getSetting, getRules } from ".";
|
getSyncWithDefault,
|
||||||
|
updateSync,
|
||||||
|
getSettingWithDefault,
|
||||||
|
getRulesWithDefault,
|
||||||
|
setSetting,
|
||||||
|
setRules,
|
||||||
|
} from "./storage";
|
||||||
import { apiSyncData } from "../apis";
|
import { apiSyncData } from "../apis";
|
||||||
|
import { sha256, removeEndchar } from "./utils";
|
||||||
|
import { createClient, getPatcher } from "webdav";
|
||||||
|
import { fetchApi } from "./fetch";
|
||||||
|
|
||||||
const loadOpt = async () => (await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC;
|
getPatcher().patch("request", (opts) => {
|
||||||
|
return fetchApi({
|
||||||
|
input: opts.url,
|
||||||
|
init: { method: opts.method, headers: opts.headers, body: opts.data },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
export const syncSetting = async () => {
|
const syncByWebdav = async (data, { syncUrl, syncUser, syncKey }) => {
|
||||||
const { syncUrl, syncKey, settingUpdateAt } = await loadOpt();
|
const client = createClient(syncUrl, {
|
||||||
if (!syncUrl || !syncKey) {
|
username: syncUser,
|
||||||
|
password: syncKey,
|
||||||
|
});
|
||||||
|
const pathname = `/${APP_LCNAME}`;
|
||||||
|
const filename = `/${APP_LCNAME}/${data.key}`;
|
||||||
|
|
||||||
|
if ((await client.exists(pathname)) === false) {
|
||||||
|
await client.createDirectory(pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExist = await client.exists(filename);
|
||||||
|
if (isExist) {
|
||||||
|
const cont = await client.getFileContents(filename, { format: "text" });
|
||||||
|
const webData = JSON.parse(cont);
|
||||||
|
if (webData.updateAt >= data.updateAt) {
|
||||||
|
return webData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.putFileContents(filename, JSON.stringify(data, null, 2));
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncByWorker = async (data, { syncUrl, syncKey }) => {
|
||||||
|
syncUrl = removeEndchar(syncUrl, "/");
|
||||||
|
return await apiSyncData(`${syncUrl}/sync`, syncKey, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncData = async (key, valueFn) => {
|
||||||
|
const {
|
||||||
|
syncType,
|
||||||
|
syncUrl,
|
||||||
|
syncUser,
|
||||||
|
syncKey,
|
||||||
|
syncMeta = {},
|
||||||
|
} = await getSyncWithDefault();
|
||||||
|
if (!syncUrl || !syncKey || (syncType === OPT_SYNCTYPE_WEBDAV && !syncUser)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setting = await getSetting();
|
let { updateAt = 0, syncAt = 0 } = syncMeta[key] || {};
|
||||||
const res = await apiSyncData(syncUrl, syncKey, {
|
syncAt === 0 && (updateAt = 0);
|
||||||
key: KV_SETTING_KEY,
|
|
||||||
value: setting,
|
|
||||||
updateAt: settingUpdateAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.updateAt > settingUpdateAt) {
|
const value = await valueFn();
|
||||||
await storage.putObj(STOKEY_SYNC, {
|
const data = {
|
||||||
settingUpdateAt: res.updateAt,
|
key,
|
||||||
settingSyncAt: res.updateAt,
|
value: JSON.stringify(value),
|
||||||
});
|
updateAt,
|
||||||
await storage.setObj(STOKEY_SETTING, res.value);
|
};
|
||||||
} else {
|
const args = {
|
||||||
await storage.putObj(STOKEY_SYNC, {
|
syncUrl,
|
||||||
settingSyncAt: res.updateAt,
|
syncUser,
|
||||||
});
|
syncKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res =
|
||||||
|
syncType === OPT_SYNCTYPE_WEBDAV
|
||||||
|
? await syncByWebdav(data, args)
|
||||||
|
: await syncByWorker(data, args);
|
||||||
|
|
||||||
|
syncMeta[key] = {
|
||||||
|
updateAt: res.updateAt,
|
||||||
|
syncAt: Date.now(),
|
||||||
|
};
|
||||||
|
await updateSync({ syncMeta });
|
||||||
|
|
||||||
|
return { value: JSON.parse(res.value), isNew: res.updateAt > updateAt };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步设置
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const syncSetting = async () => {
|
||||||
|
const res = await syncData(KV_SETTING_KEY, getSettingWithDefault);
|
||||||
|
if (res?.isNew) {
|
||||||
|
await setSetting(res.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const syncRules = async () => {
|
export const trySyncSetting = async () => {
|
||||||
const { syncUrl, syncKey, rulesUpdateAt } = await loadOpt();
|
|
||||||
if (!syncUrl || !syncKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rules = await getRules();
|
|
||||||
const res = await apiSyncData(syncUrl, syncKey, {
|
|
||||||
key: KV_RULES_KEY,
|
|
||||||
value: rules,
|
|
||||||
updateAt: rulesUpdateAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const syncAll = async () => {
|
|
||||||
try {
|
try {
|
||||||
await syncSetting();
|
await syncSetting();
|
||||||
await syncRules();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[sync all]", err);
|
console.log("[sync setting]", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步规则
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const syncRules = async () => {
|
||||||
|
const res = await syncData(KV_RULES_KEY, getRulesWithDefault);
|
||||||
|
if (res?.isNew) {
|
||||||
|
await setRules(res.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trySyncRules = async () => {
|
||||||
|
try {
|
||||||
|
await syncRules();
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[sync user rules]", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步分享规则
|
||||||
|
* @param {*} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
||||||
|
const data = {
|
||||||
|
key: KV_RULES_SHARE_KEY,
|
||||||
|
value: JSON.stringify(rules, null, 2),
|
||||||
|
updateAt: Date.now(),
|
||||||
|
};
|
||||||
|
const args = {
|
||||||
|
syncUrl,
|
||||||
|
syncKey,
|
||||||
|
};
|
||||||
|
await syncByWorker(data, args);
|
||||||
|
const psk = await sha256(syncKey, KV_SALT_SHARE);
|
||||||
|
const shareUrl = `${syncUrl}/rules?psk=${psk}`;
|
||||||
|
return shareUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步个人设置和规则
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const syncSettingAndRules = async () => {
|
||||||
|
await syncSetting();
|
||||||
|
await syncRules();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trySyncSettingAndRules = async () => {
|
||||||
|
await trySyncSetting();
|
||||||
|
await trySyncRules();
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,20 +3,129 @@ import {
|
|||||||
APP_LCNAME,
|
APP_LCNAME,
|
||||||
TRANS_MIN_LENGTH,
|
TRANS_MIN_LENGTH,
|
||||||
TRANS_MAX_LENGTH,
|
TRANS_MAX_LENGTH,
|
||||||
EVENT_KISS,
|
|
||||||
MSG_TRANS_CURRULE,
|
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";
|
} from "../config";
|
||||||
import { StoragesProvider } from "../hooks/Storage";
|
|
||||||
import { queryEls } from ".";
|
|
||||||
import Content from "../views/Content";
|
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 {
|
export class Translator {
|
||||||
_rule = {};
|
_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(
|
_interseObserver = new IntersectionObserver(
|
||||||
(intersections) => {
|
(intersections) => {
|
||||||
intersections.forEach((intersection) => {
|
intersections.forEach((intersection) => {
|
||||||
@@ -31,26 +140,65 @@ export class Translator {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 变化
|
||||||
_mutaObserver = new MutationObserver((mutations) => {
|
_mutaObserver = new MutationObserver((mutations) => {
|
||||||
mutations.forEach((mutation) => {
|
mutations.forEach((mutation) => {
|
||||||
mutation.addedNodes.forEach((node) => {
|
if (
|
||||||
try {
|
!this._skipNodeNames.includes(mutation.target.localName) &&
|
||||||
queryEls(this.rule.selector, node).forEach((el) => {
|
mutation.addedNodes.length > 0
|
||||||
this._interseObserver.observe(el);
|
) {
|
||||||
});
|
const nodes = Array.from(mutation.addedNodes).filter((node) => {
|
||||||
} catch (err) {
|
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 }) {
|
// 插入 shadowroot
|
||||||
fetchUpdate(fetchInterval, fetchLimit);
|
_overrideAttachShadow = () => {
|
||||||
this.rule = rule;
|
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") {
|
if (rule.transOpen === "true") {
|
||||||
this._register();
|
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() {
|
get rule() {
|
||||||
@@ -63,8 +211,9 @@ export class Translator {
|
|||||||
this._rule = rule;
|
this._rule = rule;
|
||||||
|
|
||||||
// 广播消息
|
// 广播消息
|
||||||
|
const eventName = this._eventName;
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent(EVENT_KISS, {
|
new CustomEvent(eventName, {
|
||||||
detail: {
|
detail: {
|
||||||
action: MSG_TRANS_CURRULE,
|
action: MSG_TRANS_CURRULE,
|
||||||
args: rule,
|
args: rule,
|
||||||
@@ -87,67 +236,302 @@ export class Translator {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleStyle = () => {
|
||||||
|
const textStyle =
|
||||||
|
this.rule.textStyle === OPT_STYLE_FUZZY
|
||||||
|
? OPT_STYLE_DASHLINE
|
||||||
|
: OPT_STYLE_FUZZY;
|
||||||
|
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 = () => {
|
_register = () => {
|
||||||
// 监听节点变化
|
if (this._rule.fromLang === this._rule.toLang) {
|
||||||
this._mutaObserver.observe(document, {
|
return;
|
||||||
childList: true,
|
}
|
||||||
subtree: true,
|
|
||||||
|
// 搜索节点
|
||||||
|
this._queryNodes();
|
||||||
|
|
||||||
|
this._rootNodes.forEach((node) => {
|
||||||
|
// 监听节点变化;
|
||||||
|
this._mutaObserver.observe(node, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
// characterData: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听节点显示
|
this._tranNodes.forEach((_, node) => {
|
||||||
queryEls(this.rule.selector).forEach((el) => {
|
if (
|
||||||
this._interseObserver.observe(el);
|
!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 = () => {
|
_unRegister = () => {
|
||||||
// 解除节点变化监听
|
// 解除节点变化监听
|
||||||
this._mutaObserver.disconnect();
|
this._mutaObserver.disconnect();
|
||||||
|
|
||||||
// 解除节点显示监听
|
// 解除节点显示监听
|
||||||
queryEls(this.rule.selector).forEach((el) =>
|
// this._interseObserver.disconnect();
|
||||||
this._interseObserver.unobserve(el)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 移除已插入元素
|
this._tranNodes.forEach((_, node) => {
|
||||||
queryEls(APP_LCNAME).forEach((el) => el.remove());
|
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) => {
|
_reTranslate = debounce(() => {
|
||||||
// 含子元素
|
if (this._rule.transOpen === "true") {
|
||||||
if (el.querySelector(this.rule.selector)) {
|
this._register();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
_render = (el) => {
|
||||||
|
let traEl = el.querySelector(APP_LCNAME);
|
||||||
|
|
||||||
// 已翻译
|
// 已翻译
|
||||||
if (el.querySelector(APP_LCNAME)) {
|
if (traEl) {
|
||||||
return;
|
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();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log("---> ", q);
|
// console.log("---> ", q);
|
||||||
|
|
||||||
const span = document.createElement(APP_LCNAME);
|
traEl = document.createElement(APP_LCNAME);
|
||||||
span.style.visibility = "visible";
|
traEl.style.visibility = "visible";
|
||||||
el.appendChild(span);
|
el.appendChild(traEl);
|
||||||
el.style.cssText +=
|
el.style.cssText +=
|
||||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||||
el.parentElement.style.cssText +=
|
if (el.parentElement) {
|
||||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
el.parentElement.style.cssText +=
|
||||||
|
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||||
|
}
|
||||||
|
|
||||||
const root = createRoot(span);
|
const root = createRoot(traEl);
|
||||||
root.render(
|
root.render(<Content q={q} translator={this} />);
|
||||||
<StoragesProvider>
|
|
||||||
<Content q={q} translator={this} />
|
|
||||||
</StoragesProvider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,12 @@ export const matchValue = (arr, val) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const sleep = (delay) =>
|
export const sleep = (delay) =>
|
||||||
new Promise((resolve) => setTimeout(resolve, delay));
|
new Promise((resolve) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve();
|
||||||
|
}, delay);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 防抖函数
|
* 防抖函数
|
||||||
@@ -43,11 +48,178 @@ export const sleep = (delay) =>
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const debounce = (func, delay = 200) => {
|
export const debounce = (func, delay = 200) => {
|
||||||
let timer;
|
let timer = null;
|
||||||
return (...args) => {
|
return (...args) => {
|
||||||
timer && clearTimeout(timer);
|
timer && clearTimeout(timer);
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
func(...args);
|
func(...args);
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
}, delay);
|
}, 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
|
||||||
|
* @param {*} p
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const isMatch = (s, p) => {
|
||||||
|
if (s.length === 0 || p.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
p = "*" + p + "*";
|
||||||
|
|
||||||
|
let [sIndex, pIndex] = [0, 0];
|
||||||
|
let [sRecord, pRecord] = [-1, -1];
|
||||||
|
while (sIndex < s.length && pRecord < p.length) {
|
||||||
|
if (p[pIndex] === "*") {
|
||||||
|
pIndex++;
|
||||||
|
[sRecord, pRecord] = [sIndex, pIndex];
|
||||||
|
} else if (s[sIndex] === p[pIndex]) {
|
||||||
|
sIndex++;
|
||||||
|
pIndex++;
|
||||||
|
} else if (sRecord + 1 < s.length) {
|
||||||
|
sRecord++;
|
||||||
|
[sIndex, pIndex] = [sRecord, pRecord];
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.length === pIndex) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAllchar(p, "*", pIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型检查
|
||||||
|
* @param {*} o
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const type = (o) => {
|
||||||
|
const s = Object.prototype.toString.call(o);
|
||||||
|
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sha256
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const sha256 = async (text, salt) => {
|
||||||
|
const data = new TextEncoder().encode(text + salt);
|
||||||
|
const digest = await crypto.subtle.digest({ name: "SHA-256" }, data);
|
||||||
|
return [...new Uint8Array(digest)]
|
||||||
|
.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 React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { StoragesProvider } from "./hooks/Storage";
|
import { SettingProvider } from "./hooks/Setting";
|
||||||
import ThemeProvider from "./hooks/Theme";
|
import ThemeProvider from "./hooks/Theme";
|
||||||
import Popup from "./views/Popup";
|
import Popup from "./views/Popup";
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<StoragesProvider>
|
<SettingProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Popup />
|
<Popup />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StoragesProvider>
|
</SettingProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
28
src/rules.js
Normal file
28
src/rules.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { BUILTIN_RULES } from "./config/rules";
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
// rules
|
||||||
|
try {
|
||||||
|
const data = JSON.stringify(BUILTIN_RULES, null, " ");
|
||||||
|
const file = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../build/web/kiss-translator-rules.json"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(file, data);
|
||||||
|
console.info(`Built-in rules generated: ${file}`);
|
||||||
|
} 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,104 @@ import ReactDOM from "react-dom/client";
|
|||||||
import Action from "./views/Action";
|
import Action from "./views/Action";
|
||||||
import createCache from "@emotion/cache";
|
import createCache from "@emotion/cache";
|
||||||
import { CacheProvider } from "@emotion/react";
|
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 { Translator } from "./libs/translator";
|
||||||
|
import { trySyncAllSubRules } from "./libs/subRules";
|
||||||
|
import {
|
||||||
|
MSG_TRANS_TOGGLE,
|
||||||
|
MSG_TRANS_TOGGLE_STYLE,
|
||||||
|
MSG_TRANS_GETRULE,
|
||||||
|
MSG_TRANS_PUTRULE,
|
||||||
|
} from "./config";
|
||||||
|
import { isIframe, sendIframeMsg, sendPrentMsg } 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 (
|
if (
|
||||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
|
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_OPTIONSPAGE) ||
|
||||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE2)
|
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE2)
|
||||||
) {
|
) {
|
||||||
unsafeWindow.GM = GM;
|
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
|
||||||
unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
|
unsafeWindow.GM = GM;
|
||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 翻译页面
|
// 翻译页面
|
||||||
const setting = await getSetting();
|
const setting = await getSettingWithDefault();
|
||||||
const rules = await getRules();
|
|
||||||
const rule = matchRule(rules, document.location.href);
|
if (isIframe) {
|
||||||
|
let translator;
|
||||||
|
window.addEventListener("message", (e) => {
|
||||||
|
const { action, args } = e.data || {};
|
||||||
|
switch (action) {
|
||||||
|
case MSG_TRANS_TOGGLE:
|
||||||
|
translator?.toggle();
|
||||||
|
break;
|
||||||
|
case MSG_TRANS_TOGGLE_STYLE:
|
||||||
|
translator?.toggleStyle();
|
||||||
|
break;
|
||||||
|
case MSG_TRANS_PUTRULE:
|
||||||
|
if (!translator) {
|
||||||
|
translator = new Translator(args, setting);
|
||||||
|
} else {
|
||||||
|
translator.updateRule(args || {});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sendPrentMsg(MSG_TRANS_GETRULE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const href = isIframe ? document.referrer : document.location.href;
|
||||||
|
const rules = await getRulesWithDefault();
|
||||||
|
const rule = await matchRule(rules, href, setting);
|
||||||
const translator = new Translator(rule, setting);
|
const translator = new Translator(rule, setting);
|
||||||
|
webfix(href, setting);
|
||||||
|
|
||||||
|
// 监听消息
|
||||||
|
window.addEventListener("message", (e) => {
|
||||||
|
const { action } = e.data || {};
|
||||||
|
switch (action) {
|
||||||
|
case MSG_TRANS_GETRULE:
|
||||||
|
sendIframeMsg(MSG_TRANS_PUTRULE, rule);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 浮球按钮
|
// 浮球按钮
|
||||||
const fab = await getFab();
|
const fab = await getFabWithDefault();
|
||||||
const $action = document.createElement("div");
|
const $action = document.createElement("div");
|
||||||
$action.setAttribute("id", "kiss-translator");
|
$action.setAttribute("id", "kiss-translator");
|
||||||
document.body.parentElement.appendChild($action);
|
document.body.parentElement.appendChild($action);
|
||||||
const shadowContainer = $action.attachShadow({ mode: "open" });
|
const shadowContainer = $action.attachShadow({ mode: "closed" });
|
||||||
const emotionRoot = document.createElement("style");
|
const emotionRoot = document.createElement("style");
|
||||||
const shadowRootElement = document.createElement("div");
|
const shadowRootElement = document.createElement("div");
|
||||||
shadowContainer.appendChild(emotionRoot);
|
shadowContainer.appendChild(emotionRoot);
|
||||||
@@ -54,4 +117,19 @@ import { Translator } from "./libs/translator";
|
|||||||
</CacheProvider>
|
</CacheProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 同步订阅规则
|
||||||
|
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,63 +1,51 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { limitNumber } from "../../libs/utils";
|
import { limitNumber } from "../../libs/utils";
|
||||||
import { isMobile } from "../../libs/mobile";
|
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 = (
|
const getEdgePosition = ({
|
||||||
{ x: left, y: top, edge },
|
x: left,
|
||||||
|
y: top,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
windowWidth,
|
windowWidth,
|
||||||
windowHeight,
|
windowHeight,
|
||||||
width,
|
hover,
|
||||||
height
|
}) => {
|
||||||
) => {
|
|
||||||
const right = windowWidth - left - width;
|
const right = windowWidth - left - width;
|
||||||
const bottom = windowHeight - top - height;
|
const bottom = windowHeight - top - height;
|
||||||
const min = Math.min(left, top, right, bottom);
|
const min = Math.min(left, top, right, bottom);
|
||||||
switch (min) {
|
switch (min) {
|
||||||
case right:
|
case right:
|
||||||
edge = "right";
|
left = hover ? windowWidth - width : windowWidth - width / 2;
|
||||||
left = windowWidth - width;
|
|
||||||
break;
|
break;
|
||||||
case left:
|
case left:
|
||||||
edge = "left";
|
left = hover ? 0 : -width / 2;
|
||||||
left = 0;
|
|
||||||
break;
|
break;
|
||||||
case bottom:
|
case bottom:
|
||||||
edge = "bottom";
|
top = hover ? windowHeight - height : windowHeight - height / 2;
|
||||||
top = windowHeight - height;
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
edge = "top";
|
top = hover ? 0 : -height / 2;
|
||||||
top = 0;
|
|
||||||
}
|
}
|
||||||
return { x: left, y: top, edge, hide: false };
|
return { x: left, y: top };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getHidePosition = (
|
function DraggableWrapper({ children, usePaper, ...props }) {
|
||||||
{ x: left, y: top, edge },
|
if (usePaper) {
|
||||||
windowWidth,
|
return (
|
||||||
windowHeight,
|
<Paper {...props} elevation={4}>
|
||||||
width,
|
{children}
|
||||||
height
|
</Paper>
|
||||||
) => {
|
);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
return { x: left, y: top, edge, hide: true };
|
return <div {...props}>{children}</div>;
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function Draggable({
|
export default function Draggable({
|
||||||
windowSize,
|
windowSize: { w: windowWidth, h: windowHeight },
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
left,
|
left,
|
||||||
@@ -68,66 +56,38 @@ export default function Draggable({
|
|||||||
onMove,
|
onMove,
|
||||||
handler,
|
handler,
|
||||||
children,
|
children,
|
||||||
|
usePaper,
|
||||||
}) {
|
}) {
|
||||||
const [origin, setOrigin] = useState({
|
const [hover, setHover] = useState(false);
|
||||||
x: left,
|
const [origin, setOrigin] = useState(null);
|
||||||
y: top,
|
const [position, setPosition] = useState({ x: left, y: top });
|
||||||
px: left,
|
const setFabPosition = useMemo(() => debounce(setFab, 500), []);
|
||||||
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 handlePointerDown = (e) => {
|
const handlePointerDown = (e) => {
|
||||||
!isMobile && e.target.setPointerCapture(e.pointerId);
|
!isMobile && e.target.setPointerCapture(e.pointerId);
|
||||||
onStart && onStart();
|
onStart && onStart();
|
||||||
edgeTimer && clearTimeout(edgeTimer);
|
const { x, y } = position;
|
||||||
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
||||||
setOrigin({
|
setOrigin({ x, y, clientX, clientY });
|
||||||
x: position.x,
|
|
||||||
y: position.y,
|
|
||||||
px: clientX,
|
|
||||||
py: clientY,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerMove = (e) => {
|
const handlePointerMove = (e) => {
|
||||||
onMove && onMove();
|
onMove && onMove();
|
||||||
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
||||||
if (origin) {
|
if (origin) {
|
||||||
const dx = clientX - origin.px;
|
const dx = clientX - origin.clientX;
|
||||||
const dy = clientY - origin.py;
|
const dy = clientY - origin.clientY;
|
||||||
let x = origin.x + dx;
|
let x = origin.x + dx;
|
||||||
let y = origin.y + dy;
|
let y = origin.y + dy;
|
||||||
const { w, h } = windowSize;
|
x = limitNumber(x, -width / 2, windowWidth - width / 2);
|
||||||
x = limitNumber(x, 0, w - width);
|
y = limitNumber(y, 0, windowHeight - height / 2);
|
||||||
y = limitNumber(y, 0, h - height);
|
setPosition({ x, y });
|
||||||
setPosition({ x, y, edge: null, hide: false });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerUp = (e) => {
|
const handlePointerUp = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setOrigin(null);
|
setOrigin(null);
|
||||||
if (!snapEdge) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
goEdge(windowSize.w, windowSize.h, width, height);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (e) => {
|
const handleClick = (e) => {
|
||||||
@@ -136,35 +96,48 @@ export default function Draggable({
|
|||||||
|
|
||||||
const handleMouseEnter = (e) => {
|
const handleMouseEnter = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (snapEdge && position.hide) {
|
setHover(true);
|
||||||
edgeTimer && clearTimeout(edgeTimer);
|
};
|
||||||
goEdge(windowSize.w, windowSize.h, width, height);
|
|
||||||
}
|
const handleMouseLeave = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setHover(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOrigin(null);
|
if (!snapEdge || !!origin) {
|
||||||
if (!snapEdge) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
goEdge(windowSize.w, windowSize.h, width, height);
|
|
||||||
}, [snapEdge, goEdge, windowSize.w, windowSize.h, width, height]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
setPosition((pre) => {
|
||||||
if (position.hide) {
|
const edgePosition = getEdgePosition({
|
||||||
setFab({
|
...pre,
|
||||||
x: position.x,
|
width,
|
||||||
y: position.y,
|
height,
|
||||||
|
windowWidth,
|
||||||
|
windowHeight,
|
||||||
|
hover,
|
||||||
});
|
});
|
||||||
}
|
setFabPosition(edgePosition);
|
||||||
}, [position]);
|
return edgePosition;
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
origin,
|
||||||
|
hover,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
windowWidth,
|
||||||
|
windowHeight,
|
||||||
|
snapEdge,
|
||||||
|
setFabPosition,
|
||||||
|
]);
|
||||||
|
|
||||||
const opacity = useMemo(() => {
|
const opacity = useMemo(() => {
|
||||||
if (snapEdge) {
|
if (snapEdge) {
|
||||||
return position.hide ? 0.1 : 1;
|
return hover || origin ? 1 : 0.2;
|
||||||
}
|
}
|
||||||
return origin ? 0.8 : 1;
|
return origin ? 0.8 : 1;
|
||||||
}, [origin, snapEdge, position.hide]);
|
}, [origin, snapEdge, hover]);
|
||||||
|
|
||||||
const touchProps = isMobile
|
const touchProps = isMobile
|
||||||
? {
|
? {
|
||||||
@@ -179,7 +152,8 @@ export default function Draggable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<DraggableWrapper
|
||||||
|
usePaper={usePaper}
|
||||||
style={{
|
style={{
|
||||||
opacity,
|
opacity,
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
@@ -189,6 +163,7 @@ export default function Draggable({
|
|||||||
display: show ? "block" : "none",
|
display: show ? "block" : "none",
|
||||||
}}
|
}}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -200,6 +175,6 @@ export default function Draggable({
|
|||||||
{handler}
|
{handler}
|
||||||
</div>
|
</div>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</div>
|
</DraggableWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
import Paper from "@mui/material/Paper";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Fab from "@mui/material/Fab";
|
import Fab from "@mui/material/Fab";
|
||||||
import TranslateIcon from "@mui/icons-material/Translate";
|
import TranslateIcon from "@mui/icons-material/Translate";
|
||||||
import ThemeProvider from "../../hooks/Theme";
|
import ThemeProvider from "../../hooks/Theme";
|
||||||
import Draggable from "./Draggable";
|
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 { useEffect, useState, useMemo, useCallback } from "react";
|
||||||
import { StoragesProvider } from "../../hooks/Storage";
|
import { SettingProvider } from "../../hooks/Setting";
|
||||||
import Popup from "../Popup";
|
import Popup from "../Popup";
|
||||||
import { debounce } from "../../libs/utils";
|
import { debounce } from "../../libs/utils";
|
||||||
|
import { isGm } from "../../libs/client";
|
||||||
|
import Header from "../Popup/Header";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import {
|
||||||
|
DEFAULT_SHORTCUTS,
|
||||||
|
OPT_SHORTCUT_TRANSLATE,
|
||||||
|
OPT_SHORTCUT_STYLE,
|
||||||
|
OPT_SHORTCUT_POPUP,
|
||||||
|
OPT_SHORTCUT_SETTING,
|
||||||
|
MSG_TRANS_TOGGLE,
|
||||||
|
MSG_TRANS_TOGGLE_STYLE,
|
||||||
|
} from "../../config";
|
||||||
|
import { shortcutRegister } from "../../libs/shortcut";
|
||||||
|
import { sendIframeMsg } from "../../libs/iframe";
|
||||||
|
|
||||||
export default function Action({ translator, fab }) {
|
export default function Action({ translator, fab }) {
|
||||||
const fabWidth = 40;
|
const fabWidth = 40;
|
||||||
const [showPopup, setShowPopup] = useState(false);
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
const [windowSize, setWindowSize] = useState({
|
const [windowSize, setWindowSize] = useState({
|
||||||
w: document.documentElement.clientWidth,
|
w: window.innerWidth,
|
||||||
h: document.documentElement.clientHeight,
|
h: window.innerHeight,
|
||||||
});
|
});
|
||||||
const [moved, setMoved] = useState(false);
|
const [moved, setMoved] = useState(false);
|
||||||
|
|
||||||
@@ -25,8 +35,8 @@ export default function Action({ translator, fab }) {
|
|||||||
() =>
|
() =>
|
||||||
debounce(() => {
|
debounce(() => {
|
||||||
setWindowSize({
|
setWindowSize({
|
||||||
w: document.documentElement.clientWidth,
|
w: window.innerWidth,
|
||||||
h: document.documentElement.clientHeight,
|
h: window.innerHeight,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
@@ -44,6 +54,88 @@ export default function Action({ translator, fab }) {
|
|||||||
setMoved(true);
|
setMoved(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 注册快捷键
|
||||||
|
const shortcuts = translator.setting.shortcuts || DEFAULT_SHORTCUTS;
|
||||||
|
const clearShortcuts = [
|
||||||
|
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () => {
|
||||||
|
translator.toggle();
|
||||||
|
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||||
|
setShowPopup(false);
|
||||||
|
}),
|
||||||
|
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () => {
|
||||||
|
translator.toggleStyle();
|
||||||
|
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||||
|
setShowPopup(false);
|
||||||
|
}),
|
||||||
|
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () => {
|
||||||
|
setShowPopup((pre) => !pre);
|
||||||
|
}),
|
||||||
|
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () => {
|
||||||
|
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearShortcuts.forEach((fn) => {
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [translator]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isGm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册菜单
|
||||||
|
try {
|
||||||
|
const menuCommandIds = [];
|
||||||
|
menuCommandIds.push(
|
||||||
|
GM.registerMenuCommand(
|
||||||
|
"Toggle Translate (Alt+q)",
|
||||||
|
(event) => {
|
||||||
|
translator.toggle();
|
||||||
|
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||||
|
setShowPopup(false);
|
||||||
|
},
|
||||||
|
"Q"
|
||||||
|
),
|
||||||
|
GM.registerMenuCommand(
|
||||||
|
"Toggle Style (Alt+c)",
|
||||||
|
(event) => {
|
||||||
|
translator.toggleStyle();
|
||||||
|
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("resize", handleWindowResize);
|
window.addEventListener("resize", handleWindowResize);
|
||||||
return () => {
|
return () => {
|
||||||
@@ -53,6 +145,7 @@ export default function Action({ translator, fab }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("click", handleWindowClick);
|
window.addEventListener("click", handleWindowClick);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("click", handleWindowClick);
|
window.removeEventListener("click", handleWindowClick);
|
||||||
};
|
};
|
||||||
@@ -76,12 +169,12 @@ export default function Action({ translator, fab }) {
|
|||||||
windowSize,
|
windowSize,
|
||||||
width: fabWidth,
|
width: fabWidth,
|
||||||
height: fabWidth,
|
height: fabWidth,
|
||||||
left: fab.x ?? windowSize.w - fabWidth,
|
left: fab.x ?? -fabWidth,
|
||||||
top: fab.y ?? windowSize.h / 2,
|
top: fab.y ?? windowSize.h / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StoragesProvider>
|
<SettingProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Draggable
|
<Draggable
|
||||||
key="pop"
|
key="pop"
|
||||||
@@ -89,37 +182,23 @@ export default function Action({ translator, fab }) {
|
|||||||
show={showPopup}
|
show={showPopup}
|
||||||
onStart={handleStart}
|
onStart={handleStart}
|
||||||
onMove={handleMove}
|
onMove={handleMove}
|
||||||
|
usePaper
|
||||||
handler={
|
handler={
|
||||||
<Paper style={{ cursor: "move" }} elevation={3}>
|
<Box style={{ cursor: "move" }}>
|
||||||
<Stack
|
<Header setShowPopup={setShowPopup} />
|
||||||
direction="row"
|
<Divider />
|
||||||
justifyContent="space-between"
|
</Box>
|
||||||
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>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Paper>
|
{showPopup && (
|
||||||
<Popup setShowPopup={setShowPopup} translator={translator} />
|
<Popup setShowPopup={setShowPopup} translator={translator} />
|
||||||
</Paper>
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
<Draggable
|
<Draggable
|
||||||
key="fab"
|
key="fab"
|
||||||
snapEdge
|
snapEdge
|
||||||
{...fabProps}
|
{...fabProps}
|
||||||
show={!showPopup}
|
show={translator.setting.hideFab ? false : !showPopup}
|
||||||
onStart={handleStart}
|
onStart={handleStart}
|
||||||
onMove={handleMove}
|
onMove={handleMove}
|
||||||
handler={
|
handler={
|
||||||
@@ -137,6 +216,6 @@ export default function Action({ translator, fab }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StoragesProvider>
|
</SettingProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,14 @@
|
|||||||
import { DEFAULT_COLOR } from "../../config";
|
import { loadingSvg } from "../../libs/svg";
|
||||||
|
|
||||||
export default function LoadingIcon() {
|
export default function LoadingIcon() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<div
|
||||||
viewBox="0 0 100 100"
|
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "1.2em",
|
display: "inline-block",
|
||||||
maxHeight: "1.2em",
|
width: "1.2em",
|
||||||
|
height: "1em",
|
||||||
}}
|
}}
|
||||||
>
|
dangerouslySetInnerHTML={{ __html: loadingSvg }}
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import LoadingIcon from "./LoadingIcon";
|
import LoadingIcon from "./LoadingIcon";
|
||||||
import {
|
import {
|
||||||
OPT_STYLE_LINE,
|
OPT_STYLE_LINE,
|
||||||
@@ -6,26 +6,99 @@ import {
|
|||||||
OPT_STYLE_DASHLINE,
|
OPT_STYLE_DASHLINE,
|
||||||
OPT_STYLE_WAVYLINE,
|
OPT_STYLE_WAVYLINE,
|
||||||
OPT_STYLE_FUZZY,
|
OPT_STYLE_FUZZY,
|
||||||
OPT_STYLE_HIGHTLIGHT,
|
OPT_STYLE_HIGHLIGHT,
|
||||||
|
OPT_STYLE_DIY,
|
||||||
DEFAULT_COLOR,
|
DEFAULT_COLOR,
|
||||||
EVENT_KISS,
|
|
||||||
MSG_TRANS_CURRULE,
|
MSG_TRANS_CURRULE,
|
||||||
|
TRANS_NEWLINE_LENGTH,
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { useTranslate } from "../../hooks/Translate";
|
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 }) {
|
export default function Content({ q, translator }) {
|
||||||
const [rule, setRule] = useState(translator.rule);
|
const [rule, setRule] = useState(translator.rule);
|
||||||
const [hover, setHover] = useState(false);
|
const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
|
||||||
const { text, sameLang, loading } = useTranslate(q, rule);
|
const { textStyle, bgColor = "", textDiyStyle = "" } = rule;
|
||||||
const { textStyle, bgColor } = rule;
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const { newlineLength = TRANS_NEWLINE_LENGTH } = translator.setting;
|
||||||
setHover(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
setHover(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKissEvent = (e) => {
|
const handleKissEvent = (e) => {
|
||||||
const { action, args } = e.detail;
|
const { action, args } = e.detail;
|
||||||
@@ -34,63 +107,20 @@ export default function Content({ q, translator }) {
|
|||||||
setRule(args);
|
setRule(args);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// console.log(`[popup] kissEvent action skip: ${action}`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener(EVENT_KISS, handleKissEvent);
|
window.addEventListener(translator.eventName, handleKissEvent);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener(EVENT_KISS, handleKissEvent);
|
window.removeEventListener(translator.eventName, handleKissEvent);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [translator.eventName]);
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{q.length > 40 ? <br /> : " "}
|
{q.length > newlineLength ? <br /> : " "}
|
||||||
<LoadingIcon />
|
<LoadingIcon />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -99,14 +129,14 @@ export default function Content({ q, translator }) {
|
|||||||
if (text && !sameLang) {
|
if (text && !sameLang) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{q.length > 40 ? <br /> : " "}
|
{q.length > newlineLength ? <br /> : " "}
|
||||||
<span
|
<StyledSpan
|
||||||
style={style}
|
textStyle={textStyle}
|
||||||
onMouseEnter={handleMouseEnter}
|
textDiyStyle={textDiyStyle}
|
||||||
onMouseLeave={handleMouseLeave}
|
bgColor={bgColor}
|
||||||
>
|
>
|
||||||
{text}
|
{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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,20 +1,15 @@
|
|||||||
import PropTypes from "prop-types";
|
|
||||||
import AppBar from "@mui/material/AppBar";
|
import AppBar from "@mui/material/AppBar";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import MenuIcon from "@mui/icons-material/Menu";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useDarkModeSwitch } from "../../hooks/ColorMode";
|
import Link from "@mui/material/Link";
|
||||||
import { useDarkMode } from "../../hooks/ColorMode";
|
|
||||||
import LightModeIcon from "@mui/icons-material/LightMode";
|
|
||||||
import DarkModeIcon from "@mui/icons-material/DarkMode";
|
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
|
import DarkModeButton from "./DarkModeButton";
|
||||||
|
|
||||||
function Header(props) {
|
function Header(props) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { onDrawerToggle } = props;
|
const { onDrawerToggle } = props;
|
||||||
const switchColorMode = useDarkModeSwitch();
|
|
||||||
const darkMode = useDarkMode();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
@@ -35,19 +30,18 @@ function Header(props) {
|
|||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ flexGrow: 1 }}>{`${i18n("app_name")} v${
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
process.env.REACT_APP_VERSION
|
<Link
|
||||||
}`}</Box>
|
underline="none"
|
||||||
<IconButton onClick={switchColorMode} color="inherit">
|
color="inherit"
|
||||||
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
|
href={process.env.REACT_APP_HOMEPAGE}
|
||||||
</IconButton>
|
target="_blank"
|
||||||
|
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
|
||||||
|
</Box>
|
||||||
|
<DarkModeButton />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Header.propTypes = {
|
|
||||||
onDrawerToggle: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
|||||||
19
src/views/Options/HelpButton.js
Normal file
19
src/views/Options/HelpButton.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import { useI18n } from "../../hooks/I18n";
|
||||||
|
import HelpIcon from "@mui/icons-material/Help";
|
||||||
|
|
||||||
|
export default function HelpButton({ url }) {
|
||||||
|
const i18n = useI18n();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => {
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}}
|
||||||
|
startIcon={<HelpIcon />}
|
||||||
|
>
|
||||||
|
{i18n("help")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
178
src/views/Options/InputSetting.js
Normal file
178
src/views/Options/InputSetting.js
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import { useI18n } from "../../hooks/I18n";
|
||||||
|
import {
|
||||||
|
OPT_TRANS_ALL,
|
||||||
|
OPT_LANGS_FROM,
|
||||||
|
OPT_LANGS_TO,
|
||||||
|
OPT_INPUT_TRANS_SIGNS,
|
||||||
|
} from "../../config";
|
||||||
|
import ShortcutInput from "./ShortcutInput";
|
||||||
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
import { useInputRule } from "../../hooks/InputRule";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import { limitNumber } from "../../libs/utils";
|
||||||
|
|
||||||
|
export default function InputSetting() {
|
||||||
|
const i18n = useI18n();
|
||||||
|
const { inputRule, updateInputRule } = useInputRule();
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let { name, value } = e.target;
|
||||||
|
switch (name) {
|
||||||
|
case "triggerTime":
|
||||||
|
value = limitNumber(value, 10, 1000);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
updateInputRule({
|
||||||
|
[name]: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShortcutInput = useCallback(
|
||||||
|
(val) => {
|
||||||
|
updateInputRule({ triggerShortcut: val });
|
||||||
|
},
|
||||||
|
[updateInputRule]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
transOpen,
|
||||||
|
translator,
|
||||||
|
fromLang,
|
||||||
|
toLang,
|
||||||
|
triggerShortcut,
|
||||||
|
triggerCount,
|
||||||
|
triggerTime,
|
||||||
|
transSign,
|
||||||
|
} = inputRule;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
name="transOpen"
|
||||||
|
checked={transOpen}
|
||||||
|
onChange={() => {
|
||||||
|
updateInputRule({ transOpen: !transOpen });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n("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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import Box from "@mui/material/Box";
|
|||||||
import Navigator from "./Navigator";
|
import Navigator from "./Navigator";
|
||||||
import Header from "./Header";
|
import Header from "./Header";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { syncAll } from "../../libs/sync";
|
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const navWidth = 256;
|
const navWidth = 256;
|
||||||
@@ -21,7 +20,6 @@ export default function Layout() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
syncAll();
|
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import InfoIcon from "@mui/icons-material/Info";
|
|||||||
import DesignServicesIcon from "@mui/icons-material/DesignServices";
|
import DesignServicesIcon from "@mui/icons-material/DesignServices";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import SyncIcon from "@mui/icons-material/Sync";
|
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 }) {
|
function LinkItem({ label, url, icon }) {
|
||||||
const match = useMatch(url);
|
const match = useMatch(url);
|
||||||
@@ -36,12 +39,30 @@ export default function Navigator(props) {
|
|||||||
url: "/rules",
|
url: "/rules",
|
||||||
icon: <DesignServicesIcon />,
|
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",
|
id: "sync",
|
||||||
label: i18n("sync_setting"),
|
label: i18n("sync_setting"),
|
||||||
url: "/sync",
|
url: "/sync",
|
||||||
icon: <SyncIcon />,
|
icon: <SyncIcon />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "webfix",
|
||||||
|
label: i18n("patch_setting"),
|
||||||
|
url: "/webfix",
|
||||||
|
icon: <SendTimeExtensionIcon />,
|
||||||
|
},
|
||||||
{ id: "about", label: i18n("about"), url: "/about", icon: <InfoIcon /> },
|
{ id: "about", label: i18n("about"), url: "/about", icon: <InfoIcon /> },
|
||||||
];
|
];
|
||||||
return (
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import Box from "@mui/material/Box";
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
import {
|
import {
|
||||||
GLOBAL_KEY,
|
GLOBAL_KEY,
|
||||||
DEFAULT_RULE,
|
DEFAULT_RULE,
|
||||||
@@ -9,8 +11,12 @@ import {
|
|||||||
OPT_LANGS_TO,
|
OPT_LANGS_TO,
|
||||||
OPT_TRANS_ALL,
|
OPT_TRANS_ALL,
|
||||||
OPT_STYLE_ALL,
|
OPT_STYLE_ALL,
|
||||||
|
OPT_STYLE_DIY,
|
||||||
|
OPT_STYLE_USE_COLOR,
|
||||||
|
URL_KISS_RULES_NEW_ISSUE,
|
||||||
|
OPT_SYNCTYPE_WORKER,
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef, useEffect, useMemo } from "react";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Accordion from "@mui/material/Accordion";
|
import Accordion from "@mui/material/Accordion";
|
||||||
@@ -22,9 +28,34 @@ import MenuItem from "@mui/material/MenuItem";
|
|||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import FileDownloadIcon from "@mui/icons-material/FileDownload";
|
import FileDownloadIcon from "@mui/icons-material/FileDownload";
|
||||||
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
||||||
|
import { useSetting } from "../../hooks/Setting";
|
||||||
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
import Tabs from "@mui/material/Tabs";
|
||||||
|
import Tab from "@mui/material/Tab";
|
||||||
|
import Radio from "@mui/material/Radio";
|
||||||
|
import RadioGroup from "@mui/material/RadioGroup";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import ShareIcon from "@mui/icons-material/Share";
|
||||||
|
import SyncIcon from "@mui/icons-material/Sync";
|
||||||
|
import { useSubRules } from "../../hooks/SubRules";
|
||||||
|
import { syncSubRules } from "../../libs/subRules";
|
||||||
|
import { loadOrFetchSubRules } from "../../libs/subRules";
|
||||||
|
import { useAlert } from "../../hooks/Alert";
|
||||||
|
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 }) {
|
function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||||
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
|
const initFormValues = rule || {
|
||||||
|
...DEFAULT_RULE,
|
||||||
|
transOpen: "true",
|
||||||
|
};
|
||||||
const editMode = !!rule;
|
const editMode = !!rule;
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -40,6 +71,7 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
textStyle,
|
textStyle,
|
||||||
transOpen,
|
transOpen,
|
||||||
bgColor,
|
bgColor,
|
||||||
|
textDiyStyle,
|
||||||
} = formValues;
|
} = formValues;
|
||||||
|
|
||||||
const hasSamePattern = (str) => {
|
const hasSamePattern = (str) => {
|
||||||
@@ -57,10 +89,21 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
setErrors((pre) => ({ ...pre, [name]: "" }));
|
setErrors((pre) => ({ ...pre, [name]: "" }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePatternChange = useMemo(
|
||||||
|
() =>
|
||||||
|
debounce(async (patterns) => {
|
||||||
|
setKeyword(patterns.trim());
|
||||||
|
}, 500),
|
||||||
|
[setKeyword]
|
||||||
|
);
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormValues((pre) => ({ ...pre, [name]: value }));
|
setFormValues((pre) => ({ ...pre, [name]: value }));
|
||||||
|
if (name === "pattern" && !editMode) {
|
||||||
|
handlePatternChange(value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = (e) => {
|
const handleCancel = (e) => {
|
||||||
@@ -103,7 +146,7 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const globalItem = rule?.pattern !== "*" && (
|
const GlobalItem = rule?.pattern !== "*" && (
|
||||||
<MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}>
|
<MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}>
|
||||||
{GLOBAL_KEY}
|
{GLOBAL_KEY}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -122,6 +165,7 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
disabled={rule?.pattern === "*" || disabled}
|
disabled={rule?.pattern === "*" || disabled}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
|
multiline
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -134,8 +178,6 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
multiline
|
multiline
|
||||||
minRows={2}
|
|
||||||
maxRows={10}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
@@ -151,7 +193,7 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{globalItem}
|
{GlobalItem}
|
||||||
<MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem>
|
<MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem>
|
||||||
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
|
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
@@ -167,7 +209,7 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{globalItem}
|
{GlobalItem}
|
||||||
{OPT_TRANS_ALL.map((item) => (
|
{OPT_TRANS_ALL.map((item) => (
|
||||||
<MenuItem key={item} value={item}>
|
<MenuItem key={item} value={item}>
|
||||||
{item}
|
{item}
|
||||||
@@ -186,7 +228,7 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{globalItem}
|
{GlobalItem}
|
||||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||||
<MenuItem key={lang} value={lang}>
|
<MenuItem key={lang} value={lang}>
|
||||||
{name}
|
{name}
|
||||||
@@ -205,7 +247,7 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{globalItem}
|
{GlobalItem}
|
||||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||||
<MenuItem key={lang} value={lang}>
|
<MenuItem key={lang} value={lang}>
|
||||||
{name}
|
{name}
|
||||||
@@ -224,7 +266,7 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{globalItem}
|
{GlobalItem}
|
||||||
{OPT_STYLE_ALL.map((item) => (
|
{OPT_STYLE_ALL.map((item) => (
|
||||||
<MenuItem key={item} value={item}>
|
<MenuItem key={item} value={item}>
|
||||||
{i18n(item)}
|
{i18n(item)}
|
||||||
@@ -232,75 +274,123 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||||
<TextField
|
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||||
size="small"
|
<TextField
|
||||||
fullWidth
|
size="small"
|
||||||
name="bgColor"
|
fullWidth
|
||||||
value={bgColor}
|
name="bgColor"
|
||||||
label={i18n("bg_color")}
|
value={bgColor}
|
||||||
disabled={disabled}
|
label={i18n("bg_color")}
|
||||||
onChange={handleChange}
|
disabled={disabled}
|
||||||
/>
|
onChange={handleChange}
|
||||||
</Grid>
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{editMode ? (
|
{textStyle === OPT_STYLE_DIY && (
|
||||||
// 编辑
|
<TextField
|
||||||
<Stack direction="row" spacing={2}>
|
size="small"
|
||||||
{disabled ? (
|
label={i18n("diy_style")}
|
||||||
<>
|
helperText={i18n("diy_style_helper")}
|
||||||
<Button
|
name="textDiyStyle"
|
||||||
size="small"
|
value={textDiyStyle}
|
||||||
variant="contained"
|
disabled={disabled}
|
||||||
onClick={(e) => {
|
onChange={handleChange}
|
||||||
e.preventDefault();
|
multiline
|
||||||
setDisabled(false);
|
/>
|
||||||
}}
|
)}
|
||||||
>
|
|
||||||
{i18n("edit")}
|
{rules &&
|
||||||
</Button>
|
(editMode ? (
|
||||||
{rule?.pattern !== "*" && (
|
// 编辑
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
{disabled ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDisabled(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n("edit")}
|
||||||
|
</Button>
|
||||||
|
{rule?.pattern !== "*" && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
rules.del(rule.pattern);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n("delete")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button size="small" variant="contained" type="submit">
|
||||||
|
{i18n("save")}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={(e) => {
|
onClick={handleCancel}
|
||||||
e.preventDefault();
|
|
||||||
rules.del(rule.pattern);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{i18n("delete")}
|
{i18n("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</>
|
||||||
</>
|
)}
|
||||||
) : (
|
</Stack>
|
||||||
<>
|
) : (
|
||||||
<Button size="small" variant="contained" type="submit">
|
// 添加
|
||||||
{i18n("save")}
|
<Stack direction="row" spacing={2}>
|
||||||
</Button>
|
<Button size="small" variant="contained" type="submit">
|
||||||
<Button size="small" variant="outlined" onClick={handleCancel}>
|
{i18n("save")}
|
||||||
{i18n("cancel")}
|
</Button>
|
||||||
</Button>
|
<Button size="small" variant="outlined" onClick={handleCancel}>
|
||||||
</>
|
{i18n("cancel")}
|
||||||
)}
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
))}
|
||||||
// 添加
|
|
||||||
<Stack direction="row" spacing={2}>
|
|
||||||
<Button size="small" variant="contained" type="submit">
|
|
||||||
{i18n("save")}
|
|
||||||
</Button>
|
|
||||||
<Button size="small" variant="outlined" onClick={handleCancel}>
|
|
||||||
{i18n("cancel")}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RuleAccordion({ rule, rules }) {
|
||||||
|
const i18n = useI18n();
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
setExpanded((pre) => !pre);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion expanded={expanded} onChange={handleChange}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
|
<Typography
|
||||||
|
style={{
|
||||||
|
opacity: rules ? 1 : 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rule.pattern === GLOBAL_KEY
|
||||||
|
? `[${i18n("global_rule")}] ${rule.pattern}`
|
||||||
|
: rule.pattern}
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
{expanded && <RuleFields rule={rule} rules={rules} />}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function DownloadButton({ data, text, fileName }) {
|
function DownloadButton({ data, text, fileName }) {
|
||||||
const handleClick = (e) => {
|
const handleClick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -351,10 +441,57 @@ function UploadButton({ onChange, text }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Rules() {
|
function ShareButton({ rules, injectRules, selectedUrl }) {
|
||||||
|
const alert = useAlert();
|
||||||
|
const i18n = useI18n();
|
||||||
|
const handleClick = async () => {
|
||||||
|
try {
|
||||||
|
const { syncType, syncUrl, syncKey } = await getSyncWithDefault();
|
||||||
|
if (syncType !== OPT_SYNCTYPE_WORKER || !syncUrl || !syncKey) {
|
||||||
|
alert.warning(i18n("error_sync_setting"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareRules = [...rules.list];
|
||||||
|
if (injectRules) {
|
||||||
|
const subRules = await loadOrFetchSubRules(selectedUrl);
|
||||||
|
shareRules.splice(-1, 0, ...subRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await syncShareRules({
|
||||||
|
rules: shareRules,
|
||||||
|
syncUrl,
|
||||||
|
syncKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.open(url, "_blank");
|
||||||
|
} catch (err) {
|
||||||
|
alert.warning(i18n("error_got_some_wrong"));
|
||||||
|
console.log("[share rules]", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleClick}
|
||||||
|
startIcon={<ShareIcon />}
|
||||||
|
>
|
||||||
|
{i18n("share")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserRules({ subRules }) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const rules = useRules();
|
const rules = useRules();
|
||||||
const [showAdd, setShowAdd] = useState(false);
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
|
const { setting, updateSetting } = useSetting();
|
||||||
|
const [keyword, setKeyword] = useState("");
|
||||||
|
|
||||||
|
const injectRules = !!setting?.injectRules;
|
||||||
|
const { selectedUrl, selectedRules } = subRules;
|
||||||
|
|
||||||
const handleImport = (e) => {
|
const handleImport = (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
@@ -378,43 +515,377 @@ export default function Rules() {
|
|||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInject = () => {
|
||||||
|
updateSetting({
|
||||||
|
injectRules: !injectRules,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showAdd) {
|
||||||
|
setKeyword("");
|
||||||
|
}
|
||||||
|
}, [showAdd]);
|
||||||
|
|
||||||
|
if (!rules.list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={2}
|
||||||
|
useFlexGap
|
||||||
|
flexWrap="wrap"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
disabled={showAdd}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowAdd(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n("add")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<UploadButton text={i18n("import")} onChange={handleImport} />
|
||||||
|
<DownloadButton
|
||||||
|
data={JSON.stringify([...rules.list].reverse(), null, "\t")}
|
||||||
|
text={i18n("export")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ShareButton
|
||||||
|
rules={rules}
|
||||||
|
injectRules={injectRules}
|
||||||
|
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
|
||||||
|
size="small"
|
||||||
|
checked={injectRules}
|
||||||
|
onChange={handleInject}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n("inject_rules")}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{showAdd && (
|
||||||
|
<RuleFields
|
||||||
|
rules={rules}
|
||||||
|
setShow={setShowAdd}
|
||||||
|
setKeyword={setKeyword}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
{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,
|
||||||
|
syncAt,
|
||||||
|
selectedUrl,
|
||||||
|
delSub,
|
||||||
|
setSelectedRules,
|
||||||
|
updateDataCache,
|
||||||
|
deleteDataCache,
|
||||||
|
}) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleDel = async () => {
|
||||||
|
try {
|
||||||
|
await delSub(url);
|
||||||
|
await delSubRules(url);
|
||||||
|
await deleteDataCache(url);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[del subrules]", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSync = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
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 {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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} />
|
||||||
|
) : (
|
||||||
|
<IconButton size="small" onClick={handleSync}>
|
||||||
|
<SyncIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{index !== 0 && selectedUrl !== url && (
|
||||||
|
<IconButton size="small" onClick={handleDel}>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
setShowInput(false);
|
||||||
|
setInputText("");
|
||||||
|
setInputError("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const url = inputText.trim();
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
setInputError(i18n("error_cant_be_blank"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subList.find((item) => item.url === url)) {
|
||||||
|
setInputError(i18n("error_duplicate_values"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const rules = await syncSubRules(url);
|
||||||
|
if (rules.length === 0) {
|
||||||
|
throw new Error("empty rules");
|
||||||
|
}
|
||||||
|
await addSub(url);
|
||||||
|
await updateDataCache(url);
|
||||||
|
setShowInput(false);
|
||||||
|
setInputText("");
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[fetch rules]", err);
|
||||||
|
setInputError(i18n("error_fetch_url"));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInput = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setInputText(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setInputError("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={2}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
disabled={showInput}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowInput(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n("add")}
|
||||||
|
</Button>
|
||||||
|
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{showInput && (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={inputText}
|
||||||
|
error={!!inputError}
|
||||||
|
helperText={inputError}
|
||||||
|
onChange={handleInput}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
label={i18n("subscribe_url")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack direction="row" alignItems="center" spacing={2}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{i18n("save")}
|
||||||
|
</Button>
|
||||||
|
<Button size="small" variant="outlined" onClick={handleCancel}>
|
||||||
|
{i18n("cancel")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
selectSub(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadSync();
|
||||||
|
}, [selectedRules, reloadSync]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<SubRulesEdit
|
||||||
|
subList={subList}
|
||||||
|
addSub={addSub}
|
||||||
|
updateDataCache={updateDataCache}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RadioGroup value={selectedUrl} onChange={handleSelect}>
|
||||||
|
{subList.map((item, index) => (
|
||||||
|
<SubRulesItem
|
||||||
|
key={item.url}
|
||||||
|
url={item.url}
|
||||||
|
syncAt={dataCaches[item.url]}
|
||||||
|
index={index}
|
||||||
|
selectedUrl={selectedUrl}
|
||||||
|
delSub={delSub}
|
||||||
|
setSelectedRules={setSelectedRules}
|
||||||
|
updateDataCache={updateDataCache}
|
||||||
|
deleteDataCache={deleteDataCache}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
{loading ? (
|
||||||
|
<center>
|
||||||
|
<CircularProgress />
|
||||||
|
</center>
|
||||||
|
) : (
|
||||||
|
selectedRules.map((rule) => (
|
||||||
|
<RuleAccordion key={rule.pattern} rule={rule} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Rules() {
|
||||||
|
const i18n = useI18n();
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const subRules = useSubRules();
|
||||||
|
|
||||||
|
const handleTabChange = (e, newValue) => {
|
||||||
|
setActiveTab(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Stack direction="row" spacing={2}>
|
<Alert severity="info">
|
||||||
<Button
|
{i18n("rules_warn_1")}
|
||||||
size="small"
|
<br />
|
||||||
variant="contained"
|
{i18n("rules_warn_2")}
|
||||||
disabled={showAdd}
|
</Alert>
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setShowAdd(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{i18n("add")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<UploadButton text={i18n("import")} onChange={handleImport} />
|
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||||
<DownloadButton
|
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||||
data={JSON.stringify([...rules.list].reverse(), null, "\t")}
|
<Tab label={i18n("personal_rules")} />
|
||||||
text={i18n("export")}
|
<Tab label={i18n("subscribe_rules")} />
|
||||||
/>
|
<Tab label={i18n("overwrite_subscribe_rules")} />
|
||||||
</Stack>
|
</Tabs>
|
||||||
|
|
||||||
{showAdd && <RuleFields rules={rules} setShow={setShowAdd} />}
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
{rules.list.map((rule) => (
|
|
||||||
<Accordion key={rule.pattern}>
|
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
|
||||||
<Typography>{rule.pattern}</Typography>
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
<RuleFields rule={rule} rules={rules} />
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
))}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
<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>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,30 +5,85 @@ import TextField from "@mui/material/TextField";
|
|||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import FormControl from "@mui/material/FormControl";
|
import FormControl from "@mui/material/FormControl";
|
||||||
import Select from "@mui/material/Select";
|
import Select from "@mui/material/Select";
|
||||||
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
|
import Link from "@mui/material/Link";
|
||||||
|
import FormHelperText from "@mui/material/FormHelperText";
|
||||||
|
import { useSetting } from "../../hooks/Setting";
|
||||||
import { limitNumber } from "../../libs/utils";
|
import { limitNumber } from "../../libs/utils";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import { UI_LANGS } from "../../config";
|
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() {
|
export default function Settings() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const setting = useSetting();
|
const { setting, updateSetting } = useSetting();
|
||||||
const updateSetting = useSettingUpdate();
|
const alert = useAlert();
|
||||||
|
|
||||||
if (!setting) {
|
const handleChange = (e) => {
|
||||||
return;
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearCache = () => {
|
||||||
|
try {
|
||||||
|
caches.delete(CACHE_NAME);
|
||||||
|
alert.success(i18n("clear_success"));
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[clear cache]", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
uiLang,
|
uiLang,
|
||||||
googleUrl,
|
|
||||||
fetchLimit,
|
fetchLimit,
|
||||||
fetchInterval,
|
fetchInterval,
|
||||||
openaiUrl,
|
minLength,
|
||||||
openaiKey,
|
maxLength,
|
||||||
openaiModel,
|
|
||||||
openaiPrompt,
|
|
||||||
clearCache,
|
clearCache,
|
||||||
|
newlineLength = TRANS_NEWLINE_LENGTH,
|
||||||
|
mouseKey = OPT_MOUSEKEY_DISABLE,
|
||||||
|
hideFab = false,
|
||||||
} = setting;
|
} = setting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,13 +92,10 @@ export default function Settings() {
|
|||||||
<FormControl size="small">
|
<FormControl size="small">
|
||||||
<InputLabel>{i18n("ui_lang")}</InputLabel>
|
<InputLabel>{i18n("ui_lang")}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
|
name="uiLang"
|
||||||
value={uiLang}
|
value={uiLang}
|
||||||
label={i18n("ui_lang")}
|
label={i18n("ui_lang")}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
updateSetting({
|
|
||||||
uiLang: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{UI_LANGS.map(([lang, name]) => (
|
{UI_LANGS.map(([lang, name]) => (
|
||||||
<MenuItem key={lang} value={lang}>
|
<MenuItem key={lang} value={lang}>
|
||||||
@@ -57,100 +109,126 @@ export default function Settings() {
|
|||||||
size="small"
|
size="small"
|
||||||
label={i18n("fetch_limit")}
|
label={i18n("fetch_limit")}
|
||||||
type="number"
|
type="number"
|
||||||
|
name="fetchLimit"
|
||||||
defaultValue={fetchLimit}
|
defaultValue={fetchLimit}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
updateSetting({
|
|
||||||
fetchLimit: limitNumber(e.target.value, 1, 100),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("fetch_interval")}
|
label={i18n("fetch_interval")}
|
||||||
type="number"
|
type="number"
|
||||||
|
name="fetchInterval"
|
||||||
defaultValue={fetchInterval}
|
defaultValue={fetchInterval}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
updateSetting({
|
/>
|
||||||
fetchInterval: limitNumber(e.target.value, 0, 5000),
|
|
||||||
});
|
<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">
|
<FormControl size="small">
|
||||||
<InputLabel>{i18n("clear_cache")}</InputLabel>
|
<InputLabel>{i18n("mouseover_translation")}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={clearCache}
|
name="mouseKey"
|
||||||
label={i18n("clear_cache")}
|
value={mouseKey}
|
||||||
onChange={(e) => {
|
label={i18n("mouseover_translation")}
|
||||||
updateSetting({
|
onChange={handleChange}
|
||||||
clearCache: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
|
{OPT_MOUSEKEY_ALL.map((item) => (
|
||||||
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
|
<MenuItem key={item} value={item}>
|
||||||
|
{i18n(item)}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<TextField
|
{isExt ? (
|
||||||
size="small"
|
<FormControl size="small">
|
||||||
label={i18n("google_api")}
|
<InputLabel>{i18n("if_clear_cache")}</InputLabel>
|
||||||
defaultValue={googleUrl}
|
<Select
|
||||||
onChange={(e) => {
|
name="clearCache"
|
||||||
updateSetting({
|
value={clearCache}
|
||||||
googleUrl: e.target.value,
|
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
|
<Box>
|
||||||
size="small"
|
<Grid container spacing={2} columns={12}>
|
||||||
label={i18n("openai_api")}
|
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||||
defaultValue={openaiUrl}
|
<ShortcutItem
|
||||||
onChange={(e) => {
|
action={OPT_SHORTCUT_TRANSLATE}
|
||||||
updateSetting({
|
label={i18n("toggle_translate_shortcut")}
|
||||||
openaiUrl: e.target.value,
|
/>
|
||||||
});
|
</Grid>
|
||||||
}}
|
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||||
/>
|
<ShortcutItem
|
||||||
|
action={OPT_SHORTCUT_STYLE}
|
||||||
<TextField
|
label={i18n("toggle_style_shortcut")}
|
||||||
size="small"
|
/>
|
||||||
type="password"
|
</Grid>
|
||||||
label={i18n("openai_key")}
|
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||||
defaultValue={openaiKey}
|
<ShortcutItem
|
||||||
onChange={(e) => {
|
action={OPT_SHORTCUT_POPUP}
|
||||||
updateSetting({
|
label={i18n("toggle_popup_shortcut")}
|
||||||
openaiKey: e.target.value,
|
/>
|
||||||
});
|
</Grid>
|
||||||
}}
|
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||||
/>
|
<ShortcutItem
|
||||||
|
action={OPT_SHORTCUT_SETTING}
|
||||||
<TextField
|
label={i18n("open_setting_shortcut")}
|
||||||
size="small"
|
/>
|
||||||
label={i18n("openai_model")}
|
</Grid>
|
||||||
defaultValue={openaiModel}
|
</Grid>
|
||||||
onChange={(e) => {
|
</Box>
|
||||||
updateSetting({
|
</>
|
||||||
openaiModel: e.target.value,
|
)}
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
size="small"
|
|
||||||
label={i18n("openai_prompt")}
|
|
||||||
defaultValue={openaiPrompt}
|
|
||||||
onChange={(e) => {
|
|
||||||
updateSetting({
|
|
||||||
openaiPrompt: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
multiline
|
|
||||||
minRows={2}
|
|
||||||
maxRows={10}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,56 +3,136 @@ import Stack from "@mui/material/Stack";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import { useSync } from "../../hooks/Sync";
|
import { useSync } from "../../hooks/Sync";
|
||||||
import { syncAll } from "../../libs/sync";
|
|
||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import { URL_KISS_WORKER } from "../../config";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import {
|
||||||
|
URL_KISS_WORKER,
|
||||||
|
OPT_SYNCTYPE_ALL,
|
||||||
|
OPT_SYNCTYPE_WORKER,
|
||||||
|
OPT_SYNCTYPE_WEBDAV,
|
||||||
|
} from "../../config";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { syncSettingAndRules } from "../../libs/sync";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
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() {
|
export default function SyncSetting() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const sync = useSync();
|
const { sync, updateSync } = useSync();
|
||||||
|
const alert = useAlert();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { reloadSetting } = useSetting();
|
||||||
|
|
||||||
if (!sync.opt) {
|
const handleChange = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { name, value } = e.target;
|
||||||
|
await updateSync({
|
||||||
|
[name]: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!sync) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { syncUrl, syncKey } = sync.opt;
|
const {
|
||||||
const handleSyncBlur = () => {
|
syncType = OPT_SYNCTYPE_WORKER,
|
||||||
syncAll();
|
syncUrl = "",
|
||||||
};
|
syncUser = "",
|
||||||
|
syncKey = "",
|
||||||
|
} = sync;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Alert severity="warning">{i18n("sync_warn")}</Alert>
|
<Alert severity="warning">{i18n("sync_warn")}</Alert>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
name="syncType"
|
||||||
|
value={syncType}
|
||||||
|
label={i18n("data_sync_type")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{OPT_SYNCTYPE_ALL.map((item) => (
|
||||||
|
<MenuItem key={item} value={item}>
|
||||||
|
{item}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("data_sync_url")}
|
label={i18n("data_sync_url")}
|
||||||
defaultValue={syncUrl}
|
name="syncUrl"
|
||||||
onChange={(e) => {
|
value={syncUrl}
|
||||||
sync.update({
|
onChange={handleChange}
|
||||||
syncUrl: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onBlur={handleSyncBlur}
|
|
||||||
helperText={
|
helperText={
|
||||||
<Link href={URL_KISS_WORKER}>{i18n("about_sync_api")}</Link>
|
syncType === OPT_SYNCTYPE_WORKER && (
|
||||||
|
<Link href={URL_KISS_WORKER} target="_blank">
|
||||||
|
{i18n("about_sync_api")}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{syncType === OPT_SYNCTYPE_WEBDAV && (
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("data_sync_user")}
|
||||||
|
name="syncUser"
|
||||||
|
value={syncUser}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
type="password"
|
type="password"
|
||||||
label={i18n("data_sync_key")}
|
label={i18n("data_sync_key")}
|
||||||
defaultValue={syncKey}
|
name="syncKey"
|
||||||
onChange={(e) => {
|
value={syncKey}
|
||||||
sync.update({
|
onChange={handleChange}
|
||||||
syncKey: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onBlur={handleSyncBlur}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<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>
|
</Stack>
|
||||||
</Box>
|
</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,36 +4,66 @@ import Rules from "./Rules";
|
|||||||
import Setting from "./Setting";
|
import Setting from "./Setting";
|
||||||
import Layout from "./Layout";
|
import Layout from "./Layout";
|
||||||
import SyncSetting from "./SyncSetting";
|
import SyncSetting from "./SyncSetting";
|
||||||
import { StoragesProvider } from "../../hooks/Storage";
|
import { SettingProvider } from "../../hooks/Setting";
|
||||||
import ThemeProvider from "../../hooks/Theme";
|
import ThemeProvider from "../../hooks/Theme";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { isGm } from "../../libs/browser";
|
import { isGm } from "../../libs/client";
|
||||||
import { sleep } from "../../libs/utils";
|
import { sleep } from "../../libs/utils";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
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() {
|
export default function Options() {
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState("");
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isGm) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
let i = 0;
|
if (isGm) {
|
||||||
for (;;) {
|
// 等待GM注入
|
||||||
if (window.APP_NAME === process.env.REACT_APP_NAME) {
|
let i = 0;
|
||||||
setReady(true);
|
for (;;) {
|
||||||
break;
|
if (window?.APP_INFO?.name === process.env.REACT_APP_NAME) {
|
||||||
}
|
const { version, eventName } = window.APP_INFO;
|
||||||
|
|
||||||
if (++i > 8) {
|
// 检查版本是否一致
|
||||||
setError(true);
|
if (version !== process.env.REACT_APP_VERSION) {
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
await sleep(1000);
|
if (eventName) {
|
||||||
|
// 注入GM接口
|
||||||
|
adaptScript(eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步数据
|
||||||
|
await trySyncSettingAndRules();
|
||||||
|
setReady(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++i > 8) {
|
||||||
|
setError("Time out. (连接超时)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(1000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 同步数据
|
||||||
|
await trySyncSettingAndRules();
|
||||||
|
setReady(true);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -41,42 +71,76 @@ export default function Options() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<center>
|
<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>
|
<h2>
|
||||||
Please confirm whether to install or enable{" "}
|
Please confirm whether to install or enable KISS Translator
|
||||||
<a href={process.env.REACT_APP_HOMEPAGE}>KISS Translator</a>{" "}
|
GreaseMonkey script? (请检查是否安装或启用简约翻译油猴脚本)
|
||||||
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.
|
|
||||||
</h2>
|
</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>
|
</center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGm && !ready) {
|
if (!ready) {
|
||||||
return (
|
return (
|
||||||
<center>
|
<center>
|
||||||
|
<Divider>
|
||||||
|
<Link
|
||||||
|
href={process.env.REACT_APP_HOMEPAGE}
|
||||||
|
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||||
|
</Divider>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</center>
|
</center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StoragesProvider>
|
<SettingProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<HashRouter>
|
<AlertProvider>
|
||||||
<Routes>
|
<HashRouter>
|
||||||
<Route path="/" element={<Layout />}>
|
<Routes>
|
||||||
<Route index element={<Setting />} />
|
<Route path="/" element={<Layout />}>
|
||||||
<Route path="rules" element={<Rules />} />
|
<Route index element={<Setting />} />
|
||||||
<Route path="sync" element={<SyncSetting />} />
|
<Route path="rules" element={<Rules />} />
|
||||||
<Route path="about" element={<About />} />
|
<Route path="input" element={<InputSetting />} />
|
||||||
</Route>
|
<Route path="apis" element={<Apis />} />
|
||||||
</Routes>
|
<Route path="sync" element={<SyncSetting />} />
|
||||||
</HashRouter>
|
<Route path="webfix" element={<Webfix />} />
|
||||||
|
<Route path="about" element={<About />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</HashRouter>
|
||||||
|
</AlertProvider>
|
||||||
</ThemeProvider>
|
</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 FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
import Switch from "@mui/material/Switch";
|
import Switch from "@mui/material/Switch";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import { sendTabMsg } from "../../libs/msg";
|
import { sendTabMsg, getTabInfo } from "../../libs/msg";
|
||||||
import { browser, isExt } from "../../libs/browser";
|
import { browser } from "../../libs/browser";
|
||||||
|
import { isExt } from "../../libs/client";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import Header from "./Header";
|
||||||
import {
|
import {
|
||||||
MSG_TRANS_TOGGLE,
|
MSG_TRANS_TOGGLE,
|
||||||
MSG_TRANS_GETRULE,
|
MSG_TRANS_GETRULE,
|
||||||
@@ -17,7 +20,11 @@ import {
|
|||||||
OPT_LANGS_FROM,
|
OPT_LANGS_FROM,
|
||||||
OPT_LANGS_TO,
|
OPT_LANGS_TO,
|
||||||
OPT_STYLE_ALL,
|
OPT_STYLE_ALL,
|
||||||
|
OPT_STYLE_USE_COLOR,
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
|
import { sendIframeMsg } from "../../libs/iframe";
|
||||||
|
import { saveRule } from "../../libs/rules";
|
||||||
|
import { tryClearCaches } from "../../libs";
|
||||||
|
|
||||||
export default function Popup({ setShowPopup, translator: tran }) {
|
export default function Popup({ setShowPopup, translator: tran }) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -40,6 +47,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
await sendTabMsg(MSG_TRANS_TOGGLE);
|
await sendTabMsg(MSG_TRANS_TOGGLE);
|
||||||
} else {
|
} else {
|
||||||
tran.toggle();
|
tran.toggle();
|
||||||
|
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[toggle trans]", err);
|
console.log("[toggle trans]", err);
|
||||||
@@ -55,12 +63,30 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||||
} else {
|
} else {
|
||||||
tran.updateRule({ [name]: value });
|
tran.updateRule({ [name]: value });
|
||||||
|
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[update rule]", err);
|
console.log("[update rule]", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClearCache = () => {
|
||||||
|
tryClearCaches();
|
||||||
|
};
|
||||||
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isExt) {
|
if (!isExt) {
|
||||||
return;
|
return;
|
||||||
@@ -79,8 +105,14 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
|
|
||||||
if (!rule) {
|
if (!rule) {
|
||||||
return (
|
return (
|
||||||
<Box minWidth={300} sx={{ p: 2 }}>
|
<Box minWidth={300}>
|
||||||
<Stack spacing={3}>
|
{isExt && (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Stack sx={{ p: 2 }} spacing={3}>
|
||||||
<Button variant="text" onClick={handleOpenSetting}>
|
<Button variant="text" onClick={handleOpenSetting}>
|
||||||
{i18n("setting")}
|
{i18n("setting")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -92,17 +124,35 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
const { transOpen, translator, fromLang, toLang, textStyle, bgColor } = rule;
|
const { transOpen, translator, fromLang, toLang, textStyle, bgColor } = rule;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box minWidth={300} sx={{ p: 2 }}>
|
<Box minWidth={300}>
|
||||||
<Stack spacing={2}>
|
{isExt && (
|
||||||
<FormControlLabel
|
<>
|
||||||
control={
|
<Header />
|
||||||
<Switch
|
<Divider />
|
||||||
checked={transOpen === "true"}
|
</>
|
||||||
onChange={handleTransToggle}
|
)}
|
||||||
/>
|
<Stack sx={{ p: 2 }} spacing={2}>
|
||||||
}
|
<Stack
|
||||||
label={i18n("translate")}
|
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
|
<TextField
|
||||||
select
|
select
|
||||||
@@ -158,7 +208,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
size="small"
|
size="small"
|
||||||
value={textStyle}
|
value={textStyle}
|
||||||
name="textStyle"
|
name="textStyle"
|
||||||
label={i18n("text_style")}
|
label={i18n("text_style_alt")}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{OPT_STYLE_ALL.map((item) => (
|
{OPT_STYLE_ALL.map((item) => (
|
||||||
@@ -168,17 +218,29 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
<TextField
|
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||||
size="small"
|
<TextField
|
||||||
name="bgColor"
|
size="small"
|
||||||
value={bgColor}
|
name="bgColor"
|
||||||
label={i18n("bg_color")}
|
value={bgColor}
|
||||||
onChange={handleChange}
|
label={i18n("bg_color")}
|
||||||
/>
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button variant="text" onClick={handleOpenSetting}>
|
<Stack
|
||||||
{i18n("setting")}
|
direction="row"
|
||||||
</Button>
|
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>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user