Compare commits

...

65 Commits

Author SHA1 Message Date
Gabe Yuan
64d827fdcd v1.6.9 2023-09-09 19:51:32 +08:00
Gabe Yuan
74ad812f37 text opacity 2023-09-09 19:43:12 +08:00
Gabe Yuan
364c829119 fix sync bug 2023-09-09 19:26:22 +08:00
Gabe Yuan
1ac2c5b61e fix shortcut 2023-09-09 17:15:13 +08:00
Gabe Yuan
0766199353 check shortcut length 2023-09-09 15:26:05 +08:00
Gabe Yuan
878bccf151 hide fab & open setting shortcut 2023-09-09 15:08:34 +08:00
Gabe Yuan
acbd258296 shorten english tab name 2023-09-09 14:10:01 +08:00
Gabe Yuan
54a14e6e5a shortcut set blank 2023-09-09 14:05:45 +08:00
Gabe Yuan
298e4b52f0 v1.6.8 2023-09-08 23:43:34 +08:00
Gabe Yuan
bee1fbcf88 follow global style bug 2023-09-08 23:38:36 +08:00
Gabe Yuan
345a34287e help button 2023-09-08 21:57:42 +08:00
Gabe Yuan
441a2ca2da webfix 2023-09-08 21:41:32 +08:00
Gabe Yuan
1ff1b21355 fix get setting with default 2023-09-08 17:10:54 +08:00
Gabe Yuan
117ca4e05b webfix setting 2023-09-08 16:56:00 +08:00
Gabe Yuan
07d457be4e add clear all rules button 2023-09-08 15:32:42 +08:00
Gabe Yuan
d48296046e show subrule sync time 2023-09-08 15:16:10 +08:00
Gabe Yuan
56350de2cf fix shortcut 2023-09-08 13:53:33 +08:00
Gabe Yuan
850dc0e83b fix throttle func 2023-09-08 10:52:42 +08:00
Gabe Yuan
35f01478b1 add throttle func 2023-09-08 10:32:44 +08:00
Gabe Yuan
f9a3ec012f fix shortcut 2023-09-07 23:51:08 +08:00
Gabe Yuan
3b9b404482 shortcuts dev 2023-09-07 23:47:24 +08:00
Gabe Yuan
d8b0cc4834 shortcuts dev... 2023-09-07 18:12:45 +08:00
Gabe Yuan
da13f5e218 add isAllchar func 2023-09-07 10:20:08 +08:00
Gabe Yuan
08e14ae11c fix i18n text 2023-09-07 00:03:19 +08:00
Gabe Yuan
c2902dff28 fix i18n text 2023-09-06 23:58:11 +08:00
Gabe Yuan
c4fb39f02f fix var text 2023-09-06 23:44:01 +08:00
Gabe Yuan
b7df44c35a hide clear cache in userscript 2023-09-06 23:39:11 +08:00
Gabe Yuan
9a2b21eee5 mouseover dev 2023-09-06 23:35:09 +08:00
Gabe Yuan
bdac67df88 mouseover dev... 2023-09-06 18:00:18 +08:00
Gabe Yuan
0b8f19bfad remove styled-components 2023-09-06 15:12:17 +08:00
Gabe Yuan
c7c5866131 customize api 2023-09-06 14:57:02 +08:00
Gabe Yuan
f772fa000c customize api... 2023-09-06 00:25:46 +08:00
Gabe Yuan
93fd82fcd9 fuzzy style 2023-09-05 17:00:18 +08:00
Gabe Yuan
3ae10bfd04 fix herf match func 2023-09-05 16:11:33 +08:00
Gabe Yuan
a44747ccad fix herf match func 2023-09-05 16:06:48 +08:00
Gabe Yuan
87ab45f936 fix readme text 2023-09-05 13:35:12 +08:00
Gabe Yuan
37b046eb46 fix rule selector 2023-09-05 13:27:04 +08:00
Gabe Yuan
c6f8a45027 v1.6.7 2023-09-04 23:28:41 +08:00
Gabe Yuan
6ec16e1f98 fix popup header 2023-09-04 23:24:50 +08:00
Gabe Yuan
40adf85b20 fix help link 2023-09-04 23:12:35 +08:00
Gabe Yuan
4c78f469c1 update readme 2023-09-04 23:08:00 +08:00
Gabe Yuan
55af58faac add rules help button 2023-09-04 22:29:39 +08:00
Gabe Yuan
4200caa641 fix popup header 2023-09-04 22:03:17 +08:00
Gabe Yuan
0ac06f8e3d use new rules link 2023-09-04 17:04:28 +08:00
Gabe Yuan
966c78fb16 optimize test link 2023-09-03 22:18:33 +08:00
Gabe Yuan
5c5a35d3bb v1.6.6 2023-09-03 21:47:38 +08:00
Gabe Yuan
2c24214f48 fix Violentmonkey --> .connect 2023-09-03 21:45:06 +08:00
Gabe Yuan
67d9e70b3c v1,6,5 2023-09-03 21:10:35 +08:00
Gabe Yuan
000a55f43b modify content.html 2023-09-03 21:08:45 +08:00
Gabe Yuan
4096a6976c add clear cache & api test button 2023-09-03 13:11:04 +08:00
Gabe Yuan
df4c4ebd50 fix i18n text 2023-09-03 00:26:57 +08:00
Gabe Yuan
b43bd4e0e2 add deepl @connect 2023-09-03 00:10:07 +08:00
Gabe Yuan
2660dbf866 update readme 2023-09-02 23:45:40 +08:00
Gabe Yuan
e0b7c60099 v1.6.4 2023-09-02 20:07:24 +08:00
Gabe Yuan
536b58bf67 fix fuzzy style hover bug 2023-09-02 19:55:26 +08:00
Gabe Yuan
6bb742f828 v1.6.3 2023-09-02 19:16:14 +08:00
Gabe Yuan
72742e5e12 fix build:script 2023-09-02 19:09:12 +08:00
Gabe Yuan
3667e0a509 update readme 2023-09-02 18:14:42 +08:00
Gabe Yuan
c2d7668ba7 v1.6.2 2023-09-02 17:26:37 +08:00
Gabe Yuan
aa830f5e20 update readme 2023-09-02 17:22:35 +08:00
Gabe Yuan
b593fa4146 add deepl api 2023-09-02 16:57:09 +08:00
Gabe Yuan
b00b906484 optimize inject script 2023-09-02 15:54:51 +08:00
Gabe Yuan
c1bd6a1be6 use random eventname 2023-09-02 14:14:27 +08:00
Gabe Yuan
36739f04b3 add more shortcut 2023-09-02 13:14:27 +08:00
Gabe Yuan
23eb92853e register a shortcut for userscript 2023-09-02 00:42:15 +08:00
46 changed files with 2389 additions and 988 deletions

9
.env
View File

@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译 REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=1.6.1 REACT_APP_VERSION=1.6.9
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator 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_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
REACT_APP_LOGOURL2=https://kiss-translator.rayjar.com/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_RULESURL=https://fishjar.github.io/kiss-rules/kiss-rules.json
REACT_APP_RULESURL2=https://kiss-translator.rayjar.com/kiss-translator-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_VERSIONFILE=https://fishjar.github.io/kiss-translator/version.txt
REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt

View File

@@ -1,10 +1,10 @@
## KISS Translator # KISS Translator
A minimalist [bilingual translation Extension & Greasemonkey Script](https://github.com/fishjar/kiss-translator). A minimalist [bilingual translation Extension & Greasemonkey Script](https://github.com/fishjar/kiss-translator).
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f) [kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
### Inspiration ## Inspiration
The inspiration for this project comes from [Immersive Translate](https://github.com/immersive-translate/immersive-translate). After trying it out, I found that it can be used together with the [Webpage Word Translation Extension](https://github.com/fishjar/kiss-dictionary) developed by me earlier, which just forms a very good supplement. The inspiration for this project comes from [Immersive Translate](https://github.com/immersive-translate/immersive-translate). After trying it out, I found that it can be used together with the [Webpage Word Translation Extension](https://github.com/fishjar/kiss-dictionary) developed by me earlier, which just forms a very good supplement.
@@ -14,11 +14,39 @@ It just so happens that I am obsessed with translation tools. Based on the conce
If you also like a little more simplicity, welcome to pick it up. If you also like a little more simplicity, welcome to pick it up.
### Features ## Features
- Keep it simple, smart - Keep it simple, smart
### Schedule ## 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.
## Description
### Support shortcut keys
- `Alt+Q` Toggle Translation
- `Alt+C` Toggle Styles
- `Alt+K` Open Menu
## Schedule
- [x] Provide trial installation package - [x] Provide trial installation package
- [x] Adapt browser - [x] Adapt browser
@@ -30,8 +58,8 @@ If you also like a little more simplicity, welcome to pick it up.
- [x] Support translation services - [x] Support translation services
- [x] Google - [x] Google
- [x] Microsoft - [x] Microsoft
- [x] DeepL
- [x] OpenAI - [x] OpenAI
- [ ] DeepL
- [x] Upload to app Store - [x] Upload to app Store
- [x] Chrome [Install Link](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof) - [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] Edge [Install Link](https://microsoftedge.microsoft.com/addons/detail/kiss-translator/jemckldkclkinpjighnoilpbldbdmmlh)
@@ -42,9 +70,10 @@ If you also like a little more simplicity, welcome to pick it up.
- [x] Data Synchronization Function - [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] 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] [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] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
### Guide ## Guide
```sh ```sh
git clone https://github.com/fishjar/kiss-translator.git git clone https://github.com/fishjar/kiss-translator.git
@@ -53,10 +82,6 @@ yarn install
yarn build yarn build
``` ```
### Data Sync ## Discussion
Goto: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
### Discussion
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl) - Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)

View File

