Compare commits

..

125 Commits

Author SHA1 Message Date
Gabe Yuan
c2658d5dd0 v1.6.12 2023-09-11 14:35:52 +08:00
Gabe Yuan
13684884c7 i18n text 2023-09-11 13:45:26 +08:00
Gabe Yuan
f216a9254e update readme 2023-09-11 13:42:44 +08:00
Gabe Yuan
dbdbcbba2d update readme 2023-09-11 13:37:19 +08:00
Gabe Yuan
2ee4609192 fix sync bug 2023-09-11 11:33:28 +08:00
Gabe Yuan
0d93cf78f7 fix i18n text 2023-09-10 21:59:28 +08:00
Gabe Yuan
3398ca0dd7 fix i18n text 2023-09-10 21:56:06 +08:00
Gabe Yuan
c1778fbcbb v1.6.11 2023-09-10 15:23:50 +08:00
Gabe Yuan
1ef9974c05 fix sync webfix 2023-09-10 15:04:41 +08:00
Gabe Yuan
399c6b6fed add global rule text 2023-09-10 14:51:16 +08:00
Gabe Yuan
62a60eee44 update help text 2023-09-10 14:34:55 +08:00
Gabe Yuan
54339af885 update readme 2023-09-10 14:19:31 +08:00
Gabe Yuan
06cfd33e60 add open options shortcut for ext 2023-09-10 13:56:11 +08:00
Gabe Yuan
08c9d78d2a add save rule button 2023-09-10 13:44:34 +08:00
Gabe Yuan
e7a5e5dce1 add save rule button 2023-09-10 13:09:19 +08:00
Gabe Yuan
3a59a127d1 add save rule button 2023-09-10 12:35:03 +08:00
Gabe Yuan
26f213cad2 add fontsize fixer 2023-09-09 23:32:17 +08:00
Gabe Yuan
7b6148302d v1.6.10 2023-09-09 20:15:09 +08:00
Gabe Yuan
38c781b8f3 fix open setting shortcut 2023-09-09 20:13:36 +08:00
Gabe Yuan
64d827fdcd v1.6.9 2023-09-09 19:51:32 +08:00
Gabe Yuan
74ad812f37 text opacity 2023-09-09 19:43:12 +08:00
Gabe Yuan
364c829119 fix sync bug 2023-09-09 19:26:22 +08:00
Gabe Yuan
1ac2c5b61e fix shortcut 2023-09-09 17:15:13 +08:00
Gabe Yuan
0766199353 check shortcut length 2023-09-09 15:26:05 +08:00
Gabe Yuan
878bccf151 hide fab & open setting shortcut 2023-09-09 15:08:34 +08:00
Gabe Yuan
acbd258296 shorten english tab name 2023-09-09 14:10:01 +08:00
Gabe Yuan
54a14e6e5a shortcut set blank 2023-09-09 14:05:45 +08:00
Gabe Yuan
298e4b52f0 v1.6.8 2023-09-08 23:43:34 +08:00
Gabe Yuan
bee1fbcf88 follow global style bug 2023-09-08 23:38:36 +08:00
Gabe Yuan
345a34287e help button 2023-09-08 21:57:42 +08:00
Gabe Yuan
441a2ca2da webfix 2023-09-08 21:41:32 +08:00
Gabe Yuan
1ff1b21355 fix get setting with default 2023-09-08 17:10:54 +08:00
Gabe Yuan
117ca4e05b webfix setting 2023-09-08 16:56:00 +08:00
Gabe Yuan
07d457be4e add clear all rules button 2023-09-08 15:32:42 +08:00
Gabe Yuan
d48296046e show subrule sync time 2023-09-08 15:16:10 +08:00
Gabe Yuan
56350de2cf fix shortcut 2023-09-08 13:53:33 +08:00
Gabe Yuan
850dc0e83b fix throttle func 2023-09-08 10:52:42 +08:00
Gabe Yuan
35f01478b1 add throttle func 2023-09-08 10:32:44 +08:00
Gabe Yuan
f9a3ec012f fix shortcut 2023-09-07 23:51:08 +08:00
Gabe Yuan
3b9b404482 shortcuts dev 2023-09-07 23:47:24 +08:00
Gabe Yuan
d8b0cc4834 shortcuts dev... 2023-09-07 18:12:45 +08:00
Gabe Yuan
da13f5e218 add isAllchar func 2023-09-07 10:20:08 +08:00
Gabe Yuan
08e14ae11c fix i18n text 2023-09-07 00:03:19 +08:00
Gabe Yuan
c2902dff28 fix i18n text 2023-09-06 23:58:11 +08:00
Gabe Yuan
c4fb39f02f fix var text 2023-09-06 23:44:01 +08:00
Gabe Yuan
b7df44c35a hide clear cache in userscript 2023-09-06 23:39:11 +08:00
Gabe Yuan
9a2b21eee5 mouseover dev 2023-09-06 23:35:09 +08:00
Gabe Yuan
bdac67df88 mouseover dev... 2023-09-06 18:00:18 +08:00
Gabe Yuan
0b8f19bfad remove styled-components 2023-09-06 15:12:17 +08:00
Gabe Yuan
c7c5866131 customize api 2023-09-06 14:57:02 +08:00
Gabe Yuan
f772fa000c customize api... 2023-09-06 00:25:46 +08:00
Gabe Yuan
93fd82fcd9 fuzzy style 2023-09-05 17:00:18 +08:00
Gabe Yuan
3ae10bfd04 fix herf match func 2023-09-05 16:11:33 +08:00
Gabe Yuan
a44747ccad fix herf match func 2023-09-05 16:06:48 +08:00
Gabe Yuan
87ab45f936 fix readme text 2023-09-05 13:35:12 +08:00
Gabe Yuan
37b046eb46 fix rule selector 2023-09-05 13:27:04 +08:00
Gabe Yuan
c6f8a45027 v1.6.7 2023-09-04 23:28:41 +08:00
Gabe Yuan
6ec16e1f98 fix popup header 2023-09-04 23:24:50 +08:00
Gabe Yuan
40adf85b20 fix help link 2023-09-04 23:12:35 +08:00
Gabe Yuan
4c78f469c1 update readme 2023-09-04 23:08:00 +08:00
Gabe Yuan
55af58faac add rules help button 2023-09-04 22:29:39 +08:00
Gabe Yuan
4200caa641 fix popup header 2023-09-04 22:03:17 +08:00
Gabe Yuan
0ac06f8e3d use new rules link 2023-09-04 17:04:28 +08:00
Gabe Yuan
966c78fb16 optimize test link 2023-09-03 22:18:33 +08:00
Gabe Yuan
5c5a35d3bb v1.6.6 2023-09-03 21:47:38 +08:00
Gabe Yuan
2c24214f48 fix Violentmonkey --> .connect 2023-09-03 21:45:06 +08:00
Gabe Yuan
67d9e70b3c v1,6,5 2023-09-03 21:10:35 +08:00
Gabe Yuan
000a55f43b modify content.html 2023-09-03 21:08:45 +08:00
Gabe Yuan
4096a6976c add clear cache & api test button 2023-09-03 13:11:04 +08:00
Gabe Yuan
df4c4ebd50 fix i18n text 2023-09-03 00:26:57 +08:00
Gabe Yuan
b43bd4e0e2 add deepl @connect 2023-09-03 00:10:07 +08:00
Gabe Yuan
2660dbf866 update readme 2023-09-02 23:45:40 +08:00
Gabe Yuan
e0b7c60099 v1.6.4 2023-09-02 20:07:24 +08:00
Gabe Yuan
536b58bf67 fix fuzzy style hover bug 2023-09-02 19:55:26 +08:00
Gabe Yuan
6bb742f828 v1.6.3 2023-09-02 19:16:14 +08:00
Gabe Yuan
72742e5e12 fix build:script 2023-09-02 19:09:12 +08:00
Gabe Yuan
3667e0a509 update readme 2023-09-02 18:14:42 +08:00
Gabe Yuan
c2d7668ba7 v1.6.2 2023-09-02 17:26:37 +08:00
Gabe Yuan
aa830f5e20 update readme 2023-09-02 17:22:35 +08:00
Gabe Yuan
b593fa4146 add deepl api 2023-09-02 16:57:09 +08:00
Gabe Yuan
b00b906484 optimize inject script 2023-09-02 15:54:51 +08:00
Gabe Yuan
c1bd6a1be6 use random eventname 2023-09-02 14:14:27 +08:00
Gabe Yuan
36739f04b3 add more shortcut 2023-09-02 13:14:27 +08:00
Gabe Yuan
23eb92853e register a shortcut for userscript 2023-09-02 00:42:15 +08:00
Gabe Yuan
5ab2910dc7 v1.6.1 2023-09-01 22:46:41 +08:00
Gabe Yuan
40d07f6764 fix proxy link 2023-09-01 22:28:28 +08:00
Gabe Yuan
5c8e216169 overwrite subscribe rules 2023-09-01 22:27:25 +08:00
Gabe Yuan
5ba061deda merge yarn 2023-09-01 19:35:48 +08:00
Gabe Yuan
935c83185d fix style text 2023-09-01 17:07:21 +08:00
Gabe Yuan
6327391e65 hide color input when diy style 2023-09-01 17:02:47 +08:00
Gabe Yuan
3d656cf5b0 use debounce to sync setting 2023-09-01 16:39:57 +08:00
Gabe Yuan
d570a0f1a2 replace 'document.documentElement.clientWidth' to 'window.innerWidth' 2023-09-01 16:11:31 +08:00
Gabe Yuan
503a71302c support diy text styles 2023-09-01 15:57:05 +08:00
Gabe Yuan
3e36ceb5b9 num of newline characters 2023-09-01 11:21:06 +08:00
Gabe Yuan
cde7a1d49f add kiss-proxy link 2023-09-01 11:03:53 +08:00
Gabe Yuan
b14a38e4fb sync then reload setting 2023-09-01 10:15:57 +08:00
Gabe Yuan
732a526a8e v1.6.0 2023-08-31 21:24:40 +08:00
Gabe Yuan
2da5ffef44 update userscript download link 2023-08-31 21:21:36 +08:00
Gabe Yuan
2e6e52004f fix firefox bug 2023-08-31 20:57:51 +08:00
Gabe Yuan
4486ad353c dev.....! 2023-08-31 13:38:06 +08:00
Gabe Yuan
aa795e2731 dev...... 2023-08-31 00:18:57 +08:00
Gabe Yuan
c46fe7d1c6 dev... 2023-08-30 18:05:37 +08:00
Gabe Yuan
d7cee8cca6 catch caches is not defined 2023-08-29 21:33:27 +08:00
Gabe Yuan
11f790ace5 catch caches is not defined 2023-08-29 21:24:25 +08:00
Gabe Yuan
13e7c1b754 fix safari webkit 2023-08-29 17:34:37 +08:00
Gabe Yuan
d314d5515f v1.5.8 2023-08-29 16:52:48 +08:00
Gabe Yuan
09b19e3ca0 fix webkit style in safari 2023-08-29 16:48:38 +08:00
Gabe Yuan
687bd11fd1 fix some text 2023-08-29 13:14:12 +08:00
Gabe Yuan
56cb1cd30d fix links 2023-08-29 11:53:02 +08:00
Gabe Yuan
7a3df25521 generate version file to web 2023-08-29 10:41:20 +08:00
Gabe Yuan
ea8919ba07 update readme 2023-08-29 10:30:18 +08:00
Gabe Yuan
3dece4fcdb add version tag to loading page 2023-08-29 01:35:09 +08:00
Gabe Yuan
df950a1bd2 use createElement script 2023-08-29 01:17:22 +08:00
Gabe Yuan
74b9ee31fa eslint-disable-line 2023-08-29 00:52:37 +08:00
Gabe Yuan
64cd55fe58 set no-eval 2023-08-29 00:42:11 +08:00
Gabe Yuan
e80ede14fb v1.5.7 2023-08-29 00:09:09 +08:00
Gabe Yuan
45ba9d3320 use inject-into replace unsafeWindow 2023-08-29 00:06:50 +08:00
Gabe Yuan
47c7048538 injectscript... 2023-08-28 17:59:51 +08:00
Gabe Yuan
f9bfa8101f fix detectLanguage 2023-08-28 11:14:03 +08:00
Gabe Yuan
620ac464eb v1.5.6 2023-08-27 17:59:47 +08:00
Gabe Yuan
62289f8ab8 catch detect lang err 2023-08-27 17:43:27 +08:00
Gabe Yuan
d84594da96 catch global error and display on top of page 2023-08-27 16:45:57 +08:00
Gabe Yuan
e1d74aae6a catch global error and display on top of page 2023-08-27 16:41:14 +08:00
Gabe Yuan
c4980d9eb7 fix rules 2023-08-26 22:12:48 +08:00
Gabe Yuan
882d83c6b7 update helper text 2023-08-26 15:08:21 +08:00
65 changed files with 4186 additions and 1835 deletions

