Compare commits

...

136 Commits

Author SHA1 Message Date
Gabe Yuan
7b4b3b020c v1.7.6 2023-10-07 15:09:12 +08:00
Gabe Yuan
3e96540b56 fix createCache options key 2023-10-07 15:08:10 +08:00
Gabe Yuan
88b791bd73 fix createCache options key 2023-10-07 15:01:23 +08:00
Gabe Yuan
7817019e70 fix createCache options key 2023-10-07 14:57:36 +08:00
Gabe Yuan
6013bbd32c v1.7.5 2023-09-28 16:19:39 +08:00
Gabe Yuan
40e0b96f39 fix GM.xmlHttpRequest response 2023-09-28 16:18:28 +08:00
Gabe Yuan
16560fbdf0 fix install links 2023-09-27 15:39:57 +08:00
Gabe Yuan
16fdd704aa v1.7.4 2023-09-27 15:08:45 +08:00
Gabe Yuan
44e84d9259 minor revision 2023-09-25 11:56:46 +08:00
Gabe Yuan
18d29461ce minor revision 2023-09-25 11:47:12 +08:00
Gabe Yuan
31fb749e93 minor revision 2023-09-25 11:43:14 +08:00
Gabe Yuan
e17931493b add fab for ext 2023-09-23 19:16:51 +08:00
Gabe Yuan
a395f0b31b change newline length to 20 2023-09-22 16:37:35 +08:00
Gabe Yuan
c819896b43 change newline length to 30 2023-09-22 16:33:00 +08:00
Gabe Yuan
733ec92c9c v1.7.3 2023-09-22 15:40:02 +08:00
Gabe Yuan
7c67bb7181 change shortcut e.key to e.code 2023-09-22 15:33:37 +08:00
Gabe Yuan
87f099dd7f Solve the problem of multi-layer shadowroot selector and input box translation 2023-09-22 15:33:02 +08:00
Gabe Yuan
5306d81284 fix shortcut bug 2023-09-22 11:21:38 +08:00
Gabe Yuan
471a4a3159 fix workflows: replace yarn to pnpm 2023-09-21 16:58:49 +08:00
Gabe Yuan
a2f99da3b4 v1.7.2 2023-09-21 16:48:51 +08:00
Gabe Yuan
accab22d56 update readme 2023-09-21 16:41:49 +08:00
Gabe Yuan
6ea5228a5f fix sync 2023-09-21 16:13:00 +08:00
Gabe Yuan
a07d2cafb6 fix sync 2023-09-21 11:47:22 +08:00
Gabe Yuan
1b38f19cc1 replace yarn to pnpm 2023-09-21 10:31:45 +08:00
Gabe Yuan
aa5b286e0b replace yarn to pnpm 2023-09-21 10:15:03 +08:00
Gabe Yuan
6b6bbed330 fix sync 2023-09-20 22:15:09 +08:00
Gabe Yuan
489bc9534b fix sync 2023-09-20 17:47:23 +08:00
Gabe Yuan
01ebc184ad add globalThis.ContextType 2023-09-20 16:02:17 +08:00
Gabe Yuan
f591d66365 fix clear cache 2023-09-19 12:18:01 +08:00
Gabe Yuan
80782287d8 sync webdav cors 2023-09-19 11:56:19 +08:00
Gabe Yuan
3494bb1297 sync webdav 2023-09-18 17:36:10 +08:00
Gabe Yuan
92ffda5220 sync by worker 2023-09-18 15:45:32 +08:00
Gabe Yuan
fbaeff6b7b update api pathname 2023-09-18 13:28:36 +08:00
Gabe Yuan
248d3726dd fix storage hook 2023-09-17 22:34:28 +08:00
Gabe Yuan
1553559b1a fix storage hook 2023-09-17 21:50:17 +08:00
Gabe Yuan
8935ced75a fix iframe bug 2023-09-17 20:45:05 +08:00
Gabe Yuan
a865d6d74f update readme 2023-09-16 21:16:04 +08:00
Gabe Yuan
6d976554fd update readme 2023-09-16 20:11:10 +08:00
Gabe Yuan
189b7f480a v1.7.1 2023-09-15 22:04:30 +08:00
Gabe Yuan
5e3aa7e2d1 update readme 2023-09-15 22:03:50 +08:00
Gabe Yuan
730be678ef input box trans 2023-09-15 21:39:41 +08:00
Gabe Yuan
9293f422f3 input box trans 2023-09-15 20:44:01 +08:00
Gabe Yuan
6e8158bb34 input box trans 2023-09-15 17:58:00 +08:00
Gabe Yuan
3078d3ca91 input box trans 2023-09-15 17:52:06 +08:00
Gabe Yuan
947e1c7f08 input box trans 2023-09-15 17:29:42 +08:00
Gabe Yuan
938c123412 input box trans 2023-09-15 17:25:58 +08:00
Gabe Yuan
e7a57ad3b2 fix svg 2023-09-15 15:45:51 +08:00
Gabe Yuan
1e40f81bf7 input box trans 2023-09-14 16:35:42 +08:00
Gabe Yuan
72b2f44e32 input box trans 2023-09-14 14:45:22 +08:00
Gabe Yuan
76f54461e7 input box trans 2023-09-14 10:59:50 +08:00
Gabe Yuan
14ca13e31d input box trans 2023-09-13 23:24:55 +08:00
Gabe Yuan
556fd71275 Merge branch 'dev' of github.com:fishjar/kiss-translator into dev 2023-09-13 22:12:08 +08:00
Gabe Yuan
a8002bba9f input box trans 2023-09-13 22:11:33 +08:00
Gabe Yuan
ddd9371fbd sync webfix interval 2023-09-13 18:02:51 +08:00
Gabe Yuan
0ea97b73e3 input box trans 2023-09-13 15:53:40 +08:00
Gabe Yuan
f8c8a4ebeb ui fix 2023-09-13 11:16:56 +08:00
Gabe Yuan
5f613ab558 sync webfix interval 2023-09-13 10:26:30 +08:00
Gabe Yuan
56281f9e82 fix save rule 2023-09-12 17:20:56 +08:00
Gabe Yuan
5e8743dbb7 change fab ui 2023-09-12 15:44:30 +08:00
Gabe Yuan
f4e4c84712 popup ui 2023-09-12 11:00:54 +08:00
Gabe Yuan
c57a0a11fa v1.7.0 2023-09-11 23:21:15 +08:00
Gabe Yuan
fa244b2097 subrules sync time 2023-09-11 22:53:04 +08:00
Gabe Yuan
79612f8a1b subrules sync time 2023-09-11 17:56:31 +08:00
Gabe Yuan
2bf79dbc51 rootSlector -> rootSelector 2023-09-11 16:12:37 +08:00
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
63 changed files with 13937 additions and 16236 deletions

9
.env
View File

@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=1.6.4
REACT_APP_VERSION=1.7.6
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
@@ -13,8 +13,11 @@ REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
REACT_APP_LOGOURL2=https://kiss-translator.rayjar.com/images/logo192.png
REACT_APP_RULESURL=https://fishjar.github.io/kiss-translator/kiss-translator-rules.json
REACT_APP_RULESURL2=https://kiss-translator.rayjar.com/kiss-translator-rules.json
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

View File

@@ -10,12 +10,15 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8.7.6
- uses: actions/setup-node@v3
with:
node-version: "18.17.0"
cache: "yarn"
- run: yarn install
- run: yarn build
cache: "pnpm"
- run: pnpm install
- run: pnpm build
- uses: actions/upload-artifact@v3
with:
name: build-artifacts

View File

@@ -1 +0,0 @@
nodeLinker: node-modules

View File