@@ -1,10 +1,10 @@
## 简约翻译 # 简约翻译
一个简约的 [双语网页翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。 一个简约的 [双语网页翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f) [kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
### 缘由 ## 缘由
本项目灵感来源于 [Immersive Translate](https://github.com/immersive-translate/immersive-translate),在试用了后,发现搭配本人早前开发的 [网页划词翻译扩展](https://github.com/fishjar/kiss-dictionary) 一起使用,刚好形成很好补充。 本项目灵感来源于 [Immersive Translate](https://github.com/immersive-translate/immersive-translate),在试用了后,发现搭配本人早前开发的 [网页划词翻译扩展](https://github.com/fishjar/kiss-dictionary) 一起使用,刚好形成很好补充。
@@ -14,11 +14,39 @@
如果你也喜欢简约一点的,欢迎自取。 如果你也喜欢简约一点的,欢迎自取。
### 特点 ## 特点
- 保持简约 - 保持简约
### 进度 ## 关联项目
- 数据同步服务: [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)
- 搭配本项目一起使用的划词翻译插件。
- 支持英文单词、句子、汉字的查询。
- 支持历史记录、单词收藏。
## 简要说明
### 支持快捷键
- `Alt+Q` 开启翻译
- `Alt+C` 切换样式
- `Alt+K` 打开菜单
## 进度
- [x] 提供试用安装包 - [x] 提供试用安装包
- [x] 适配浏览器 - [x] 适配浏览器
@@ -30,8 +58,8 @@
- [x] 支持翻译服务 - [x] 支持翻译服务
- [x] Google - [x] Google
- [x] Microsoft - [x] Microsoft
- [x] DeepL
- [x] OpenAI - [x] OpenAI
- [ ] DeepL
- [x] 上架应用市场 - [x] 上架应用市场
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN) - [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] Edge [安装地址](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
@@ -42,9 +70,10 @@
- [x] 数据同步功能 - [x] 数据同步功能
- [x] 油猴脚本 ([设置页面 1](https://fishjar.github.io/kiss-translator/options.html)、[设置页面 2](https://kiss-translator.rayjar.com/options)) - [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] [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] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
### 指引 ## 指引
```sh ```sh
git clone https://github.com/fishjar/kiss-translator.git git clone https://github.com/fishjar/kiss-translator.git
@@ -53,10 +82,6 @@ yarn install
yarn build yarn build
``` ```
### 数据同步 ## 交流
移步: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
### 交流
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl) - 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)

View File

@@ -93,6 +93,8 @@ const userscriptWebpack = (config, env) => {
// @connect translate.googleapis.com // @connect translate.googleapis.com
// @connect api-edge.cognitive.microsofttranslator.com // @connect api-edge.cognitive.microsofttranslator.com
// @connect edge.microsoft.com // @connect edge.microsoft.com
// @connect api-free.deepl.com
// @connect api.deepl.com
// @connect api.openai.com // @connect api.openai.com
// @connect openai.azure.com // @connect openai.azure.com
// @connect workers.dev // @connect workers.dev
@@ -100,6 +102,7 @@ const userscriptWebpack = (config, env) => {
// @connect githubusercontent.com // @connect githubusercontent.com
// @connect kiss-translator.rayjar.com // @connect kiss-translator.rayjar.com
// @connect ghproxy.com // @connect ghproxy.com
// @connect localhost:3000
// @run-at document-end // @run-at document-end
// ==/UserScript== // ==/UserScript==

View File

@@ -1,7 +1,7 @@
{ {
"name": "kiss-translator", "name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script", "description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.6.1", "version": "1.6.9",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"private": true, "private": true,
"dependencies": { "dependencies": {
@@ -15,7 +15,6 @@
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-router-dom": "^6.10.0", "react-router-dom": "^6.10.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"styled-components": "^6.0.7",
"webextension-polyfill": "^0.10.0" "webextension-polyfill": "^0.10.0"
}, },
"scripts": { "scripts": {
@@ -25,10 +24,10 @@
"build:edge": "rm -rf build/edge && cp -r build/chrome build/edge", "build:edge": "rm -rf build/edge && cp -r build/chrome build/edge",
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json", "build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json",
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build", "build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/kiss-translator.user.js build/userscript/kiss-translator.user.js", "build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
"build:userscript-ios": "file1=build/userscript/kiss-translator.user.js file2=build/userscript/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2", "build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
"build:rules": "babel-node src/rules.js", "build:rules": "babel-node src/rules.js",
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript && yarn build:userscript-ios && yarn build:rules", "build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript-ios && yarn build:userscript && yarn build:rules",
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator", "deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
"test": "react-app-rewired test", "test": "react-app-rewired test",
"eject": "react-scripts eject" "eject": "react-scripts eject"

View File

@@ -64,8 +64,25 @@
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"> <div id="root">
<div id="content">
<p>You need to enable JavaScript to run <span>this app.</span></p>
The <span>embargo</span> has just lifted to confirm that AmpereOne is
coming to Google Cloud with the C3A instances.
<br />
But these upcoming instances for now are only in private preview form.
<br />
<br />
Needless to say I also haven't had any AmpereOne access to check out the
performance and power efficiency of these new Arm server processors from
Ampere Computing.
<br />
</div>
<h2> <h2>
<p><span>React is a JavaScript library for building user interfaces.</span></p> <p>
<span
>React is a JavaScript library for building user interfaces.</span
>
</p>
</h2> </h2>
<div id="addtitle"></div> <div id="addtitle"></div>
<h2>Shadow 1</h2> <h2>Shadow 1</h2>

View File

@@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.6.1", "version": "1.6.9",
"default_locale": "en", "default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator", "homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -17,6 +17,11 @@
} }
], ],
"commands": { "commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Alt+K"
}
},
"toggleTranslate": { "toggleTranslate": {
"suggested_key": { "suggested_key": {
"default": "Alt+Q" "default": "Alt+Q"

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.6.1", "version": "1.6.9",
"default_locale": "en", "default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator", "homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -18,6 +18,11 @@
} }
], ],
"commands": { "commands": {
"_execute_action": {
"suggested_key": {
"default": "Alt+K"
}
},
"toggleTranslate": { "toggleTranslate": {
"suggested_key": { "suggested_key": {
"default": "Alt+Q" "default": "Alt+Q"

View File

@@ -3,8 +3,9 @@ import { fetchPolyfill } from "../libs/fetch";
import { import {
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
URL_MICROSOFT_TRANS, OPT_TRANS_CUSTOMIZE,
OPT_LANGS_SPECIAL, OPT_LANGS_SPECIAL,
PROMPT_PLACE_FROM, PROMPT_PLACE_FROM,
PROMPT_PLACE_TO, PROMPT_PLACE_TO,
@@ -32,13 +33,12 @@ export const apiSyncData = async (url, key, data, isBg = false) =>
}); });
/** /**
* 下载订阅规则 * 下载数据
* @param {*} url * @param {*} url
* @param {*} isBg * @param {*} isBg
* @returns * @returns
*/ */
export const apiFetchRules = (url, isBg = false) => export const apiFetch = (url, isBg = false) => fetchPolyfill(url, { isBg });
fetchPolyfill(url, { isBg });
/** /**
* 谷歌翻译 * 谷歌翻译
@@ -47,8 +47,13 @@ export const apiFetchRules = (url, isBg = false) =>
* @param {*} from * @param {*} from
* @returns * @returns
*/ */
const apiGoogleTranslate = async (translator, text, to, from, setting) => { const apiGoogleTranslate = async (
const { googleUrl } = setting; translator,
text,
to,
from,
{ url, key, useCache = true }
) => {
const params = { const params = {
client: "gtx", client: "gtx",
dt: "t", dt: "t",
@@ -58,15 +63,20 @@ const apiGoogleTranslate = async (translator, text, to, from, setting) => {
tl: to, tl: to,
q: text, q: text,
}; };
const input = `${googleUrl}?${queryString.stringify(params)}`; const input = `${url}?${queryString.stringify(params)}`;
return fetchPolyfill(input, { const res = await fetchPolyfill(input, {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
}, },
useCache: true, useCache,
usePool: true, usePool: true,
translator, translator,
token: key,
}); });
const trText = res.sentences.map((item) => item.trans).join(" ");
const isSame = to === res.src;
return [trText, isSame];
}; };
/** /**
@@ -76,23 +86,72 @@ const apiGoogleTranslate = async (translator, text, to, from, setting) => {
* @param {*} from * @param {*} from
* @returns * @returns
*/ */
const apiMicrosoftTranslate = (translator, text, to, from) => { const apiMicrosoftTranslate = async (
translator,
text,
to,
from,
{ url, useCache = true }
) => {
const params = { const params = {
from, from,
to, to,
"api-version": "3.0", "api-version": "3.0",
}; };
const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`; const input = `${url}?${queryString.stringify(params)}`;
return fetchPolyfill(input, { const res = await fetchPolyfill(input, {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
}, },
method: "POST", method: "POST",
body: JSON.stringify([{ Text: text }]), body: JSON.stringify([{ Text: text }]),
useCache: true, useCache,
usePool: true, usePool: true,
translator, translator,
}); });
const trText = res[0].translations[0].text;
const isSame = to === res[0].detectedLanguage?.language;
return [trText, isSame];
};
/**
* DeepL翻译
* @param {*} text
* @param {*} to
* @param {*} from
* @returns
*/
const apiDeepLTranslate = async (
translator,
text,
to,
from,
{ url, key, useCache = true }
) => {
const data = {
text: [text],
target_lang: to,
split_sentences: "0",
};
if (from) {
data.source_lang = from;
}
const res = await fetchPolyfill(url, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
useCache,
usePool: true,
translator,
token: key,
});
const trText = res.translations.map((item) => item.text).join(" ");
const isSame = to === res.translations[0].detected_source_language;
return [trText, isSame];
}; };
/** /**
@@ -102,18 +161,23 @@ const apiMicrosoftTranslate = (translator, text, to, from) => {
* @param {*} from * @param {*} from
* @returns * @returns
*/ */
const apiOpenaiTranslate = async (translator, text, to, from, setting) => { const apiOpenaiTranslate = async (
const { openaiUrl, openaiKey, openaiModel, openaiPrompt } = setting; translator,
let prompt = openaiPrompt text,
to,
from,
{ url, key, model, prompt, useCache = true }
) => {
prompt = prompt
.replaceAll(PROMPT_PLACE_FROM, from) .replaceAll(PROMPT_PLACE_FROM, from)
.replaceAll(PROMPT_PLACE_TO, to); .replaceAll(PROMPT_PLACE_TO, to);
return fetchPolyfill(openaiUrl, { const res = await fetchPolyfill(url, {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
}, },
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
model: openaiModel, model: model,
messages: [ messages: [
{ {
role: "system", role: "system",
@@ -127,11 +191,52 @@ const apiOpenaiTranslate = async (translator, text, to, from, setting) => {
temperature: 0, temperature: 0,
max_tokens: 256, max_tokens: 256,
}), }),
useCache: true, useCache,
usePool: true, usePool: true,
translator, translator,
token: openaiKey, token: key,
}); });
const trText = res?.choices?.[0].message.content;
const sLang = await tryDetectLang(text);
const tLang = await tryDetectLang(trText);
const isSame = text === trText || (sLang && tLang && sLang === tLang);
return [trText, isSame];
};
/**
* 自定义接口 翻译
* @param {*} text
* @param {*} to
* @param {*} from
* @returns
*/
const apiCustomTranslate = async (
translator,
text,
to,
from,
{ url, key, useCache = true }
) => {
const res = await fetchPolyfill(url, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify({
text,
from,
to,
}),
useCache,
usePool: true,
translator,
token: key,
});
const trText = res.text;
const isSame = to === res.from;
return [trText, isSame];
}; };
/** /**
@@ -139,34 +244,29 @@ const apiOpenaiTranslate = async (translator, text, to, from, setting) => {
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export const apiTranslate = async ({ export const apiTranslate = ({
translator, translator,
q, text,
fromLang, fromLang,
toLang, toLang,
setting, apiSetting,
}) => { }) => {
let trText = ""; const from = OPT_LANGS_SPECIAL[translator]?.get(fromLang) ?? fromLang;
let isSame = false; const to = OPT_LANGS_SPECIAL[translator]?.get(toLang) ?? toLang;
const callApi = (api) => api(translator, text, to, from, apiSetting);
let from = OPT_LANGS_SPECIAL?.[translator]?.get(fromLang) ?? fromLang; switch (translator) {
let to = OPT_LANGS_SPECIAL?.[translator]?.get(toLang) ?? toLang; case OPT_TRANS_GOOGLE:
return callApi(apiGoogleTranslate);
if (translator === OPT_TRANS_GOOGLE) { case OPT_TRANS_MICROSOFT:
const res = await apiGoogleTranslate(translator, q, to, from, setting); return callApi(apiMicrosoftTranslate);
trText = res.sentences.map((item) => item.trans).join(" "); case OPT_TRANS_DEEPL:
isSame = to === res.src; return callApi(apiDeepLTranslate);
} else if (translator === OPT_TRANS_MICROSOFT) { case OPT_TRANS_OPENAI:
const res = await apiMicrosoftTranslate(translator, q, to, from); return callApi(apiOpenaiTranslate);
trText = res[0].translations[0].text; case OPT_TRANS_CUSTOMIZE:
isSame = to === res[0].detectedLanguage.language; return callApi(apiCustomTranslate);
} else if (translator === OPT_TRANS_OPENAI) { default:
const res = await apiOpenaiTranslate(translator, q, to, from, setting); return ["", false];
trText = res?.choices?.[0].message.content;
const sLang = await tryDetectLang(q);
const tLang = await tryDetectLang(trText);
isSame = q === trText || (sLang && tLang && sLang === tLang);
} }
return [trText, isSame];
}; };

View File

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

View File

@@ -25,6 +25,7 @@ export const STOKEY_RULES = `${APP_NAME}_rules`;
export const STOKEY_SYNC = `${APP_NAME}_sync`; export const STOKEY_SYNC = `${APP_NAME}_sync`;
export const STOKEY_FAB = `${APP_NAME}_fab`; export const STOKEY_FAB = `${APP_NAME}_fab`;
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`; export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
export const STOKEY_WEBFIXCACHE_PREFIX = `${APP_NAME}_webfixcache_`;
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate"; export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
export const CMD_TOGGLE_STYLE = "toggleStyle"; export const CMD_TOGGLE_STYLE = "toggleStyle";
@@ -53,26 +54,29 @@ export const MSG_TRANS_GETRULE = "trans_getrule";
export const MSG_TRANS_PUTRULE = "trans_putrule"; export const MSG_TRANS_PUTRULE = "trans_putrule";
export const MSG_TRANS_CURRULE = "trans_currule"; export const MSG_TRANS_CURRULE = "trans_currule";
export const EVENT_KISS = "kissEvent";
export const THEME_LIGHT = "light"; export const THEME_LIGHT = "light";
export const THEME_DARK = "dark"; export const THEME_DARK = "dark";
export const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker"; export const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker";
export const URL_KISS_PROXY = "https://github.com/fishjar/kiss-proxy"; export const URL_KISS_PROXY = "https://github.com/fishjar/kiss-proxy";
export const URL_KISS_RULES = "https://github.com/fishjar/kiss-rules";
export const URL_KISS_RULES_NEW_ISSUE =
"https://github.com/fishjar/kiss-rules/issues/new";
export const URL_RAW_PREFIX = export const URL_RAW_PREFIX =
"https://raw.githubusercontent.com/fishjar/kiss-translator/master"; "https://raw.githubusercontent.com/fishjar/kiss-translator/master";
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth"; export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
export const URL_MICROSOFT_TRANS =
"https://api-edge.cognitive.microsofttranslator.com/translate";
export const OPT_TRANS_GOOGLE = "Google"; export const OPT_TRANS_GOOGLE = "Google";
export const OPT_TRANS_MICROSOFT = "Microsoft"; export const OPT_TRANS_MICROSOFT = "Microsoft";
export const OPT_TRANS_DEEPL = "DeepL";
export const OPT_TRANS_OPENAI = "OpenAI"; export const OPT_TRANS_OPENAI = "OpenAI";
export const OPT_TRANS_CUSTOMIZE = "Custom";
export const OPT_TRANS_ALL = [ export const OPT_TRANS_ALL = [
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
OPT_TRANS_CUSTOMIZE,
]; ];
export const OPT_LANGS_TO = [ export const OPT_LANGS_TO = [
@@ -121,9 +125,16 @@ export const OPT_LANGS_SPECIAL = {
["zh-CN", "zh-Hans"], ["zh-CN", "zh-Hans"],
["zh-TW", "zh-Hant"], ["zh-TW", "zh-Hant"],
]), ]),
[OPT_TRANS_DEEPL]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
["auto", ""],
["zh-CN", "ZH"],
["zh-TW", "ZH"],
]),
[OPT_TRANS_OPENAI]: new Map( [OPT_TRANS_OPENAI]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
), ),
[OPT_TRANS_CUSTOMIZE]: new Map([["auto", ""]]),
}; };
export const OPT_STYLE_NONE = "style_none"; // 无 export const OPT_STYLE_NONE = "style_none"; // 无
@@ -152,6 +163,19 @@ export const OPT_STYLE_USE_COLOR = [
OPT_STYLE_HIGHLIGHT, OPT_STYLE_HIGHLIGHT,
]; ];
export const OPT_MOUSEKEY_DISABLE = "mk_disable";
export const OPT_MOUSEKEY_MOUSEOVER = "mk_mouseover";
export const OPT_MOUSEKEY_CONTROL = "mk_ctrlKey";
export const OPT_MOUSEKEY_SHIFT = "mk_shiftKey";
export const OPT_MOUSEKEY_ALT = "mk_altKey";
export const OPT_MOUSEKEY_ALL = [
OPT_MOUSEKEY_DISABLE,
OPT_MOUSEKEY_MOUSEOVER,
OPT_MOUSEKEY_CONTROL,
OPT_MOUSEKEY_SHIFT,
OPT_MOUSEKEY_ALT,
];
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量 export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间 export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
@@ -177,14 +201,56 @@ export const GLOBLA_RULE = {
export const DEFAULT_SUBRULES_LIST = [ export const DEFAULT_SUBRULES_LIST = [
{ {
url: process.env.REACT_APP_RULESURL, url: process.env.REACT_APP_RULESURL,
selected: false,
},
{
url: process.env.REACT_APP_RULESURL_ON,
selected: true, selected: true,
}, },
{ {
url: process.env.REACT_APP_RULESURL2, url: process.env.REACT_APP_RULESURL_OFF,
selected: false, selected: false,
}, },
]; ];
// 翻译接口
export const DEFAULT_TRANS_APIS = {
[OPT_TRANS_GOOGLE]: {
url: "https://translate.googleapis.com/translate_a/single",
key: "",
},
[OPT_TRANS_MICROSOFT]: {
url: "https://api-edge.cognitive.microsofttranslator.com/translate",
authUrl: "https://edge.microsoft.com/translate/auth",
},
[OPT_TRANS_DEEPL]: {
url: "https://api-free.deepl.com/v2/translate",
key: "",
},
[OPT_TRANS_OPENAI]: {
url: "https://api.openai.com/v1/chat/completion",
key: "",
model: "gpt-4",
prompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
},
[OPT_TRANS_CUSTOMIZE]: {
url: "",
key: "",
},
};
// 默认快捷键
export const OPT_SHORTCUT_TRANSLATE = "toggleTranslate";
export const OPT_SHORTCUT_STYLE = "toggleStyle";
export const OPT_SHORTCUT_POPUP = "togglePopup";
export const OPT_SHORTCUT_SETTING = "openSetting";
export const DEFAULT_SHORTCUTS = {
[OPT_SHORTCUT_TRANSLATE]: ["Alt", "q"],
[OPT_SHORTCUT_STYLE]: ["Alt", "c"],
[OPT_SHORTCUT_POPUP]: ["Alt", "k"],
[OPT_SHORTCUT_SETTING]: ["Alt", "o"],
};
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度 export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度 export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
export const TRANS_NEWLINE_LENGTH = 40; // 换行字符数 export const TRANS_NEWLINE_LENGTH = 40; // 换行字符数
@@ -199,13 +265,13 @@ export const DEFAULT_SETTING = {
newlineLength: TRANS_NEWLINE_LENGTH, newlineLength: TRANS_NEWLINE_LENGTH,
clearCache: false, // 是否在浏览器下次启动时清除缓存 clearCache: false, // 是否在浏览器下次启动时清除缓存
injectRules: true, // 是否注入订阅规则 injectRules: true, // 是否注入订阅规则
injectWebfix: true, // 是否注入修复补丁
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表 subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则 owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口 transApis: DEFAULT_TRANS_APIS, // 翻译接口
openaiUrl: "https://api.openai.com/v1/chat/completions", mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译
openaiKey: "", shortcuts: DEFAULT_SHORTCUTS, // 快捷键
openaiModel: "gpt-4", hideFab: false, // 是否隐藏按钮
openaiPrompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
}; };
export const DEFAULT_RULES = [GLOBLA_RULE]; export const DEFAULT_RULES = [GLOBLA_RULE];

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})`; 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, ...DEFAULT_RULE,
...item, ...item,
transOpen: "true", transOpen: "true",

View File

@@ -9,6 +9,7 @@ import { getSettingWithDefault, getRulesWithDefault } from "./libs/storage";
import { Translator } from "./libs/translator"; import { Translator } from "./libs/translator";
import { isIframe } from "./libs/iframe"; import { isIframe } from "./libs/iframe";
import { matchRule } from "./libs/rules"; import { matchRule } from "./libs/rules";
import { webfix } from "./libs/webfix";
/** /**
* 入口函数 * 入口函数
@@ -19,6 +20,7 @@ const init = async () => {
const rules = await getRulesWithDefault(); const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting); const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting); const translator = new Translator(rule, setting);
webfix(href, setting);
// 监听消息 // 监听消息
browser?.runtime.onMessage.addListener(async ({ action, args }) => { browser?.runtime.onMessage.addListener(async ({ action, args }) => {
@@ -45,9 +47,10 @@ const init = async () => {
try { try {
await init(); await init();
} catch (err) { } catch (err) {
console.error("[KISS-Translator]", err);
const $err = document.createElement("div"); const $err = document.createElement("div");
$err.innerText = `KISS-Translator: ${err.message}`; $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); 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 };
}

View File

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

View File

@@ -1,22 +1,20 @@
import { STOKEY_SETTING, DEFAULT_SETTING } from "../config"; import { STOKEY_SETTING, DEFAULT_SETTING } from "../config";
import { useStorage } from "./Storage"; import { useStorage } from "./Storage";
import { useSync } from "./Sync";
import { trySyncSetting } from "../libs/sync"; import { trySyncSetting } from "../libs/sync";
import { createContext, useCallback, useContext, useMemo } from "react"; import { createContext, useCallback, useContext, useMemo } from "react";
import { debounce } from "../libs/utils"; import { debounce } from "../libs/utils";
const SettingContext = createContext({ const SettingContext = createContext({
setting: null, setting: {},
updateSetting: async () => {}, updateSetting: async () => {},
reloadSetting: async () => {}, reloadSetting: async () => {},
}); });
export function SettingProvider({ children }) { export function SettingProvider({ children }) {
const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING); const { data, update, reload, loading } = useStorage(
const { STOKEY_SETTING,
sync: { settingUpdateAt }, DEFAULT_SETTING
updateSync, );
} = useSync();
const syncSetting = useMemo( const syncSetting = useMemo(
() => () =>
@@ -28,14 +26,16 @@ export function SettingProvider({ children }) {
const updateSetting = useCallback( const updateSetting = useCallback(
async (obj) => { async (obj) => {
const updateAt = settingUpdateAt ? Date.now() : 0;
await update(obj); await update(obj);
await updateSync({ settingUpdateAt: updateAt });
syncSetting(); syncSetting();
}, },
[settingUpdateAt, update, updateSync, syncSetting] [update, syncSetting]
); );
if (loading) {
return;
}
return ( return (
<SettingContext.Provider <SettingContext.Provider
value={{ 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

@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react";
import { storage } from "../libs/storage"; import { storage } from "../libs/storage";
export function useStorage(key, defaultVal = null) { export function useStorage(key, defaultVal = null) {
const [loading, setLoading] = useState(true);
const [data, setData] = useState(defaultVal); const [data, setData] = useState(defaultVal);
const save = useCallback( const save = useCallback(
@@ -36,9 +37,16 @@ export function useStorage(key, defaultVal = null) {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
await reload(); try {
setLoading(true);
await reload();
} catch (err) {
//
} finally {
setLoading(false);
}
})(); })();
}, [reload]); }, [reload]);
return { data, save, update, remove, reload }; return { data, save, update, remove, reload, loading };
} }

View File

@@ -32,10 +32,23 @@ export function useSubRules() {
[list, updateSetting] [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( const addSub = useCallback(
async (url) => { async (url) => {
const subrulesList = [...list]; const subrulesList = [...list];
subrulesList.push({ url, selected: false }); subrulesList.push({ url, selected: false, syncAt: Date.now() });
await updateSetting({ subrulesList }); await updateSetting({ subrulesList });
}, },
[list, updateSetting] [list, updateSetting]
@@ -70,6 +83,7 @@ export function useSubRules() {
return { return {
subList: list, subList: list,
selectSub, selectSub,
updateSub,
addSub, addSub,
delSub, delSub,
selectedSub, selectedSub,

View File

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

View File

@@ -7,6 +7,7 @@ import {
MSG_FETCH_CLEAR, MSG_FETCH_CLEAR,
CACHE_NAME, CACHE_NAME,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
DEFAULT_FETCH_INTERVAL, DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT,
@@ -66,11 +67,15 @@ const newCacheReq = async (request) => {
* @returns * @returns
*/ */
const fetchApi = async ({ input, init = {}, translator, token }) => { const fetchApi = async ({ input, init = {}, translator, token }) => {
if (translator === OPT_TRANS_MICROSOFT) { if (token) {
init.headers["Authorization"] = `Bearer ${token}`; if (translator === OPT_TRANS_DEEPL) {
} else if (translator === OPT_TRANS_OPENAI) { init.headers["Authorization"] = `DeepL-Auth-Key ${token}`; // DeepL
init.headers["Authorization"] = `Bearer ${token}`; // // OpenAI } else if (translator === OPT_TRANS_OPENAI) {
init.headers["api-key"] = token; // Azure OpenAI init.headers["Authorization"] = `Bearer ${token}`; // OpenAI
init.headers["api-key"] = token; // Azure OpenAI
} else {
init.headers["Authorization"] = `Bearer ${token}`; // Microsoft & others
}
} }
if (isGm) { if (isGm) {
@@ -80,7 +85,9 @@ const fetchApi = async ({ input, init = {}, translator, token }) => {
} else { } else {
info = GM.info; 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 url = new URL(input);
const isSafe = connects.find((item) => url.hostname.endsWith(item)); const isSafe = connects.find((item) => url.hostname.endsWith(item));
if (isSafe) { if (isSafe) {
@@ -170,6 +177,10 @@ export const fetchData = async (
* @returns * @returns
*/ */
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => { export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
if (!input.trim()) {
throw new Error("URL is empty");
}
// 插件 // 插件
if (isExt && !isBg) { if (isExt && !isBg) {
const res = await sendBgMsg(MSG_FETCH, { input, opts }); const res = await sendBgMsg(MSG_FETCH, { input, opts });

View File

@@ -1,20 +1,31 @@
import { fetchGM } from "./fetch"; import { fetchGM } from "./fetch";
import { genEventName } from "./utils";
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
const MSG_GM_setValue = "setValue";
const MSG_GM_getValue = "getValue";
const MSG_GM_deleteValue = "deleteValue";
const MSG_GM_info = "info";
/** /**
* 注入页面的脚本请求并接受GM接口信息 * 注入页面的脚本请求并接受GM接口信息
* @param {*} param0 * @param {*} param0
*/ */
export const injectScript = (ping) => { export const injectScript = (ping) => {
const MSG_GM_xmlHttpRequest = "xmlHttpRequest"; window.APP_INFO = {
const MSG_GM_setValue = "setValue"; name: process.env.REACT_APP_NAME,
const MSG_GM_getValue = "getValue"; version: process.env.REACT_APP_VERSION,
const MSG_GM_deleteValue = "deleteValue"; eventName: ping,
const MSG_GM_info = "info"; };
let GM_info; };
/**
* 适配GM脚本
*/
export const adaptScript = (ping) => {
const promiseGM = (action, args, timeout = 5000) => const promiseGM = (action, args, timeout = 5000) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const pong = btoa(Math.random()).slice(3, 11); const pong = genEventName();
const handleEvent = (e) => { const handleEvent = (e) => {
window.removeEventListener(pong, handleEvent); window.removeEventListener(pong, handleEvent);
const { data, error } = e.detail; const { data, error } = e.detail;
@@ -41,14 +52,13 @@ export const injectScript = (ping) => {
setValue: (key, val) => promiseGM(MSG_GM_setValue, { key, val }), setValue: (key, val) => promiseGM(MSG_GM_setValue, { key, val }),
getValue: (key) => promiseGM(MSG_GM_getValue, { key }), getValue: (key) => promiseGM(MSG_GM_getValue, { key }),
deleteValue: (key) => promiseGM(MSG_GM_deleteValue, { key }), deleteValue: (key) => promiseGM(MSG_GM_deleteValue, { key }),
getInfo: () => { getInfo: async () => {
if (GM_info) { if (!window.GM_info) {
return GM_info; window.GM_info = await promiseGM(MSG_GM_info);
} }
return promiseGM(MSG_GM_info); return window.GM_info;
}, },
}; };
window.APP_NAME = process.env.REACT_APP_NAME;
}; };
/** /**
@@ -56,11 +66,6 @@ export const injectScript = (ping) => {
* @param {*} param0 * @param {*} param0
*/ */
export const handlePing = async (e) => { export const handlePing = async (e) => {
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
const MSG_GM_setValue = "setValue";
const MSG_GM_getValue = "getValue";
const MSG_GM_deleteValue = "deleteValue";
const MSG_GM_info = "info";
const { action, args, pong } = e.detail; const { action, args, pong } = e.detail;
let res; let res;
try { try {

View File

@@ -60,32 +60,25 @@ export const matchRule = async (
const rule = rules.find((r) => const rule = rules.find((r) =>
r.pattern.split(",").some((p) => isMatch(href, p.trim())) r.pattern.split(",").some((p) => isMatch(href, p.trim()))
); );
const globalRule = rules.find((r) => r.pattern === GLOBAL_KEY) || GLOBLA_RULE;
const globalRule =
rules.find((r) =>
r.pattern.split(",").some((p) => p.trim() === GLOBAL_KEY)
) || GLOBLA_RULE;
if (!rule) { if (!rule) {
return globalRule; return globalRule;
} }
rule.selector = rule.selector = rule.selector?.trim() || globalRule.selector;
rule?.selector?.trim() || if (rule.textStyle === GLOBAL_KEY) {
globalRule?.selector?.trim() || rule.textStyle = globalRule.textStyle;
GLOBLA_RULE.selector; rule.bgColor = globalRule.bgColor;
rule.textDiyStyle = globalRule.textDiyStyle;
rule.bgColor = rule?.bgColor?.trim() || globalRule?.bgColor?.trim(); } else {
rule.textDiyStyle = rule.bgColor = rule.bgColor?.trim() || globalRule.bgColor;
rule?.textDiyStyle?.trim() || globalRule?.textDiyStyle?.trim(); rule.textDiyStyle = rule.textDiyStyle?.trim() || globalRule.textDiyStyle;
}
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach( ["translator", "fromLang", "toLang", "transOpen"].forEach((key) => {
(key) => { if (rule[key] === GLOBAL_KEY) {
if (rule[key] === GLOBAL_KEY) { rule[key] = globalRule[key];
rule[key] = globalRule[key];
}
} }
); });
return rule; return rule;
}; };

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

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

View File

@@ -5,6 +5,7 @@ import {
STOKEY_SYNC, STOKEY_SYNC,
STOKEY_MSAUTH, STOKEY_MSAUTH,
STOKEY_RULESCACHE_PREFIX, STOKEY_RULESCACHE_PREFIX,
STOKEY_WEBFIXCACHE_PREFIX,
DEFAULT_SETTING, DEFAULT_SETTING,
DEFAULT_RULES, DEFAULT_RULES,
DEFAULT_SYNC, DEFAULT_SYNC,
@@ -82,10 +83,8 @@ export const storage = {
* 设置信息 * 设置信息
*/ */
export const getSetting = () => getObj(STOKEY_SETTING); export const getSetting = () => getObj(STOKEY_SETTING);
export const getSettingWithDefault = async () => ({ export const getSettingWithDefault = async () =>
...DEFAULT_SETTING, (await getSetting()) || DEFAULT_SETTING;
...((await getSetting()) || {}),
});
export const setSetting = (val) => setObj(STOKEY_SETTING, val); export const setSetting = (val) => setObj(STOKEY_SETTING, val);
export const updateSetting = (obj) => putObj(STOKEY_SETTING, obj); export const updateSetting = (obj) => putObj(STOKEY_SETTING, obj);
@@ -106,6 +105,14 @@ export const delSubRules = (url) => del(STOKEY_RULESCACHE_PREFIX + url);
export const setSubRules = (url, val) => export const setSubRules = (url, val) =>
setObj(STOKEY_RULESCACHE_PREFIX + 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位置 * fab位置
*/ */

View File

@@ -4,9 +4,11 @@ import {
updateSync, updateSync,
setSubRules, setSubRules,
getSubRules, getSubRules,
updateSetting,
} from "./storage"; } from "./storage";
import { apiFetchRules } from "../apis"; import { apiFetch } from "../apis";
import { checkRules } from "./rules"; import { checkRules } from "./rules";
import { isAllchar } from "./utils";
/** /**
* 同步订阅规则 * 同步订阅规则
@@ -14,9 +16,9 @@ import { checkRules } from "./rules";
* @returns * @returns
*/ */
export const syncSubRules = async (url, isBg = false) => { export const syncSubRules = async (url, isBg = false) => {
const res = await apiFetchRules(url, isBg); const res = await apiFetch(url, isBg);
const rules = checkRules(res).filter( const rules = checkRules(res).filter(
(rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== "" ({ pattern }) => !isAllchar(pattern, GLOBAL_KEY)
); );
if (rules.length > 0) { if (rules.length > 0) {
await setSubRules(url, rules); await setSubRules(url, rules);
@@ -53,6 +55,10 @@ export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
await syncAllSubRules(subrulesList, isBg); await syncAllSubRules(subrulesList, isBg);
await updateSync({ subRulesSyncAt: now }); await updateSync({ subRulesSyncAt: now });
} }
subrulesList.forEach((item) => {
item.syncAt = now;
});
await updateSetting({ subrulesList });
} catch (err) { } catch (err) {
console.log("[try sync all subrules]", err); console.log("[try sync all subrules]", err);
} }

View File

@@ -20,7 +20,7 @@ import { sha256 } from "./utils";
* @returns * @returns
*/ */
const syncSetting = async (isBg = false) => { const syncSetting = async (isBg = false) => {
const { syncUrl, syncKey, settingUpdateAt } = await getSyncWithDefault(); const { syncUrl, syncKey, settingUpdateAt = 0 } = await getSyncWithDefault();
if (!syncUrl || !syncKey) { if (!syncUrl || !syncKey) {
return; return;
} }
@@ -37,16 +37,15 @@ const syncSetting = async (isBg = false) => {
isBg isBg
); );
if (res && res.updateAt > settingUpdateAt) { if (res.updateAt > settingUpdateAt) {
await updateSync({
settingUpdateAt: res.updateAt,
settingSyncAt: res.updateAt,
});
await setSetting(res.value); await setSetting(res.value);
return res.value;
} else {
await updateSync({ settingSyncAt: res.updateAt });
} }
await updateSync({
settingUpdateAt: res.updateAt,
settingSyncAt: Date.now(),
});
return res.value;
}; };
export const trySyncSetting = async (isBg = false) => { export const trySyncSetting = async (isBg = false) => {
@@ -79,16 +78,15 @@ const syncRules = async (isBg = false) => {
isBg isBg
); );
if (res && res.updateAt > rulesUpdateAt) { if (res.updateAt > rulesUpdateAt) {
await updateSync({
rulesUpdateAt: res.updateAt,
rulesSyncAt: res.updateAt,
});
await setRules(res.value); await setRules(res.value);
return res.value;
} else {
await updateSync({ rulesSyncAt: res.updateAt });
} }
await updateSync({
rulesUpdateAt: res.updateAt,
rulesSyncAt: Date.now(),
});
return res.value;
}; };
export const trySyncRules = async (isBg = false) => { export const trySyncRules = async (isBg = false) => {

View File

@@ -3,15 +3,16 @@ import {
APP_LCNAME, APP_LCNAME,
TRANS_MIN_LENGTH, TRANS_MIN_LENGTH,
TRANS_MAX_LENGTH, TRANS_MAX_LENGTH,
EVENT_KISS,
MSG_TRANS_CURRULE, MSG_TRANS_CURRULE,
OPT_STYLE_DASHLINE, OPT_STYLE_DASHLINE,
OPT_STYLE_FUZZY, OPT_STYLE_FUZZY,
SHADOW_KEY, SHADOW_KEY,
OPT_MOUSEKEY_DISABLE,
OPT_MOUSEKEY_MOUSEOVER,
} from "../config"; } from "../config";
import Content from "../views/Content"; import Content from "../views/Content";
import { updateFetchPool, clearFetchPool } from "./fetch"; import { updateFetchPool, clearFetchPool } from "./fetch";
import { debounce } from "./utils"; import { debounce, genEventName } from "./utils";
/** /**
* 翻译类 * 翻译类
@@ -37,6 +38,7 @@ export class Translator {
"script", "script",
"iframe", "iframe",
]; ];
_eventName = genEventName();
// 显示 // 显示
_interseObserver = new IntersectionObserver( _interseObserver = new IntersectionObserver(
@@ -105,6 +107,10 @@ export class Translator {
return this._setting; return this._setting;
} }
get eventName() {
return this._eventName;
}
get rule() { get rule() {
// console.log("get rule", this._rule); // console.log("get rule", this._rule);
return this._rule; return this._rule;
@@ -115,8 +121,9 @@ export class Translator {
this._rule = rule; this._rule = rule;
// 广播消息 // 广播消息
const eventName = this._eventName;
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(EVENT_KISS, { new CustomEvent(eventName, {
detail: { detail: {
action: MSG_TRANS_CURRULE, action: MSG_TRANS_CURRULE,
args: rule, args: rule,
@@ -206,6 +213,10 @@ export class Translator {
}; };
_register = () => { _register = () => {
if (this._rule.fromLang === this._rule.toLang) {
return;
}
// 搜索节点 // 搜索节点
this._queryNodes(); this._queryNodes();
@@ -219,20 +230,47 @@ export class Translator {
}); });
this._tranNodes.forEach((_, node) => { this._tranNodes.forEach((_, node) => {
// 监听节点显示 if (
this._interseObserver.observe(node); !this._setting.mouseKey ||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
) {
// 监听节点显示
this._interseObserver.observe(node);
} else {
// 监听鼠标悬停
node.addEventListener("mouseover", this._handleMouseover);
}
}); });
}; };
_handleMouseover = (e) => {
const key = this._setting.mouseKey.slice(3);
if (this._setting.mouseKey === OPT_MOUSEKEY_MOUSEOVER || e[key]) {
e.target.removeEventListener("mouseover", this._handleMouseover);
this._render(e.target);
}
};
_unRegister = () => { _unRegister = () => {
// 解除节点变化监听 // 解除节点变化监听
this._mutaObserver.disconnect(); this._mutaObserver.disconnect();
// 解除节点显示监听 // 解除节点显示监听
this._interseObserver.disconnect(); // this._interseObserver.disconnect();
// 移除已插入元素
this._tranNodes.forEach((_, node) => { this._tranNodes.forEach((_, node) => {
if (
!this._setting.mouseKey ||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
) {
// 解除节点显示监听
this._interseObserver.unobserve(node);
} else {
// 移除鼠标悬停监听
node.removeEventListener("mouseover", this._handleMouseover);
}
// 移除已插入元素
node.querySelector(APP_LCNAME)?.remove(); node.querySelector(APP_LCNAME)?.remove();
}); });

View File

@@ -48,15 +48,61 @@ export const sleep = (delay) =>
* @returns * @returns
*/ */
export const debounce = (func, delay = 200) => { export const debounce = (func, delay = 200) => {
let timer; let timer = null;
return (...args) => { return (...args) => {
timer && clearTimeout(timer); timer && clearTimeout(timer);
timer = setTimeout(() => { timer = setTimeout(() => {
func(...args); func(...args);
clearTimeout(timer);
timer = null;
}, delay); }, delay);
}; };
}; };
/**
* 节流函数
* @param {*} func
* @param {*} delay
* @returns
*/
export const throttle = (func, delay = 200) => {
let timer = null;
let cache = null;
return (...args) => {
if (!timer) {
func(...args);
cache = null;
timer = setTimeout(() => {
if (cache) {
func(...cache);
cache = null;
}
clearTimeout(timer);
timer = null;
}, delay);
} else {
cache = args;
}
};
};
/**
* 判断字符串全是某个字符
* @param {*} s
* @param {*} c
* @param {*} i
* @returns
*/
export const isAllchar = (s, c, i = 0) => {
while (i < s.length) {
if (s[i] !== c) {
return false;
}
i++;
}
return true;
};
/** /**
* 字符串通配符(*)匹配 * 字符串通配符(*)匹配
* @param {*} s * @param {*} s
@@ -68,7 +114,7 @@ export const isMatch = (s, p) => {
return false; return false;
} }
p = `*${p}*`; p = "*" + p + "*";
let [sIndex, pIndex] = [0, 0]; let [sIndex, pIndex] = [0, 0];
let [sRecord, pRecord] = [-1, -1]; let [sRecord, pRecord] = [-1, -1];
@@ -91,7 +137,7 @@ export const isMatch = (s, p) => {
return true; return true;
} }
return p.slice(pIndex).replaceAll("*", "") === ""; return isAllchar(p, "*", pIndex);
}; };
/** /**
@@ -116,3 +162,20 @@ export const sha256 = async (text, salt) => {
.map((b) => b.toString(16).padStart(2, "0")) .map((b) => b.toString(16).padStart(2, "0"))
.join(""); .join("");
}; };
/**
* 生成随机事件名称
* @returns
*/
export const genEventName = () => btoa(Math.random()).slice(3, 11);
/**
* 判断两个 Set 是否相同
* @param {*} a
* @param {*} b
* @returns
*/
export const isSameSet = (a, b) => {
const s = new Set([...a, ...b]);
return s.size === a.size && s.size === b.size;
};

176
src/libs/webfix.js Normal file
View File

@@ -0,0 +1,176 @@
import { isMatch } from "./utils";
import { getWebfix, setWebfix } from "./storage";
import { apiFetch } from "../apis";
/**
* 修复程序类型
*/
const WEBFIX_BR = "br";
/**
* 需要修复的站点列表
* - pattern 匹配网址
* - selector 需要修复的选择器
* - rootSlector 需要监听的选择器,可留空
* - fixer 修复函数,可针对不同网址,选用不同修复函数
*/
const DEFAULT_SITES = [
{
pattern: "www.phoronix.com",
selector: ".content",
rootSlector: "",
fixer: WEBFIX_BR,
},
{
pattern: "t.me/s/*",
selector: ".tgme_widget_message_text",
rootSlector: ".tgme_channel_history",
fixer: WEBFIX_BR,
},
];
/**
* 修复过的标记
*/
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;
}
/**
* 修复程序映射
*/
const fixerMap = {
[WEBFIX_BR]: brFixer,
};
/**
* 查找、监听节点,并执行修复函数
* @param {*} selector
* @param {*} fixer
* @param {*} rootSlector
*/
function run(selector, fixer, rootSlector) {
var mutaObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addNode) {
addNode.querySelectorAll(selector).forEach(fixer);
});
});
});
var rootNodes = [document];
if (rootSlector) {
rootNodes = document.querySelectorAll(rootSlector);
}
rootNodes.forEach(function (rootNode) {
rootNode.querySelectorAll(selector).forEach(fixer);
mutaObserver.observe(rootNode, {
childList: true,
});
});
}
/**
* 同步远程数据
* @param {*} url
* @returns
*/
export const syncWebfix = async (url) => {
const sites = await apiFetch(url);
await setWebfix(url, sites);
return sites;
};
/**
* 从缓存或远程加载修复站点
* @param {*} url
* @returns
*/
export const loadOrFetchWebfix = async (url) => {
try {
let sites = await getWebfix(url);
if (sites?.length) {
return sites;
}
return syncWebfix(url);
} catch (err) {
console.log("[load webfix]", err.message);
return DEFAULT_SITES;
}
};
/**
* 匹配站点
*/
export async function webfix(href, { injectWebfix }) {
try {
if (!injectWebfix) {
return;
}
const sites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
for (var i = 0; i < sites.length; i++) {
var site = sites[i];
if (isMatch(href, site.pattern)) {
if (fixerMap[site.fixer]) {
run(site.selector, fixerMap[site.fixer], site.rootSlector);
}
break;
}
}
} catch (err) {
console.error(`[kiss-webfix]: ${err.message}`);
}
}

View File

@@ -10,11 +10,12 @@ import {
} from "./libs/storage"; } from "./libs/storage";
import { Translator } from "./libs/translator"; import { Translator } from "./libs/translator";
import { trySyncAllSubRules } from "./libs/subRules"; import { trySyncAllSubRules } from "./libs/subRules";
import { isGm } from "./libs/client";
import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config"; import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config";
import { isIframe } from "./libs/iframe"; import { isIframe } from "./libs/iframe";
import { handlePing, injectScript } from "./libs/gm"; import { handlePing, injectScript } from "./libs/gm";
import { matchRule } from "./libs/rules"; import { matchRule } from "./libs/rules";
import { genEventName } from "./libs/utils";
import { webfix } from "./libs/webfix";
/** /**
* 入口函数 * 入口函数
@@ -28,9 +29,12 @@ const init = async () => {
) { ) {
if (GM?.info?.script?.grant?.includes("unsafeWindow")) { if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
unsafeWindow.GM = GM; unsafeWindow.GM = GM;
unsafeWindow.APP_NAME = process.env.REACT_APP_NAME; unsafeWindow.APP_INFO = {
name: process.env.REACT_APP_NAME,
version: process.env.REACT_APP_VERSION,
};
} else { } else {
const ping = btoa(Math.random()).slice(3, 11); const ping = genEventName();
window.addEventListener(ping, handlePing); window.addEventListener(ping, handlePing);
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line // window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
const script = document.createElement("script"); const script = document.createElement("script");
@@ -47,6 +51,7 @@ const init = async () => {
const rules = await getRulesWithDefault(); const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting); const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting); const translator = new Translator(rule, setting);
webfix(href, setting);
if (isIframe) { if (isIframe) {
// iframe // iframe
@@ -88,28 +93,6 @@ const init = async () => {
</React.StrictMode> </React.StrictMode>
); );
// 注册菜单
if (isGm) {
try {
GM.registerMenuCommand(
"Toggle Translate",
(event) => {
translator.toggle();
},
"Q"
);
GM.registerMenuCommand(
"Toggle Style",
(event) => {
translator.toggleStyle();
},
"C"
);
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}
// 同步订阅规则 // 同步订阅规则
trySyncAllSubRules(setting); trySyncAllSubRules(setting);
}; };
@@ -118,9 +101,10 @@ const init = async () => {
try { try {
await init(); await init();
} catch (err) { } catch (err) {
console.error("[KISS-Translator]", err);
const $err = document.createElement("div"); const $err = document.createElement("div");
$err.innerText = `KISS-Translator: ${err.message}`; $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); document.body.prepend($err);
} }
})(); })();

View File

@@ -1,16 +1,22 @@
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import Box from "@mui/material/Box";
import Fab from "@mui/material/Fab"; import Fab from "@mui/material/Fab";
import TranslateIcon from "@mui/icons-material/Translate"; import TranslateIcon from "@mui/icons-material/Translate";
import ThemeProvider from "../../hooks/Theme"; import ThemeProvider from "../../hooks/Theme";
import Draggable from "./Draggable"; import Draggable from "./Draggable";
import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import Stack from "@mui/material/Stack";
import { useEffect, useState, useMemo, useCallback } from "react"; import { useEffect, useState, useMemo, useCallback } from "react";
import { SettingProvider } from "../../hooks/Setting"; import { SettingProvider } from "../../hooks/Setting";
import Popup from "../Popup"; import Popup from "../Popup";
import { debounce } from "../../libs/utils"; import { debounce } from "../../libs/utils";
import { isGm } from "../../libs/client";
import Header from "../Popup/Header";
import {
DEFAULT_SHORTCUTS,
OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP,
OPT_SHORTCUT_SETTING,
} from "../../config";
import { shortcutRegister } from "../../libs/shortcut";
export default function Action({ translator, fab }) { export default function Action({ translator, fab }) {
const fabWidth = 40; const fabWidth = 40;
@@ -44,6 +50,88 @@ export default function Action({ translator, fab }) {
setMoved(true); setMoved(true);
}, []); }, []);
useEffect(() => {
// 注册快捷键
const shortcuts = translator.setting.shortcuts || DEFAULT_SHORTCUTS;
const clearShortcuts = [
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () => {
translator.toggle();
setShowPopup(false);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () => {
translator.toggleStyle();
setShowPopup(false);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () => {
setShowPopup((pre) => !pre);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () => {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
}),
];
return () => {
clearShortcuts.forEach((fn) => {
fn();
});
};
}, [translator]);
useEffect(() => {
// 注册菜单
const menuCommandIds = [];
if (isGm) {
try {
menuCommandIds.push(
GM.registerMenuCommand(
"Toggle Translate (Alt+q)",
(event) => {
translator.toggle();
setShowPopup(false);
},
"Q"
),
GM.registerMenuCommand(
"Toggle Style (Alt+c)",
(event) => {
translator.toggleStyle();
setShowPopup(false);
},
"C"
),
GM.registerMenuCommand(
"Open Menu (Alt+k)",
(event) => {
setShowPopup((pre) => !pre);
},
"K"
),
GM.registerMenuCommand(
"Open Setting (Alt+o)",
(event) => {
setShowPopup((pre) => !pre);
},
"O"
)
);
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}
return () => {
if (isGm) {
try {
menuCommandIds.forEach((id) => {
GM.unregisterMenuCommand(id);
});
} catch (err) {
//
}
}
};
}, [translator]);
useEffect(() => { useEffect(() => {
window.addEventListener("resize", handleWindowResize); window.addEventListener("resize", handleWindowResize);
return () => { return () => {
@@ -53,6 +141,7 @@ export default function Action({ translator, fab }) {
useEffect(() => { useEffect(() => {
window.addEventListener("click", handleWindowClick); window.addEventListener("click", handleWindowClick);
return () => { return () => {
window.removeEventListener("click", handleWindowClick); window.removeEventListener("click", handleWindowClick);
}; };
@@ -91,23 +180,7 @@ export default function Action({ translator, fab }) {
onMove={handleMove} onMove={handleMove}
handler={ handler={
<Paper style={{ cursor: "move" }} elevation={3}> <Paper style={{ cursor: "move" }} elevation={3}>
<Stack <Header setShowPopup={setShowPopup} />
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Box style={{ marginLeft: 16 }}>
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
</Box>
<IconButton
onClick={() => {
setShowPopup(false);
}}
>
<CloseIcon />
</IconButton>
</Stack>
</Paper> </Paper>
} }
> >
@@ -121,7 +194,7 @@ export default function Action({ translator, fab }) {
key="fab" key="fab"
snapEdge snapEdge
{...fabProps} {...fabProps}
show={!showPopup} show={translator.setting.hideFab ? false : !showPopup}
onStart={handleStart} onStart={handleStart}
onMove={handleMove} onMove={handleMove}
handler={ handler={

View File

@@ -9,15 +9,15 @@ import {
OPT_STYLE_HIGHLIGHT, OPT_STYLE_HIGHLIGHT,
OPT_STYLE_DIY, OPT_STYLE_DIY,
DEFAULT_COLOR, DEFAULT_COLOR,
EVENT_KISS,
MSG_TRANS_CURRULE, MSG_TRANS_CURRULE,
TRANS_NEWLINE_LENGTH, TRANS_NEWLINE_LENGTH,
} from "../../config"; } from "../../config";
import { useTranslate } from "../../hooks/Translate"; 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; opacity: 0.6;
-webkit-opacity: 0.6;
text-decoration-line: underline; text-decoration-line: underline;
text-decoration-style: ${(props) => props.$lineStyle}; text-decoration-style: ${(props) => props.$lineStyle};
text-decoration-color: ${(props) => props.$lineColor}; text-decoration-color: ${(props) => props.$lineColor};
@@ -30,26 +30,25 @@ const LineSpan = styled.span`
-webkit-text-underline-offset: 0.3em; -webkit-text-underline-offset: 0.3em;
&:hover { &:hover {
opacity: 1; opacity: 1;
-webkit-opacity: 1;
} }
`; `;
const FuzzySpan = styled.span` const FuzzySpan = styled("span")`
filter: blur(5px); filter: blur(0.2em);
transition: filter 0.2s ease-in-out; -webkit-filter: blur(0.2em);
&hover: { &:hover {
filter: none; filter: none;
-webkit-filter: none;
} }
`; `;
const HighlightSpan = styled.span` const HighlightSpan = styled("span")`
color: #fff; color: #fff;
background-color: ${(props) => props.$bgColor}; background-color: ${(props) => props.$bgColor};
&hover: {
filter: none;
}
`; `;
const DiySpan = styled.span` const DiySpan = styled("span")`
${(props) => props.$diyStyle} ${(props) => props.$diyStyle}
`; `;
@@ -112,11 +111,11 @@ export default function Content({ q, translator }) {
}; };
useEffect(() => { useEffect(() => {
window.addEventListener(EVENT_KISS, handleKissEvent); window.addEventListener(translator.eventName, handleKissEvent);
return () => { return () => {
window.removeEventListener(EVENT_KISS, handleKissEvent); window.removeEventListener(translator.eventName, handleKissEvent);
}; };
}, []); }, [translator.eventName]);
if (loading) { if (loading) {
return ( return (

174
src/views/Options/Apis.js Normal file
View File

@@ -0,0 +1,174 @@
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import CircularProgress from "@mui/material/CircularProgress";
import {
OPT_TRANS_ALL,
OPT_TRANS_MICROSOFT,
OPT_TRANS_OPENAI,
OPT_TRANS_CUSTOMIZE,
URL_KISS_PROXY,
} from "../../config";
import { useState } from "react";
import { useI18n } from "../../hooks/I18n";
import Typography from "@mui/material/Typography";
import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import Alert from "@mui/material/Alert";
import { useAlert } from "../../hooks/Alert";
import { useApi } from "../../hooks/Api";
import { apiTranslate } from "../../apis";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
function TestButton({ translator, api }) {
const i18n = useI18n();
const alert = useAlert();
const [loading, setLoading] = useState(false);
const handleApiTest = async () => {
try {
setLoading(true);
const [text] = await apiTranslate({
translator,
text: "hello world",
fromLang: "en",
toLang: "zh-CN",
apiSetting: { ...api, useCache: false },
});
if (!text) {
throw new Error("empty reault");
}
alert.success(i18n("test_success"));
} catch (err) {
alert.error(`${i18n("test_failed")}: ${err.message}`);
} finally {
setLoading(false);
}
};
if (loading) {
return <CircularProgress size={16} />;
}
return (
<Button size="small" variant="contained" onClick={handleApiTest}>
{i18n("click_test")}
</Button>
);
}
function ApiFields({ translator }) {
const i18n = useI18n();
const { api, updateApi, resetApi } = useApi(translator);
const { url = "", key = "", model = "", prompt = "" } = api;
const handleChange = (e) => {
const { name, value } = e.target;
updateApi({
[name]: value,
});
};
return (
<Stack spacing={3}>
{translator !== OPT_TRANS_MICROSOFT && (
<>
<TextField
size="small"
label={"URL"}
name="url"
value={url}
onChange={handleChange}
/>
<TextField
size="small"
label={"KEY"}
name="key"
value={key}
onChange={handleChange}
/>
</>
)}
{translator === OPT_TRANS_OPENAI && (
<>
<TextField
size="small"
label={"MODEL"}
name="model"
value={model}
onChange={handleChange}
/>
<TextField
size="small"
label={"PROMPT"}
name="prompt"
value={prompt}
onChange={handleChange}
multiline
/>
</>
)}
<Stack direction="row" spacing={2}>
<TestButton translator={translator} api={api} />
{translator !== OPT_TRANS_MICROSOFT && (
<Button
size="small"
variant="outlined"
onClick={() => {
resetApi();
}}
>
{i18n("restore_default")}
</Button>
)}
</Stack>
{translator === OPT_TRANS_CUSTOMIZE && (
<pre>{i18n("custom_api_help")}</pre>
)}
</Stack>
);
}
function ApiAccordion({ translator }) {
const [expanded, setExpanded] = useState(false);
const handleChange = (e) => {
setExpanded((pre) => !pre);
};
return (
<Accordion expanded={expanded} onChange={handleChange}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{translator}</Typography>
</AccordionSummary>
<AccordionDetails>
{expanded && <ApiFields translator={translator} />}
</AccordionDetails>
</Accordion>
);
}
export default function Apis() {
const i18n = useI18n();
return (
<Box>
<Stack spacing={3}>
<Alert severity="info">
<Link href={URL_KISS_PROXY} target="_blank">
{i18n("about_api_proxy")}
</Link>
</Alert>
<Box>
{OPT_TRANS_ALL.map((translator) => (
<ApiAccordion key={translator} translator={translator} />
))}
</Box>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import IconButton from "@mui/material/IconButton";
import { useDarkMode } from "../../hooks/ColorMode";
import LightModeIcon from "@mui/icons-material/LightMode";
import DarkModeIcon from "@mui/icons-material/DarkMode";
export default function DarkModeButton() {
const { darkMode, toggleDarkMode } = useDarkMode();
return (
<IconButton onClick={toggleDarkMode} color="inherit">
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
</IconButton>
);
}

View File

@@ -4,16 +4,13 @@ import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from "@mui/icons-material/Menu";
import Toolbar from "@mui/material/Toolbar"; import Toolbar from "@mui/material/Toolbar";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { 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 Link from "@mui/material/Link";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import DarkModeButton from "./DarkModeButton";
function Header(props) { function Header(props) {
const i18n = useI18n(); const i18n = useI18n();
const { onDrawerToggle } = props; const { onDrawerToggle } = props;
const { darkMode, toggleDarkMode } = useDarkMode();
return ( return (
<AppBar <AppBar
@@ -39,11 +36,10 @@ function Header(props) {
underline="none" underline="none"
color="inherit" color="inherit"
href={process.env.REACT_APP_HOMEPAGE} href={process.env.REACT_APP_HOMEPAGE}
target="_blank"
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link> >{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
</Box> </Box>
<IconButton onClick={toggleDarkMode} color="inherit"> <DarkModeButton />
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
</IconButton>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
); );

View File

@@ -0,0 +1,19 @@
import Button from "@mui/material/Button";
import { useI18n } from "../../hooks/I18n";
import HelpIcon from "@mui/icons-material/Help";
export default function HelpButton({ url }) {
const i18n = useI18n();
return (
<Button
size="small"
variant="outlined"
onClick={() => {
window.open(url, "_blank");
}}
startIcon={<HelpIcon />}
>
{i18n("help")}
</Button>
);
}

View File

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

View File

@@ -13,6 +13,7 @@ import {
OPT_STYLE_ALL, OPT_STYLE_ALL,
OPT_STYLE_DIY, OPT_STYLE_DIY,
OPT_STYLE_USE_COLOR, OPT_STYLE_USE_COLOR,
URL_KISS_RULES_NEW_ISSUE,
} from "../../config"; } from "../../config";
import { useState, useRef, useEffect, useMemo } from "react"; import { useState, useRef, useEffect, useMemo } from "react";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
@@ -45,6 +46,8 @@ import { syncShareRules } from "../../libs/sync";
import { debounce } from "../../libs/utils"; import { debounce } from "../../libs/utils";
import { delSubRules, getSyncWithDefault } from "../../libs/storage"; import { delSubRules, getSyncWithDefault } from "../../libs/storage";
import OwSubRule from "./OwSubRule"; import OwSubRule from "./OwSubRule";
import ClearAllIcon from "@mui/icons-material/ClearAll";
import HelpButton from "./HelpButton";
function RuleFields({ rule, rules, setShow, setKeyword }) { function RuleFields({ rule, rules, setShow, setKeyword }) {
const initFormValues = rule || { const initFormValues = rule || {
@@ -470,7 +473,7 @@ function ShareButton({ rules, injectRules, selectedUrl }) {
onClick={handleClick} onClick={handleClick}
startIcon={<ShareIcon />} startIcon={<ShareIcon />}
> >
{"分享"} {i18n("share")}
</Button> </Button>
); );
} }
@@ -552,6 +555,19 @@ function UserRules({ subRules }) {
selectedUrl={selectedUrl} selectedUrl={selectedUrl}
/> />
<Button
size="small"
variant="outlined"
onClick={() => {
rules.clear();
}}
startIcon={<ClearAllIcon />}
>
{i18n("clear_all")}
</Button>
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
<FormControlLabel <FormControlLabel
control={ control={
<Switch <Switch
@@ -599,7 +615,15 @@ function UserRules({ subRules }) {
); );
} }
function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) { function SubRulesItem({
index,
url,
syncAt,
selectedUrl,
delSub,
updateSub,
setSelectedRules,
}) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const handleDel = async () => { const handleDel = async () => {
@@ -618,6 +642,7 @@ function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
if (rules.length > 0 && url === selectedUrl) { if (rules.length > 0 && url === selectedUrl) {
setSelectedRules(rules); setSelectedRules(rules);
} }
await updateSub(url, { syncAt: Date.now() });
} catch (err) { } catch (err) {
console.log("[sync sub rules]", err); console.log("[sync sub rules]", err);
} finally { } finally {
@@ -629,6 +654,12 @@ function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
<Stack direction="row" alignItems="center" spacing={2}> <Stack direction="row" alignItems="center" spacing={2}>
<FormControlLabel value={url} control={<Radio />} label={url} /> <FormControlLabel value={url} control={<Radio />} label={url} />
{syncAt && (
<span style={{ marginLeft: "0.5em", opacity: 0.5 }}>
[{new Date(syncAt).toLocaleString()}]
</span>
)}
{loading ? ( {loading ? (
<CircularProgress size={16} /> <CircularProgress size={16} />
) : ( ) : (
@@ -715,6 +746,7 @@ function SubRulesEdit({ subList, addSub }) {
> >
{i18n("add")} {i18n("add")}
</Button> </Button>
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
</Stack> </Stack>
{showInput && ( {showInput && (
@@ -752,6 +784,7 @@ function SubRules({ subRules }) {
const { const {
subList, subList,
selectSub, selectSub,
updateSub,
addSub, addSub,
delSub, delSub,
selectedUrl, selectedUrl,
@@ -774,9 +807,11 @@ function SubRules({ subRules }) {
<SubRulesItem <SubRulesItem
key={item.url} key={item.url}
url={item.url} url={item.url}
syncAt={item.syncAt}
index={index} index={index}
selectedUrl={selectedUrl} selectedUrl={selectedUrl}
delSub={delSub} delSub={delSub}
updateSub={updateSub}
setSelectedRules={setSelectedRules} setSelectedRules={setSelectedRules}
/> />
))} ))}

View File

@@ -6,14 +6,84 @@ import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl"; import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select"; import Select from "@mui/material/Select";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import FormHelperText from "@mui/material/FormHelperText";
import { useSetting } from "../../hooks/Setting"; import { useSetting } from "../../hooks/Setting";
import { limitNumber } from "../../libs/utils"; import { limitNumber } from "../../libs/utils";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import { UI_LANGS, URL_KISS_PROXY, TRANS_NEWLINE_LENGTH } from "../../config"; import { useAlert } from "../../hooks/Alert";
import { isExt } from "../../libs/client";
import IconButton from "@mui/material/IconButton";
import EditIcon from "@mui/icons-material/Edit";
import Grid from "@mui/material/Grid";
import {
UI_LANGS,
TRANS_NEWLINE_LENGTH,
CACHE_NAME,
OPT_MOUSEKEY_ALL,
OPT_MOUSEKEY_DISABLE,
OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP,
OPT_SHORTCUT_SETTING,
} from "../../config";
import { useEffect, useState, useRef } from "react";
import { useShortcut } from "../../hooks/Shortcut";
import { shortcutListener } from "../../libs/shortcut";
function ShortcutItem({ action, label }) {
const { shortcut, setShortcut } = useShortcut(action);
const [disabled, setDisabled] = useState(true);
const inputRef = useRef(null);
useEffect(() => {
if (disabled) {
return;
}
inputRef.current.focus();
setShortcut([]);
const clearShortcut = shortcutListener((curkeys, allkeys) => {
setShortcut(allkeys);
if (curkeys.length === 0) {
setDisabled(true);
}
}, inputRef.current);
return () => {
clearShortcut();
};
}, [disabled, setShortcut]);
return (
<Stack direction="row">
<TextField
size="small"
label={label}
name={label}
value={shortcut.join(" + ")}
fullWidth
inputRef={inputRef}
disabled={disabled}
onBlur={() => {
setDisabled(true);
}}
/>
<IconButton
onClick={() => {
setDisabled(false);
}}
>
{<EditIcon />}
</IconButton>
</Stack>
);
}
export default function Settings() { export default function Settings() {
const i18n = useI18n(); const i18n = useI18n();
const { setting, updateSetting } = useSetting(); const { setting, updateSetting } = useSetting();
const alert = useAlert();
const handleChange = (e) => { const handleChange = (e) => {
e.preventDefault(); e.preventDefault();
@@ -41,19 +111,25 @@ export default function Settings() {
}); });
}; };
const handleClearCache = () => {
try {
caches.delete(CACHE_NAME);
alert.success(i18n("clear_success"));
} catch (err) {
console.log("[clear cache]", err);
}
};
const { const {
uiLang, uiLang,
googleUrl,
fetchLimit, fetchLimit,
fetchInterval, fetchInterval,
minLength, minLength,
maxLength, maxLength,
openaiUrl,
openaiKey,
openaiModel,
openaiPrompt,
clearCache, clearCache,
newlineLength = TRANS_NEWLINE_LENGTH, newlineLength = TRANS_NEWLINE_LENGTH,
mouseKey = OPT_MOUSEKEY_DISABLE,
hideFab = false,
} = setting; } = setting;
return ( return (
@@ -121,65 +197,81 @@ export default function Settings() {
/> />
<FormControl size="small"> <FormControl size="small">
<InputLabel>{i18n("clear_cache")}</InputLabel> <InputLabel>{i18n("mouseover_translation")}</InputLabel>
<Select <Select
name="clearCache" name="mouseKey"
value={clearCache} value={mouseKey}
label={i18n("clear_cache")} label={i18n("mouseover_translation")}
onChange={handleChange} onChange={handleChange}
> >
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem> {OPT_MOUSEKEY_ALL.map((item) => (
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem> <MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
<TextField {isExt ? (
size="small" <FormControl size="small">
label={i18n("google_api")} <InputLabel>{i18n("if_clear_cache")}</InputLabel>
name="googleUrl" <Select
value={googleUrl} name="clearCache"
onChange={handleChange} value={clearCache}
helperText={ label={i18n("if_clear_cache")}
<Link href={URL_KISS_PROXY}>{i18n("about_api_proxy")}</Link> onChange={handleChange}
} >
/> <MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
<TextField </Select>
size="small" <FormHelperText>
label={i18n("openai_api")} <Link component="button" onClick={handleClearCache}>
name="openaiUrl" {i18n("clear_all_cache_now")}
value={openaiUrl} </Link>
onChange={handleChange} </FormHelperText>
helperText={ </FormControl>
<Link href={URL_KISS_PROXY}>{i18n("about_api_proxy")}</Link> ) : (
} <>
/> <FormControl size="small">
<InputLabel>{i18n("hide_fab_button")}</InputLabel>
<TextField <Select
size="small" name="hideFab"
type="password" value={hideFab}
label={i18n("openai_key")} label={i18n("hide_fab_button")}
name="openaiKey" onChange={handleChange}
value={openaiKey} >
onChange={handleChange} <MenuItem value={false}>{i18n("show")}</MenuItem>
/> <MenuItem value={true}>{i18n("hide")}</MenuItem>
</Select>
<TextField </FormControl>
size="small" <Grid container rowSpacing={2} columns={12}>
label={i18n("openai_model")} <Grid item xs={12} sm={12} md={3} lg={3}>
name="openaiModel" <ShortcutItem
value={openaiModel} action={OPT_SHORTCUT_TRANSLATE}
onChange={handleChange} label={i18n("toggle_translate_shortcut")}
/> />
</Grid>
<TextField <Grid item xs={12} sm={12} md={3} lg={3}>
size="small" <ShortcutItem
label={i18n("openai_prompt")} action={OPT_SHORTCUT_STYLE}
name="openaiPrompt" label={i18n("toggle_style_shortcut")}
value={openaiPrompt} />
onChange={handleChange} </Grid>
multiline <Grid item xs={12} sm={12} md={3} lg={3}>
/> <ShortcutItem
action={OPT_SHORTCUT_POPUP}
label={i18n("toggle_popup_shortcut")}
/>
</Grid>
<Grid item xs={12} sm={12} md={3} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_SETTING}
label={i18n("open_setting_shortcut")}
/>
</Grid>
</Grid>
</>
)}
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -35,10 +35,10 @@ export default function SyncSetting() {
setLoading(true); setLoading(true);
await syncSettingAndRules(); await syncSettingAndRules();
await reloadSetting(); await reloadSetting();
alert.success(i18n("data_sync_success")); alert.success(i18n("sync_success"));
} catch (err) { } catch (err) {
console.log("[sync all]", err); console.log("[sync all]", err);
alert.error(i18n("data_sync_error")); alert.error(i18n("sync_failed"));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -58,7 +58,9 @@ export default function SyncSetting() {
value={syncUrl} value={syncUrl}
onChange={handleChange} onChange={handleChange}
helperText={ helperText={
<Link href={URL_KISS_WORKER}>{i18n("about_sync_api")}</Link> <Link href={URL_KISS_WORKER} target="_blank">
{i18n("about_sync_api")}
</Link>
} }
/> />
@@ -85,7 +87,7 @@ export default function SyncSetting() {
onClick={handleSyncTest} onClick={handleSyncTest}
startIcon={<SyncIcon />} startIcon={<SyncIcon />}
> >
{i18n("data_sync_test")} {i18n("sync_now")}
</Button> </Button>
{loading && <CircularProgress size={16} />} {loading && <CircularProgress size={16} />}
</Stack> </Stack>

160
src/views/Options/Webfix.js Normal file
View File

@@ -0,0 +1,160 @@
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import { useEffect, useState } from "react";
import { useI18n } from "../../hooks/I18n";
import Typography from "@mui/material/Typography";
import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import { useSetting } from "../../hooks/Setting";
import CircularProgress from "@mui/material/CircularProgress";
import { syncWebfix, loadOrFetchWebfix } from "../../libs/webfix";
import Button from "@mui/material/Button";
import SyncIcon from "@mui/icons-material/Sync";
import { useAlert } from "../../hooks/Alert";
import HelpButton from "./HelpButton";
import { URL_KISS_RULES_NEW_ISSUE } from "../../config";
function ApiFields({ site }) {
const { selector, rootSlector, fixer } = site;
return (
<Stack spacing={3}>
<TextField
size="small"
label={"rootSlector"}
name="rootSlector"
value={rootSlector || "document"}
disabled
/>
<TextField
size="small"
label={"selector"}
name="selector"
value={selector}
disabled
/>
<TextField
size="small"
label={"fixer"}
name="fixer"
value={fixer}
disabled
/>
</Stack>
);
}
function ApiAccordion({ site }) {
const [expanded, setExpanded] = useState(false);
const handleChange = (e) => {
setExpanded((pre) => !pre);
};
return (
<Accordion expanded={expanded} onChange={handleChange}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{site.pattern}</Typography>
</AccordionSummary>
<AccordionDetails>
{expanded && <ApiFields site={site} />}
</AccordionDetails>
</Accordion>
);
}
export default function Webfix() {
const [loading, setLoading] = useState(false);
const [sites, setSites] = useState([]);
const i18n = useI18n();
const alert = useAlert();
const { setting, updateSetting } = useSetting();
const handleSyncTest = async (e) => {
e.preventDefault();
try {
setLoading(true);
await syncWebfix(process.env.REACT_APP_WEBFIXURL);
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);
const sites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
setSites(sites);
} catch (err) {
console.log("[load webfix]", err.message);
} finally {
setLoading(false);
}
})();
}, []);
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

@@ -15,9 +15,13 @@ import { AlertProvider } from "../../hooks/Alert";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import { adaptScript } from "../../libs/gm";
import Alert from "@mui/material/Alert";
import Apis from "./Apis";
import Webfix from "./Webfix";
export default function Options() { export default function Options() {
const [error, setError] = useState(false); const [error, setError] = useState("");
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
useEffect(() => { useEffect(() => {
@@ -26,7 +30,22 @@ export default function Options() {
// 等待GM注入 // 等待GM注入
let i = 0; let i = 0;
for (;;) { for (;;) {
if (window.APP_NAME === process.env.REACT_APP_NAME) { if (window?.APP_INFO?.name === process.env.REACT_APP_NAME) {
const { version, eventName } = window.APP_INFO;
// 检查版本是否一致
if (version !== process.env.REACT_APP_VERSION) {
setError(
`The version of the script(v${version}) and this page(v${process.env.REACT_APP_VERSION}) are inconsistent.`
);
break;
}
if (eventName) {
// 注入GM接口
adaptScript(eventName);
}
// 同步数据 // 同步数据
await trySyncSettingAndRules(); await trySyncSettingAndRules();
setReady(true); setReady(true);
@@ -34,7 +53,7 @@ export default function Options() {
} }
if (++i > 8) { if (++i > 8) {
setError(true); setError("Time out.");
break; break;
} }
@@ -51,6 +70,7 @@ export default function Options() {
if (error) { if (error) {
return ( return (
<center> <center>
<Alert severity="error">{error}</Alert>
<Divider> <Divider>
<Link <Link
href={process.env.REACT_APP_HOMEPAGE} href={process.env.REACT_APP_HOMEPAGE}
@@ -106,7 +126,9 @@ export default function Options() {
<Route path="/" element={<Layout />}> <Route path="/" element={<Layout />}>
<Route index element={<Setting />} /> <Route index element={<Setting />} />
<Route path="rules" element={<Rules />} /> <Route path="rules" element={<Rules />} />
<Route path="apis" element={<Apis />} />
<Route path="sync" element={<SyncSetting />} /> <Route path="sync" element={<SyncSetting />} />
<Route path="webfix" element={<Webfix />} />
<Route path="about" element={<About />} /> <Route path="about" element={<About />} />
</Route> </Route>
</Routes> </Routes>

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

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

View File

@@ -10,6 +10,8 @@ import { browser } from "../../libs/browser";
import { isExt } from "../../libs/client"; import { isExt } from "../../libs/client";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Divider from "@mui/material/Divider";
import Header from "./Header";
import { import {
MSG_TRANS_TOGGLE, MSG_TRANS_TOGGLE,
MSG_TRANS_GETRULE, MSG_TRANS_GETRULE,
@@ -19,6 +21,7 @@ import {
OPT_LANGS_TO, OPT_LANGS_TO,
OPT_STYLE_ALL, OPT_STYLE_ALL,
OPT_STYLE_USE_COLOR, OPT_STYLE_USE_COLOR,
CACHE_NAME,
} from "../../config"; } from "../../config";
import { sendIframeMsg } from "../../libs/iframe"; import { sendIframeMsg } from "../../libs/iframe";
@@ -66,6 +69,14 @@ export default function Popup({ setShowPopup, translator: tran }) {
} }
}; };
const handleClearCache = () => {
try {
caches.delete(CACHE_NAME);
} catch (err) {
console.log("[clear cache]", err);
}
};
useEffect(() => { useEffect(() => {
if (!isExt) { if (!isExt) {
return; return;
@@ -84,8 +95,14 @@ export default function Popup({ setShowPopup, translator: tran }) {
if (!rule) { if (!rule) {
return ( return (
<Box minWidth={300} sx={{ p: 2 }}> <Box minWidth={300}>
<Stack spacing={3}> {isExt && (
<>
<Header />
<Divider />
</>
)}
<Stack sx={{ p: 2 }} spacing={3}>
<Button variant="text" onClick={handleOpenSetting}> <Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")} {i18n("setting")}
</Button> </Button>
@@ -97,17 +114,35 @@ export default function Popup({ setShowPopup, translator: tran }) {
const { transOpen, translator, fromLang, toLang, textStyle, bgColor } = rule; const { transOpen, translator, fromLang, toLang, textStyle, bgColor } = rule;
return ( return (
<Box minWidth={300} sx={{ p: 2 }}> <Box minWidth={300}>
<Stack spacing={2}> {isExt && (
<FormControlLabel <>
control={ <Header />
<Switch <Divider />
checked={transOpen === "true"} </>
onChange={handleTransToggle} )}
/> <Stack sx={{ p: 2 }} spacing={2}>
} <Stack
label={i18n("translate_alt")} direction="row"
/> justifyContent="space-between"
alignItems="center"
spacing={2}
>
<FormControlLabel
control={
<Switch
checked={transOpen === "true"}
onChange={handleTransToggle}
/>
}
label={i18n("translate_alt")}
/>
{!isExt && (
<Button variant="text" onClick={handleClearCache}>
{i18n("clear_cache")}
</Button>
)}
</Stack>
<TextField <TextField
select select

1098
yarn.lock

File diff suppressed because it is too large Load Diff