30
.env
View File

@@ -2,12 +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.5.5 REACT_APP_VERSION=1.6.12
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_RULESURL=https://kiss-translator.rayjar.com/kiss-translator-rules.json REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
REACT_APP_USERSCRIPT_DOWNLOADURL=https://kiss-translator.rayjar.com/kiss-translator.user.js REACT_APP_LOGOURL2=https://kiss-translator.rayjar.com/images/logo192.png
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
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

View File

@@ -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,37 +14,58 @@ 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
- [x] Upload to app Store
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
- [x] [Edge](https://microsoftedge.microsoft.com/addons/detail/kiss-translator/jemckldkclkinpjighnoilpbldbdmmlh)
- [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] Data synchronization function
- [x] Custom rules + rule subscription
- [x] Custom style
- [x] Custom shortcut keys
- `Alt+Q` Toggle Translation
- `Alt+C` Toggle Styles
- `Alt+K` Open Popup
- `Alt+O` Open Options
### Guide ## Download
- [x] Browser extension
- [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [Installation address](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] GreaseMonkey Script
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、 [Installation link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- Greasy Fork [Installation address](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [Installation link 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、 [Installation link 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
## Associated ProjectS
- Data synchronization service: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
- Data synchronization service available for this project.
- Can also be used to share personal private rule lists.
- Deploy by yourself, manage by yourself, data is private.
- Community subscription rules: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
- Provides the latest and most complete list of subscription rules maintained by the community.
- Help with rules-related issues.
- Web page correction script: [https://github.com/fishjar/kiss-webfixer](https://github.com/fishjar/kiss-webfixer)
- Fixed scripts for some special sites.
- So that the translation software can get better display effect.
- Translation interface agent: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
- If you encounter network problems when accessing a certain translation interface, this proxy service may help you.
- Deploy and manage by yourself.
- Minimalistic Dictionary Plugin: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
- A word-marking translation plug-in used with this project.
- Supports query of English words, sentences and Chinese characters.
- Supports history records and word collections.
## Development Guidelines
```sh ```sh
git clone https://github.com/fishjar/kiss-translator.git git clone https://github.com/fishjar/kiss-translator.git
@@ -53,10 +74,6 @@ yarn install
yarn build yarn 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)

View File

@@ -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,37 +14,58 @@
如果你也喜欢简约一点的,欢迎自取。 如果你也喜欢简约一点的,欢迎自取。
### 特点 ## 特点
- 保持简约 - [x] 保持简约
### 进度
- [x] 提供试用安装包
- [x] 适配浏览器
- [x] Chrome
- [x] Edge
- [x] Firefox
- [ ] Safari
- [x] Kiwi
- [x] 支持翻译服务
- [x] Google
- [x] Microsoft
- [x] OpenAI
- [ ] DeepL
- [x] 上架应用市场
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] [Edge](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] [Firefox](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] 开放源代码 - [x] 开放源代码
- [x] 适配常见浏览器
- [x] Chrome/Edge/Firefox/Kiwi
- [ ] Safari
- [x] 支持多种翻译服务
- [x] Google/Microsoft/DeepL/OpenAI
- [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] 自定义规则 + 规则订阅
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) - [x] 自定义样式
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (待测) - [x] 自定义快捷键
- `Alt+Q` 开启翻译
- `Alt+C` 切换样式
- `Alt+K` 打开弹窗
- `Alt+O` 打开设置
### 指引 ## 下载
- [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
@@ -53,10 +74,6 @@ yarn install
yarn build yarn 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)

View File

@@ -93,6 +93,8 @@ const userscriptWebpack = (config, env) => {
// @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
@@ -100,6 +102,7 @@ const userscriptWebpack = (config, env) => {
// @connect githubusercontent.com // @connect githubusercontent.com
// @connect kiss-translator.rayjar.com // @connect kiss-translator.rayjar.com
// @connect ghproxy.com // @connect ghproxy.com
// @connect localhost:3000
// @run-at document-end // @run-at document-end
// ==/UserScript== // ==/UserScript==

View File

@@ -1,7 +1,7 @@
{ {
"name": "kiss-translator", "name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script", "description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.5.5", "version": "1.6.12",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"private": true, "private": true,
"dependencies": { "dependencies": {
@@ -24,9 +24,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:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
"build:rules": "babel-node src/rules.js", "build:rules": "babel-node src/rules.js",
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript && yarn build:rules", "build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript-ios && yarn build:userscript && yarn build:rules",
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator", "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"

View File

@@ -10,5 +10,8 @@
}, },
"toggle_style": { "toggle_style": {
"message": "Toggle Style" "message": "Toggle Style"
},
"open_options": {
"message": "Open Options"
} }
} }

View File

@@ -6,9 +6,12 @@
"message": "一个简约的双语网页翻译扩展 & 油猴脚本" "message": "一个简约的双语网页翻译扩展 & 油猴脚本"
}, },
"toggle_translate": { "toggle_translate": {
"message": "切换翻译" "message": "开启翻译"
}, },
"toggle_style": { "toggle_style": {
"message": "切换样式" "message": "切换样式"
},
"open_options": {
"message": "打开设置"
} }
} }

View File

@@ -64,8 +64,25 @@
<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">
<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> <h2>
<p><span>React is a JavaScript library for building user interfaces.</span></p> <p>
<span
>React is a JavaScript library for building user interfaces.</span
>
</p>
</h2> </h2>
<div id="addtitle"></div> <div id="addtitle"></div>
<h2>Shadow 1</h2> <h2>Shadow 1</h2>

View File

@@ -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.5.5", "version": "1.6.12",
"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",
@@ -17,6 +17,11 @@
} }
], ],
"commands": { "commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Alt+K"
}
},
"toggleTranslate": { "toggleTranslate": {
"suggested_key": { "suggested_key": {
"default": "Alt+Q" "default": "Alt+Q"
@@ -28,6 +33,12 @@
"default": "Alt+C" "default": "Alt+C"
}, },
"description": "__MSG_toggle_style__" "description": "__MSG_toggle_style__"
},
"openOptions": {
"suggested_key": {
"default": "Alt+O"
},
"description": "__MSG_open_options__"
} }
}, },
"permissions": ["<all_urls>", "storage"], "permissions": ["<all_urls>", "storage"],

View File

@@ -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.5.5", "version": "1.6.12",
"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",
@@ -18,6 +18,11 @@
} }
], ],
"commands": { "commands": {
"_execute_action": {
"suggested_key": {
"default": "Alt+K"
}
},
"toggleTranslate": { "toggleTranslate": {
"suggested_key": { "suggested_key": {
"default": "Alt+Q" "default": "Alt+Q"
@@ -29,6 +34,12 @@
"default": "Alt+C" "default": "Alt+C"
}, },
"description": "__MSG_toggle_style__" "description": "__MSG_toggle_style__"
},
"openOptions": {
"suggested_key": {
"default": "Alt+O"
},
"description": "__MSG_open_options__"
} }
}, },
"permissions": ["storage"], "permissions": ["storage"],

View File