@@ -16,48 +16,78 @@ If you also like a little more simplicity, welcome to pick it up.
## Features
- Keep it simple, smart
## Schedule
- [x] Provide trial installation package
- [x] Adapt browser
- [x] Chrome
- [x] Edge
- [x] Firefox
- [ ] Safari
- [x] Kiwi
- [x] Support translation services
- [x] Google
- [x] Microsoft
- [x] OpenAI
- [x] DeepL
- [x] Upload to app Store
- [x] Chrome [Install Link](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
- [x] Edge [Install Link](https://microsoftedge.microsoft.com/addons/detail/kiss-translator/jemckldkclkinpjighnoilpbldbdmmlh)
- [x] Firefox [Install Link](https://addons.mozilla.org/en-US/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] Greasy Fork [Install Link](https://greasyfork.org/en/scripts/472840-kiss-translator)
- [x] Keep it simple, smart
- [x] Open source
- [x] Data Synchronization Function
- [x] Greasemonkey Script ([Setting Page 1](https://fishjar.github.io/kiss-translator/options.html)、[Setting Page 2](https://kiss-translator.rayjar.com/options))
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- [x] [Violentmonkey](https://violentmonkey.github.io/) (Chrome/Edge/Firefox) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
- [x] Adapt to common browsers
- [x] Chrome/Edge/Firefox/Kiwi
- [ ] Safari
- [x] Supports multiple translation services
- [x] Google/Microsoft/DeepL/OpenAI
- [x] Custom translation interface
- [x] Covers common translation scenarios
- [x] Web bilingual translation
- [x] Input box translation
- [x] Mouseover translation
- [x] YouTube subtitle translation
- [x] Cross-client data synchronization
- [x] KISS-Workercloudflare/docker
- [x] WebDAV
- [x] Custom translation rules
- [x] Rule subscription/rule sharing
- [x] Custom translation style
- [x] Custom shortcut keys
- `Alt+Q` Toggle Translation
- `Alt+C` Toggle Styles
- `Alt+K` Open Popup
- `Alt+O` Open Options
- `Alt+I` Input Box Translation
## Guide
## Install
> Note: For the following reasons, it is recommended to use browser extensions first
>
> - Browser extension can use local language recognition
> - Grease Monkey script will encounter more usage problems
- [x] Browser extension
- [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [Installation address](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] GreaseMonkey Script
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、 [Installation link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- Greasy Fork [Installation address](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [Installation link 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、 [Installation link 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
## Associated Projects
- Data synchronization service: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
- Data synchronization service available for this project.
- Can also be used to share personal private rule lists.
- Deploy by yourself, manage by yourself, data is private.
- Community subscription rules: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
- Provides the latest and most complete list of subscription rules maintained by the community.
- Help with rules-related issues.
- Web page correction script: [https://github.com/fishjar/kiss-webfixer](https://github.com/fishjar/kiss-webfixer)
- Fixed scripts for some special sites.
- So that the translation software can get better display effect.
- Translation interface agent: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
- If you encounter network problems when accessing a certain translation interface, this proxy service may help you.
- Deploy and manage by yourself.
- Minimalistic Dictionary Plugin: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
- A word-marking translation plug-in used with this project.
- Supports query of English words, sentences and Chinese characters.
- Supports history records and word collections.
## Development Guidelines
```sh
git clone https://github.com/fishjar/kiss-translator.git
cd kiss-translator
yarn install
yarn build
pnpm install
pnpm build
```
## Data Sync
Goto: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
## Discussion
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)

View File

@@ -1,6 +1,6 @@
# 简约翻译
一个简约的 [双语网页翻译扩展 & 油猴脚本](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)
@@ -14,50 +14,80 @@
如果你也喜欢简约一点的,欢迎自取。
## 特
## 特
- 保持简约
## 进度
- [x] 提供试用安装包
- [x] 适配浏览器
- [x] Chrome
- [x] Edge
- [x] Firefox
- [x] 保持简约
- [x] 开放源代码
- [x] 适配常见浏览器
- [x] Chrome/Edge/Firefox/Kiwi
- [ ] Safari
- [x] Kiwi
- [x] 支持翻译服务
- [x] Google
- [x] Microsoft
- [x] OpenAI
- [x] DeepL
- [x] 上架应用市场
- [x] 支持多种翻译服务
- [x] Google/Microsoft/DeepL/OpenAI
- [x] 自定义翻译接口
- [x] 覆盖常见翻译场景
- [x] 网页双语翻译
- [x] 输入框翻译
- [x] 鼠标悬停翻译
- [x] YouTube 字幕翻译
- [x] 跨客户端数据同步
- [x] KISS-Workercloudflare/docker
- [x] WebDAV
- [x] 自定义翻译规则
- [x] 规则订阅/规则分享
- [x] 自定义译文样式
- [x] 自定义快捷键
- `Alt+Q` 开启翻译
- `Alt+C` 切换样式
- `Alt+K` 打开弹窗
- `Alt+O` 打开设置
- `Alt+I` 输入框翻译
## 安装
> 注:基于以下原因,建议优先使用浏览器扩展
>
> - 浏览器扩展可以使用本地的语言识别
> - 油猴脚本会遇到更多使用上的问题
- [x] 浏览器扩展
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Edge [安装地址](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] Greasy Fork [安装地址](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] 开放源代码
- [x] 数据同步功能
- [x] 油猴脚本 ([设置页面 1](https://fishjar.github.io/kiss-translator/options.html)、[设置页面 2](https://kiss-translator.rayjar.com/options))
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- [x] [Violentmonkey](https://violentmonkey.github.io/) (Chrome/Edge/Firefox) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
- [x] 油猴脚本
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、 [安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- Greasy Fork [安装地址](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、 [安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
## 指引
## 关联项目
- 数据同步服务: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
- 可用于本项目的数据同步服务。
- 亦可用于分享个人的私有规则列表。
- 自己部署,自己管理,数据私有。
- 社区订阅规则: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
- 提供社区维护的,最新最全的订阅规则列表。
- 求助规则相关的问题。
- 网页修正脚本: [https://github.com/fishjar/kiss-webfixer](https://github.com/fishjar/kiss-webfixer)
- 针对一些特殊网站的修正脚本。
- 以便翻译软件得到更好的展示效果。
- 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮你到你。
- 自己部署,自己管理。
- 简约词典插件: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
- 搭配本项目一起使用的划词翻译插件。
- 支持英文单词、句子、汉字的查询。
- 支持历史记录、单词收藏。
## 开发指引
```sh
git clone https://github.com/fishjar/kiss-translator.git
cd kiss-translator
yarn install
yarn build
pnpm install
pnpm build
```
## 数据同步
移步: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
## 交流
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)

View File

@@ -75,7 +75,7 @@ const userscriptWebpack = (config, env) => {
// @name ${process.env.REACT_APP_NAME}
// @namespace ${process.env.REACT_APP_HOMEPAGE}
// @version ${process.env.REACT_APP_VERSION}
// @description A minimalist bilingual translation Extension & Greasemonkey Script (一个简约的双语网页翻译扩展 & 油猴脚本)
// @description A minimalist bilingual translation Extension & Greasemonkey Script (一个简约的网页双语翻译扩展 & 油猴脚本)
// @author Gabe<yugang2002@gmail.com>
// @homepageURL ${process.env.REACT_APP_HOMEPAGE}
// @license GPL-3.0
@@ -93,6 +93,8 @@ const userscriptWebpack = (config, env) => {
// @connect translate.googleapis.com
// @connect api-edge.cognitive.microsofttranslator.com
// @connect edge.microsoft.com
// @connect api-free.deepl.com
// @connect api.deepl.com
// @connect api.openai.com
// @connect openai.azure.com
// @connect workers.dev
@@ -100,6 +102,8 @@ const userscriptWebpack = (config, env) => {
// @connect githubusercontent.com
// @connect kiss-translator.rayjar.com
// @connect ghproxy.com
// @connect dav.jianguoyun.com
// @connect localhost:3000
// @run-at document-end
// ==/UserScript==

View File

@@ -1,22 +1,22 @@
{
"name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.6.4",
"version": "1.7.6",
"author": "Gabe<yugang2002@gmail.com>",
"private": true,
"dependencies": {
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.10.8",
"@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.12",
"@violentmonkey/shortcut": "^1.3.0",
"query-string": "^8.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.7",
"react-router-dom": "^6.10.0",
"react-scripts": "5.0.1",
"styled-components": "^6.0.7",
"webdav": "^5.3.0",
"webextension-polyfill": "^0.10.0"
},
"scripts": {
@@ -29,7 +29,7 @@
"build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
"build:rules": "babel-node src/rules.js",
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript-ios && yarn build:userscript && yarn build:rules",
"build": "pnpm build:chrome && pnpm build:edge && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
@@ -41,7 +41,8 @@
],
"globals": {
"GM": true,
"unsafeWindow": true
"unsafeWindow": true,
"globalThis": true
}
},
"browserslist": {
@@ -63,5 +64,6 @@
"@babel/preset-env": "^7.22.10",
"react-app-rewired": "^2.2.1",
"wrangler": "^3.4.0"
}
},
"packageManager": "yarn@3.6.3"
}

10738
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -64,9 +64,31 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<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>
<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>
<hr />
<input id="input1" style="width: 80%;" />
<hr />
<textarea id="textarea1" style="width: 80%;">test</textarea>
<hr />
<div id="addtitle"></div>
<h2>Shadow 1</h2>
<div id="shadow1"></div>

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.6.4",
"version": "1.7.6",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -33,6 +33,12 @@
"default": "Alt+C"
},
"description": "__MSG_toggle_style__"
},
"openOptions": {
"suggested_key": {
"default": "Alt+O"
},
"description": "__MSG_open_options__"
}
},
"permissions": ["<all_urls>", "storage"],

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.6.4",
"version": "1.7.6",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -34,6 +34,12 @@
"default": "Alt+C"
},
"description": "__MSG_toggle_style__"
},
"openOptions": {
"suggested_key": {
"default": "Alt+O"
},
"description": "__MSG_open_options__"
}
},
"permissions": ["storage"],

View File

@@ -5,7 +5,7 @@ import {
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI,
URL_MICROSOFT_TRANS,
OPT_TRANS_CUSTOMIZE,
OPT_LANGS_SPECIAL,
PROMPT_PLACE_FROM,
PROMPT_PLACE_TO,
@@ -21,7 +21,7 @@ import { sha256 } from "../libs/utils";
* @param {*} data
* @returns
*/
export const apiSyncData = async (url, key, data, isBg = false) =>
export const apiSyncData = async (url, key, data) =>
fetchPolyfill(url, {
headers: {
"Content-type": "application/json",
@@ -29,17 +29,14 @@ export const apiSyncData = async (url, key, data, isBg = false) =>
},
method: "POST",
body: JSON.stringify(data),
isBg,
});
/**
* 下载订阅规则
* 下载数据
* @param {*} url
* @param {*} isBg
* @returns
*/
export const apiFetchRules = (url, isBg = false) =>
fetchPolyfill(url, { isBg });
export const apiFetch = (url) => fetchPolyfill(url);
/**
* 谷歌翻译
@@ -48,8 +45,13 @@ export const apiFetchRules = (url, isBg = false) =>
* @param {*} from
* @returns
*/
const apiGoogleTranslate = async (translator, text, to, from, setting) => {
const { googleUrl } = setting;
const apiGoogleTranslate = async (
translator,
text,
to,
from,
{ url, key, useCache = true }
) => {
const params = {
client: "gtx",
dt: "t",
@@ -59,15 +61,20 @@ const apiGoogleTranslate = async (translator, text, to, from, setting) => {
tl: to,
q: text,
};
const input = `${googleUrl}?${queryString.stringify(params)}`;
return fetchPolyfill(input, {
const input = `${url}?${queryString.stringify(params)}`;
const res = await fetchPolyfill(input, {
headers: {
"Content-type": "application/json",
},
useCache: true,
useCache,
usePool: true,
translator,
token: key,
});
const trText = res.sentences.map((item) => item.trans).join(" ");
const isSame = to === res.src;
return [trText, isSame];
};
/**
@@ -77,23 +84,33 @@ const apiGoogleTranslate = async (translator, text, to, from, setting) => {
* @param {*} from
* @returns
*/
const apiMicrosoftTranslate = (translator, text, to, from) => {
const apiMicrosoftTranslate = async (
translator,
text,
to,
from,
{ url, useCache = true }
) => {
const params = {
from,
to,
"api-version": "3.0",
};
const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`;
return fetchPolyfill(input, {
const input = `${url}?${queryString.stringify(params)}`;
const res = await fetchPolyfill(input, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify([{ Text: text }]),
useCache: true,
useCache,
usePool: true,
translator,
});
const trText = res[0].translations[0].text;
const isSame = to === res[0].detectedLanguage?.language;
return [trText, isSame];
};
/**
@@ -103,8 +120,13 @@ const apiMicrosoftTranslate = (translator, text, to, from) => {
* @param {*} from
* @returns
*/
const apiDeepLTranslate = (translator, text, to, from, setting) => {
const { deeplUrl, deeplKey } = setting;
const apiDeepLTranslate = async (
translator,
text,
to,
from,
{ url, key, useCache = true }
) => {
const data = {
text: [text],
target_lang: to,
@@ -113,17 +135,21 @@ const apiDeepLTranslate = (translator, text, to, from, setting) => {
if (from) {
data.source_lang = from;
}
return fetchPolyfill(deeplUrl, {
const res = await fetchPolyfill(url, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
useCache: true,
useCache,
usePool: true,
translator,
token: deeplKey,
token: key,
});
const trText = res.translations.map((item) => item.text).join(" ");
const isSame = to === res.translations[0].detected_source_language;
return [trText, isSame];
};
/**
@@ -133,18 +159,23 @@ const apiDeepLTranslate = (translator, text, to, from, setting) => {
* @param {*} from
* @returns
*/
const apiOpenaiTranslate = async (translator, text, to, from, setting) => {
const { openaiUrl, openaiKey, openaiModel, openaiPrompt } = setting;
let prompt = openaiPrompt
const apiOpenaiTranslate = async (
translator,
text,
to,
from,
{ url, key, model, prompt, useCache = true }
) => {
prompt = prompt
.replaceAll(PROMPT_PLACE_FROM, from)
.replaceAll(PROMPT_PLACE_TO, to);
return fetchPolyfill(openaiUrl, {
const res = await fetchPolyfill(url, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify({
model: openaiModel,
model: model,
messages: [
{
role: "system",
@@ -158,11 +189,52 @@ const apiOpenaiTranslate = async (translator, text, to, from, setting) => {
temperature: 0,
max_tokens: 256,
}),
useCache: true,
useCache,
usePool: true,
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];
};
/**
@@ -170,38 +242,29 @@ const apiOpenaiTranslate = async (translator, text, to, from, setting) => {
* @param {*} param0
* @returns
*/
export const apiTranslate = async ({
export const apiTranslate = ({
translator,
q,
text,
fromLang,
toLang,
setting,
apiSetting,
}) => {
let trText = "";
let isSame = false;
const from = OPT_LANGS_SPECIAL[translator]?.get(fromLang) ?? fromLang;
const to = OPT_LANGS_SPECIAL[translator]?.get(toLang) ?? toLang;
const callApi = (api) => api(translator, text, to, from, apiSetting);
let from = OPT_LANGS_SPECIAL?.[translator]?.get(fromLang) ?? fromLang;
let to = OPT_LANGS_SPECIAL?.[translator]?.get(toLang) ?? toLang;
if (translator === OPT_TRANS_GOOGLE) {
const res = await apiGoogleTranslate(translator, q, to, from, setting);
trText = res.sentences.map((item) => item.trans).join(" ");
isSame = to === res.src;
} else if (translator === OPT_TRANS_MICROSOFT) {
const res = await apiMicrosoftTranslate(translator, q, to, from);
trText = res[0].translations[0].text;
isSame = to === res[0].detectedLanguage.language;
} else if (translator === OPT_TRANS_DEEPL) {
const res = await apiDeepLTranslate(translator, q, to, from, setting);
trText = res.translations.map((item) => item.text).join(" ");
isSame = to === res.translations[0].detected_source_language;
} else if (translator === OPT_TRANS_OPENAI) {
const res = await apiOpenaiTranslate(translator, q, to, from, setting);
trText = res?.choices?.[0].message.content;
const sLang = await tryDetectLang(q);
const tLang = await tryDetectLang(trText);
isSame = q === trText || (sLang && tLang && sLang === tLang);
switch (translator) {
case OPT_TRANS_GOOGLE:
return callApi(apiGoogleTranslate);
case OPT_TRANS_MICROSOFT:
return callApi(apiMicrosoftTranslate);
case OPT_TRANS_DEEPL:
return callApi(apiDeepLTranslate);
case OPT_TRANS_OPENAI:
return callApi(apiOpenaiTranslate);
case OPT_TRANS_CUSTOMIZE:
return callApi(apiCustomTranslate);
default:
return ["", false];
}
return [trText, isSame];
};

View File

@@ -4,9 +4,12 @@ import {
MSG_FETCH_LIMIT,
MSG_FETCH_CLEAR,
MSG_TRANS_TOGGLE,
MSG_OPEN_OPTIONS,
MSG_SAVE_RULE,
MSG_TRANS_TOGGLE_STYLE,
CMD_TOGGLE_TRANSLATE,
CMD_TOGGLE_STYLE,
CMD_OPEN_OPTIONS,
} from "./config";
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
import { trySyncSettingAndRules } from "./libs/sync";
@@ -14,6 +17,9 @@ import { fetchData, fetchPool } from "./libs/fetch";
import { sendTabMsg } from "./libs/msg";
import { trySyncAllSubRules } from "./libs/subRules";
import { tryClearCaches } from "./libs";
import { saveRule } from "./libs/rules";
globalThis.ContextType = "BACKGROUND";
/**
* 插件安装
@@ -29,7 +35,7 @@ browser.runtime.onStartup.addListener(async () => {
console.log("browser onStartup");
// 同步数据
await trySyncSettingAndRules(true);
await trySyncSettingAndRules();
// 清除缓存
const setting = await getSettingWithDefault();
@@ -38,7 +44,7 @@ browser.runtime.onStartup.addListener(async () => {
}
// 同步订阅规则
trySyncAllSubRules(setting, true);
trySyncAllSubRules(setting);
});
/**
@@ -66,6 +72,12 @@ browser.runtime.onMessage.addListener(
fetchPool.clear();
sendResponse({ data: "ok" });
break;
case MSG_OPEN_OPTIONS:
browser.runtime.openOptionsPage();
break;
case MSG_SAVE_RULE:
saveRule(args);
break;
default:
sendResponse({ error: `message action is unavailable: ${action}` });
}
@@ -85,6 +97,9 @@ browser.commands.onCommand.addListener((command) => {
case CMD_TOGGLE_STYLE:
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
break;
case CMD_OPEN_OPTIONS:
browser.runtime.openOptionsPage();
break;
default:
}
});

View File

@@ -3,6 +3,101 @@ export const UI_LANGS = [
["zh", "中文"],
];
const customApiLangs = `["en", "English - English"],
["zh-CN", "Simplified Chinese - 简体中文"],
["zh-TW", "Traditional Chinese - 繁體中文"],
["ar", "Arabic - العربية"],
["bg", "Bulgarian - Български"],
["ca", "Catalan - Català"],
["hr", "Croatian - Hrvatski"],
["cs", "Czech - Čeština"],
["da", "Danish - Dansk"],
["nl", "Dutch - Nederlands"],
["fi", "Finnish - Suomi"],
["fr", "French - Français"],
["de", "German - Deutsch"],
["el", "Greek - Ελληνικά"],
["hi", "Hindi - हिन्दी"],
["hu", "Hungarian - Magyar"],
["id", "Indonesian - Indonesia"],
["it", "Italian - Italiano"],
["ja", "Japanese - 日本語"],
["ko", "Korean - 한국어"],
["ms", "Malay - Melayu"],
["mt", "Maltese - Malti"],
["nb", "Norwegian - Norsk Bokmål"],
["pl", "Polish - Polski"],
["pt", "Portuguese - Português"],
["ro", "Romanian - Română"],
["ru", "Russian - Русский"],
["sk", "Slovak - Slovenčina"],
["sl", "Slovenian - Slovenščina"],
["es", "Spanish - Español"],
["sv", "Swedish - Svenska"],
["ta", "Tamil - தமிழ்"],
["te", "Telugu - తెలుగు"],
["th", "Thai - ไทย"],
["tr", "Turkish - Türkçe"],
["uk", "Ukrainian - Українська"],
["vi", "Vietnamese - Tiếng Việt"],
`;
const customApiHelpZH = `/// 自定义翻译源接口说明
// 请求Request数据将按下面规范发送
{
url: {{YOUR_URL}},
method: "POST",
headers: {
"Content-type": "application/json",
"Authorization": "Bearer {{YOUR_KEY}}",
},
body: {
text: "", // 需要翻译的文字
from: "", // 源语言,可能为空,表示需要接口自动识别语言
to: "", // 目标语言
}
}
// 返回Response数据需符合下面的JSON规范
{
text: "", // 翻译后的文字
from: "", // 识别的源语言
to: "", // 目标语言(可选)
}
// 支持的语言代码如下
${customApiLangs}
`;
const customApiHelpEN = `/// Custom translation source interface description
// Request data will be sent according to the following specifications
{
url: {{YOUR_URL}},
method: "POST",
headers: {
"Content-type": "application/json",
"Authorization": "Bearer {{YOUR_KEY}}",
},
body: {
text: "", // text to be translated
from: "", // Source language, may be empty
to: "", // Target language
}
}
// The returned data must conform to the following JSON specification
{
text: "", // translated text
from: "", // Recognized source language
to: "", // Target language (optional)
}
// The supported language codes are as follows
${customApiLangs}
`;
export const I18N = {
app_name: {
zh: `简约翻译`,
@@ -12,6 +107,10 @@ export const I18N = {
zh: `翻译`,
en: `Translate`,
},
custom_api_help: {
zh: customApiHelpZH,
en: customApiHelpEN,
},
translate_alt: {
zh: `翻译 (Alt+Q)`,
en: `Translate (Alt+Q)`,
@@ -24,10 +123,26 @@ export const I18N = {
zh: `规则设置`,
en: `Rules Setting`,
},
apis_setting: {
zh: `接口设置`,
en: `Apis Setting`,
},
sync_setting: {
zh: `同步设置`,
en: `Sync Setting`,
},
patch_setting: {
zh: `补丁设置`,
en: `Patch Setting`,
},
patch_setting_help: {
zh: `针对一些特殊网站的修正脚本,以便翻译软件得到更好的展示效果。`,
en: `Corrected scripts for some special websites so that the translation software can get better display results.`,
},
inject_webfix: {
zh: `注入修复补丁`,
en: `Inject Webfix`,
},
about: {
zh: `关于`,
en: `About`,
@@ -68,6 +183,30 @@ export const I18N = {
zh: `翻译服务`,
en: `Translate Service`,
},
mouseover_translation: {
zh: `鼠标悬停翻译`,
en: `Mouseover translation`,
},
mk_disable: {
zh: `禁用`,
en: `Disable`,
},
mk_mouseover: {
zh: `鼠标悬停`,
en: `Mouseover`,
},
mk_ctrlKey: {
zh: `Control + 鼠标悬停`,
en: `Control + Mouseover`,
},
mk_shiftKey: {
zh: `Shift + 鼠标悬停`,
en: `Shift + Mouseover`,
},
mk_altKey: {
zh: `Alt + 鼠标悬停`,
en: `Alt + Mouseover`,
},
from_lang: {
zh: `原文语言`,
en: `Source Language`,
@@ -134,15 +273,15 @@ export const I18N = {
},
personal_rules: {
zh: `个人规则`,
en: `Personal Rules`,
en: `Rules`,
},
subscribe_rules: {
zh: `订阅规则`,
en: `Subscribe Rules`,
en: `Subscribe`,
},
overwrite_subscribe_rules: {
zh: `覆写订阅规则`,
en: `Overwrite Subscribe Rules`,
en: `Overwrite`,
},
subscribe_url: {
zh: `订阅地址`,
@@ -201,12 +340,12 @@ export const I18N = {
en: `Custom Style`,
},
diy_style_helper: {
zh: `遵循“styled-components”的语法`,
en: `Follow the syntax of "styled-components"`,
zh: `遵循“CSS”的语法`,
en: `Follow the syntax of "CSS"`,
},
setting: {
zh: `设置`,
en: `Setting`,
zh: `设置 (Alt+O)`,
en: `Setting (Alt+O)`,
},
pattern: {
zh: `匹配网址`,
@@ -284,7 +423,7 @@ export const I18N = {
zh: `OpenAI 提示词`,
en: `OpenAI Prompt`,
},
clear_cache: {
if_clear_cache: {
zh: `是否清除缓存`,
en: `Whether clear cache`,
},
@@ -296,32 +435,164 @@ export const I18N = {
zh: `重启浏览器时清除缓存`,
en: `Clear cache when restarting browser`,
},
data_sync_type: {
zh: `数据同步方式`,
en: `Data Sync Type`,
},
data_sync_url: {
zh: `数据同步接口`,
en: `Data Sync API`,
},
data_sync_user: {
zh: `数据同步账户`,
en: `Data Sync User`,
},
data_sync_key: {
zh: `数据同步密钥`,
en: `Data Sync Key`,
},
data_sync_test: {
zh: `数据同步测试`,
en: `Data Sync Test`,
sync_now: {
zh: `立即同步`,
en: `Sync Now`,
},
data_sync_success: {
zh: `数据同步成功!`,
en: `Data Sync Success`,
sync_success: {
zh: `同步成功!`,
en: `Sync Success`,
},
data_sync_error: {
zh: `数据同步失败!`,
en: `Data Sync Error`,
sync_failed: {
zh: `同步失败!`,
en: `Sync Error`,
},
error_got_some_wrong: {
zh: `抱歉,出错了!`,
en: `Sorry, something went wrong!`,
},
error_sync_setting: {
zh: `您的同步设置未填写,无法在线分享。`,
en: `Your sync settings are missing and cannot be shared online.`,
zh: `您的同步类型必须为“KISS-Worker”且需填写完整`,
en: `Your sync type must be "KISS-Worker" and must be filled in completely`,
},
click_test: {
zh: `点击测试`,
en: `Click Test`,
},
test_success: {
zh: `测试成功`,
en: `Test success`,
},
test_failed: {
zh: `测试失败`,
en: `Test failed`,
},
clear_all_cache_now: {
zh: `立即清除全部缓存`,
en: `Clear all cache now`,
},
clear_cache: {
zh: `清除缓存`,
en: `Clear Cache`,
},
clear_success: {
zh: `清除成功`,
en: `Clear success`,
},
clear_failed: {
zh: `清除失败`,
en: `Clear failed`,
},
share: {
zh: `分享`,
en: `Share`,
},
clear_all: {
zh: `清空`,
en: `Clear All`,
},
help: {
zh: `求助`,
en: `Help`,
},
restore_default: {
zh: `恢复默认`,
en: `Restore Default`,
},
shortcuts_setting: {
zh: `快捷键设置`,
en: `Shortcuts Setting`,
},
toggle_translate_shortcut: {
zh: `"开启翻译"快捷键`,
en: `"Toggle Translate" Shortcut`,
},
toggle_style_shortcut: {
zh: `"切换样式"快捷键`,
en: `"Toggle Style" Shortcut`,
},
toggle_popup_shortcut: {
zh: `"打开弹窗"快捷键`,
en: `"Open Popup" Shortcut`,
},
open_setting_shortcut: {
zh: `"打开设置"快捷键`,
en: `"Open Setting" Shortcut`,
},
hide_fab_button: {
zh: `隐藏悬浮按钮`,
en: `Hide Fab Button`,
},
show: {
zh: `显示`,
en: `Show`,
},
hide: {
zh: `隐藏`,
en: `Hide`,
},
save_rule: {
zh: `保存规则`,
en: `Save Rule`,
},
global_rule: {
zh: `全局规则`,
en: `Global Rule`,
},
input_setting: {
zh: `输入框设置`,
en: `Input Box Setting`,
},
input_box_translation: {
zh: `启用输入框翻译`,
en: `Input Box Translation`,
},
input_selector: {
zh: `输入框选择器`,
en: `Input Selector`,
},
input_selector_helper: {
zh: `用于输入框翻译。`,
en: `Used for input box translation.`,
},
trigger_trans_shortcut: {
zh: `触发翻译快捷键`,
en: `Trigger Translation Shortcut Keys`,
},
trigger_trans_shortcut_help: {
zh: `默认为单击“AltLeft+KeyI”`,
en: `Default is "AltLeft+KeyI"`,
},
shortcut_press_count: {
zh: `快捷键连击次数`,
en: `Shortcut Press Number`,
},
combo_timeout: {
zh: `连击超时时间 (10-1000ms)`,
en: `Combo Timeout (10-1000ms)`,
},
input_trans_start_sign: {
zh: `翻译起始标识`,
en: `Translation Start Sign`,
},
input_trans_start_sign_help: {
zh: `标识后面可以加目标语言代码,如: “/en 你好”、“/zh hello”`,
en: `The target language code can be added after the sign, such as: "/en 你好", "/zh hello"`,
},
};

View File

@@ -25,9 +25,11 @@ export const STOKEY_RULES = `${APP_NAME}_rules`;
export const STOKEY_SYNC = `${APP_NAME}_sync`;
export const STOKEY_FAB = `${APP_NAME}_fab`;
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
export const STOKEY_WEBFIXCACHE_PREFIX = `${APP_NAME}_webfixcache_`;
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
export const CMD_TOGGLE_STYLE = "toggleStyle";
export const CMD_OPEN_OPTIONS = "openOptions";
export const CLIENT_WEB = "web";
export const CLIENT_CHROME = "chrome";
@@ -36,9 +38,9 @@ export const CLIENT_FIREFOX = "firefox";
export const CLIENT_USERSCRIPT = "userscript";
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
export const KV_RULES_KEY = "KT_RULES";
export const KV_RULES_SHARE_KEY = "KT_RULES_SHARE";
export const KV_SETTING_KEY = "KT_SETTING";
export const KV_RULES_KEY = "kiss-rules.json";
export const KV_RULES_SHARE_KEY = "kiss-rules-share.json";
export const KV_SETTING_KEY = "kiss-setting.json";
export const KV_SALT_SYNC = "KISS-Translator-SYNC";
export const KV_SALT_SHARE = "KISS-Translator-SHARE";
@@ -47,6 +49,8 @@ export const CACHE_NAME = `${APP_NAME}_cache`;
export const MSG_FETCH = "fetch";
export const MSG_FETCH_LIMIT = "fetch_limit";
export const MSG_FETCH_CLEAR = "fetch_clear";
export const MSG_OPEN_OPTIONS = "open_options";
export const MSG_SAVE_RULE = "save_rule";
export const MSG_TRANS_TOGGLE = "trans_toggle";
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
export const MSG_TRANS_GETRULE = "trans_getrule";
@@ -58,21 +62,24 @@ export const THEME_DARK = "dark";
export const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker";
export const URL_KISS_PROXY = "https://github.com/fishjar/kiss-proxy";
export const URL_KISS_RULES = "https://github.com/fishjar/kiss-rules";
export const URL_KISS_RULES_NEW_ISSUE =
"https://github.com/fishjar/kiss-rules/issues/new";
export const URL_RAW_PREFIX =
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
export const URL_MICROSOFT_TRANS =
"https://api-edge.cognitive.microsofttranslator.com/translate";
export const OPT_TRANS_GOOGLE = "Google";
export const OPT_TRANS_MICROSOFT = "Microsoft";
export const OPT_TRANS_DEEPL = "DeepL";
export const OPT_TRANS_OPENAI = "OpenAI";
export const OPT_TRANS_CUSTOMIZE = "Custom";
export const OPT_TRANS_ALL = [
OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI,
OPT_TRANS_CUSTOMIZE,
];
export const OPT_LANGS_TO = [
@@ -130,7 +137,9 @@ export const OPT_LANGS_SPECIAL = {
[OPT_TRANS_OPENAI]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_CUSTOMIZE]: new Map([["auto", ""]]),
};
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
export const OPT_STYLE_NONE = "style_none"; // 无
export const OPT_STYLE_LINE = "under_line"; // 下划线
@@ -158,6 +167,19 @@ export const OPT_STYLE_USE_COLOR = [
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_INTERVAL = 100; // 默认任务间隔时间
@@ -179,21 +201,77 @@ export const GLOBLA_RULE = {
textDiyStyle: "",
};
// 输入框翻译
export const OPT_INPUT_TRANS_SIGNS = ["/", "//", "\\", "\\\\", ">", ">>"];
export const DEFAULT_INPUT_SHORTCUT = ["AltLeft", "KeyI"];
export const DEFAULT_INPUT_RULE = {
transOpen: true,
translator: OPT_TRANS_MICROSOFT,
fromLang: "auto",
toLang: "en",
triggerShortcut: DEFAULT_INPUT_SHORTCUT,
triggerCount: 1,
triggerTime: 200,
transSign: OPT_INPUT_TRANS_SIGNS[0],
};
// 订阅列表
export const DEFAULT_SUBRULES_LIST = [
{
url: process.env.REACT_APP_RULESURL,
selected: false,
},
{
url: process.env.REACT_APP_RULESURL_ON,
selected: true,
},
{
url: process.env.REACT_APP_RULESURL2,
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]: ["AltLeft", "KeyQ"],
[OPT_SHORTCUT_STYLE]: ["AltLeft", "KeyC"],
[OPT_SHORTCUT_POPUP]: ["AltLeft", "KeyK"],
[OPT_SHORTCUT_SETTING]: ["AltLeft", "KeyN"],
};
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
export const TRANS_NEWLINE_LENGTH = 40; // 换行字符数
export const TRANS_NEWLINE_LENGTH = 20; // 换行字符数
export const DEFAULT_SETTING = {
darkMode: false, // 深色模式
@@ -205,25 +283,27 @@ export const DEFAULT_SETTING = {
newlineLength: TRANS_NEWLINE_LENGTH,
clearCache: false, // 是否在浏览器下次启动时清除缓存
injectRules: true, // 是否注入订阅规则
injectWebfix: true, // 是否注入修复补丁
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口
deeplUrl: "https://api-free.deepl.com/v2/translate",
deeplKey: "",
openaiUrl: "https://api.openai.com/v1/chat/completions",
openaiKey: "",
openaiModel: "gpt-4",
openaiPrompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
transApis: DEFAULT_TRANS_APIS, // 翻译接口
mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
};
export const DEFAULT_RULES = [GLOBLA_RULE];
export const OPT_SYNCTYPE_WORKER = "KISS-Worker";
export const OPT_SYNCTYPE_WEBDAV = "WebDAV";
export const OPT_SYNCTYPE_ALL = [OPT_SYNCTYPE_WORKER, OPT_SYNCTYPE_WEBDAV];
export const DEFAULT_SYNC = {
syncType: OPT_SYNCTYPE_WORKER, // 同步方式
syncUrl: "", // 数据同步接口
syncUser: "", // 数据同步用户名
syncKey: "", // 数据同步密钥
settingUpdateAt: 0,
settingSyncAt: 0,
rulesUpdateAt: 0,
rulesSyncAt: 0,
syncMeta: {}, // 数据更新及同步信息
subRulesSyncAt: 0, // 订阅规则同步时间
dataCaches: {}, // 缓存同步时间
};

View File

@@ -1,4 +1,4 @@
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})`;
@@ -177,7 +177,9 @@ const RULES = [
},
];
export const BUILTIN_RULES = RULES.map((item) => ({
export const BUILTIN_RULES = RULES.sort((a, b) =>
a.pattern.localeCompare(b.pattern)
).map((item) => ({
...DEFAULT_RULE,
...item,
transOpen: "true",

View File

@@ -1,53 +1,129 @@
import { browser } from "./libs/browser";
import React from "react";
import ReactDOM from "react-dom/client";
import Action from "./views/Action";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import {
MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
APP_LCNAME,
} from "./config";
import { getSettingWithDefault, getRulesWithDefault } from "./libs/storage";
import {
getSettingWithDefault,
getRulesWithDefault,
getFabWithDefault,
} from "./libs/storage";
import { Translator } from "./libs/translator";
import { isIframe } from "./libs/iframe";
import { isIframe, sendIframeMsg, sendPrentMsg } from "./libs/iframe";
import { matchRule } from "./libs/rules";
import { webfix } from "./libs/webfix";
/**
* 入口函数
*/
const init = async () => {
const href = isIframe ? document.referrer : document.location.href;
const setting = await getSettingWithDefault();
if (isIframe) {
let translator;
window.addEventListener("message", (e) => {
const { action, args } = e.data || {};
switch (action) {
case MSG_TRANS_TOGGLE:
translator?.toggle();
break;
case MSG_TRANS_TOGGLE_STYLE:
translator?.toggleStyle();
break;
case MSG_TRANS_PUTRULE:
if (!translator) {
translator = new Translator(args, setting);
} else {
translator.updateRule(args || {});
}
break;
default:
}
});
sendPrentMsg(MSG_TRANS_GETRULE);
return;
}
const href = document.location.href;
const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);
webfix(href, setting);
// 监听消息
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) {
case MSG_TRANS_TOGGLE:
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
break;
case MSG_TRANS_TOGGLE_STYLE:
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
break;
case MSG_TRANS_GETRULE:
break;
case MSG_TRANS_PUTRULE:
translator.updateRule(args);
sendIframeMsg(MSG_TRANS_PUTRULE, args);
break;
default:
return { error: `message action is unavailable: ${action}` };
}
return { data: translator.rule };
});
window.addEventListener("message", (e) => {
const { action } = e.data || {};
switch (action) {
case MSG_TRANS_GETRULE:
sendIframeMsg(MSG_TRANS_PUTRULE, rule);
break;
default:
}
});
// 浮球按钮
const fab = await getFabWithDefault();
if (!fab.isHide) {
const $action = document.createElement("div");
$action.setAttribute("id", APP_LCNAME);
document.body.parentElement.appendChild($action);
const shadowContainer = $action.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style");
const shadowRootElement = document.createElement("div");
shadowContainer.appendChild(emotionRoot);
shadowContainer.appendChild(shadowRootElement);
const cache = createCache({
key: APP_LCNAME,
prepend: true,
container: emotionRoot,
});
ReactDOM.createRoot(shadowRootElement).render(
<React.StrictMode>
<CacheProvider value={cache}>
<Action translator={translator} fab={fab} />
</CacheProvider>
</React.StrictMode>
);
}
};
(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; z-index:10000;";
$err.style.cssText = "background:red; color:#fff;";
document.body.prepend($err);
}
})();

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

11
src/hooks/Fab.js Normal file
View File

@@ -0,0 +1,11 @@
import { STOKEY_FAB } from "../config";
import { useStorage } from "./Storage";
/**
* fab hook
* @returns
*/
export function useFab() {
const { data, update } = useStorage(STOKEY_FAB);
return { fab: data, updateFab: update };
}

18
src/hooks/InputRule.js Normal file
View File

@@ -0,0 +1,18 @@
import { useCallback } from "react";
import { DEFAULT_INPUT_RULE } from "../config";
import { useSetting } from "./Setting";
export function useInputRule() {
const { setting, updateSetting } = useSetting();
const inputRule = setting?.inputRule || DEFAULT_INPUT_RULE;
const updateInputRule = useCallback(
async (obj) => {
Object.assign(inputRule, obj);
await updateSetting({ inputRule });
},
[inputRule, updateSetting]
);
return { inputRule, updateInputRule };
}

View File

@@ -1,9 +1,9 @@
import { STOKEY_RULES, DEFAULT_RULES } from "../config";
import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
import { useStorage } from "./Storage";
import { trySyncRules } from "../libs/sync";
import { useSync } from "./Sync";
import { checkRules } from "../libs/rules";
import { useCallback } from "react";
import { useSyncMeta } from "./Sync";
/**
* 规则 hook
@@ -11,19 +11,15 @@ import { useCallback } from "react";
*/
export function useRules() {
const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
const {
sync: { rulesUpdateAt },
updateSync,
} = useSync();
const { updateSyncMeta } = useSyncMeta();
const updateRules = useCallback(
async (rules) => {
const updateAt = rulesUpdateAt ? Date.now() : 0;
await save(rules);
await updateSync({ rulesUpdateAt: updateAt });
await updateSyncMeta(KV_RULES_KEY);
trySyncRules();
},
[rulesUpdateAt, save, updateSync]
[save, updateSyncMeta]
);
const add = useCallback(
@@ -53,6 +49,12 @@ export function useRules() {
[list, updateRules]
);
const clear = useCallback(async () => {
let rules = [...list];
rules = rules.filter((item) => item.pattern === "*");
await updateRules(rules);
}, [list, updateRules]);
const put = useCallback(
async (pattern, obj) => {
const rules = [...list];
@@ -85,5 +87,5 @@ export function useRules() {
[list, updateRules]
);
return { list, add, del, put, merge };
return { list, add, del, clear, put, merge };
}

View File

@@ -1,9 +1,9 @@
import { STOKEY_SETTING, DEFAULT_SETTING } from "../config";
import { STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY } from "../config";
import { useStorage } from "./Storage";
import { useSync } from "./Sync";
import { trySyncSetting } from "../libs/sync";
import { createContext, useCallback, useContext, useMemo } from "react";
import { debounce } from "../libs/utils";
import { useSyncMeta } from "./Sync";
const SettingContext = createContext({
setting: null,
@@ -13,10 +13,7 @@ const SettingContext = createContext({
export function SettingProvider({ children }) {
const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
const {
sync: { settingUpdateAt },
updateSync,
} = useSync();
const { updateSyncMeta } = useSyncMeta();
const syncSetting = useMemo(
() =>
@@ -28,14 +25,17 @@ export function SettingProvider({ children }) {
const updateSetting = useCallback(
async (obj) => {
const updateAt = settingUpdateAt ? Date.now() : 0;
await update(obj);
await updateSync({ settingUpdateAt: updateAt });
await updateSyncMeta(KV_SETTING_KEY);
syncSetting();
},
[settingUpdateAt, update, updateSync, syncSetting]
[update, syncSetting, updateSyncMeta]
);
if (!data) {
return;
}
return (
<SettingContext.Provider
value={{

19
src/hooks/Shortcut.js Normal file
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,8 +1,9 @@
import { useCallback, useEffect, useState } from "react";
import { storage } from "../libs/storage";
export function useStorage(key, defaultVal = null) {
const [data, setData] = useState(defaultVal);
export function useStorage(key, defaultVal) {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const save = useCallback(
async (val) => {
@@ -14,7 +15,7 @@ export function useStorage(key, defaultVal = null) {
const update = useCallback(
async (obj) => {
setData((pre) => ({ ...pre, ...obj }));
setData((pre = {}) => ({ ...pre, ...obj }));
await storage.putObj(key, obj);
},
[key]
@@ -26,19 +27,37 @@ export function useStorage(key, defaultVal = null) {
}, [key]);
const reload = useCallback(async () => {
const val = await storage.getObj(key);
if (val) {
setData(val);
} else if (defaultVal) {
await storage.setObj(key, defaultVal);
try {
setLoading(true);
const val = await storage.getObj(key);
if (val) {
setData(val);
}
} catch (err) {
console.log("[storage reload]", err.message);
} finally {
setLoading(false);
}
}, [key, defaultVal]);
}, [key]);
useEffect(() => {
(async () => {
await reload();
try {
setLoading(true);
const val = await storage.getObj(key);
if (val) {
setData(val);
} else if (defaultVal) {
setData(defaultVal);
await storage.setObj(key, defaultVal);
}
} catch (err) {
console.log("[storage load]", err.message);
} finally {
setLoading(false);
}
})();
}, [reload]);
}, [key, defaultVal]);
return { data, save, update, remove, reload };
return { data, save, update, remove, reload, loading };
}

View File

@@ -32,6 +32,19 @@ export function useSubRules() {
[list, updateSetting]
);
const updateSub = useCallback(
async (url, obj) => {
const subrulesList = [...list];
subrulesList.forEach((item) => {
if (item.url === url) {
Object.assign(item, obj);
}
});
await updateSetting({ subrulesList });
},
[list, updateSetting]
);
const addSub = useCallback(
async (url) => {
const subrulesList = [...list];
@@ -70,6 +83,7 @@ export function useSubRules() {
return {
subList: list,
selectSub,
updateSub,
addSub,
delSub,
selectedSub,

View File

@@ -1,3 +1,4 @@
import { useCallback } from "react";
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
import { useStorage } from "./Storage";
@@ -6,6 +7,57 @@ import { useStorage } from "./Storage";
* @returns
*/
export function useSync() {
const { data, update } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
return { sync: data, updateSync: update };
const { data, update, reload } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
return { sync: data, updateSync: update, reloadSync: reload };
}
/**
* update syncmeta hook
* @returns
*/
export function useSyncMeta() {
const { sync, updateSync } = useSync();
const updateSyncMeta = useCallback(
async (key) => {
const syncMeta = sync?.syncMeta || {};
syncMeta[key] = { ...(syncMeta[key] || {}), updateAt: Date.now() };
await updateSync({ syncMeta });
},
[sync?.syncMeta, updateSync]
);
return { updateSyncMeta };
}
/**
* caches sync hook
* @param {*} url
* @returns
*/
export function useSyncCaches() {
const { sync, updateSync, reloadSync } = useSync();
const updateDataCache = useCallback(
async (url) => {
const dataCaches = sync?.dataCaches || {};
dataCaches[url] = Date.now();
await updateSync({ dataCaches });
},
[sync, updateSync]
);
const deleteDataCache = useCallback(
async (url) => {
const dataCaches = sync?.dataCaches || {};
delete dataCaches[url];
await updateSync({ dataCaches });
},
[sync, updateSync]
);
return {
dataCaches: sync?.dataCaches || {},
updateDataCache,
deleteDataCache,
reloadSync,
};
}

View File

@@ -2,6 +2,7 @@ import { useEffect } from "react";
import { useState } from "react";
import { tryDetectLang } from "../libs";
import { apiTranslate } from "../apis";
import { DEFAULT_TRANS_APIS } from "../config";
/**
* 翻译hook
@@ -28,10 +29,10 @@ export function useTranslate(q, rule, setting) {
} else {
const [trText, isSame] = await apiTranslate({
translator,
q,
text: q,
fromLang,
toLang,
setting,
apiSetting: (setting.transApis || DEFAULT_TRANS_APIS)[translator],
});
setText(trText);
setSamelang(isSame);

View File

@@ -32,25 +32,23 @@ function App() {
href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider>
<Stack spacing={2} direction="row" useFlexGap flexWrap="wrap">
<Stack spacing={2}>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
Install Userscript 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install Userscript 2
Install/Update Userscript for Tampermonkey/Violentmonkey
</Link>
{/* <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install/Update Userscript for Tampermonkey/Violentmonkey 2
</Link> */}
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install Userscript Safari 1
Install/Update Userscript for iOS Safari
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install Userscript Safari 2
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
Open Options Page 1
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
{/* <Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install/Update Userscript for iOS Safari 2
</Link> */}
<Link href={process.env.REACT_APP_OPTIONSPAGE}>Open Options Page</Link>
{/* <Link href={process.env.REACT_APP_OPTIONSPAGE2}>
Open Options Page 2
</Link>
</Link> */}
</Stack>
{loading ? (

View File

@@ -13,3 +13,5 @@ function _browser() {
}
export const browser = _browser();
export const isBg = () => globalThis?.ContextType === "BACKGROUND";

View File

@@ -13,6 +13,7 @@ import {
DEFAULT_FETCH_LIMIT,
} from "../config";
import { msAuth } from "./auth";
import { isBg } from "./browser";
/**
* 油猴脚本的请求封装
@@ -27,19 +28,21 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
url: input,
headers,
data: body,
onload: (response) => {
if (response.status === 200) {
const headers = new Headers();
response.responseHeaders.split("\n").forEach((line) => {
const [name, value] = line.split(":").map((item) => item.trim());
if (name && value) {
headers.append(name, value);
}
});
resolve(new Response(response.response, { headers }));
} else {
reject(new Error(`[${response.status}] ${response.responseText}`));
}
onload: ({ response, responseHeaders, status, statusText }) => {
const headers = new Headers();
responseHeaders.split("\n").forEach((line) => {
const [name, value] = line.split(":").map((item) => item.trim());
if (name && value) {
headers.append(name, value);
}
});
resolve(
new Response(response, {
headers,
status,
statusText,
})
);
},
onerror: reject,
});
@@ -66,14 +69,16 @@ const newCacheReq = async (request) => {
* @param {*} param0
* @returns
*/
const fetchApi = async ({ input, init = {}, translator, token }) => {
if (translator === OPT_TRANS_MICROSOFT) {
init.headers["Authorization"] = `Bearer ${token}`; // Microsoft
} else if (translator === OPT_TRANS_DEEPL) {
init.headers["Authorization"] = `DeepL-Auth-Key ${token}`; // DeepL
} else if (translator === OPT_TRANS_OPENAI) {
init.headers["Authorization"] = `Bearer ${token}`; // OpenAI
init.headers["api-key"] = token; // Azure OpenAI
export const fetchApi = async ({ input, init = {}, translator, token }) => {
if (token) {
if (translator === OPT_TRANS_DEEPL) {
init.headers["Authorization"] = `DeepL-Auth-Key ${token}`; // DeepL
} else if (translator === OPT_TRANS_OPENAI) {
init.headers["Authorization"] = `Bearer ${token}`; // OpenAI
init.headers["api-key"] = token; // Azure OpenAI
} else {
init.headers["Authorization"] = `Bearer ${token}`; // Microsoft & others
}
}
if (isGm) {
@@ -83,7 +88,9 @@ const fetchApi = async ({ input, init = {}, translator, token }) => {
} else {
info = GM.info;
}
const connects = info?.script?.connects || [];
// Tampermonkey --> .connects
// Violentmonkey --> .connect
const connects = info?.script?.connects || info?.script?.connect || [];
const url = new URL(input);
const isSafe = connects.find((item) => url.hostname.endsWith(item));
if (isSafe) {
@@ -172,9 +179,13 @@ export const fetchData = async (
* @param {*} opts
* @returns
*/
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
export const fetchPolyfill = async (input, opts) => {
if (!input.trim()) {
throw new Error("URL is empty");
}
// 插件
if (isExt && !isBg) {
if (isExt && !isBg()) {
const res = await sendBgMsg(MSG_FETCH, { input, opts });
if (res.error) {
throw new Error(res.error);

View File

@@ -5,3 +5,7 @@ export const sendIframeMsg = (action, args) => {
iframe.contentWindow.postMessage({ action, args }, "*");
});
};
export const sendPrentMsg = (action, args) => {
window.parent.postMessage({ action, args }, "*");
};

View File

@@ -19,3 +19,12 @@ export const sendTabMsg = async (action, args) => {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
return browser.tabs.sendMessage(tabs[0].id, { action, args });
};
/**
* 获取当前tab信息
* @returns
*/
export const getTabInfo = async () => {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
return tabs[0];
};

View File

@@ -11,6 +11,8 @@ import {
DEFAULT_OW_RULE,
} from "../config";
import { loadOrFetchSubRules } from "./subRules";
import { getRulesWithDefault, setRules } from "./storage";
import { trySyncRules } from "./sync";
/**
* 根据href匹配规则
@@ -47,9 +49,8 @@ export const matchRule = async (
mixRule[key] = val;
});
const subRules = (await loadOrFetchSubRules(selectedSub.url)).map(
(item) => ({ ...item, ...mixRule })
);
let subRules = await loadOrFetchSubRules(selectedSub.url);
subRules = subRules.map((item) => ({ ...item, ...mixRule }));
rules.splice(-1, 0, ...subRules);
}
} catch (err) {
@@ -60,32 +61,25 @@ export const matchRule = async (
const rule = rules.find((r) =>
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
);
const globalRule =
rules.find((r) =>
r.pattern.split(",").some((p) => p.trim() === GLOBAL_KEY)
) || GLOBLA_RULE;
const globalRule = rules.find((r) => r.pattern === GLOBAL_KEY) || GLOBLA_RULE;
if (!rule) {
return globalRule;
}
rule.selector =
rule?.selector?.trim() ||
globalRule?.selector?.trim() ||
GLOBLA_RULE.selector;
rule.bgColor = rule?.bgColor?.trim() || globalRule?.bgColor?.trim();
rule.textDiyStyle =
rule?.textDiyStyle?.trim() || globalRule?.textDiyStyle?.trim();
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach(
(key) => {
if (rule[key] === GLOBAL_KEY) {
rule[key] = globalRule[key];
}
rule.selector = rule.selector?.trim() || globalRule.selector;
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;
};
@@ -141,3 +135,19 @@ export const checkRules = (rules) => {
return rules;
};
/**
* 保存或更新rule
* @param {*} newRule
*/
export const saveRule = async (newRule) => {
const rules = await getRulesWithDefault();
const rule = rules.find((item) => isMatch(newRule.pattern, item.pattern));
if (rule && rule.pattern !== GLOBAL_KEY) {
Object.assign(rule, { ...newRule, pattern: rule.pattern });
} else {
rules.unshift(newRule);
}
await setRules(rules);
trySyncRules();
};

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

@@ -0,0 +1,112 @@
import { isSameSet } from "./utils";
/**
* 键盘快捷键监听
* @param {*} fn
* @param {*} target
* @param {*} timeout
* @returns
*/
export const shortcutListener = (fn, target = document, timeout = 3000) => {
const allkeys = new Set();
const curkeys = new Set();
let timer = null;
const handleKeydown = (e) => {
timer && clearTimeout(timer);
timer = setTimeout(() => {
allkeys.clear();
curkeys.clear();
clearTimeout(timer);
timer = null;
}, timeout);
if (e.code) {
allkeys.add(e.code);
curkeys.add(e.code);
fn([...curkeys], [...allkeys]);
}
};
const handleKeyup = (e) => {
curkeys.delete(e.code);
if (curkeys.size === 0) {
fn([...curkeys], [...allkeys]);
allkeys.clear();
}
};
target.addEventListener("keydown", handleKeydown);
target.addEventListener("keyup", handleKeyup);
return () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
target.removeEventListener("keydown", handleKeydown);
target.removeEventListener("keyup", handleKeyup);
};
};
/**
* 注册键盘快捷键
* @param {*} targetKeys
* @param {*} fn
* @param {*} target
* @returns
*/
export const shortcutRegister = (targetKeys = [], fn, target = document) => {
return shortcutListener((curkeys) => {
if (
targetKeys.length > 0 &&
isSameSet(new Set(targetKeys), new Set(curkeys))
) {
fn();
}
}, target);
};
/**
* 注册连续快捷键
* @param {*} targetKeys
* @param {*} fn
* @param {*} step
* @param {*} timeout
* @param {*} target
* @returns
*/
export const stepShortcutRegister = (
targetKeys = [],
fn,
step = 3,
timeout = 500,
target = document
) => {
let count = 0;
let pre = Date.now();
let timer;
return shortcutListener((curkeys, allkeys) => {
timer && clearTimeout(timer);
timer = setTimeout(() => {
clearTimeout(timer);
count = 0;
}, timeout);
if (targetKeys.length > 0 && curkeys.length === 0) {
const now = Date.now();
if (
(count === 0 || now - pre < timeout) &&
isSameSet(new Set(targetKeys), new Set(allkeys))
) {
count++;
if (count === step) {
count = 0;
fn();
}
} else {
count = 0;
}
pre = now;
}
}, target);
};

View File

@@ -5,6 +5,7 @@ import {
STOKEY_SYNC,
STOKEY_MSAUTH,
STOKEY_RULESCACHE_PREFIX,
STOKEY_WEBFIXCACHE_PREFIX,
DEFAULT_SETTING,
DEFAULT_RULES,
DEFAULT_SYNC,
@@ -82,10 +83,8 @@ export const storage = {
* 设置信息
*/
export const getSetting = () => getObj(STOKEY_SETTING);
export const getSettingWithDefault = async () => ({
...DEFAULT_SETTING,
...((await getSetting()) || {}),
});
export const getSettingWithDefault = async () =>
(await getSetting()) || DEFAULT_SETTING;
export const setSetting = (val) => setObj(STOKEY_SETTING, val);
export const updateSetting = (obj) => putObj(STOKEY_SETTING, obj);
@@ -106,12 +105,21 @@ export const delSubRules = (url) => del(STOKEY_RULESCACHE_PREFIX + url);
export const setSubRules = (url, val) =>
setObj(STOKEY_RULESCACHE_PREFIX + url, val);
/**
* 修复站点
*/
export const getWebfix = (url) => getObj(STOKEY_WEBFIXCACHE_PREFIX + url);
export const getWebfixWithDefault = async () => (await getWebfix()) || [];
export const setWebfix = (url, val) =>
setObj(STOKEY_WEBFIXCACHE_PREFIX + url, val);
/**
* fab位置
*/
export const getFab = () => getObj(STOKEY_FAB);
export const getFabWithDefault = async () => (await getFab()) || {};
export const setFab = (obj) => setObj(STOKEY_FAB, obj);
export const updateFab = (obj) => putObj(STOKEY_FAB, obj);
/**
* 数据同步

View File

@@ -5,18 +5,30 @@ import {
setSubRules,
getSubRules,
} from "./storage";
import { apiFetchRules } from "../apis";
import { apiFetch } from "../apis";
import { checkRules } from "./rules";
import { isAllchar } from "./utils";
import { syncWebfix } from "./webfix";
/**
* 更新缓存同步时间
* @param {*} url
*/
const updateSyncDataCache = async (url) => {
const { dataCaches = {} } = await getSyncWithDefault();
dataCaches[url] = Date.now();
await updateSync({ dataCaches });
};
/**
* 同步订阅规则
* @param {*} url
* @returns
*/
export const syncSubRules = async (url, isBg = false) => {
const res = await apiFetchRules(url, isBg);
export const syncSubRules = async (url) => {
const res = await apiFetch(url);
const rules = checkRules(res).filter(
(rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== ""
({ pattern }) => !isAllchar(pattern, GLOBAL_KEY)
);
if (rules.length > 0) {
await setSubRules(url, rules);
@@ -29,10 +41,11 @@ export const syncSubRules = async (url, isBg = false) => {
* @param {*} url
* @returns
*/
export const syncAllSubRules = async (subrulesList, isBg = false) => {
export const syncAllSubRules = async (subrulesList) => {
for (let subrules of subrulesList) {
try {
await syncSubRules(subrules.url, isBg);
await syncSubRules(subrules.url);
await updateSyncDataCache(subrules.url);
} catch (err) {
console.log(`[sync subrule error]: ${subrules.url}`, err);
}
@@ -44,14 +57,18 @@ export const syncAllSubRules = async (subrulesList, isBg = false) => {
* @param {*} url
* @returns
*/
export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
export const trySyncAllSubRules = async ({ subrulesList }) => {
try {
const { subRulesSyncAt } = await getSyncWithDefault();
const now = Date.now();
const interval = 24 * 60 * 60 * 1000; // 间隔一天
if (now - subRulesSyncAt > interval) {
await syncAllSubRules(subrulesList, isBg);
// 同步订阅规则
await syncAllSubRules(subrulesList);
await updateSync({ subRulesSyncAt: now });
// 同步修复规则
await syncWebfix(process.env.REACT_APP_WEBFIXURL);
}
} catch (err) {
console.log("[try sync all subrules]", err);
@@ -64,9 +81,10 @@ export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
* @returns
*/
export const loadOrFetchSubRules = async (url) => {
const rules = await getSubRules(url);
if (rules?.length) {
return rules;
let rules = await getSubRules(url);
if (!rules || rules.length === 0) {
rules = await syncSubRules(url);
await updateSyncDataCache(url);
}
return syncSubRules(url);
return rules || [];
};

34
src/libs/svg.js Normal file
View File

@@ -0,0 +1,34 @@
export const loadingSvg = `
<svg viewBox="0 0 100 100" style="display:inline-block; width:100%; height: 100%;">
<circle fill="#209CEE" stroke="none" cx="6" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 15 ; 0 -15; 0 15"
repeatCount="indefinite"
begin="0.1"
/>
</circle>
<circle fill="#209CEE" stroke="none" cx="30" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 10 ; 0 -10; 0 10"
repeatCount="indefinite"
begin="0.2"
/>
</circle>
<circle fill="#209CEE" stroke="none" cx="54" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 5 ; 0 -5; 0 5"
repeatCount="indefinite"
begin="0.3"
/>
</circle>
</svg>
`;

View File

@@ -1,8 +1,10 @@
import {
APP_LCNAME,
KV_SETTING_KEY,
KV_RULES_KEY,
KV_RULES_SHARE_KEY,
KV_SALT_SHARE,
OPT_SYNCTYPE_WEBDAV,
} from "../config";
import {
getSyncWithDefault,
@@ -13,45 +15,102 @@ import {
setRules,
} from "./storage";
import { apiSyncData } from "../apis";
import { sha256 } from "./utils";
import { sha256, removeEndchar } from "./utils";
import { createClient, getPatcher } from "webdav";
import { fetchApi } from "./fetch";
getPatcher().patch("request", (opts) => {
return fetchApi({
input: opts.url,
init: { method: opts.method, headers: opts.headers, body: opts.data },
});
});
const syncByWebdav = async (data, { syncUrl, syncUser, syncKey }) => {
const client = createClient(syncUrl, {
username: syncUser,
password: syncKey,
});
const pathname = `/${APP_LCNAME}`;
const filename = `/${APP_LCNAME}/${data.key}`;
if ((await client.exists(pathname)) === false) {
await client.createDirectory(pathname);
}
const isExist = await client.exists(filename);
if (isExist) {
const cont = await client.getFileContents(filename, { format: "text" });
const webData = JSON.parse(cont);
if (webData.updateAt >= data.updateAt) {
return webData;
}
}
await client.putFileContents(filename, JSON.stringify(data, null, 2));
return data;
};
const syncByWorker = async (data, { syncUrl, syncKey }) => {
syncUrl = removeEndchar(syncUrl, "/");
return await apiSyncData(`${syncUrl}/sync`, syncKey, data);
};
const syncData = async (key, valueFn) => {
const {
syncType,
syncUrl,
syncUser,
syncKey,
syncMeta = {},
} = await getSyncWithDefault();
if (!syncUrl || !syncKey || (syncType === OPT_SYNCTYPE_WEBDAV && !syncUser)) {
return;
}
let { updateAt = 0, syncAt = 0 } = syncMeta[key] || {};
syncAt === 0 && (updateAt = 0);
const value = await valueFn();
const data = {
key,
value: JSON.stringify(value),
updateAt,
};
const args = {
syncUrl,
syncUser,
syncKey,
};
const res =
syncType === OPT_SYNCTYPE_WEBDAV
? await syncByWebdav(data, args)
: await syncByWorker(data, args);
syncMeta[key] = {
updateAt: res.updateAt,
syncAt: Date.now(),
};
await updateSync({ syncMeta });
return { value: JSON.parse(res.value), isNew: res.updateAt > updateAt };
};
/**
* 同步设置
* @returns
*/
const syncSetting = async (isBg = false) => {
const { syncUrl, syncKey, settingUpdateAt } = await getSyncWithDefault();
if (!syncUrl || !syncKey) {
return;
}
const setting = await getSettingWithDefault();
const res = await apiSyncData(
syncUrl,
syncKey,
{
key: KV_SETTING_KEY,
value: setting,
updateAt: settingUpdateAt,
},
isBg
);
if (res && res.updateAt > settingUpdateAt) {
await updateSync({
settingUpdateAt: res.updateAt,
settingSyncAt: res.updateAt,
});
const syncSetting = async () => {
const res = await syncData(KV_SETTING_KEY, getSettingWithDefault);
if (res?.isNew) {
await setSetting(res.value);
return res.value;
} else {
await updateSync({ settingSyncAt: res.updateAt });
}
};
export const trySyncSetting = async (isBg = false) => {
export const trySyncSetting = async () => {
try {
return await syncSetting(isBg);
await syncSetting();
} catch (err) {
console.log("[sync setting]", err);
}
@@ -61,39 +120,16 @@ export const trySyncSetting = async (isBg = false) => {
* 同步规则
* @returns
*/
const syncRules = async (isBg = false) => {
const { syncUrl, syncKey, rulesUpdateAt } = await getSyncWithDefault();
if (!syncUrl || !syncKey) {
return;
}
const rules = await getRulesWithDefault();
const res = await apiSyncData(
syncUrl,
syncKey,
{
key: KV_RULES_KEY,
value: rules,
updateAt: rulesUpdateAt,
},
isBg
);
if (res && res.updateAt > rulesUpdateAt) {
await updateSync({
rulesUpdateAt: res.updateAt,
rulesSyncAt: res.updateAt,
});
const syncRules = async () => {
const res = await syncData(KV_RULES_KEY, getRulesWithDefault);
if (res?.isNew) {
await setRules(res.value);
return res.value;
} else {
await updateSync({ rulesSyncAt: res.updateAt });
}
};
export const trySyncRules = async (isBg = false) => {
export const trySyncRules = async () => {
try {
return await syncRules(isBg);
await syncRules();
} catch (err) {
console.log("[sync user rules]", err);
}
@@ -105,13 +141,18 @@ export const trySyncRules = async (isBg = false) => {
* @returns
*/
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
await apiSyncData(syncUrl, syncKey, {
const data = {
key: KV_RULES_SHARE_KEY,
value: rules,
value: JSON.stringify(rules, null, 2),
updateAt: Date.now(),
});
};
const args = {
syncUrl,
syncKey,
};
await syncByWorker(data, args);
const psk = await sha256(syncKey, KV_SALT_SHARE);
const shareUrl = `${syncUrl}?psk=${psk}`;
const shareUrl = `${syncUrl}/rules?psk=${psk}`;
return shareUrl;
};
@@ -119,10 +160,12 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
* 同步个人设置和规则
* @returns
*/
export const syncSettingAndRules = async (isBg = false) => {
return [await syncSetting(isBg), await syncRules(isBg)];
export const syncSettingAndRules = async () => {
await syncSetting();
await syncRules();
};
export const trySyncSettingAndRules = async (isBg = false) => {
return [await trySyncSetting(isBg), await trySyncRules(isBg)];
export const trySyncSettingAndRules = async () => {
await trySyncSetting();
await trySyncRules();
};

View File

@@ -7,16 +7,103 @@ import {
OPT_STYLE_DASHLINE,
OPT_STYLE_FUZZY,
SHADOW_KEY,
OPT_MOUSEKEY_DISABLE,
OPT_MOUSEKEY_MOUSEOVER,
DEFAULT_INPUT_RULE,
DEFAULT_TRANS_APIS,
DEFAULT_INPUT_SHORTCUT,
OPT_LANGS_LIST,
} from "../config";
import Content from "../views/Content";
import { updateFetchPool, clearFetchPool } from "./fetch";
import { debounce, genEventName } from "./utils";
import {
debounce,
genEventName,
removeEndchar,
matchInputStr,
sleep,
} from "./utils";
import { stepShortcutRegister } from "./shortcut";
import { apiTranslate } from "../apis";
import { tryDetectLang } from ".";
import { loadingSvg } from "./svg";
function isInputNode(node) {
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
}
function isEditAbleNode(node) {
return node.hasAttribute("contenteditable");
}
function selectContent(node) {
node.focus();
const range = document.createRange();
range.selectNodeContents(node);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
function pasteContentEvent(node, text) {
node.focus();
const data = new DataTransfer();
data.setData("text/plain", text);
const event = new ClipboardEvent("paste", { clipboardData: data });
document.dispatchEvent(event);
data.clearData();
}
function pasteContentCommand(node, text) {
node.focus();
document.execCommand("insertText", false, text);
}
function collapseToEnd(node) {
node.focus();
const selection = window.getSelection();
selection.collapseToEnd();
}
function getNodeText(node) {
if (isInputNode(node)) {
return node.value;
}
return node.innerText || node.textContent || "";
}
function addLoading(node, loadingId) {
const div = document.createElement("div");
div.id = loadingId;
div.innerHTML = loadingSvg;
div.style.cssText = `
width: ${node.offsetWidth}px;
height: ${node.offsetHeight}px;
line-height: ${node.offsetHeight}px;
position: absolute;
text-align: center;
left: ${node.offsetLeft}px;
top: ${node.offsetTop}px;
z-index: 2147483647;
`;
node.offsetParent?.appendChild(div);
}
function removeLoading(node, loadingId) {
const div = node.offsetParent.querySelector(`#${loadingId}`);
if (div) {
div.remove();
}
}
/**
* 翻译类
*/
export class Translator {
_rule = {};
_inputRule = {};
_setting = {};
_rootNodes = new Set();
_tranNodes = new Map();
@@ -99,6 +186,11 @@ export class Translator {
if (rule.transOpen === "true") {
this._register();
}
this._inputRule = setting.inputRule || DEFAULT_INPUT_RULE;
if (this._inputRule.transOpen) {
this._registerInput();
}
}
get setting() {
@@ -167,6 +259,22 @@ export class Translator {
);
};
_queryShadowNodes = (selector, rootNode) => {
this._rootNodes.add(rootNode);
this._queryFilter(selector, rootNode).forEach((item) => {
if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, "");
}
});
Array.from(rootNode.querySelectorAll("*"))
.map((item) => item.shadowRoot)
.filter(Boolean)
.forEach((item) => {
this._queryShadowNodes(selector, item);
});
};
_queryNodes = (rootNode = document) => {
// const childRoots = Array.from(rootNode.querySelectorAll("*"))
// .map((item) => item.shadowRoot)
@@ -189,14 +297,15 @@ export class Translator {
const outNodes = this._querySelectorAll(outSelector, rootNode);
outNodes.forEach((outNode) => {
if (outNode.shadowRoot) {
this._rootNodes.add(outNode.shadowRoot);
this._queryFilter(inSelector, outNode.shadowRoot).forEach(
(item) => {
if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, "");
}
}
);
// this._rootNodes.add(outNode.shadowRoot);
// this._queryFilter(inSelector, outNode.shadowRoot).forEach(
// (item) => {
// if (!this._tranNodes.has(item)) {
// this._tranNodes.set(item, "");
// }
// }
// );
this._queryShadowNodes(inSelector, outNode.shadowRoot);
}
});
}
@@ -211,6 +320,10 @@ export class Translator {
};
_register = () => {
if (this._rule.fromLang === this._rule.toLang) {
return;
}
// 搜索节点
this._queryNodes();
@@ -224,20 +337,166 @@ export class Translator {
});
this._tranNodes.forEach((_, node) => {
// 监听节点显示
this._interseObserver.observe(node);
if (
!this._setting.mouseKey ||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
) {
// 监听节点显示
this._interseObserver.observe(node);
} else {
// 监听鼠标悬停
node.addEventListener("mouseover", this._handleMouseover);
}
});
};
_registerInput = () => {
const {
triggerShortcut: initTriggerShortcut,
translator,
fromLang,
toLang: initToLang,
triggerCount: initTriggerCount,
triggerTime,
transSign,
} = this._inputRule;
const apiSetting = (this._setting.transApis || DEFAULT_TRANS_APIS)[
translator
];
let triggerShortcut = initTriggerShortcut;
let triggerCount = initTriggerCount;
if (triggerShortcut.length === 0) {
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
triggerCount = 1;
}
stepShortcutRegister(
triggerShortcut,
async () => {
let node = document.activeElement;
if (!node) {
return;
}
while (node.shadowRoot) {
node = node.shadowRoot.activeElement;
}
if (!isInputNode(node) && !isEditAbleNode(node)) {
return;
}
let initText = getNodeText(node);
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
// todo: remove multiple char
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
}
if (!initText.trim()) {
return;
}
let text = initText;
let toLang = initToLang;
if (transSign) {
const res = matchInputStr(text, transSign);
if (res) {
let lang = res[1];
if (lang === "zh" || lang === "cn") {
lang = "zh-CN";
} else if (lang === "tw" || lang === "hk") {
lang = "zh-TW";
}
if (lang && OPT_LANGS_LIST.includes(lang)) {
toLang = lang;
}
text = res[2];
}
}
// console.log("input -->", text);
const loadingId = "kiss-" + genEventName();
try {
addLoading(node, loadingId);
const deLang = await tryDetectLang(text);
if (deLang && toLang.includes(deLang)) {
return;
}
const [trText, isSame] = await apiTranslate({
translator,
text,
fromLang,
toLang,
apiSetting,
});
if (!trText || isSame) {
return;
}
if (isInputNode(node)) {
node.value = trText;
node.dispatchEvent(
new Event("input", { bubbles: true, cancelable: true })
);
return;
}
selectContent(node);
await sleep(200);
pasteContentEvent(node, trText);
await sleep(200);
// todo: use includes?
if (getNodeText(node).startsWith(initText)) {
pasteContentCommand(node, trText);
await sleep(100);
} else {
collapseToEnd(node);
}
} catch (err) {
console.log("[translate input]", err.message);
} finally {
removeLoading(node, loadingId);
}
},
triggerCount,
triggerTime
);
};
_handleMouseover = (e) => {
const key = this._setting.mouseKey.slice(3);
if (this._setting.mouseKey === OPT_MOUSEKEY_MOUSEOVER || e[key]) {
e.target.removeEventListener("mouseover", this._handleMouseover);
this._render(e.target);
}
};
_unRegister = () => {
// 解除节点变化监听
this._mutaObserver.disconnect();
// 解除节点显示监听
this._interseObserver.disconnect();
// this._interseObserver.disconnect();
// 移除已插入元素
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();
});

View File

@@ -48,15 +48,61 @@ export const sleep = (delay) =>
* @returns
*/
export const debounce = (func, delay = 200) => {
let timer;
let timer = null;
return (...args) => {
timer && clearTimeout(timer);
timer = setTimeout(() => {
func(...args);
clearTimeout(timer);
timer = null;
}, delay);
};
};
/**
* 节流函数
* @param {*} func
* @param {*} delay
* @returns
*/
export const throttle = (func, delay = 200) => {
let timer = null;
let cache = null;
return (...args) => {
if (!timer) {
func(...args);
cache = null;
timer = setTimeout(() => {
if (cache) {
func(...cache);
cache = null;
}
clearTimeout(timer);
timer = null;
}, delay);
} else {
cache = args;
}
};
};
/**
* 判断字符串全是某个字符
* @param {*} s
* @param {*} c
* @param {*} i
* @returns
*/
export const isAllchar = (s, c, i = 0) => {
while (i < s.length) {
if (s[i] !== c) {
return false;
}
i++;
}
return true;
};
/**
* 字符串通配符(*)匹配
* @param {*} s
@@ -68,7 +114,7 @@ export const isMatch = (s, p) => {
return false;
}
p = `*${p}*`;
p = "*" + p + "*";
let [sIndex, pIndex] = [0, 0];
let [sRecord, pRecord] = [-1, -1];
@@ -91,7 +137,7 @@ export const isMatch = (s, p) => {
return true;
}
return p.slice(pIndex).replaceAll("*", "") === "";
return isAllchar(p, "*", pIndex);
};
/**
@@ -122,3 +168,58 @@ export const sha256 = async (text, salt) => {
* @returns
*/
export const genEventName = () => btoa(Math.random()).slice(3, 11);
/**
* 判断两个 Set 是否相同
* @param {*} a
* @param {*} b
* @returns
*/
export const isSameSet = (a, b) => {
const s = new Set([...a, ...b]);
return s.size === a.size && s.size === b.size;
};
/**
* 去掉字符串末尾某个字符
* @param {*} s
* @param {*} c
* @param {*} count
* @returns
*/
export const removeEndchar = (s, c, count = 1) => {
let i = s.length;
while (i > s.length - count && s[i - 1] === c) {
i--;
}
return s.slice(0, i);
};
/**
* 匹配字符串及语言标识
* @param {*} str
* @param {*} sign
* @returns
*/
export const matchInputStr = (str, sign) => {
let reg = /\/([\w-]+)\s+([^]+)/;
switch (sign) {
case "//":
reg = /\/\/([\w-]+)\s+([^]+)/;
break;
case "\\":
reg = /\\([\w-]+)\s+([^]+)/;
break;
case "\\\\":
reg = /\\\\([\w-]+)\s+([^]+)/;
break;
case ">":
reg = />([\w-]+)\s+([^]+)/;
break;
case ">>":
reg = />>([\w-]+)\s+([^]+)/;
break;
default:
}
return str.match(reg);
};

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 需要修复的选择器
* - rootSelector 需要监听的选择器,可留空
* - fixer 修复函数,可针对不同网址,选用不同修复函数
*/
const DEFAULT_SITES = [
{
pattern: "www.phoronix.com",
selector: ".content",
rootSelector: "",
fixer: FIXER_BR,
},
{
pattern: "t.me/s/",
selector: ".tgme_widget_message_text",
rootSelector: ".tgme_channel_history",
fixer: FIXER_BR,
},
{
pattern: "baidu.com",
selector: "html",
rootSelector: "",
fixer: FIXER_FONTSIZE,
},
];
/**
* 修复过的标记
*/
const fixedSign = "kissfixed";
/**
* 采用 `br` 换行网站的修复函数
* 目标是将 `br` 替换成 `p`
* @param {*} node
* @returns
*/
function brFixer(node) {
if (node.hasAttribute(fixedSign)) {
return;
}
node.setAttribute(fixedSign, "true");
var gapTags = ["BR", "WBR"];
var newlineTags = [
"DIV",
"UL",
"OL",
"LI",
"H1",
"H2",
"H3",
"H4",
"H5",
"H6",
"P",
"HR",
"PRE",
"TABLE",
];
var html = "";
node.childNodes.forEach(function (child, index) {
if (index === 0) {
html += "<p>";
}
if (gapTags.indexOf(child.nodeName) !== -1) {
html += "</p><p>";
} else if (newlineTags.indexOf(child.nodeName) !== -1) {
html += "</p>" + child.outerHTML + "<p>";
} else if (child.outerHTML) {
html += child.outerHTML;
} else if (child.nodeValue) {
html += child.nodeValue;
}
if (index === node.childNodes.length - 1) {
html += "</p>";
}
});
node.innerHTML = html;
}
/**
* 修复字体大小问题,如 baidu.com
* @param {*} node
*/
function fontSizeFixer(node) {
node.style.cssText += "font-size:1em;";
}
/**
* 修复程序映射
*/
const fixerMap = {
[FIXER_BR]: brFixer,
[FIXER_FONTSIZE]: fontSizeFixer,
};
/**
* 查找、监听节点,并执行修复函数
* @param {*} selector
* @param {*} fixer
* @param {*} rootSelector
*/
function run(selector, fixer, rootSelector) {
var mutaObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addNode) {
addNode.querySelectorAll(selector).forEach(fixer);
});
});
});
var rootNodes = [document];
if (rootSelector) {
rootNodes = document.querySelectorAll(rootSelector);
}
rootNodes.forEach(function (rootNode) {
rootNode.querySelectorAll(selector).forEach(fixer);
mutaObserver.observe(rootNode, {
childList: true,
});
});
}
/**
* 同步远程数据
* @param {*} url
* @returns
*/
export const syncWebfix = async (url) => {
const sites = await apiFetch(url);
await setWebfix(url, sites);
return sites;
};
/**
* 从缓存或远程加载修复站点
* @param {*} url
* @returns
*/
export const loadOrFetchWebfix = async (url) => {
try {
let sites = await getWebfix(url);
if (sites?.length) {
return sites;
}
return syncWebfix(url);
} catch (err) {
console.log("[load webfix]", err.message);
return DEFAULT_SITES;
}
};
/**
* 匹配站点
*/
export async function webfix(href, { injectWebfix }) {
try {
if (!injectWebfix) {
return;
}
const sites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
for (var i = 0; i < sites.length; i++) {
var site = sites[i];
if (isMatch(href, site.pattern)) {
if (fixerMap[site.fixer]) {
run(site.selector, fixerMap[site.fixer], site.rootSelector);
}
break;
}
}
} catch (err) {
console.error(`[kiss-webfix]: ${err.message}`);
}
}

View File

@@ -10,11 +10,18 @@ import {
} from "./libs/storage";
import { Translator } from "./libs/translator";
import { trySyncAllSubRules } from "./libs/subRules";
import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config";
import { isIframe } from "./libs/iframe";
import {
MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
APP_LCNAME,
} from "./config";
import { isIframe, sendIframeMsg, sendPrentMsg } from "./libs/iframe";
import { handlePing, injectScript } from "./libs/gm";
import { matchRule } from "./libs/rules";
import { genEventName } from "./libs/utils";
import { webfix } from "./libs/webfix";
/**
* 入口函数
@@ -45,33 +52,54 @@ const init = async () => {
}
// 翻译页面
const href = isIframe ? document.referrer : document.location.href;
const setting = await getSettingWithDefault();
const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);
if (isIframe) {
// iframe
let translator;
window.addEventListener("message", (e) => {
const action = e?.data?.action;
const { action, args } = e.data || {};
switch (action) {
case MSG_TRANS_TOGGLE:
translator.toggle();
translator?.toggle();
break;
case MSG_TRANS_TOGGLE_STYLE:
translator?.toggleStyle();
break;
case MSG_TRANS_PUTRULE:
translator.updateRule(e.data.args || {});
if (!translator) {
translator = new Translator(args, setting);
} else {
translator.updateRule(args || {});
}
break;
default:
}
});
sendPrentMsg(MSG_TRANS_GETRULE);
return;
}
const href = isIframe ? document.referrer : document.location.href;
const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);
webfix(href, setting);
// 监听消息
window.addEventListener("message", (e) => {
const { action } = e.data || {};
switch (action) {
case MSG_TRANS_GETRULE:
sendIframeMsg(MSG_TRANS_PUTRULE, rule);
break;
default:
}
});
// 浮球按钮
const fab = await getFabWithDefault();
const $action = document.createElement("div");
$action.setAttribute("id", "kiss-translator");
$action.setAttribute("id", APP_LCNAME);
document.body.parentElement.appendChild($action);
const shadowContainer = $action.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style");
@@ -79,7 +107,7 @@ const init = async () => {
shadowContainer.appendChild(emotionRoot);
shadowContainer.appendChild(shadowRootElement);
const cache = createCache({
key: "css",
key: APP_LCNAME,
prepend: true,
container: emotionRoot,
});
@@ -99,9 +127,10 @@ const init = 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; z-index:10000;";
$err.style.cssText = "background:red; color:#fff;";
document.body.prepend($err);
}
})();

View File

@@ -1,65 +1,51 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { limitNumber } from "../../libs/utils";
import { isMobile } from "../../libs/mobile";
import { setFab } from "../../libs/storage";
import { updateFab } from "../../libs/storage";
import { debounce } from "../../libs/utils";
import Paper from "@mui/material/Paper";
const getEdgePosition = (
{ x: left, y: top, edge },
const getEdgePosition = ({
x: left,
y: top,
width,
height,
windowWidth,
windowHeight,
width,
height
) => {
hover,
}) => {
const right = windowWidth - left - width;
const bottom = windowHeight - top - height;
const min = Math.min(left, top, right, bottom);
switch (min) {
case right:
edge = "right";
left = windowWidth - width;
left = hover ? windowWidth - width : windowWidth - width / 2;
break;
case left:
edge = "left";
left = 0;
left = hover ? 0 : -width / 2;
break;
case bottom:
edge = "bottom";
top = windowHeight - height;
top = hover ? windowHeight - height : windowHeight - height / 2;
break;
default:
edge = "top";
top = 0;
top = hover ? 0 : -height / 2;
}
left = limitNumber(left, 0, windowWidth - width);
top = limitNumber(top, 0, windowHeight - height);
return { x: left, y: top, edge, hide: false };
return { x: left, y: top };
};
const getHidePosition = (
{ x: left, y: top, edge },
windowWidth,
windowHeight,
width,
height
) => {
switch (edge) {
case "right":
left = windowWidth - width / 2;
break;
case "left":
left = -width / 2;
break;
case "bottom":
top = windowHeight - height / 2;
break;
default:
top = -height / 2;
function DraggableWrapper({ children, usePaper, ...props }) {
if (usePaper) {
return (
<Paper {...props} elevation={4}>
{children}
</Paper>
);
}
return { x: left, y: top, edge, hide: true };
};
return <div {...props}>{children}</div>;
}
export default function Draggable({
windowSize,
windowSize: { w: windowWidth, h: windowHeight },
width,
height,
left,
@@ -70,66 +56,38 @@ export default function Draggable({
onMove,
handler,
children,
usePaper,
}) {
const [origin, setOrigin] = useState({
x: left,
y: top,
px: left,
py: top,
});
const [position, setPosition] = useState({
x: left,
y: top,
edge: null,
hide: false,
});
const [edgeTimer, setEdgeTimer] = useState(null);
const goEdge = useCallback((w, h, width, height) => {
setPosition((pre) => getEdgePosition(pre, w, h, width, height));
setEdgeTimer(
setTimeout(() => {
setPosition((pre) => getHidePosition(pre, w, h, width, height));
}, 1500)
);
}, []);
const [hover, setHover] = useState(false);
const [origin, setOrigin] = useState(null);
const [position, setPosition] = useState({ x: left, y: top });
const setFabPosition = useMemo(() => debounce(updateFab, 500), []);
const handlePointerDown = (e) => {
!isMobile && e.target.setPointerCapture(e.pointerId);
onStart && onStart();
edgeTimer && clearTimeout(edgeTimer);
const { x, y } = position;
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
setOrigin({
x: position.x,
y: position.y,
px: clientX,
py: clientY,
});
setOrigin({ x, y, clientX, clientY });
};
const handlePointerMove = (e) => {
onMove && onMove();
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
if (origin) {
const dx = clientX - origin.px;
const dy = clientY - origin.py;
const dx = clientX - origin.clientX;
const dy = clientY - origin.clientY;
let x = origin.x + dx;
let y = origin.y + dy;
const { w, h } = windowSize;
x = limitNumber(x, 0, w - width);
y = limitNumber(y, 0, h - height);
setPosition({ x, y, edge: null, hide: false });
x = limitNumber(x, -width / 2, windowWidth - width / 2);
y = limitNumber(y, 0, windowHeight - height / 2);
setPosition({ x, y });
}
};
const handlePointerUp = (e) => {
e.stopPropagation();
setOrigin(null);
if (!snapEdge) {
return;
}
goEdge(windowSize.w, windowSize.h, width, height);
};
const handleClick = (e) => {
@@ -138,35 +96,48 @@ export default function Draggable({
const handleMouseEnter = (e) => {
e.stopPropagation();
if (snapEdge && position.hide) {
edgeTimer && clearTimeout(edgeTimer);
goEdge(windowSize.w, windowSize.h, width, height);
}
setHover(true);
};
const handleMouseLeave = (e) => {
e.stopPropagation();
setHover(false);
};
useEffect(() => {
setOrigin(null);
if (!snapEdge) {
if (!snapEdge || !!origin) {
return;
}
goEdge(windowSize.w, windowSize.h, width, height);
}, [snapEdge, goEdge, windowSize.w, windowSize.h, width, height]);
useEffect(() => {
if (position.hide) {
setFab({
x: position.x,
y: position.y,
setPosition((pre) => {
const edgePosition = getEdgePosition({
...pre,
width,
height,
windowWidth,
windowHeight,
hover,
});
}
}, [position.x, position.y, position.hide]);
setFabPosition(edgePosition);
return edgePosition;
});
}, [
origin,
hover,
width,
height,
windowWidth,
windowHeight,
snapEdge,
setFabPosition,
]);
const opacity = useMemo(() => {
if (snapEdge) {
return position.hide ? 0.2 : 1;
return hover || origin ? 1 : 0.2;
}
return origin ? 0.8 : 1;
}, [origin, snapEdge, position.hide]);
}, [origin, snapEdge, hover]);
const touchProps = isMobile
? {
@@ -181,7 +152,8 @@ export default function Draggable({
};
return (
<div
<DraggableWrapper
usePaper={usePaper}
style={{
opacity,
position: "fixed",
@@ -191,6 +163,7 @@ export default function Draggable({
display: show ? "block" : "none",
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
>
<div
@@ -202,6 +175,6 @@ export default function Draggable({
{handler}
</div>
<div>{children}</div>
</div>
</DraggableWrapper>
);
}

View File

@@ -1,18 +1,26 @@
import Paper from "@mui/material/Paper";
import Box from "@mui/material/Box";
import Fab from "@mui/material/Fab";
import TranslateIcon from "@mui/icons-material/Translate";
import ThemeProvider from "../../hooks/Theme";
import Draggable from "./Draggable";
import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import Stack from "@mui/material/Stack";
import { useEffect, useState, useMemo, useCallback } from "react";
import { SettingProvider } from "../../hooks/Setting";
import Popup from "../Popup";
import { debounce } from "../../libs/utils";
import * as shortcut from "@violentmonkey/shortcut";
import { isGm } from "../../libs/client";
import Header from "../Popup/Header";
import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider";
import {
DEFAULT_SHORTCUTS,
OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP,
OPT_SHORTCUT_SETTING,
MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
} from "../../config";
import { shortcutRegister } from "../../libs/shortcut";
import { sendIframeMsg } from "../../libs/iframe";
export default function Action({ translator, fab }) {
const fabWidth = 40;
@@ -47,70 +55,89 @@ export default function Action({ translator, fab }) {
}, []);
useEffect(() => {
if (!isGm) {
return;
}
// 注册快捷键
shortcut.register("a-q", () => {
translator.toggle();
setShowPopup(false);
});
shortcut.register("a-c", () => {
translator.toggleStyle();
setShowPopup(false);
});
shortcut.register("a-k", () => {
setShowPopup((pre) => !pre);
});
const shortcuts = translator.setting.shortcuts || DEFAULT_SHORTCUTS;
const clearShortcuts = [
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () => {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
setShowPopup(false);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () => {
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
setShowPopup(false);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () => {
setShowPopup((pre) => !pre);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () => {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
}),
];
return () => {
shortcut.disable();
clearShortcuts.forEach((fn) => {
fn();
});
};
}, [translator]);
useEffect(() => {
// 注册菜单
const menuCommandIds = [];
if (isGm) {
try {
menuCommandIds.push(
GM.registerMenuCommand(
"Toggle Translate",
(event) => {
translator.toggle();
setShowPopup(false);
},
"Q"
),
GM.registerMenuCommand(
"Toggle Style",
(event) => {
translator.toggleStyle();
setShowPopup(false);
},
"C"
),
GM.registerMenuCommand(
"Open Menu",
(event) => {
setShowPopup((pre) => !pre);
},
"K"
)
);
} catch (err) {
console.log("[registerMenuCommand]", err);
}
if (!isGm) {
return;
}
return () => {
if (isGm) {
try {
menuCommandIds.forEach((id) => {
GM.unregisterMenuCommand(id);
});
} catch (err) {
//
}
}
};
// 注册菜单
try {
const menuCommandIds = [];
menuCommandIds.push(
GM.registerMenuCommand(
"Toggle Translate (Alt+q)",
(event) => {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
setShowPopup(false);
},
"Q"
),
GM.registerMenuCommand(
"Toggle Style (Alt+c)",
(event) => {
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
setShowPopup(false);
},
"C"
),
GM.registerMenuCommand(
"Open Menu (Alt+k)",
(event) => {
setShowPopup((pre) => !pre);
},
"K"
),
GM.registerMenuCommand(
"Open Setting (Alt+o)",
(event) => {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
},
"O"
)
);
return () => {
menuCommandIds.forEach((id) => {
GM.unregisterMenuCommand(id);
});
};
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}, [translator]);
useEffect(() => {
@@ -146,7 +173,7 @@ export default function Action({ translator, fab }) {
windowSize,
width: fabWidth,
height: fabWidth,
left: fab.x ?? 0,
left: fab.x ?? -fabWidth,
top: fab.y ?? windowSize.h / 2,
};
@@ -159,39 +186,23 @@ export default function Action({ translator, fab }) {
show={showPopup}
onStart={handleStart}
onMove={handleMove}
usePaper
handler={
<Paper style={{ cursor: "move" }} elevation={3}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Box style={{ marginLeft: 16 }}>
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
</Box>
<IconButton
onClick={() => {
setShowPopup(false);
}}
>
<CloseIcon />
</IconButton>
</Stack>
</Paper>
<Box style={{ cursor: "move" }}>
<Header setShowPopup={setShowPopup} />
<Divider />
</Box>
}
>
<Paper>
{showPopup && (
<Popup setShowPopup={setShowPopup} translator={translator} />
)}
</Paper>
{showPopup && (
<Popup setShowPopup={setShowPopup} translator={translator} />
)}
</Draggable>
<Draggable
key="fab"
snapEdge
{...fabProps}
show={!showPopup}
show={fab.isHide ? false : !showPopup}
onStart={handleStart}
onMove={handleMove}
handler={

View File

@@ -1,44 +1,14 @@
import { DEFAULT_COLOR } from "../../config";
import { loadingSvg } from "../../libs/svg";
export default function LoadingIcon() {
return (
<svg
viewBox="0 0 100 100"
<div
style={{
maxWidth: "1.2em",
maxHeight: "1.2em",
display: "inline-block",
width: "1.2em",
height: "1em",
}}
>
<circle fill={DEFAULT_COLOR} stroke="none" cx="6" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 15 ; 0 -15; 0 15"
repeatCount="indefinite"
begin="0.1"
/>
</circle>
<circle fill={DEFAULT_COLOR} stroke="none" cx="30" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 10 ; 0 -10; 0 10"
repeatCount="indefinite"
begin="0.2"
/>
</circle>
<circle fill={DEFAULT_COLOR} stroke="none" cx="54" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 5 ; 0 -5; 0 5"
repeatCount="indefinite"
begin="0.3"
/>
</circle>
</svg>
dangerouslySetInnerHTML={{ __html: loadingSvg }}
/>
);
}

View File

@@ -13,9 +13,9 @@ import {
TRANS_NEWLINE_LENGTH,
} from "../../config";
import { useTranslate } from "../../hooks/Translate";
import styled from "styled-components";
import { styled } from "@mui/material/styles";
const LineSpan = styled.span`
const LineSpan = styled("span")`
opacity: 0.6;
-webkit-opacity: 0.6;
text-decoration-line: underline;
@@ -34,21 +34,21 @@ const LineSpan = styled.span`
}
`;
const FuzzySpan = styled.span`
filter: blur(5px);
-webkit-filter: blur(5px);
const FuzzySpan = styled("span")`
filter: blur(0.2em);
-webkit-filter: blur(0.2em);
&:hover {
filter: none;
-webkit-filter: none;
}
`;
const HighlightSpan = styled.span`
const HighlightSpan = styled("span")`
color: #fff;
background-color: ${(props) => props.$bgColor};
`;
const DiySpan = styled.span`
const DiySpan = styled("span")`
${(props) => props.$diyStyle}
`;
@@ -120,7 +120,7 @@ export default function Content({ q, translator }) {
if (loading) {
return (
<>
{q.length > newlineLength ? <br /> : " "}
{q.length >= newlineLength ? <br /> : " "}
<LoadingIcon />
</>
);
@@ -129,7 +129,7 @@ export default function Content({ q, translator }) {
if (text && !sameLang) {
return (
<>
{q.length > newlineLength ? <br /> : " "}
{q.length >= newlineLength ? <br /> : " "}
<StyledSpan
textStyle={textStyle}
textDiyStyle={textDiyStyle}

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

@@ -1,19 +1,15 @@
import PropTypes from "prop-types";
import AppBar from "@mui/material/AppBar";
import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu";
import Toolbar from "@mui/material/Toolbar";
import Box from "@mui/material/Box";
import { useDarkMode } from "../../hooks/ColorMode";
import LightModeIcon from "@mui/icons-material/LightMode";
import DarkModeIcon from "@mui/icons-material/DarkMode";
import Link from "@mui/material/Link";
import { useI18n } from "../../hooks/I18n";
import DarkModeButton from "./DarkModeButton";
function Header(props) {
const i18n = useI18n();
const { onDrawerToggle } = props;
const { darkMode, toggleDarkMode } = useDarkMode();
return (
<AppBar
@@ -39,18 +35,13 @@ function Header(props) {
underline="none"
color="inherit"
href={process.env.REACT_APP_HOMEPAGE}
target="_blank"
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
</Box>
<IconButton onClick={toggleDarkMode} color="inherit">
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
</IconButton>
<DarkModeButton />
</Toolbar>
</AppBar>
);
}
Header.propTypes = {
onDrawerToggle: PropTypes.func.isRequired,
};
export default Header;

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

@@ -0,0 +1,178 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import { useI18n } from "../../hooks/I18n";
import {
OPT_TRANS_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_INPUT_TRANS_SIGNS,
} from "../../config";
import ShortcutInput from "./ShortcutInput";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import { useInputRule } from "../../hooks/InputRule";
import { useCallback } from "react";
import Grid from "@mui/material/Grid";
import { limitNumber } from "../../libs/utils";
export default function InputSetting() {
const i18n = useI18n();
const { inputRule, updateInputRule } = useInputRule();
const handleChange = (e) => {
e.preventDefault();
let { name, value } = e.target;
switch (name) {
case "triggerTime":
value = limitNumber(value, 10, 1000);
break;
default:
}
updateInputRule({
[name]: value,
});
};
const handleShortcutInput = useCallback(
(val) => {
updateInputRule({ triggerShortcut: val });
},
[updateInputRule]
);
const {
transOpen,
translator,
fromLang,
toLang,
triggerShortcut,
triggerCount,
triggerTime,
transSign,
} = inputRule;
return (
<Box>
<Stack spacing={3}>
<FormControlLabel
control={
<Switch
size="small"
name="transOpen"
checked={transOpen}
onChange={() => {
updateInputRule({ transOpen: !transOpen });
}}
/>
}
label={i18n("input_box_translation")}
/>
<TextField
select
size="small"
name="translator"
value={translator}
label={i18n("translate_service")}
onChange={handleChange}
>
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField
select
size="small"
name="fromLang"
value={fromLang}
label={i18n("from_lang")}
onChange={handleChange}
>
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField
select
size="small"
name="toLang"
value={toLang}
label={i18n("to_lang")}
onChange={handleChange}
>
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField
select
size="small"
name="transSign"
value={transSign}
label={i18n("input_trans_start_sign")}
onChange={handleChange}
helperText={i18n("input_trans_start_sign_help")}
>
<MenuItem value={""}>{i18n("style_none")}</MenuItem>
{OPT_INPUT_TRANS_SIGNS.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={12} md={4} lg={4}>
<ShortcutInput
value={triggerShortcut}
onChange={handleShortcutInput}
label={i18n("trigger_trans_shortcut")}
helperText={i18n("trigger_trans_shortcut_help")}
/>
</Grid>
<Grid item xs={12} sm={12} md={4} lg={4}>
<TextField
select
fullWidth
size="small"
name="triggerCount"
value={triggerCount}
label={i18n("shortcut_press_count")}
onChange={handleChange}
>
{[1, 2, 3, 4, 5].map((val) => (
<MenuItem key={val} value={val}>
{val}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={4} lg={4}>
<TextField
fullWidth
size="small"
label={i18n("combo_timeout")}
type="number"
name="triggerTime"
defaultValue={triggerTime}
onChange={handleChange}
/>
</Grid>
</Grid>
</Box>
</Stack>
</Box>
);
}

View File

@@ -10,6 +10,9 @@ import InfoIcon from "@mui/icons-material/Info";
import DesignServicesIcon from "@mui/icons-material/DesignServices";
import { useI18n } from "../../hooks/I18n";
import SyncIcon from "@mui/icons-material/Sync";
import ApiIcon from "@mui/icons-material/Api";
import SendTimeExtensionIcon from "@mui/icons-material/SendTimeExtension";
import InputIcon from "@mui/icons-material/Input";
function LinkItem({ label, url, icon }) {
const match = useMatch(url);
@@ -36,12 +39,30 @@ export default function Navigator(props) {
url: "/rules",
icon: <DesignServicesIcon />,
},
{
id: "input_setting",
label: i18n("input_setting"),
url: "/input",
icon: <InputIcon />,
},
{
id: "apis_setting",
label: i18n("apis_setting"),
url: "/apis",
icon: <ApiIcon />,
},
{
id: "sync",
label: i18n("sync_setting"),
url: "/sync",
icon: <SyncIcon />,
},
{
id: "webfix",
label: i18n("patch_setting"),
url: "/webfix",
icon: <SendTimeExtensionIcon />,
},
{ id: "about", label: i18n("about"), url: "/about", icon: <InfoIcon /> },
];
return (

View File

@@ -13,6 +13,8 @@ import {
OPT_STYLE_ALL,
OPT_STYLE_DIY,
OPT_STYLE_USE_COLOR,
URL_KISS_RULES_NEW_ISSUE,
OPT_SYNCTYPE_WORKER,
} from "../../config";
import { useState, useRef, useEffect, useMemo } from "react";
import { useI18n } from "../../hooks/I18n";
@@ -45,6 +47,9 @@ import { syncShareRules } from "../../libs/sync";
import { debounce } from "../../libs/utils";
import { delSubRules, getSyncWithDefault } from "../../libs/storage";
import OwSubRule from "./OwSubRule";
import ClearAllIcon from "@mui/icons-material/ClearAll";
import HelpButton from "./HelpButton";
import { useSyncCaches } from "../../hooks/Sync";
function RuleFields({ rule, rules, setShow, setKeyword }) {
const initFormValues = rule || {
@@ -359,6 +364,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
}
function RuleAccordion({ rule, rules }) {
const i18n = useI18n();
const [expanded, setExpanded] = useState(false);
const handleChange = (e) => {
@@ -373,7 +379,9 @@ function RuleAccordion({ rule, rules }) {
opacity: rules ? 1 : 0.5,
}}
>
{rule.pattern}
{rule.pattern === GLOBAL_KEY
? `[${i18n("global_rule")}] ${rule.pattern}`
: rule.pattern}
</Typography>
</AccordionSummary>
<AccordionDetails>
@@ -438,8 +446,8 @@ function ShareButton({ rules, injectRules, selectedUrl }) {
const i18n = useI18n();
const handleClick = async () => {
try {
const { syncUrl, syncKey } = await getSyncWithDefault();
if (!syncUrl || !syncKey) {
const { syncType, syncUrl, syncKey } = await getSyncWithDefault();
if (syncType !== OPT_SYNCTYPE_WORKER || !syncUrl || !syncKey) {
alert.warning(i18n("error_sync_setting"));
return;
}
@@ -470,7 +478,7 @@ function ShareButton({ rules, injectRules, selectedUrl }) {
onClick={handleClick}
startIcon={<ShareIcon />}
>
{"分享"}
{i18n("share")}
</Button>
);
}
@@ -519,6 +527,10 @@ function UserRules({ subRules }) {
}
}, [showAdd]);
if (!rules.list) {
return;
}
return (
<Stack spacing={3}>
<Stack
@@ -542,7 +554,7 @@ function UserRules({ subRules }) {
<UploadButton text={i18n("import")} onChange={handleImport} />
<DownloadButton
data={JSON.stringify([...rules.list].reverse(), null, "\t")}
data={JSON.stringify([...rules.list].reverse(), null, 2)}
text={i18n("export")}
/>
@@ -552,6 +564,19 @@ function UserRules({ subRules }) {
selectedUrl={selectedUrl}
/>
<Button
size="small"
variant="outlined"
onClick={() => {
rules.clear();
}}
startIcon={<ClearAllIcon />}
>
{i18n("clear_all")}
</Button>
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
<FormControlLabel
control={
<Switch
@@ -599,13 +624,23 @@ function UserRules({ subRules }) {
);
}
function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
function SubRulesItem({
index,
url,
syncAt,
selectedUrl,
delSub,
setSelectedRules,
updateDataCache,
deleteDataCache,
}) {
const [loading, setLoading] = useState(false);
const handleDel = async () => {
try {
await delSub(url);
await delSubRules(url);
await deleteDataCache(url);
} catch (err) {
console.log("[del subrules]", err);
}
@@ -618,6 +653,7 @@ function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
if (rules.length > 0 && url === selectedUrl) {
setSelectedRules(rules);
}
await updateDataCache(url);
} catch (err) {
console.log("[sync sub rules]", err);
} finally {
@@ -629,6 +665,12 @@ function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
<Stack direction="row" alignItems="center" spacing={2}>
<FormControlLabel value={url} control={<Radio />} label={url} />
{syncAt && (
<span style={{ marginLeft: "0.5em", opacity: 0.5 }}>
[{new Date(syncAt).toLocaleString()}]
</span>
)}
{loading ? (
<CircularProgress size={16} />
) : (
@@ -646,7 +688,7 @@ function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
);
}
function SubRulesEdit({ subList, addSub }) {
function SubRulesEdit({ subList, addSub, updateDataCache }) {
const i18n = useI18n();
const [inputText, setInputText] = useState("");
const [inputError, setInputError] = useState("");
@@ -681,6 +723,7 @@ function SubRulesEdit({ subList, addSub }) {
throw new Error("empty rules");
}
await addSub(url);
await updateDataCache(url);
setShowInput(false);
setInputText("");
} catch (err) {
@@ -715,6 +758,7 @@ function SubRulesEdit({ subList, addSub }) {
>
{i18n("add")}
</Button>
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
</Stack>
{showInput && (
@@ -759,25 +803,38 @@ function SubRules({ subRules }) {
setSelectedRules,
loading,
} = subRules;
const { dataCaches, updateDataCache, deleteDataCache, reloadSync } =
useSyncCaches();
const handleSelect = (e) => {
const url = e.target.value;
selectSub(url);
};
useEffect(() => {
reloadSync();
}, [selectedRules, reloadSync]);
return (
<Stack spacing={3}>
<SubRulesEdit subList={subList} addSub={addSub} />
<SubRulesEdit
subList={subList}
addSub={addSub}
updateDataCache={updateDataCache}
/>
<RadioGroup value={selectedUrl} onChange={handleSelect}>
{subList.map((item, index) => (
<SubRulesItem
key={item.url}
url={item.url}
syncAt={dataCaches[item.url]}
index={index}
selectedUrl={selectedUrl}
delSub={delSub}
setSelectedRules={setSelectedRules}
updateDataCache={updateDataCache}
deleteDataCache={deleteDataCache}
/>
))}
</RadioGroup>

View File

@@ -6,14 +6,40 @@ import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
import Link from "@mui/material/Link";
import FormHelperText from "@mui/material/FormHelperText";
import { useSetting } from "../../hooks/Setting";
import { limitNumber } from "../../libs/utils";
import { useI18n } from "../../hooks/I18n";
import { UI_LANGS, URL_KISS_PROXY, TRANS_NEWLINE_LENGTH } from "../../config";
import { useAlert } from "../../hooks/Alert";
import { isExt } from "../../libs/client";
import Grid from "@mui/material/Grid";
import {
UI_LANGS,
TRANS_NEWLINE_LENGTH,
CACHE_NAME,
OPT_MOUSEKEY_ALL,
OPT_MOUSEKEY_DISABLE,
OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP,
OPT_SHORTCUT_SETTING,
} from "../../config";
import { useShortcut } from "../../hooks/Shortcut";
import ShortcutInput from "./ShortcutInput";
import { useFab } from "../../hooks/Fab";
function ShortcutItem({ action, label }) {
const { shortcut, setShortcut } = useShortcut(action);
return (
<ShortcutInput value={shortcut} onChange={setShortcut} label={label} />
);
}
export default function Settings() {
const i18n = useI18n();
const { setting, updateSetting } = useSetting();
const alert = useAlert();
const { fab, updateFab } = useFab();
const handleChange = (e) => {
e.preventDefault();
@@ -41,22 +67,26 @@ export default function Settings() {
});
};
const handleClearCache = () => {
try {
caches.delete(CACHE_NAME);
alert.success(i18n("clear_success"));
} catch (err) {
console.log("[clear cache]", err);
}
};
const {
uiLang,
googleUrl,
fetchLimit,
fetchInterval,
minLength,
maxLength,
openaiUrl,
deeplUrl = "",
deeplKey = "",
openaiKey,
openaiModel,
openaiPrompt,
clearCache,
newlineLength = TRANS_NEWLINE_LENGTH,
mouseKey = OPT_MOUSEKEY_DISABLE,
} = setting;
const { isHide = false } = fab || {};
return (
<Box>
@@ -82,7 +112,7 @@ export default function Settings() {
label={i18n("fetch_limit")}
type="number"
name="fetchLimit"
value={fetchLimit}
defaultValue={fetchLimit}
onChange={handleChange}
/>
@@ -91,7 +121,7 @@ export default function Settings() {
label={i18n("fetch_interval")}
type="number"
name="fetchInterval"
value={fetchInterval}
defaultValue={fetchInterval}
onChange={handleChange}
/>
@@ -100,7 +130,7 @@ export default function Settings() {
label={i18n("min_translate_length")}
type="number"
name="minLength"
value={minLength}
defaultValue={minLength}
onChange={handleChange}
/>
@@ -109,7 +139,7 @@ export default function Settings() {
label={i18n("max_translate_length")}
type="number"
name="maxLength"
value={maxLength}
defaultValue={maxLength}
onChange={handleChange}
/>
@@ -118,86 +148,91 @@ export default function Settings() {
label={i18n("num_of_newline_characters")}
type="number"
name="newlineLength"
value={newlineLength}
defaultValue={newlineLength}
onChange={handleChange}
/>
<FormControl size="small">
<InputLabel>{i18n("clear_cache")}</InputLabel>
<InputLabel>{i18n("mouseover_translation")}</InputLabel>
<Select
name="clearCache"
value={clearCache}
label={i18n("clear_cache")}
name="mouseKey"
value={mouseKey}
label={i18n("mouseover_translation")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
{OPT_MOUSEKEY_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
size="small"
label={i18n("google_api")}
name="googleUrl"
value={googleUrl}
onChange={handleChange}
helperText={
<Link href={URL_KISS_PROXY}>{i18n("about_api_proxy")}</Link>
}
/>
<FormControl size="small">
<InputLabel>{i18n("hide_fab_button")}</InputLabel>
<Select
name="isHide"
value={isHide}
label={i18n("hide_fab_button")}
onChange={(e) => {
updateFab({ isHide: e.target.value });
}}
>
<MenuItem value={false}>{i18n("show")}</MenuItem>
<MenuItem value={true}>{i18n("hide")}</MenuItem>
</Select>
</FormControl>
<TextField
size="small"
label={i18n("deepl_api")}
name="deeplUrl"
value={deeplUrl}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("deepl_key")}
name="deeplKey"
value={deeplKey}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("openai_api")}
name="openaiUrl"
value={openaiUrl}
onChange={handleChange}
helperText={
<Link href={URL_KISS_PROXY}>{i18n("about_api_proxy")}</Link>
}
/>
<TextField
size="small"
type="password"
label={i18n("openai_key")}
name="openaiKey"
value={openaiKey}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("openai_model")}
name="openaiModel"
value={openaiModel}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("openai_prompt")}
name="openaiPrompt"
value={openaiPrompt}
onChange={handleChange}
multiline
/>
{isExt ? (
<FormControl size="small">
<InputLabel>{i18n("if_clear_cache")}</InputLabel>
<Select
name="clearCache"
value={clearCache}
label={i18n("if_clear_cache")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
</Select>
<FormHelperText>
<Link component="button" onClick={handleClearCache}>
{i18n("clear_all_cache_now")}
</Link>
</FormHelperText>
</FormControl>
) : (
<>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={12} md={3} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_TRANSLATE}
label={i18n("toggle_translate_shortcut")}
/>
</Grid>
<Grid item xs={12} sm={12} md={3} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_STYLE}
label={i18n("toggle_style_shortcut")}
/>
</Grid>
<Grid item xs={12} sm={12} md={3} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_POPUP}
label={i18n("toggle_popup_shortcut")}
/>
</Grid>
<Grid item xs={12} sm={12} md={3} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_SETTING}
label={i18n("open_setting_shortcut")}
/>
</Grid>
</Grid>
</Box>
</>
)}
</Stack>
</Box>
);

View File

@@ -0,0 +1,56 @@
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton";
import EditIcon from "@mui/icons-material/Edit";
import { useEffect, useState, useRef } from "react";
import { shortcutListener } from "../../libs/shortcut";
export default function ShortcutInput({ value, onChange, label, helperText }) {
const [disabled, setDisabled] = useState(true);
const inputRef = useRef(null);
useEffect(() => {
if (disabled) {
return;
}
inputRef.current.focus();
onChange([]);
const clearShortcut = shortcutListener((curkeys, allkeys) => {
onChange(allkeys);
if (curkeys.length === 0) {
setDisabled(true);
}
}, inputRef.current);
return () => {
clearShortcut();
};
}, [disabled, onChange]);
return (
<Stack direction="row" alignItems="flex-start">
<TextField
size="small"
label={label}
name={label}
value={value.map((item) => (item === " " ? "Space" : item)).join(" + ")}
fullWidth
inputRef={inputRef}
disabled={disabled}
onBlur={() => {
setDisabled(true);
}}
helperText={helperText}
/>
<IconButton
onClick={() => {
setDisabled(false);
}}
>
{<EditIcon />}
</IconButton>
</Stack>
);
}

View File

@@ -5,7 +5,13 @@ import { useI18n } from "../../hooks/I18n";
import { useSync } from "../../hooks/Sync";
import Alert from "@mui/material/Alert";
import Link from "@mui/material/Link";
import { URL_KISS_WORKER } from "../../config";
import MenuItem from "@mui/material/MenuItem";
import {
URL_KISS_WORKER,
OPT_SYNCTYPE_ALL,
OPT_SYNCTYPE_WORKER,
OPT_SYNCTYPE_WEBDAV,
} from "../../config";
import { useState } from "react";
import { syncSettingAndRules } from "../../libs/sync";
import Button from "@mui/material/Button";
@@ -35,22 +41,46 @@ export default function SyncSetting() {
setLoading(true);
await syncSettingAndRules();
await reloadSetting();
alert.success(i18n("data_sync_success"));
alert.success(i18n("sync_success"));
} catch (err) {
console.log("[sync all]", err);
alert.error(i18n("data_sync_error"));
alert.error(i18n("sync_failed"));
} finally {
setLoading(false);
}
};
const { syncUrl, syncKey } = sync;
if (!sync) {
return;
}
const {
syncType = OPT_SYNCTYPE_WORKER,
syncUrl = "",
syncUser = "",
syncKey = "",
} = sync;
return (
<Box>
<Stack spacing={3}>
<Alert severity="warning">{i18n("sync_warn")}</Alert>
<TextField
select
size="small"
name="syncType"
value={syncType}
label={i18n("data_sync_type")}
onChange={handleChange}
>
{OPT_SYNCTYPE_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField
size="small"
label={i18n("data_sync_url")}
@@ -58,10 +88,24 @@ export default function SyncSetting() {
value={syncUrl}
onChange={handleChange}
helperText={
<Link href={URL_KISS_WORKER}>{i18n("about_sync_api")}</Link>
syncType === OPT_SYNCTYPE_WORKER && (
<Link href={URL_KISS_WORKER} target="_blank">
{i18n("about_sync_api")}
</Link>
)
}
/>
{syncType === OPT_SYNCTYPE_WEBDAV && (
<TextField
size="small"
label={i18n("data_sync_user")}
name="syncUser"
value={syncUser}
onChange={handleChange}
/>
)}
<TextField
size="small"
type="password"
@@ -85,7 +129,7 @@ export default function SyncSetting() {
onClick={handleSyncTest}
startIcon={<SyncIcon />}
>
{i18n("data_sync_test")}
{i18n("sync_now")}
</Button>
{loading && <CircularProgress size={16} />}
</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, rootSelector, fixer } = site;
return (
<Stack spacing={3}>
<TextField
size="small"
label={"rootSelector"}
name="rootSelector"
value={rootSelector || "document"}
disabled
/>
<TextField
size="small"
label={"selector"}
name="selector"
value={selector}
disabled
/>
<TextField
size="small"
label={"fixer"}
name="fixer"
value={fixer}
disabled
/>
</Stack>
);
}
function ApiAccordion({ site }) {
const [expanded, setExpanded] = useState(false);
const handleChange = (e) => {
setExpanded((pre) => !pre);
};
return (
<Accordion expanded={expanded} onChange={handleChange}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{site.pattern}</Typography>
</AccordionSummary>
<AccordionDetails>
{expanded && <ApiFields site={site} />}
</AccordionDetails>
</Accordion>
);
}
export default function Webfix() {
const [loading, setLoading] = useState(false);
const [sites, setSites] = useState([]);
const i18n = useI18n();
const alert = useAlert();
const { setting, updateSetting } = useSetting();
const loadSites = useCallback(async () => {
const sites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
setSites(sites);
}, []);
const handleSyncTest = async (e) => {
e.preventDefault();
try {
setLoading(true);
await syncWebfix(process.env.REACT_APP_WEBFIXURL);
await loadSites();
alert.success(i18n("sync_success"));
} catch (err) {
console.log("[sync webfix]", err);
alert.error(i18n("sync_failed"));
} finally {
setLoading(false);
}
};
useEffect(() => {
(async () => {
try {
setLoading(true);
await loadSites();
} catch (err) {
console.log("[load webfix]", err.message);
} finally {
setLoading(false);
}
})();
}, [loadSites]);
return (
<Box>
<Stack spacing={3}>
<Alert severity="info">{i18n("patch_setting_help")}</Alert>
<Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
<Button
size="small"
variant="outlined"
disabled={loading}
onClick={handleSyncTest}
startIcon={<SyncIcon />}
>
{i18n("sync_now")}
</Button>
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
<FormControlLabel
control={
<Switch
size="small"
checked={!!setting.injectWebfix}
onChange={() => {
updateSetting({
injectWebfix: !setting.injectWebfix,
});
}}
/>
}
label={i18n("inject_webfix")}
/>
</Stack>
{setting.injectWebfix && (
<Box>
{loading ? (
<center>
<CircularProgress size={16} />
</center>
) : (
sites.map((site) => (
<ApiAccordion key={site.pattern} site={site} />
))
)}
</Box>
)}
</Stack>
</Box>
);
}

View File

@@ -17,6 +17,9 @@ import Divider from "@mui/material/Divider";
import Stack from "@mui/material/Stack";
import { adaptScript } from "../../libs/gm";
import Alert from "@mui/material/Alert";
import Apis from "./Apis";
import Webfix from "./Webfix";
import InputSetting from "./InputSetting";
export default function Options() {
const [error, setError] = useState("");
@@ -34,7 +37,7 @@ export default function Options() {
// 检查版本是否一致
if (version !== process.env.REACT_APP_VERSION) {
setError(
`The version of the script(v${version}) and this page(v${process.env.REACT_APP_VERSION}) are inconsistent.`
`The version of the local script(v${version}) is not the latest version(v${process.env.REACT_APP_VERSION}). 本地脚本之版本(v${version})非最新版(v${process.env.REACT_APP_VERSION})。`
);
break;
}
@@ -51,7 +54,9 @@ export default function Options() {
}
if (++i > 8) {
setError("Time out.");
setError(
"Time out. Please confirm whether to install or enable KISS Translator GreaseMonkey script? 连接超时,请检查是否安装或启用简约翻译油猴脚本。"
);
break;
}
@@ -68,35 +73,25 @@ export default function Options() {
if (error) {
return (
<center>
<Alert severity="error">{error}</Alert>
<Divider>
<Link
href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider>
<h2>
Please confirm whether to install or enable KISS Translator
GreaseMonkey script?
</h2>
<Alert severity="error">{error}</Alert>
<Stack spacing={2}>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
Install Userscript 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install Userscript 2
Install/Update Userscript for Tampermonkey/Violentmonkey
</Link>
{/* <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install/Update Userscript for Tampermonkey/Violentmonkey 2
</Link> */}
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install Userscript Safari 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install Userscript Safari 2
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
Open Options Page 1
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
Open Options Page 2
Install/Update Userscript for iOS Safari
</Link>
{/* <Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install/Update Userscript for iOS Safari 2
</Link> */}
</Stack>
</center>
);
@@ -124,7 +119,10 @@ export default function Options() {
<Route path="/" element={<Layout />}>
<Route index element={<Setting />} />
<Route path="rules" element={<Rules />} />
<Route path="input" element={<InputSetting />} />
<Route path="apis" element={<Apis />} />
<Route path="sync" element={<SyncSetting />} />
<Route path="webfix" element={<Webfix />} />
<Route path="about" element={<About />} />
</Route>
</Routes>

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

@@ -0,0 +1,47 @@
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import HomeIcon from "@mui/icons-material/Home";
import Stack from "@mui/material/Stack";
import DarkModeButton from "../Options/DarkModeButton";
export default function Header({ setShowPopup }) {
const handleHomepage = () => {
window.open(process.env.REACT_APP_HOMEPAGE, "_blank");
};
return (
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Stack direction="row" justifyContent="flex-start" alignItems="center">
<IconButton onClick={handleHomepage}>
<HomeIcon />
</IconButton>
<Box
sx={{
userSelect: "none",
WebkitUserSelect: "none",
}}
>
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
</Box>
</Stack>
{setShowPopup ? (
<IconButton
onClick={() => {
setShowPopup(false);
}}
>
<CloseIcon />
</IconButton>
) : (
<DarkModeButton />
)}
</Stack>
);
}

View File

@@ -5,15 +5,19 @@ import MenuItem from "@mui/material/MenuItem";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Button from "@mui/material/Button";
import { sendTabMsg } from "../../libs/msg";
import { sendBgMsg, sendTabMsg, getTabInfo } from "../../libs/msg";
import { browser } from "../../libs/browser";
import { isExt } from "../../libs/client";
import { useI18n } from "../../hooks/I18n";
import TextField from "@mui/material/TextField";
import Divider from "@mui/material/Divider";
import Header from "./Header";
import {
MSG_TRANS_TOGGLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
MSG_OPEN_OPTIONS,
MSG_SAVE_RULE,
OPT_TRANS_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
@@ -21,14 +25,18 @@ import {
OPT_STYLE_USE_COLOR,
} from "../../config";
import { sendIframeMsg } from "../../libs/iframe";
import { saveRule } from "../../libs/rules";
import { tryClearCaches } from "../../libs";
export default function Popup({ setShowPopup, translator: tran }) {
const i18n = useI18n();
const [rule, setRule] = useState(tran?.rule);
const handleOpenSetting = () => {
if (isExt) {
if (!tran) {
browser?.runtime.openOptionsPage();
} else if (isExt) {
sendBgMsg(MSG_OPEN_OPTIONS);
} else {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
}
@@ -39,7 +47,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
try {
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
if (isExt) {
if (!tran) {
await sendTabMsg(MSG_TRANS_TOGGLE);
} else {
tran.toggle();
@@ -55,7 +63,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
const { name, value } = e.target;
setRule((pre) => ({ ...pre, [name]: value }));
if (isExt) {
if (!tran) {
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
} else {
tran.updateRule({ [name]: value });
@@ -66,8 +74,30 @@ export default function Popup({ setShowPopup, translator: tran }) {
}
};
const handleClearCache = () => {
tryClearCaches();
};
const handleSaveRule = async () => {
try {
let href = window.location.href;
if (!tran) {
const tab = await getTabInfo();
href = tab.url;
}
const newRule = { ...rule, pattern: href };
if (isExt && tran) {
sendBgMsg(MSG_SAVE_RULE, newRule);
} else {
saveRule(newRule);
}
} catch (err) {
console.log("[save rule]", err);
}
};
useEffect(() => {
if (!isExt) {
if (tran) {
return;
}
(async () => {
@@ -80,12 +110,18 @@ export default function Popup({ setShowPopup, translator: tran }) {
console.log("[query rule]", err);
}
})();
}, []);
}, [tran]);
if (!rule) {
return (
<Box minWidth={300} sx={{ p: 2 }}>
<Stack spacing={3}>
<Box minWidth={300}>
{!tran && (
<>
<Header />
<Divider />
</>
)}
<Stack sx={{ p: 2 }} spacing={3}>
<Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")}
</Button>
@@ -97,17 +133,35 @@ export default function Popup({ setShowPopup, translator: tran }) {
const { transOpen, translator, fromLang, toLang, textStyle, bgColor } = rule;
return (
<Box minWidth={300} sx={{ p: 2 }}>
<Stack spacing={2}>
<FormControlLabel
control={
<Switch
checked={transOpen === "true"}
onChange={handleTransToggle}
/>
}
label={i18n("translate_alt")}
/>
<Box minWidth={300}>
{!tran && (
<>
<Header />
<Divider />
</>
)}
<Stack sx={{ p: 2 }} spacing={2}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<FormControlLabel
control={
<Switch
checked={transOpen === "true"}
onChange={handleTransToggle}
/>
}
label={i18n("translate_alt")}
/>
{!isExt && (
<Button variant="text" onClick={handleClearCache}>
{i18n("clear_cache")}
</Button>
)}
</Stack>
<TextField
select
@@ -183,9 +237,19 @@ export default function Popup({ setShowPopup, translator: tran }) {
/>
)}
<Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")}
</Button>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Button variant="text" onClick={handleSaveRule}>
{i18n("save_rule")}
</Button>
<Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")}
</Button>
</Stack>
</Stack>
</Box>
);

15461
yarn.lock

File diff suppressed because it is too large Load Diff