@@ -3,14 +3,15 @@ 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_SALT_SYNC, KV_SALT_SYNC,
} from "../config"; } from "../config";
import { getSetting, detectLang } from "../libs"; import { tryDetectLang } from "../libs";
import { sha256 } from "../libs/utils"; import { sha256 } from "../libs/utils";
/** /**
@@ -31,6 +32,14 @@ export const apiSyncData = async (url, key, data, isBg = false) =>
isBg, isBg,
}); });
/**
* 下载数据
* @param {*} url
* @param {*} isBg
* @returns
*/
export const apiFetch = (url, isBg = false) => fetchPolyfill(url, { isBg });
/** /**
* 谷歌翻译 * 谷歌翻译
* @param {*} text * @param {*} text
@@ -38,7 +47,13 @@ export const apiSyncData = async (url, key, data, isBg = false) =>
* @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",
@@ -48,16 +63,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(input, {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
}, },
useCache: true, useCache,
usePool: true, usePool: true,
translator, translator,
token: key,
}); });
const trText = res.sentences.map((item) => item.trans).join(" ");
const isSame = to === res.src;
return [trText, isSame];
}; };
/** /**
@@ -67,23 +86,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(input, { const res = await fetchPolyfill(input, {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
}, },
method: "POST", method: "POST",
body: JSON.stringify([{ Text: text }]), body: JSON.stringify([{ Text: text }]),
useCache: true, useCache,
usePool: true, usePool: true,
translator, 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];
}; };
/** /**
@@ -93,19 +161,23 @@ 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(openaiUrl, { const res = await fetchPolyfill(url, {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
}, },
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
model: openaiModel, model: model,
messages: [ messages: [
{ {
role: "system", role: "system",
@@ -119,11 +191,52 @@ const apiOpenaiTranslate = async (translator, text, to, from) => {
temperature: 0, temperature: 0,
max_tokens: 256, max_tokens: 256,
}), }),
useCache: true, useCache,
usePool: true, usePool: true,
translator, translator,
token: openaiKey, 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];
}; };
/** /**
@@ -131,26 +244,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];
}; };

View File

@@ -7,35 +7,20 @@ import {
MSG_TRANS_TOGGLE_STYLE, MSG_TRANS_TOGGLE_STYLE,
CMD_TOGGLE_TRANSLATE, CMD_TOGGLE_TRANSLATE,
CMD_TOGGLE_STYLE, CMD_TOGGLE_STYLE,
DEFAULT_SETTING, CMD_OPEN_OPTIONS,
DEFAULT_RULES,
DEFAULT_SYNC,
STOKEY_SETTING,
STOKEY_RULES,
STOKEY_SYNC,
CACHE_NAME,
STOKEY_RULESCACHE_PREFIX,
BUILTIN_RULES,
} 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 { trySyncAll } from "./libs/sync";
import { fetchData, fetchPool } from "./libs/fetch"; import { fetchData, fetchPool } from "./libs/fetch";
import { sendTabMsg } from "./libs/msg"; import { sendTabMsg } from "./libs/msg";
import { trySyncAllSubRules } from "./libs/rules"; import { trySyncAllSubRules } from "./libs/subRules";
import { tryClearCaches } from "./libs";
/** /**
* 插件安装 * 插件安装
*/ */
browser.runtime.onInstalled.addListener(() => { browser.runtime.onInstalled.addListener(() => {
console.log("KISS Translator onInstalled"); tryInitDefaultData();
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES);
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
storage.trySetObj(
`${STOKEY_RULESCACHE_PREFIX}${process.env.REACT_APP_RULESURL}`,
BUILTIN_RULES
);
}); });
/** /**
@@ -45,12 +30,12 @@ browser.runtime.onStartup.addListener(async () => {
console.log("browser onStartup"); console.log("browser onStartup");
// 同步数据 // 同步数据
await trySyncAll(true); await trySyncSettingAndRules(true);
// 清除缓存 // 清除缓存
const setting = await getSetting(); const setting = await getSettingWithDefault();
if (setting.clearCache) { if (setting.clearCache) {
caches.delete(CACHE_NAME); tryClearCaches();
} }
// 同步订阅规则 // 同步订阅规则
@@ -101,6 +86,9 @@ browser.commands.onCommand.addListener((command) => {
case CMD_TOGGLE_STYLE: case CMD_TOGGLE_STYLE:
sendTabMsg(MSG_TRANS_TOGGLE_STYLE); sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
break; break;
case CMD_OPEN_OPTIONS:
browser.runtime.openOptionsPage();
break;
default: default:
} }
}); });

4
src/config/app.js Normal file
View 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();

View File

@@ -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,10 @@ export const I18N = {
zh: `翻译`, zh: `翻译`,
en: `Translate`, en: `Translate`,
}, },
custom_api_help: {
zh: customApiHelpZH,
en: customApiHelpEN,
},
translate_alt: { translate_alt: {
zh: `翻译 (Alt+Q)`, zh: `翻译 (Alt+Q)`,
en: `Translate (Alt+Q)`, en: `Translate (Alt+Q)`,
@@ -24,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`,
@@ -60,10 +175,38 @@ export const I18N = {
zh: `最大翻译长度 (100-10000)`, zh: `最大翻译长度 (100-10000)`,
en: `Max Translate Length (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`,
@@ -84,6 +227,10 @@ export const I18N = {
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`,
@@ -126,11 +273,15 @@ export const I18N = {
}, },
personal_rules: { personal_rules: {
zh: `个人规则`, zh: `个人规则`,
en: `Personal Rules`, en: `Rules`,
}, },
subscribe_rules: { subscribe_rules: {
zh: `订阅规则`, zh: `订阅规则`,
en: `Subscribe Rules`, en: `Subscribe`,
},
overwrite_subscribe_rules: {
zh: `覆写订阅规则`,
en: `Overwrite`,
}, },
subscribe_url: { subscribe_url: {
zh: `订阅地址`, zh: `订阅地址`,
@@ -152,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`,
@@ -180,9 +335,17 @@ 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: `匹配网址`,
@@ -193,8 +356,8 @@ export const I18N = {
en: `1. The asterisk (*) wildcard is supported. 2. 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. 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: `开启翻译`,
@@ -236,6 +399,14 @@ export const I18N = {
zh: `请检查url地址是否正确或稍后再试。`, zh: `请检查url地址是否正确或稍后再试。`,
en: `Please check if the url address is correct or try again later.`, 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`,
@@ -252,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`,
}, },
@@ -272,24 +443,108 @@ export const I18N = {
zh: `数据同步密钥`, zh: `数据同步密钥`,
en: `Data Sync Key`, en: `Data Sync Key`,
}, },
data_sync_test: { sync_now: {
zh: `数据同步测试`, zh: `立即同步`,
en: `Data Sync Test`, en: `Sync Now`,
}, },
data_sync_success: { sync_success: {
zh: `数据同步成功!`, zh: `同步成功!`,
en: `Data Sync Success`, en: `Sync Success`,
}, },
data_sync_error: { sync_failed: {
zh: `数据同步失败!`, zh: `同步失败!`,
en: `Data Sync Error`, en: `Sync Error`,
}, },
error_got_some_wrong: { error_got_some_wrong: {
zh: "抱歉,出错了!", zh: `抱歉,出错了!`,
en: "Sorry, something went wrong!", en: `Sorry, something went wrong!`,
}, },
error_sync_setting: { error_sync_setting: {
zh: "您的同步设置未填写,无法在线分享。", zh: `您的同步设置未填写,无法在线分享。`,
en: "Your sync settings are missing and cannot be shared online.", en: `Your sync settings are missing and cannot be shared online.`,
},
click_test: {
zh: `点击测试`,
en: `Click Test`,
},
test_success: {
zh: `测试成功`,
en: `Test success`,
},
test_failed: {
zh: `测试失败`,
en: `Test failed`,
},
clear_all_cache_now: {
zh: `立即清除全部缓存`,
en: `Clear all cache now`,
},
clear_cache: {
zh: `清除缓存`,
en: `Clear Cache`,
},
clear_success: {
zh: `清除成功`,
en: `Clear success`,
},
clear_failed: {
zh: `清除失败`,
en: `Clear failed`,
},
share: {
zh: `分享`,
en: `Share`,
},
clear_all: {
zh: `清空`,
en: `Clear All`,
},
help: {
zh: `求助`,
en: `Help`,
},
restore_default: {
zh: `恢复默认`,
en: `Restore Default`,
},
shortcuts_setting: {
zh: `快捷键设置`,
en: `Shortcuts Setting`,
},
toggle_translate_shortcut: {
zh: `"开启翻译"快捷键`,
en: `"Toggle Translate" Shortcut`,
},
toggle_style_shortcut: {
zh: `"切换样式"快捷键`,
en: `"Toggle Style" Shortcut`,
},
toggle_popup_shortcut: {
zh: `"打开弹窗"快捷键`,
en: `"Open Popup" Shortcut`,
},
open_setting_shortcut: {
zh: `"打开设置"快捷键`,
en: `"Open Setting" Shortcut`,
},
hide_fab_button: {
zh: `隐藏悬浮按钮`,
en: `Hide Fab Button`,
},
show: {
zh: `显示`,
en: `Show`,
},
hide: {
zh: `隐藏`,
en: `Hide`,
},
save_rule: {
zh: `保存规则`,
en: `Save Rule`,
},
global_rule: {
zh: `全局规则`,
en: `Global Rule`,
}, },
}; };

View File

@@ -1,16 +1,23 @@
import { import {
DEFAULT_SELECTOR, DEFAULT_SELECTOR,
GLOBAL_KEY, GLOBAL_KEY,
REMAIN_KEY,
SHADOW_KEY, SHADOW_KEY,
DEFAULT_RULE, DEFAULT_RULE,
DEFAULT_OW_RULE,
BUILTIN_RULES, BUILTIN_RULES,
} from "./rules"; } from "./rules";
import { APP_NAME, APP_LCNAME } from "./app";
export { I18N, UI_LANGS } from "./i18n"; export { I18N, UI_LANGS } from "./i18n";
export { GLOBAL_KEY, SHADOW_KEY, DEFAULT_RULE, BUILTIN_RULES }; export {
GLOBAL_KEY,
const APP_NAME = process.env.REACT_APP_NAME.trim().split(/\s+/).join("-"); REMAIN_KEY,
SHADOW_KEY,
export const APP_LCNAME = APP_NAME.toLowerCase(); 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`;
@@ -18,9 +25,11 @@ 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_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
export const STOKEY_WEBFIXCACHE_PREFIX = `${APP_NAME}_webfixcache_`;
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate"; export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
export const CMD_TOGGLE_STYLE = "toggleStyle"; 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";
@@ -46,25 +55,29 @@ 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 = [
@@ -113,9 +126,16 @@ 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_STYLE_NONE = "style_none"; // 无 export const OPT_STYLE_NONE = "style_none"; // 无
@@ -124,7 +144,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,
@@ -132,7 +153,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; // 默认最大任务数量
@@ -153,21 +195,66 @@ export const GLOBLA_RULE = {
textStyle: OPT_STYLE_DASHLINE, textStyle: OPT_STYLE_DASHLINE,
transOpen: "false", transOpen: "false",
bgColor: "", bgColor: "",
textDiyStyle: "",
}; };
// 订阅列表 // 订阅列表
export const DEFAULT_SUBRULES_LIST = [ export const DEFAULT_SUBRULES_LIST = [
{ {
url: process.env.REACT_APP_RULESURL, url: process.env.REACT_APP_RULESURL,
selected: false,
},
{
url: process.env.REACT_APP_RULESURL_ON,
selected: true, selected: true,
}, },
{ {
url: "https://fishjar.github.io/kiss-translator/kiss-translator-rules.json", url: process.env.REACT_APP_RULESURL_OFF,
selected: false,
}, },
]; ];
// 翻译接口
export const DEFAULT_TRANS_APIS = {
[OPT_TRANS_GOOGLE]: {
url: "https://translate.googleapis.com/translate_a/single",
key: "",
},
[OPT_TRANS_MICROSOFT]: {
url: "https://api-edge.cognitive.microsofttranslator.com/translate",
authUrl: "https://edge.microsoft.com/translate/auth",
},
[OPT_TRANS_DEEPL]: {
url: "https://api-free.deepl.com/v2/translate",
key: "",
},
[OPT_TRANS_OPENAI]: {
url: "https://api.openai.com/v1/chat/completion",
key: "",
model: "gpt-4",
prompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
},
[OPT_TRANS_CUSTOMIZE]: {
url: "",
key: "",
},
};
// 默认快捷键
export const OPT_SHORTCUT_TRANSLATE = "toggleTranslate";
export const OPT_SHORTCUT_STYLE = "toggleStyle";
export const OPT_SHORTCUT_POPUP = "togglePopup";
export const OPT_SHORTCUT_SETTING = "openSetting";
export const DEFAULT_SHORTCUTS = {
[OPT_SHORTCUT_TRANSLATE]: ["Alt", "q"],
[OPT_SHORTCUT_STYLE]: ["Alt", "c"],
[OPT_SHORTCUT_POPUP]: ["Alt", "k"],
[OPT_SHORTCUT_SETTING]: ["Alt", "o"],
};
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度 export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度 export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
export const TRANS_NEWLINE_LENGTH = 40; // 换行字符数
export const DEFAULT_SETTING = { export const DEFAULT_SETTING = {
darkMode: false, // 深色模式 darkMode: false, // 深色模式
@@ -176,14 +263,16 @@ export const DEFAULT_SETTING = {
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间 fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
minLength: TRANS_MIN_LENGTH, minLength: TRANS_MIN_LENGTH,
maxLength: TRANS_MAX_LENGTH, maxLength: TRANS_MAX_LENGTH,
newlineLength: TRANS_NEWLINE_LENGTH,
clearCache: false, // 是否在浏览器下次启动时清除缓存 clearCache: false, // 是否在浏览器下次启动时清除缓存
injectRules: true, // 是否注入订阅规则 injectRules: true, // 是否注入订阅规则
injectWebfix: true, // 是否注入修复补丁
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表 subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口 owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
openaiUrl: "https://api.openai.com/v1/chat/completions", transApis: DEFAULT_TRANS_APIS, // 翻译接口
openaiKey: "", mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译
openaiModel: "gpt-4", shortcuts: DEFAULT_SHORTCUTS, // 快捷键
openaiPrompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`, hideFab: false, // 是否隐藏按钮
}; };
export const DEFAULT_RULES = [GLOBLA_RULE]; export const DEFAULT_RULES = [GLOBLA_RULE];

View File

@@ -1,8 +1,9 @@
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 GLOBAL_KEY = "*"; export const GLOBAL_KEY = "*";
export const REMAIN_KEY = "-";
export const SHADOW_KEY = ">>>"; export const SHADOW_KEY = ">>>";
@@ -15,6 +16,30 @@ export const DEFAULT_RULE = {
textStyle: GLOBAL_KEY, textStyle: GLOBAL_KEY,
transOpen: GLOBAL_KEY, transOpen: GLOBAL_KEY,
bgColor: "", 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 = [ const RULES = [
@@ -28,7 +53,7 @@ const RULES = [
}, },
{ {
pattern: `www.foxnews.com`, pattern: `www.foxnews.com`,
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content :is(li, p, h1, h2, h3, h4, h5, h6, dd); [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`, 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`,
@@ -152,7 +177,9 @@ const RULES = [
}, },
]; ];
export const BUILTIN_RULES = RULES.map((item) => ({ export const BUILTIN_RULES = RULES.sort((a, b) =>
a.pattern.localeCompare(b.pattern)
).map((item) => ({
...DEFAULT_RULE, ...DEFAULT_RULE,
...item, ...item,
transOpen: "true", transOpen: "true",

View File

@@ -5,19 +5,22 @@ import {
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 } from "./libs/iframe"; import { isIframe } from "./libs/iframe";
import { matchRule } from "./libs/rules";
import { webfix } from "./libs/webfix";
/** /**
* 入口函数 * 入口函数
*/ */
(async () => { const init = async () => {
const href = isIframe ? document.referrer : document.location.href; const href = isIframe ? document.referrer : document.location.href;
const setting = await getSetting(); const setting = await getSettingWithDefault();
const rules = await getRules(); const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting); 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 }) => {
@@ -38,4 +41,16 @@ import { isIframe } from "./libs/iframe";
} }
return { data: translator.rule }; return { data: translator.rule };
}); });
};
(async () => {
try {
await init();
} catch (err) {
console.error("[KISS-Translator]", err);
const $err = document.createElement("div");
$err.innerText = `KISS-Translator: ${err.message}`;
$err.style.cssText = "background:red; color:#fff;";
document.body.prepend($err);
}
})(); })();

View File

@@ -20,11 +20,6 @@ export function AlertProvider({ children }) {
const [severity, setSeverity] = useState("info"); const [severity, setSeverity] = useState("info");
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const error = (msg) => showAlert(msg, "error");
const warning = (msg) => showAlert(msg, "warning");
const info = (msg) => showAlert(msg, "info");
const success = (msg) => showAlert(msg, "success");
const showAlert = (msg, type) => { const showAlert = (msg, type) => {
setOpen(true); setOpen(true);
setMessage(msg); setMessage(msg);
@@ -38,6 +33,11 @@ export function AlertProvider({ children }) {
setOpen(false); 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 ( return (
<AlertContext.Provider value={{ error, warning, info, success }}> <AlertContext.Provider value={{ error, warning, info, success }}>
{children} {children}

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

View File

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

View File

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

View File

@@ -1,107 +1,88 @@
import { STOKEY_RULES, DEFAULT_SUBRULES_LIST } from "../config"; import { STOKEY_RULES, DEFAULT_RULES } from "../config";
import storage from "../libs/storage"; import { useStorage } from "./Storage";
import { useStorages } from "./Storage";
import { trySyncRules } from "../libs/sync"; import { trySyncRules } from "../libs/sync";
import { useSync } from "./Sync";
import { useSetting, useSettingUpdate } from "./Setting";
import { checkRules } from "../libs/rules"; import { checkRules } from "../libs/rules";
import { useCallback } from "react";
/** /**
* 匹配规则增删改查 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 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 }); trySyncRules(false, true);
trySyncRules(); },
}; [save]
);
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);
await update(rules);
};
const merge = async (newRules) => {
const rules = [...list];
newRules = checkRules(newRules);
newRules.forEach((newRule) => {
const rule = rules.find((oldRule) => oldRule.pattern === newRule.pattern);
if (rule) {
Object.assign(rule, newRule);
} else {
rules.unshift(newRule);
} }
}); const rule = rules.find((r) => r.pattern === pattern);
await update(rules); rule && Object.assign(rule, obj);
}; await updateRules(rules);
},
[list, updateRules]
);
return { list, add, del, put, merge }; const merge = useCallback(
} async (newRules) => {
const rules = [...list];
/** newRules = checkRules(newRules);
* 订阅规则 newRules.forEach((newRule) => {
* @returns const rule = rules.find(
*/ (oldRule) => oldRule.pattern === newRule.pattern
export function useSubrules() { );
const setting = useSetting(); if (rule) {
const updateSetting = useSettingUpdate(); Object.assign(rule, newRule);
const list = setting?.subrulesList || DEFAULT_SUBRULES_LIST; } else {
rules.unshift(newRule);
const select = async (url) => { }
const subrulesList = [...list]; });
subrulesList.forEach((item) => { await updateRules(rules);
if (item.url === url) { },
item.selected = true; [list, updateRules]
} else { );
item.selected = false;
} return { list, add, del, clear, put, merge };
});
await updateSetting({ subrulesList });
};
const add = async (url) => {
const subrulesList = [...list];
subrulesList.push({ url });
await updateSetting({ subrulesList });
};
const del = async (url) => {
let subrulesList = [...list];
subrulesList = subrulesList.filter((item) => item.url !== url);
await updateSetting({ subrulesList });
};
return { list, select, add, del };
} }

View File

@@ -1,28 +1,58 @@
import { STOKEY_SETTING } from "../config"; import { STOKEY_SETTING, DEFAULT_SETTING } from "../config";
import storage from "../libs/storage"; import { useStorage } from "./Storage";
import { useStorages } from "./Storage";
import { useSync } from "./Sync";
import { trySyncSetting } from "../libs/sync"; import { trySyncSetting } from "../libs/sync";
import { createContext, useCallback, useContext, useMemo } from "react";
import { debounce } from "../libs/utils";
const SettingContext = createContext({
setting: {},
updateSetting: async () => {},
reloadSetting: async () => {},
});
export function SettingProvider({ children }) {
const { data, update, reload, loading } = useStorage(
STOKEY_SETTING,
DEFAULT_SETTING
);
const syncSetting = useMemo(
() =>
debounce(() => {
trySyncSetting(false, true);
}, [2000]),
[]
);
const updateSetting = useCallback(
async (obj) => {
await update(obj);
syncSetting();
},
[update, syncSetting]
);
if (loading) {
return;
}
return (
<SettingContext.Provider
value={{
setting: data,
updateSetting,
reloadSetting: reload,
}}
>
{children}
</SettingContext.Provider>
);
}
/** /**
* 设置hook * 设置 hook
* @returns * @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 });
trySyncSetting();
};
} }

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

View File

@@ -1,91 +1,52 @@
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_SYNC,
DEFAULT_SETTING,
DEFAULT_RULES,
DEFAULT_SYNC,
} from "../config";
import storage from "../libs/storage";
/** export function useStorage(key, defaultVal = null) {
* 默认配置 const [loading, setLoading] = useState(true);
*/ const [data, setData] = useState(defaultVal);
export const defaultStorage = {
[STOKEY_SETTING]: DEFAULT_SETTING,
[STOKEY_RULES]: DEFAULT_RULES,
[STOKEY_SYNC]: DEFAULT_SYNC,
};
const activeKeys = Object.keys(defaultStorage); const save = useCallback(
async (val) => {
setData(val);
await storage.setObj(key, val);
},
[key]
);
const StoragesContext = createContext(null); const update = useCallback(
async (obj) => {
setData((pre) => ({ ...pre, ...obj }));
await storage.putObj(key, obj);
},
[key]
);
export function StoragesProvider({ children }) { const remove = useCallback(async () => {
const [storages, setStorages] = useState(null); setData(null);
await storage.del(key);
}, [key]);
const handleChanged = (changes) => { const reload = useCallback(async () => {
if (isWeb || isGm) { const val = await storage.getObj(key);
const { key, oldValue, newValue } = changes; if (val) {
changes = { setData(val);
[key]: { } else if (defaultVal) {
oldValue, await storage.setObj(key, defaultVal);
newValue,
},
};
} }
const newStorages = {}; }, [key, defaultVal]);
Object.entries(changes)
.filter(
([key, { oldValue, newValue }]) =>
activeKeys.includes(key) && oldValue !== newValue
)
.forEach(([key, { newValue }]) => {
newStorages[key] = JSON.parse(newValue);
});
if (Object.keys(newStorages).length !== 0) {
setStorages((pre) => ({ ...pre, ...newStorages }));
}
};
useEffect(() => { useEffect(() => {
// 首次从storage同步配置到内存
(async () => { (async () => {
const curStorages = {}; try {
for (const key of activeKeys) { setLoading(true);
const val = await storage.get(key); await reload();
if (val) { } catch (err) {
curStorages[key] = JSON.parse(val); //
} else { } finally {
await storage.setObj(key, defaultStorage[key]); setLoading(false);
curStorages[key] = defaultStorage[key];
}
} }
setStorages(curStorages);
})(); })();
}, [reload]);
// 监听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
View 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, syncAt: Date.now() });
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 };
}

View File

@@ -1,20 +1,11 @@
import { useCallback } from "react"; import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
import { STOKEY_SYNC } from "../config"; import { useStorage } from "./Storage";
import storage from "../libs/storage";
import { useStorages } from "./Storage";
/** /**
* sync hook * sync hook
* @returns * @returns
*/ */
export function useSync() { export function useSync() {
const storages = useStorages(); const { data, update } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
const opt = storages?.[STOKEY_SYNC]; return { sync: data, updateSync: update };
const update = useCallback(async (obj) => {
await storage.putObj(STOKEY_SYNC, obj);
}, []);
return {
opt,
update,
};
} }

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import ReactMarkdown from "react-markdown";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import Button from "@mui/material/Button"; 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";
@@ -26,7 +27,32 @@ function App() {
{lang === "zh" ? "ENGLISH" : "中文"} {lang === "zh" ? "ENGLISH" : "中文"}
</Button> </Button>
</Stack> </Stack>
<Divider>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Divider> <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 />

View File

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

View File

@@ -1,4 +1,4 @@
import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config"; // import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
/** /**
* 浏览器兼容插件,另可用于判断是插件模式还是网页模式,方便开发 * 浏览器兼容插件,另可用于判断是插件模式还是网页模式,方便开发
@@ -13,7 +13,3 @@ function _browser() {
} }
export const browser = _browser(); export const browser = _browser();
export const client = process.env.REACT_APP_CLIENT;
export const isExt = CLIENT_EXTS.includes(client);
export const isGm = client === CLIENT_USERSCRIPT;
export const isWeb = client === CLIENT_WEB;

6
src/libs/client.js Normal file
View 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;

View File

@@ -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,6 +7,7 @@ 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,
@@ -19,7 +20,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,
@@ -66,19 +67,35 @@ const newCacheReq = async (request) => {
* @returns * @returns
*/ */
const fetchApi = async ({ input, init = {}, translator, token }) => { 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) { if (isGm) {
const connects = GM?.info?.script?.connects || []; let info;
if (window.KISS_GM) {
info = await window.KISS_GM.getInfo();
} else {
info = GM.info;
}
// Tampermonkey --> .connects
// Violentmonkey --> .connect
const connects = info?.script?.connects || info?.script?.connect || [];
const url = new URL(input); const url = new URL(input);
const isSafe = connects.find((item) => url.hostname.endsWith(item)); const isSafe = connects.find((item) => url.hostname.endsWith(item));
if (isSafe) { if (isSafe) {
return fetchGM(input, init); if (window.KISS_GM) {
return window.KISS_GM.fetch(input, init);
} else {
return fetchGM(input, init);
}
} }
} }
return fetch(input, init); return fetch(input, init);
@@ -111,15 +128,15 @@ export const fetchData = async (
{ useCache, usePool, translator, token, ...init } = {} { useCache, usePool, translator, token, ...init } = {}
) => { ) => {
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);
} }
} }
@@ -138,9 +155,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);
} }
} }
} }
@@ -159,9 +177,13 @@ export const fetchData = async (
* @returns * @returns
*/ */
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => { export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
if (!input.trim()) {
throw new Error("URL is empty");
}
// 插件 // 插件
if (isExt && !isBg) { if (isExt && !isBg) {
const res = await sendMsg(MSG_FETCH, { input, 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);
} }
@@ -177,9 +199,9 @@ export const fetchPolyfill = async (input, { isBg = false, ...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);
} }
@@ -191,9 +213,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
View 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 } })
);
}
};

View File

@@ -1,96 +1,15 @@
import storage from "./storage"; import { CACHE_NAME } from "../config";
import {
DEFAULT_SETTING,
STOKEY_SETTING,
STOKEY_RULES,
STOKEY_FAB,
GLOBLA_RULE,
GLOBAL_KEY,
DEFAULT_SUBRULES_LIST,
} from "../config";
import { browser } from "./browser"; import { browser } from "./browser";
import { isMatch } from "./utils";
import { loadSubRules } from "./rules";
/** /**
* 查询storage中的设置 * 清除缓存数据
* @returns
*/ */
export const getSetting = async () => ({ export const tryClearCaches = async () => {
...DEFAULT_SETTING, try {
...((await storage.getObj(STOKEY_SETTING)) || {}), caches.delete(CACHE_NAME);
}); } catch (err) {
console.log("[clean caches]", err.message);
/**
* 查询规则列表
* @returns
*/
export const getRules = async () => (await storage.getObj(STOKEY_RULES)) || [];
/**
* 查询fab位置信息
* @returns
*/
export const getFab = async () => (await storage.getObj(STOKEY_FAB)) || {};
/**
* 设置fab位置信息
* @returns
*/
export const setFab = async (obj) => await storage.setObj(STOKEY_FAB, obj);
/**
* 根据href匹配规则
* @param {*} rules
* @param {string} href
* @returns
*/
export const matchRule = async (
rules,
href,
{ injectRules = true, subrulesList = DEFAULT_SUBRULES_LIST }
) => {
rules = [...rules];
if (injectRules) {
try {
const selectedSub = subrulesList.find((item) => item.selected);
if (selectedSub?.url) {
const subRules = await loadSubRules(selectedSub.url);
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.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;
}; };
/** /**
@@ -98,7 +17,11 @@ export const matchRule = async (
* @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);
}
}; };

View File

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

View File

@@ -1,18 +1,89 @@
import storage from "./storage"; import { matchValue, type, isMatch } from "./utils";
import { fetchPolyfill } from "./fetch";
import { matchValue, type } from "./utils";
import { import {
STOKEY_RULESCACHE_PREFIX,
GLOBAL_KEY, GLOBAL_KEY,
REMAIN_KEY,
OPT_TRANS_ALL, OPT_TRANS_ALL,
OPT_STYLE_ALL, OPT_STYLE_ALL,
OPT_LANGS_FROM, OPT_LANGS_FROM,
OPT_LANGS_TO, OPT_LANGS_TO,
GLOBLA_RULE,
DEFAULT_SUBRULES_LIST,
DEFAULT_OW_RULE,
} from "../config"; } from "../config";
import { syncOpt } from "./sync"; import { loadOrFetchSubRules } from "./subRules";
import { getRulesWithDefault, setRules } from "./storage";
import { trySyncRules } from "./sync";
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]); /**
const toLangs = OPT_LANGS_TO.map((item) => item[0]); * 根据href匹配规则
* @param {*} rules
* @param {string} href
* @returns
*/
export const matchRule = async (
rules,
href,
{
injectRules = true,
subrulesList = DEFAULT_SUBRULES_LIST,
owSubrule = DEFAULT_OW_RULE,
}
) => {
rules = [...rules];
if (injectRules) {
try {
const selectedSub = subrulesList.find((item) => item.selected);
if (selectedSub?.url) {
const mixRule = {};
Object.entries(owSubrule)
.filter(([key, val]) => {
if (
owSubrule.textStyle === REMAIN_KEY &&
(key === "bgColor" || key === "textDiyStyle")
) {
return false;
}
return val !== REMAIN_KEY;
})
.forEach(([key, val]) => {
mixRule[key] = val;
});
const subRules = (await loadOrFetchSubRules(selectedSub.url)).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 * 检查过滤rules
@@ -27,6 +98,8 @@ export const checkRules = (rules) => {
throw new Error("data error"); 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(); const patternSet = new Set();
rules = rules rules = rules
.filter((rule) => type(rule) === "object") .filter((rule) => type(rule) === "object")
@@ -47,10 +120,12 @@ export const checkRules = (rules) => {
textStyle, textStyle,
transOpen, transOpen,
bgColor, bgColor,
textDiyStyle,
}) => ({ }) => ({
pattern: pattern.trim(), pattern: pattern.trim(),
selector: type(selector) === "string" ? selector : "", selector: type(selector) === "string" ? selector : "",
bgColor: type(bgColor) === "string" ? bgColor : "", bgColor: type(bgColor) === "string" ? bgColor : "",
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator), translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang), fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang), toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
@@ -63,83 +138,17 @@ export const checkRules = (rules) => {
}; };
/** /**
* 订阅规则的本地缓存 * 保存或更新rule
* @param {*} newRule
*/ */
export const rulesCache = { export const saveRule = async (newRule) => {
fetch: async (url, isBg = false) => { const rules = await getRulesWithDefault();
const res = await fetchPolyfill(url, { isBg }); const rule = rules.find((item) => isMatch(newRule.pattern, item.pattern));
const rules = checkRules(res).filter( if (rule && rule.pattern !== GLOBAL_KEY) {
(rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== "" Object.assign(rule, { ...newRule, pattern: rule.pattern });
); } else {
return rules; rules.unshift(newRule);
},
set: async (url, rules) => {
await storage.setObj(`${STOKEY_RULESCACHE_PREFIX}${url}`, rules);
},
get: async (url) => {
return await storage.getObj(`${STOKEY_RULESCACHE_PREFIX}${url}`);
},
del: async (url) => {
await storage.del(`${STOKEY_RULESCACHE_PREFIX}${url}`);
},
};
/**
* 同步订阅规则
* @param {*} url
* @returns
*/
export const syncSubRules = async (url, isBg = false) => {
const rules = await rulesCache.fetch(url, isBg);
if (rules.length > 0) {
await rulesCache.set(url, rules);
} }
return rules; await setRules(rules);
}; trySyncRules(false, true);
/**
* 同步所有订阅规则
* @param {*} url
* @returns
*/
export const syncAllSubRules = async (subrulesList, isBg = false) => {
for (let subrules of subrulesList) {
try {
await syncSubRules(subrules.url, isBg);
} catch (err) {
console.log(`[sync subrule error]: ${subrules.url}`, err);
}
}
};
/**
* 根据时间同步所有订阅规则
* @param {*} url
* @returns
*/
export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
try {
const { subRulesSyncAt } = await syncOpt.load();
const now = Date.now();
const interval = 24 * 60 * 60 * 1000; // 间隔一天
if (now - subRulesSyncAt > interval) {
await syncAllSubRules(subrulesList, isBg);
await syncOpt.update({ subRulesSyncAt: now });
}
} catch (err) {
console.log("[try sync all subrules]", err);
}
};
/**
* 从缓存或远程加载订阅规则
* @param {*} url
* @returns
*/
export const loadSubRules = async (url) => {
const rules = await rulesCache.get(url);
if (rules?.length) {
return rules;
}
return await syncSubRules(url);
}; };

67
src/libs/shortcut.js Normal file
View File

@@ -0,0 +1,67 @@
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);
};

View File

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

78
src/libs/subRules.js Normal file
View File

@@ -0,0 +1,78 @@
import { GLOBAL_KEY } from "../config";
import {
getSyncWithDefault,
updateSync,
setSubRules,
getSubRules,
updateSetting,
} from "./storage";
import { apiFetch } from "../apis";
import { checkRules } from "./rules";
import { isAllchar } from "./utils";
/**
* 同步订阅规则
* @param {*} url
* @returns
*/
export const syncSubRules = async (url, isBg = false) => {
const res = await apiFetch(url, isBg);
const rules = checkRules(res).filter(
({ pattern }) => !isAllchar(pattern, GLOBAL_KEY)
);
if (rules.length > 0) {
await setSubRules(url, rules);
}
return rules;
};
/**
* 同步所有订阅规则
* @param {*} url
* @returns
*/
export const syncAllSubRules = async (subrulesList, isBg = false) => {
for (let subrules of subrulesList) {
try {
await syncSubRules(subrules.url, isBg);
} catch (err) {
console.log(`[sync subrule error]: ${subrules.url}`, err);
}
}
};
/**
* 根据时间同步所有订阅规则
* @param {*} url
* @returns
*/
export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
try {
const { subRulesSyncAt } = await getSyncWithDefault();
const now = Date.now();
const interval = 24 * 60 * 60 * 1000; // 间隔一天
if (now - subRulesSyncAt > interval) {
await syncAllSubRules(subrulesList, isBg);
await updateSync({ subRulesSyncAt: now });
}
subrulesList.forEach((item) => {
item.syncAt = now;
});
await updateSetting({ subrulesList });
} catch (err) {
console.log("[try sync all subrules]", err);
}
};
/**
* 从缓存或远程加载订阅规则
* @param {*} url
* @returns
*/
export const loadOrFetchSubRules = async (url) => {
const rules = await getSubRules(url);
if (rules?.length) {
return rules;
}
return syncSubRules(url);
};

View File

@@ -1,64 +1,65 @@
import { import {
STOKEY_SYNC,
DEFAULT_SYNC,
KV_SETTING_KEY, KV_SETTING_KEY,
KV_RULES_KEY, KV_RULES_KEY,
KV_RULES_SHARE_KEY, KV_RULES_SHARE_KEY,
STOKEY_SETTING,
STOKEY_RULES,
KV_SALT_SHARE, KV_SALT_SHARE,
} 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 } from "./utils"; import { sha256 } from "./utils";
/**
* 同步相关数据
*/
export const syncOpt = {
load: async () => (await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC,
update: async (obj) => {
await storage.putObj(STOKEY_SYNC, obj);
},
};
/** /**
* 同步设置 * 同步设置
* @returns * @returns
*/ */
export const syncSetting = async (isBg = false) => { const syncSetting = async (isBg = false, isForce = false) => {
const { syncUrl, syncKey, settingUpdateAt } = await syncOpt.load(); let {
syncUrl,
syncKey,
settingUpdateAt = 0,
settingSyncAt = 0,
} = await getSyncWithDefault();
if (!syncUrl || !syncKey) { if (!syncUrl || !syncKey) {
return; return;
} }
const setting = await getSetting(); if (isForce) {
settingUpdateAt = Date.now();
}
const setting = await getSettingWithDefault();
const res = await apiSyncData( const res = await apiSyncData(
syncUrl, syncUrl,
syncKey, syncKey,
{ {
key: KV_SETTING_KEY, key: KV_SETTING_KEY,
value: setting, value: setting,
updateAt: settingUpdateAt, updateAt: settingSyncAt === 0 ? 0 : settingUpdateAt,
}, },
isBg isBg
); );
if (res && res.updateAt > settingUpdateAt) { if (res.updateAt > settingUpdateAt) {
await syncOpt.update({ await setSetting(res.value);
settingUpdateAt: res.updateAt,
settingSyncAt: res.updateAt,
});
await storage.setObj(STOKEY_SETTING, res.value);
} else {
await syncOpt.update({ settingSyncAt: res.updateAt });
} }
await updateSync({
settingUpdateAt: res.updateAt,
settingSyncAt: Date.now(),
});
return res.value;
}; };
export const trySyncSetting = async (isBg = false) => { export const trySyncSetting = async (isBg = false, isForce = false) => {
try { try {
await syncSetting(isBg); return await syncSetting(isBg, isForce);
} catch (err) { } catch (err) {
console.log("[sync setting]", err); console.log("[sync setting]", err);
} }
@@ -68,38 +69,47 @@ export const trySyncSetting = async (isBg = false) => {
* 同步规则 * 同步规则
* @returns * @returns
*/ */
export const syncRules = async (isBg = false) => { const syncRules = async (isBg = false, isForce = false) => {
const { syncUrl, syncKey, rulesUpdateAt } = await syncOpt.load(); let {
syncUrl,
syncKey,
rulesUpdateAt = 0,
rulesSyncAt = 0,
} = await getSyncWithDefault();
if (!syncUrl || !syncKey) { if (!syncUrl || !syncKey) {
return; return;
} }
const rules = await getRules(); if (isForce) {
rulesUpdateAt = Date.now();
}
const rules = await getRulesWithDefault();
const res = await apiSyncData( const res = await apiSyncData(
syncUrl, syncUrl,
syncKey, syncKey,
{ {
key: KV_RULES_KEY, key: KV_RULES_KEY,
value: rules, value: rules,
updateAt: rulesUpdateAt, updateAt: rulesSyncAt === 0 ? 0 : rulesUpdateAt,
}, },
isBg isBg
); );
if (res && res.updateAt > rulesUpdateAt) { if (res.updateAt > rulesUpdateAt) {
await syncOpt.update({ await setRules(res.value);
rulesUpdateAt: res.updateAt,
rulesSyncAt: res.updateAt,
});
await storage.setObj(STOKEY_RULES, res.value);
} else {
await syncOpt.update({ rulesSyncAt: res.updateAt });
} }
await updateSync({
rulesUpdateAt: res.updateAt,
rulesSyncAt: Date.now(),
});
return res.value;
}; };
export const trySyncRules = async (isBg = false) => { export const trySyncRules = async (isBg = false, isForce = false) => {
try { try {
await syncRules(isBg); return await syncRules(isBg, isForce);
} catch (err) { } catch (err) {
console.log("[sync user rules]", err); console.log("[sync user rules]", err);
} }
@@ -125,12 +135,10 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
* 同步个人设置和规则 * 同步个人设置和规则
* @returns * @returns
*/ */
export const syncAll = async (isBg = false) => { export const syncSettingAndRules = async (isBg = false) => {
await syncSetting(isBg); return [await syncSetting(isBg), await syncRules(isBg)];
await syncRules(isBg);
}; };
export const trySyncAll = async (isBg = false) => { export const trySyncSettingAndRules = async (isBg = false) => {
await trySyncSetting(isBg); return [await trySyncSetting(isBg), await trySyncRules(isBg)];
await trySyncRules(isBg);
}; };

View File

@@ -3,23 +3,25 @@ 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_DASHLINE,
OPT_STYLE_FUZZY, OPT_STYLE_FUZZY,
SHADOW_KEY, SHADOW_KEY,
OPT_MOUSEKEY_DISABLE,
OPT_MOUSEKEY_MOUSEOVER,
} from "../config"; } from "../config";
import Content from "../views/Content"; import Content from "../views/Content";
import { fetchUpdate, fetchClear } from "./fetch"; import { updateFetchPool, clearFetchPool } from "./fetch";
import { debounce } from "./utils"; import { debounce, genEventName } from "./utils";
/** /**
* 翻译类 * 翻译类
*/ */
export class Translator { export class Translator {
_rule = {}; _rule = {};
_minLength = 0; _setting = {};
_maxLength = 0; _rootNodes = new Set();
_tranNodes = new Map();
_skipNodeNames = [ _skipNodeNames = [
APP_LCNAME, APP_LCNAME,
"style", "style",
@@ -36,8 +38,7 @@ export class Translator {
"script", "script",
"iframe", "iframe",
]; ];
_rootNodes = new Set(); _eventName = genEventName();
_tranNodes = new Map();
// 显示 // 显示
_interseObserver = new IntersectionObserver( _interseObserver = new IntersectionObserver(
@@ -89,17 +90,27 @@ export class Translator {
}; };
}; };
constructor(rule, { fetchInterval, fetchLimit, minLength, maxLength }) { constructor(rule, setting) {
fetchUpdate(fetchInterval, fetchLimit); const { fetchInterval, fetchLimit } = setting;
updateFetchPool(fetchInterval, fetchLimit);
this._overrideAttachShadow(); this._overrideAttachShadow();
this._minLength = minLength ?? TRANS_MIN_LENGTH;
this._maxLength = maxLength ?? TRANS_MAX_LENGTH; this._setting = setting;
this.rule = rule; this._rule = rule;
if (rule.transOpen === "true") { if (rule.transOpen === "true") {
this._register(); this._register();
} }
} }
get setting() {
return this._setting;
}
get eventName() {
return this._eventName;
}
get rule() { get rule() {
// console.log("get rule", this._rule); // console.log("get rule", this._rule);
return this._rule; return this._rule;
@@ -110,8 +121,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,
@@ -201,6 +213,10 @@ export class Translator {
}; };
_register = () => { _register = () => {
if (this._rule.fromLang === this._rule.toLang) {
return;
}
// 搜索节点 // 搜索节点
this._queryNodes(); this._queryNodes();
@@ -214,20 +230,47 @@ export class Translator {
}); });
this._tranNodes.forEach((_, node) => { this._tranNodes.forEach((_, node) => {
// 监听节点显示 if (
this._interseObserver.observe(node); !this._setting.mouseKey ||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
) {
// 监听节点显示
this._interseObserver.observe(node);
} else {
// 监听鼠标悬停
node.addEventListener("mouseover", this._handleMouseover);
}
}); });
}; };
_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();
// 解除节点显示监听 // 解除节点显示监听
this._interseObserver.disconnect(); // this._interseObserver.disconnect();
// 移除已插入元素
this._tranNodes.forEach((_, node) => { this._tranNodes.forEach((_, node) => {
if (
!this._setting.mouseKey ||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
) {
// 解除节点显示监听
this._interseObserver.unobserve(node);
} else {
// 移除鼠标悬停监听
node.removeEventListener("mouseover", this._handleMouseover);
}
// 移除已插入元素
node.querySelector(APP_LCNAME)?.remove(); node.querySelector(APP_LCNAME)?.remove();
}); });
@@ -236,7 +279,7 @@ export class Translator {
this._tranNodes.clear(); this._tranNodes.clear();
// 清空任务池 // 清空任务池
fetchClear(); clearFetchPool();
}; };
_reTranslate = debounce(() => { _reTranslate = debounce(() => {
@@ -268,7 +311,11 @@ export class Translator {
this._tranNodes.set(el, q); this._tranNodes.set(el, q);
// 太长或太短 // 太长或太短
if (!q || q.length < this._minLength || q.length > this._maxLength) { if (
!q ||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH)
) {
return; return;
} }

View File

@@ -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,15 +48,61 @@ 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 {*} s
@@ -63,7 +114,7 @@ export const isMatch = (s, p) => {
return false; return false;
} }
p = `*${p}*`; p = "*" + p + "*";
let [sIndex, pIndex] = [0, 0]; let [sIndex, pIndex] = [0, 0];
let [sRecord, pRecord] = [-1, -1]; let [sRecord, pRecord] = [-1, -1];
@@ -86,7 +137,7 @@ export const isMatch = (s, p) => {
return true; return true;
} }
return p.slice(pIndex).replaceAll("*", "") === ""; return isAllchar(p, "*", pIndex);
}; };
/** /**
@@ -111,3 +162,20 @@ export const sha256 = async (text, salt) => {
.map((b) => b.toString(16).padStart(2, "0")) .map((b) => b.toString(16).padStart(2, "0"))
.join(""); .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;
};

192
src/libs/webfix.js Normal file
View 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 需要修复的选择器
* - rootSlector 需要监听的选择器,可留空
* - fixer 修复函数,可针对不同网址,选用不同修复函数
*/
const DEFAULT_SITES = [
{
pattern: "www.phoronix.com",
selector: ".content",
rootSlector: "",
fixer: FIXER_BR,
},
{
pattern: "t.me/s/",
selector: ".tgme_widget_message_text",
rootSlector: ".tgme_channel_history",
fixer: FIXER_BR,
},
{
pattern: "baidu.com",
selector: "html",
rootSlector: "",
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 {*} rootSlector
*/
function run(selector, fixer, rootSlector) {
var mutaObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addNode) {
addNode.querySelectorAll(selector).forEach(fixer);
});
});
});
var rootNodes = [document];
if (rootSlector) {
rootNodes = document.querySelectorAll(rootSlector);
}
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.rootSlector);
}
break;
}
}
} catch (err) {
console.error(`[kiss-webfix]: ${err.message}`);
}
}

View File

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

View File

@@ -3,6 +3,7 @@ import path from "path";
import { BUILTIN_RULES } from "./config/rules"; import { BUILTIN_RULES } from "./config/rules";
(() => { (() => {
// rules
try { try {
const data = JSON.stringify(BUILTIN_RULES, null, " "); const data = JSON.stringify(BUILTIN_RULES, null, " ");
const file = path.resolve( const file = path.resolve(
@@ -14,4 +15,14 @@ import { BUILTIN_RULES } from "./config/rules";
} catch (err) { } catch (err) {
console.error(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);
}
})(); })();

View File

@@ -3,34 +3,55 @@ 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/rules"; import { trySyncAllSubRules } from "./libs/subRules";
import { isGm } from "./libs/browser";
import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config"; import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config";
import { isIframe } from "./libs/iframe"; import { isIframe } from "./libs/iframe";
import { handlePing, injectScript } from "./libs/gm";
import { matchRule } from "./libs/rules";
import { genEventName } from "./libs/utils";
import { webfix } from "./libs/webfix";
/** /**
* 入口函数 * 入口函数
*/ */
(async () => { const init = async () => {
// 设置页面 // 设置页面
if ( 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;
unsafeWindow.APP_INFO = {
name: process.env.REACT_APP_NAME,
version: process.env.REACT_APP_VERSION,
};
} else {
const ping = genEventName();
window.addEventListener(ping, handlePing);
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
const script = document.createElement("script");
script.textContent = `(${injectScript})("${ping}")`;
document.head.append(script);
}
return; return;
} }
// 翻译页面 // 翻译页面
const href = isIframe ? document.referrer : document.location.href; const href = isIframe ? document.referrer : document.location.href;
const setting = await getSetting(); const setting = await getSettingWithDefault();
const rules = await getRules(); const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting); const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting); const translator = new Translator(rule, setting);
webfix(href, setting);
if (isIframe) { if (isIframe) {
// iframe // iframe
@@ -50,7 +71,7 @@ import { isIframe } from "./libs/iframe";
} }
// 浮球按钮 // 浮球按钮
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);
@@ -72,24 +93,18 @@ import { isIframe } from "./libs/iframe";
</React.StrictMode> </React.StrictMode>
); );
// 注册菜单
if (isGm) {
GM.registerMenuCommand(
"Toggle Translate",
(event) => {
translator.toggle();
},
"Q"
);
GM.registerMenuCommand(
"Toggle Style",
(event) => {
translator.toggleStyle();
},
"C"
);
}
// 同步订阅规则 // 同步订阅规则
trySyncAllSubRules(setting); 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);
}
})(); })();

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, 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";
const getEdgePosition = ( const getEdgePosition = (
{ x: left, y: top, edge }, { x: left, y: top, edge },
@@ -159,7 +159,7 @@ export default function Draggable({
y: position.y, y: position.y,
}); });
} }
}, [position]); }, [position.x, position.y, position.hide]);
const opacity = useMemo(() => { const opacity = useMemo(() => {
if (snapEdge) { if (snapEdge) {

View File

@@ -1,23 +1,29 @@
import Paper from "@mui/material/Paper"; 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 {
DEFAULT_SHORTCUTS,
OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP,
OPT_SHORTCUT_SETTING,
} from "../../config";
import { shortcutRegister } from "../../libs/shortcut";
export default function Action({ translator, fab }) { 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 +31,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 +50,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();
setShowPopup(false);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () => {
translator.toggleStyle();
setShowPopup(false);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () => {
setShowPopup((pre) => !pre);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () => {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
}),
];
return () => {
clearShortcuts.forEach((fn) => {
fn();
});
};
}, [translator]);
useEffect(() => {
// 注册菜单
const menuCommandIds = [];
if (isGm) {
try {
menuCommandIds.push(
GM.registerMenuCommand(
"Toggle Translate (Alt+q)",
(event) => {
translator.toggle();
setShowPopup(false);
},
"Q"
),
GM.registerMenuCommand(
"Toggle Style (Alt+c)",
(event) => {
translator.toggleStyle();
setShowPopup(false);
},
"C"
),
GM.registerMenuCommand(
"Open Menu (Alt+k)",
(event) => {
setShowPopup((pre) => !pre);
},
"K"
),
GM.registerMenuCommand(
"Open Setting (Alt+o)",
(event) => {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
},
"O"
)
);
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}
return () => {
if (isGm) {
try {
menuCommandIds.forEach((id) => {
GM.unregisterMenuCommand(id);
});
} catch (err) {
//
}
}
};
}, [translator]);
useEffect(() => { useEffect(() => {
window.addEventListener("resize", handleWindowResize); window.addEventListener("resize", handleWindowResize);
return () => { return () => {
@@ -53,6 +141,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);
}; };
@@ -81,7 +170,7 @@ export default function Action({ translator, fab }) {
}; };
return ( return (
<StoragesProvider> <SettingProvider>
<ThemeProvider> <ThemeProvider>
<Draggable <Draggable
key="pop" key="pop"
@@ -91,23 +180,7 @@ export default function Action({ translator, fab }) {
onMove={handleMove} onMove={handleMove}
handler={ handler={
<Paper style={{ cursor: "move" }} elevation={3}> <Paper style={{ cursor: "move" }} elevation={3}>
<Stack <Header setShowPopup={setShowPopup} />
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Box style={{ marginLeft: 16 }}>
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
</Box>
<IconButton
onClick={() => {
setShowPopup(false);
}}
>
<CloseIcon />
</IconButton>
</Stack>
</Paper> </Paper>
} }
> >
@@ -121,7 +194,7 @@ export default function Action({ translator, fab }) {
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={
@@ -139,6 +212,6 @@ export default function Action({ translator, fab }) {
} }
/> />
</ThemeProvider> </ThemeProvider>
</StoragesProvider> </SettingProvider>
); );
} }

View File

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

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

View File

@@ -4,17 +4,13 @@ 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,12 +31,15 @@ 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>
); );

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

View File

@@ -10,6 +10,8 @@ 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";
function LinkItem({ label, url, icon }) { function LinkItem({ label, url, icon }) {
const match = useMatch(url); const match = useMatch(url);
@@ -36,12 +38,24 @@ export default function Navigator(props) {
url: "/rules", url: "/rules",
icon: <DesignServicesIcon />, icon: <DesignServicesIcon />,
}, },
{
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 (

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

View File

@@ -11,6 +11,9 @@ 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,
} from "../../config"; } from "../../config";
import { useState, useRef, useEffect, useMemo } from "react"; import { useState, useRef, useEffect, useMemo } from "react";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
@@ -24,7 +27,7 @@ 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, useSettingUpdate } from "../../hooks/Setting"; import { useSetting } from "../../hooks/Setting";
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 Tabs from "@mui/material/Tabs"; import Tabs from "@mui/material/Tabs";
@@ -35,14 +38,22 @@ import DeleteIcon from "@mui/icons-material/Delete";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import ShareIcon from "@mui/icons-material/Share"; import ShareIcon from "@mui/icons-material/Share";
import SyncIcon from "@mui/icons-material/Sync"; import SyncIcon from "@mui/icons-material/Sync";
import { useSubrules } from "../../hooks/Rules"; import { useSubRules } from "../../hooks/SubRules";
import { rulesCache, loadSubRules, syncSubRules } from "../../libs/rules"; import { syncSubRules } from "../../libs/subRules";
import { loadOrFetchSubRules } from "../../libs/subRules";
import { useAlert } from "../../hooks/Alert"; import { useAlert } from "../../hooks/Alert";
import { syncOpt, syncShareRules } from "../../libs/sync"; import { syncShareRules } from "../../libs/sync";
import { debounce } from "../../libs/utils"; 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";
function RuleFields({ rule, rules, setShow, setKeyword }) { 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();
@@ -58,6 +69,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
textStyle, textStyle,
transOpen, transOpen,
bgColor, bgColor,
textDiyStyle,
} = formValues; } = formValues;
const hasSamePattern = (str) => { const hasSamePattern = (str) => {
@@ -132,7 +144,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
} }
}; };
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>
@@ -179,7 +191,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
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>
@@ -195,7 +207,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
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}
@@ -214,7 +226,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
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}
@@ -233,7 +245,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
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}
@@ -252,7 +264,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
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)}
@@ -260,20 +272,35 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
))} ))}
</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>
{textStyle === OPT_STYLE_DIY && (
<TextField
size="small"
label={i18n("diy_style")}
helperText={i18n("diy_style_helper")}
name="textDiyStyle"
value={textDiyStyle}
disabled={disabled}
onChange={handleChange}
multiline
/>
)}
{rules && {rules &&
(editMode ? ( (editMode ? (
// 编辑 // 编辑
@@ -335,6 +362,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
} }
function RuleAccordion({ rule, rules }) { function RuleAccordion({ rule, rules }) {
const i18n = useI18n();
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const handleChange = (e) => { const handleChange = (e) => {
@@ -349,7 +377,9 @@ function RuleAccordion({ rule, rules }) {
opacity: rules ? 1 : 0.5, opacity: rules ? 1 : 0.5,
}} }}
> >
{rule.pattern} {rule.pattern === GLOBAL_KEY
? `[${i18n("global_rule")}] ${rule.pattern}`
: rule.pattern}
</Typography> </Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
@@ -409,12 +439,12 @@ function UploadButton({ onChange, text }) {
); );
} }
function ShareButton({ rules, injectRules, selectedSub }) { function ShareButton({ rules, injectRules, selectedUrl }) {
const alert = useAlert(); const alert = useAlert();
const i18n = useI18n(); const i18n = useI18n();
const handleClick = async () => { const handleClick = async () => {
try { try {
const { syncUrl, syncKey } = await syncOpt.load(); const { syncUrl, syncKey } = await getSyncWithDefault();
if (!syncUrl || !syncKey) { if (!syncUrl || !syncKey) {
alert.warning(i18n("error_sync_setting")); alert.warning(i18n("error_sync_setting"));
return; return;
@@ -422,7 +452,7 @@ function ShareButton({ rules, injectRules, selectedSub }) {
const shareRules = [...rules.list]; const shareRules = [...rules.list];
if (injectRules) { if (injectRules) {
const subRules = await loadSubRules(selectedSub?.url); const subRules = await loadOrFetchSubRules(selectedUrl);
shareRules.splice(-1, 0, ...subRules); shareRules.splice(-1, 0, ...subRules);
} }
@@ -446,24 +476,20 @@ function ShareButton({ rules, injectRules, selectedSub }) {
onClick={handleClick} onClick={handleClick}
startIcon={<ShareIcon />} startIcon={<ShareIcon />}
> >
{"分享"} {i18n("share")}
</Button> </Button>
); );
} }
function UserRules() { 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 = useSetting(); const { setting, updateSetting } = useSetting();
const updateSetting = useSettingUpdate();
const subrules = useSubrules();
const [subRules, setSubRules] = useState([]);
const [keyword, setKeyword] = useState(""); const [keyword, setKeyword] = useState("");
const selectedSub = subrules.list.find((item) => item.selected);
const injectRules = !!setting?.injectRules; 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];
@@ -493,19 +519,6 @@ function UserRules() {
}); });
}; };
useEffect(() => {
(async () => {
if (selectedSub?.url) {
try {
const rules = await loadSubRules(selectedSub?.url);
setSubRules(rules);
} catch (err) {
console.log("[load rules]", err);
}
}
})();
}, [selectedSub?.url]);
useEffect(() => { useEffect(() => {
if (!showAdd) { if (!showAdd) {
setKeyword(""); setKeyword("");
@@ -514,7 +527,13 @@ function UserRules() {
return ( return (
<Stack spacing={3}> <Stack spacing={3}>
<Stack direction="row" alignItems="center" spacing={2} useFlexGap flexWrap="wrap"> <Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
<Button <Button
size="small" size="small"
variant="contained" variant="contained"
@@ -536,9 +555,22 @@ function UserRules() {
<ShareButton <ShareButton
rules={rules} rules={rules}
injectRules={injectRules} injectRules={injectRules}
selectedSub={selectedSub} selectedUrl={selectedUrl}
/> />
<Button
size="small"
variant="outlined"
onClick={() => {
rules.clear();
}}
startIcon={<ClearAllIcon />}
>
{i18n("clear_all")}
</Button>
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
<FormControlLabel <FormControlLabel
control={ control={
<Switch <Switch
@@ -572,7 +604,7 @@ function UserRules() {
{injectRules && ( {injectRules && (
<Box> <Box>
{subRules {selectedRules
.filter( .filter(
(rule) => (rule) =>
rule.pattern.includes(keyword) || keyword.includes(rule.pattern) rule.pattern.includes(keyword) || keyword.includes(rule.pattern)
@@ -586,13 +618,21 @@ function UserRules() {
); );
} }
function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) { function SubRulesItem({
index,
url,
syncAt,
selectedUrl,
delSub,
updateSub,
setSelectedRules,
}) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const handleDel = async () => { const handleDel = async () => {
try { try {
await subrules.del(url); await delSub(url);
await rulesCache.del(url); await delSubRules(url);
} catch (err) { } catch (err) {
console.log("[del subrules]", err); console.log("[del subrules]", err);
} }
@@ -603,8 +643,9 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
setLoading(true); setLoading(true);
const rules = await syncSubRules(url); const rules = await syncSubRules(url);
if (rules.length > 0 && url === selectedUrl) { if (rules.length > 0 && url === selectedUrl) {
setRules(rules); setSelectedRules(rules);
} }
await updateSub(url, { syncAt: Date.now() });
} catch (err) { } catch (err) {
console.log("[sync sub rules]", err); console.log("[sync sub rules]", err);
} finally { } finally {
@@ -616,6 +657,12 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
<Stack direction="row" alignItems="center" spacing={2}> <Stack direction="row" alignItems="center" spacing={2}>
<FormControlLabel value={url} control={<Radio />} label={url} /> <FormControlLabel value={url} control={<Radio />} label={url} />
{syncAt && (
<span style={{ marginLeft: "0.5em", opacity: 0.5 }}>
[{new Date(syncAt).toLocaleString()}]
</span>
)}
{loading ? ( {loading ? (
<CircularProgress size={16} /> <CircularProgress size={16} />
) : ( ) : (
@@ -633,7 +680,7 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
); );
} }
function SubRulesEdit({ subrules }) { function SubRulesEdit({ subList, addSub }) {
const i18n = useI18n(); const i18n = useI18n();
const [inputText, setInputText] = useState(""); const [inputText, setInputText] = useState("");
const [inputError, setInputError] = useState(""); const [inputError, setInputError] = useState("");
@@ -656,7 +703,7 @@ function SubRulesEdit({ subrules }) {
return; return;
} }
if (subrules.list.find((item) => item.url === url)) { if (subList.find((item) => item.url === url)) {
setInputError(i18n("error_duplicate_values")); setInputError(i18n("error_duplicate_values"));
return; return;
} }
@@ -667,7 +714,7 @@ function SubRulesEdit({ subrules }) {
if (rules.length === 0) { if (rules.length === 0) {
throw new Error("empty rules"); throw new Error("empty rules");
} }
await subrules.add(url); await addSub(url);
setShowInput(false); setShowInput(false);
setInputText(""); setInputText("");
} catch (err) { } catch (err) {
@@ -702,6 +749,7 @@ function SubRulesEdit({ subrules }) {
> >
{i18n("add")} {i18n("add")}
</Button> </Button>
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
</Stack> </Stack>
{showInput && ( {showInput && (
@@ -735,47 +783,39 @@ function SubRulesEdit({ subrules }) {
); );
} }
function SubRules() { function SubRules({ subRules }) {
const [loading, setLoading] = useState(false); const {
const [rules, setRules] = useState([]); subList,
const subrules = useSubrules(); selectSub,
const selectedSub = subrules.list.find((item) => item.selected); updateSub,
addSub,
delSub,
selectedUrl,
selectedRules,
setSelectedRules,
loading,
} = subRules;
const handleSelect = (e) => { const handleSelect = (e) => {
const url = e.target.value; const url = e.target.value;
subrules.select(url); selectSub(url);
}; };
useEffect(() => {
(async () => {
if (selectedSub?.url) {
try {
setLoading(true);
const rules = await loadSubRules(selectedSub?.url);
setRules(rules);
} catch (err) {
console.log("[load rules]", err);
} finally {
setLoading(false);
}
}
})();
}, [selectedSub?.url]);
return ( return (
<Stack spacing={3}> <Stack spacing={3}>
<SubRulesEdit subrules={subrules} /> <SubRulesEdit subList={subList} addSub={addSub} />
<RadioGroup value={selectedSub?.url} onChange={handleSelect}> <RadioGroup value={selectedUrl} onChange={handleSelect}>
{subrules.list.map((item, index) => ( {subList.map((item, index) => (
<SubRulesItem <SubRulesItem
key={item.url} key={item.url}
url={item.url} url={item.url}
syncAt={item.syncAt}
index={index} index={index}
selectedUrl={selectedSub?.url} selectedUrl={selectedUrl}
subrules={subrules} delSub={delSub}
setRules={setRules} updateSub={updateSub}
setSelectedRules={setSelectedRules}
/> />
))} ))}
</RadioGroup> </RadioGroup>
@@ -786,7 +826,9 @@ function SubRules() {
<CircularProgress /> <CircularProgress />
</center> </center>
) : ( ) : (
rules.map((rule) => <RuleAccordion key={rule.pattern} rule={rule} />) selectedRules.map((rule) => (
<RuleAccordion key={rule.pattern} rule={rule} />
))
)} )}
</Box> </Box>
</Stack> </Stack>
@@ -796,6 +838,7 @@ function SubRules() {
export default function Rules() { export default function Rules() {
const i18n = useI18n(); const i18n = useI18n();
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const subRules = useSubRules();
const handleTabChange = (e, newValue) => { const handleTabChange = (e, newValue) => {
setActiveTab(newValue); setActiveTab(newValue);
@@ -814,10 +857,16 @@ export default function Rules() {
<Tabs value={activeTab} onChange={handleTabChange}> <Tabs value={activeTab} onChange={handleTabChange}>
<Tab label={i18n("personal_rules")} /> <Tab label={i18n("personal_rules")} />
<Tab label={i18n("subscribe_rules")} /> <Tab label={i18n("subscribe_rules")} />
<Tab label={i18n("overwrite_subscribe_rules")} />
</Tabs> </Tabs>
</Box> </Box>
<div hidden={activeTab !== 0}>{activeTab === 0 && <UserRules />}</div> <div hidden={activeTab !== 0}>
<div hidden={activeTab !== 1}>{activeTab === 1 && <SubRules />}</div> {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>
); );

View File

@@ -5,60 +5,131 @@ 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 { limitNumber, debounce } from "../../libs/utils"; import FormHelperText from "@mui/material/FormHelperText";
import { useSetting } from "../../hooks/Setting";
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 { useMemo } from "react"; import { isExt } from "../../libs/client";
import IconButton from "@mui/material/IconButton";
import EditIcon from "@mui/icons-material/Edit";
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 { useEffect, useState, useRef } from "react";
import { useShortcut } from "../../hooks/Shortcut";
import { shortcutListener } from "../../libs/shortcut";
function ShortcutItem({ action, label }) {
const { shortcut, setShortcut } = useShortcut(action);
const [disabled, setDisabled] = useState(true);
const inputRef = useRef(null);
useEffect(() => {
if (disabled) {
return;
}
inputRef.current.focus();
setShortcut([]);
const clearShortcut = shortcutListener((curkeys, allkeys) => {
setShortcut(allkeys);
if (curkeys.length === 0) {
setDisabled(true);
}
}, inputRef.current);
return () => {
clearShortcut();
};
}, [disabled, setShortcut]);
return (
<Stack direction="row">
<TextField
size="small"
label={label}
name={label}
value={shortcut.join(" + ")}
fullWidth
inputRef={inputRef}
disabled={disabled}
onBlur={() => {
setDisabled(true);
}}
/>
<IconButton
onClick={() => {
setDisabled(false);
}}
>
{<EditIcon />}
</IconButton>
</Stack>
);
}
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();
const handleChange = useMemo( const handleChange = (e) => {
() => e.preventDefault();
debounce((e) => { let { name, value } = e.target;
e.preventDefault(); switch (name) {
let { name, value } = e.target; case "fetchLimit":
switch (name) { value = limitNumber(value, 1, 100);
case "fetchLimit": break;
value = limitNumber(value, 1, 100); case "fetchInterval":
break; value = limitNumber(value, 0, 5000);
case "fetchInterval": break;
value = limitNumber(value, 0, 5000); case "minLength":
break; value = limitNumber(value, 1, 100);
case "minLength": break;
value = limitNumber(value, 1, 100); case "maxLength":
break; value = limitNumber(value, 100, 10000);
case "maxLength": break;
value = limitNumber(value, 100, 10000); case "newlineLength":
break; value = limitNumber(value, 1, 1000);
default: break;
} default:
updateSetting({ }
[name]: value, updateSetting({
}); [name]: value,
}, 500), });
[updateSetting] };
);
if (!setting) { const handleClearCache = () => {
return; 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,
minLength, minLength,
maxLength, maxLength,
openaiUrl,
openaiKey,
openaiModel,
openaiPrompt,
clearCache, clearCache,
newlineLength = TRANS_NEWLINE_LENGTH,
mouseKey = OPT_MOUSEKEY_DISABLE,
hideFab = false,
} = setting; } = setting;
return ( return (
@@ -85,7 +156,7 @@ export default function Settings() {
label={i18n("fetch_limit")} label={i18n("fetch_limit")}
type="number" type="number"
name="fetchLimit" name="fetchLimit"
defaultValue={fetchLimit} value={fetchLimit}
onChange={handleChange} onChange={handleChange}
/> />
@@ -94,7 +165,7 @@ export default function Settings() {
label={i18n("fetch_interval")} label={i18n("fetch_interval")}
type="number" type="number"
name="fetchInterval" name="fetchInterval"
defaultValue={fetchInterval} value={fetchInterval}
onChange={handleChange} onChange={handleChange}
/> />
@@ -103,7 +174,7 @@ export default function Settings() {
label={i18n("min_translate_length")} label={i18n("min_translate_length")}
type="number" type="number"
name="minLength" name="minLength"
defaultValue={minLength} value={minLength}
onChange={handleChange} onChange={handleChange}
/> />
@@ -112,64 +183,95 @@ export default function Settings() {
label={i18n("max_translate_length")} label={i18n("max_translate_length")}
type="number" type="number"
name="maxLength" name="maxLength"
defaultValue={maxLength} value={maxLength}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("num_of_newline_characters")}
type="number"
name="newlineLength"
value={newlineLength}
onChange={handleChange} onChange={handleChange}
/> />
<FormControl size="small"> <FormControl size="small">
<InputLabel>{i18n("clear_cache")}</InputLabel> <InputLabel>{i18n("mouseover_translation")}</InputLabel>
<Select <Select
name="clearCache" name="mouseKey"
value={clearCache} value={mouseKey}
label={i18n("clear_cache")} label={i18n("mouseover_translation")}
onChange={handleChange} onChange={handleChange}
> >
<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>
name="googleUrl" <Select
defaultValue={googleUrl} name="clearCache"
onChange={handleChange} value={clearCache}
/> label={i18n("if_clear_cache")}
onChange={handleChange}
<TextField >
size="small" <MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
label={i18n("openai_api")} <MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
name="openaiUrl" </Select>
defaultValue={openaiUrl} <FormHelperText>
onChange={handleChange} <Link component="button" onClick={handleClearCache}>
/> {i18n("clear_all_cache_now")}
</Link>
<TextField </FormHelperText>
size="small" </FormControl>
type="password" ) : (
label={i18n("openai_key")} <>
name="openaiKey" <FormControl size="small">
defaultValue={openaiKey} <InputLabel>{i18n("hide_fab_button")}</InputLabel>
onChange={handleChange} <Select
/> name="hideFab"
value={hideFab}
<TextField label={i18n("hide_fab_button")}
size="small" onChange={handleChange}
label={i18n("openai_model")} >
name="openaiModel" <MenuItem value={false}>{i18n("show")}</MenuItem>
defaultValue={openaiModel} <MenuItem value={true}>{i18n("hide")}</MenuItem>
onChange={handleChange} </Select>
/> </FormControl>
<Grid container rowSpacing={2} columns={12}>
<TextField <Grid item xs={12} sm={12} md={3} lg={3}>
size="small" <ShortcutItem
label={i18n("openai_prompt")} action={OPT_SHORTCUT_TRANSLATE}
name="openaiPrompt" label={i18n("toggle_translate_shortcut")}
defaultValue={openaiPrompt} />
onChange={handleChange} </Grid>
multiline <Grid item xs={12} sm={12} md={3} lg={3}>
/> <ShortcutItem
action={OPT_SHORTCUT_STYLE}
label={i18n("toggle_style_shortcut")}
/>
</Grid>
<Grid item xs={12} sm={12} md={3} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_POPUP}
label={i18n("toggle_popup_shortcut")}
/>
</Grid>
<Grid item xs={12} sm={12} md={3} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_SETTING}
label={i18n("open_setting_shortcut")}
/>
</Grid>
</Grid>
</>
)}
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -6,52 +6,45 @@ import { useSync } from "../../hooks/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 { URL_KISS_WORKER } from "../../config";
import { debounce } from "../../libs/utils"; import { useState } from "react";
import { useMemo, useState } from "react"; import { syncSettingAndRules } from "../../libs/sync";
import { syncAll } from "../../libs/sync";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { useAlert } from "../../hooks/Alert"; import { useAlert } from "../../hooks/Alert";
import SyncIcon from "@mui/icons-material/Sync"; import SyncIcon from "@mui/icons-material/Sync";
import CircularProgress from "@mui/material/CircularProgress"; 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 alert = useAlert();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { reloadSetting } = useSetting();
const handleChange = useMemo( const handleChange = async (e) => {
() => e.preventDefault();
debounce(async (e) => { const { name, value } = e.target;
e.preventDefault(); await updateSync({
const { name, value } = e.target; [name]: value,
await sync.update({ });
[name]: value, };
});
// trySyncAll();
}, 500),
[sync]
);
const handleSyncTest = async (e) => { const handleSyncTest = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
setLoading(true); setLoading(true);
await syncAll(); await syncSettingAndRules();
alert.success(i18n("data_sync_success")); await reloadSetting();
alert.success(i18n("sync_success"));
} catch (err) { } catch (err) {
console.log("[sync all]", err); console.log("[sync all]", err);
alert.error(i18n("data_sync_error")); alert.error(i18n("sync_failed"));
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
if (!sync.opt) { const { syncUrl, syncKey } = sync;
return;
}
const { syncUrl, syncKey } = sync.opt;
return ( return (
<Box> <Box>
@@ -62,10 +55,12 @@ export default function SyncSetting() {
size="small" size="small"
label={i18n("data_sync_url")} label={i18n("data_sync_url")}
name="syncUrl" name="syncUrl"
defaultValue={syncUrl} value={syncUrl}
onChange={handleChange} onChange={handleChange}
helperText={ helperText={
<Link href={URL_KISS_WORKER}>{i18n("about_sync_api")}</Link> <Link href={URL_KISS_WORKER} target="_blank">
{i18n("about_sync_api")}
</Link>
} }
/> />
@@ -74,11 +69,17 @@ export default function SyncSetting() {
type="password" type="password"
label={i18n("data_sync_key")} label={i18n("data_sync_key")}
name="syncKey" name="syncKey"
defaultValue={syncKey} value={syncKey}
onChange={handleChange} onChange={handleChange}
/> />
<Stack direction="row" alignItems="center" spacing={2} useFlexGap flexWrap="wrap"> <Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
<Button <Button
size="small" size="small"
variant="contained" variant="contained"
@@ -86,7 +87,7 @@ export default function SyncSetting() {
onClick={handleSyncTest} onClick={handleSyncTest}
startIcon={<SyncIcon />} startIcon={<SyncIcon />}
> >
{i18n("data_sync_test")} {i18n("sync_now")}
</Button> </Button>
{loading && <CircularProgress size={16} />} {loading && <CircularProgress size={16} />}
</Stack> </Stack>

165
src/views/Options/Webfix.js Normal file
View 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, rootSlector, fixer } = site;
return (
<Stack spacing={3}>
<TextField
size="small"
label={"rootSlector"}
name="rootSlector"
value={rootSlector || "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>
);
}

View File

@@ -4,17 +4,24 @@ 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 { trySyncAll } from "../../libs/sync"; import { trySyncSettingAndRules } from "../../libs/sync";
import { AlertProvider } from "../../hooks/Alert"; 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";
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(() => {
@@ -23,52 +30,95 @@ export default function Options() {
// 等待GM注入 // 等待GM注入
let i = 0; let i = 0;
for (;;) { for (;;) {
if (window.APP_NAME === process.env.REACT_APP_NAME) { if (window?.APP_INFO?.name === process.env.REACT_APP_NAME) {
const { version, eventName } = window.APP_INFO;
// 检查版本是否一致
if (version !== process.env.REACT_APP_VERSION) {
setError(
`The version is inconsistent, please check whether the script(v${version}) is the latest version(v${process.env.REACT_APP_VERSION}). (版本不一致,请检查脚本(v${version})是否为最新版(v${process.env.REACT_APP_VERSION}))`
);
break;
}
if (eventName) {
// 注入GM接口
adaptScript(eventName);
}
// 同步数据
await trySyncSettingAndRules();
setReady(true); setReady(true);
break; break;
} }
if (++i > 8) { if (++i > 8) {
setError(true); setError("Time out. (连接超时)");
break; break;
} }
await sleep(1000); await sleep(1000);
} }
} else {
// 同步数据
await trySyncSettingAndRules();
setReady(true);
} }
// 同步数据
trySyncAll();
})(); })();
}, []); }, []);
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>
<AlertProvider> <AlertProvider>
<HashRouter> <HashRouter>
@@ -76,13 +126,15 @@ export default function Options() {
<Route path="/" element={<Layout />}> <Route path="/" element={<Layout />}>
<Route index element={<Setting />} /> <Route index element={<Setting />} />
<Route path="rules" element={<Rules />} /> <Route path="rules" element={<Rules />} />
<Route path="apis" element={<Apis />} />
<Route path="sync" element={<SyncSetting />} /> <Route path="sync" element={<SyncSetting />} />
<Route path="webfix" element={<Webfix />} />
<Route path="about" element={<About />} /> <Route path="about" element={<About />} />
</Route> </Route>
</Routes> </Routes>
</HashRouter> </HashRouter>
</AlertProvider> </AlertProvider>
</ThemeProvider> </ThemeProvider>
</StoragesProvider> </SettingProvider>
); );
} }

42
src/views/Popup/Header.js Normal file
View File

@@ -0,0 +1,42 @@
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>
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
</Box>
</Stack>
{setShowPopup ? (
<IconButton
onClick={() => {
setShowPopup(false);
}}
>
<CloseIcon />
</IconButton>
) : (
<DarkModeButton />
)}
</Stack>
);
}

View File

@@ -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,8 +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,
CACHE_NAME,
} from "../../config"; } from "../../config";
import { sendIframeMsg } from "../../libs/iframe"; import { sendIframeMsg } from "../../libs/iframe";
import { saveRule } from "../../libs/rules";
export default function Popup({ setShowPopup, translator: tran }) { export default function Popup({ setShowPopup, translator: tran }) {
const i18n = useI18n(); const i18n = useI18n();
@@ -64,6 +70,28 @@ export default function Popup({ setShowPopup, translator: tran }) {
} }
}; };
const handleClearCache = () => {
try {
caches.delete(CACHE_NAME);
} catch (err) {
console.log("[clear cache]", err);
}
};
const handleSaveRule = async () => {
try {
let host = window.location.host;
if (isExt) {
const tab = await getTabInfo();
const url = new URL(tab.url);
host = url.host;
}
saveRule({ ...rule, pattern: host });
} catch (err) {
console.log("[save rule]", err);
}
};
useEffect(() => { useEffect(() => {
if (!isExt) { if (!isExt) {
return; return;
@@ -82,8 +110,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>
@@ -95,17 +129,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_alt")} 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
@@ -171,17 +223,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>
); );

1423
yarn.lock

File diff suppressed because it is too large Load Diff