Compare commits

..

82 Commits

Author SHA1 Message Date
Gabe Yuan
6bb742f828 v1.6.3 2023-09-02 19:16:14 +08:00
Gabe Yuan
72742e5e12 fix build:script 2023-09-02 19:09:12 +08:00
Gabe Yuan
3667e0a509 update readme 2023-09-02 18:14:42 +08:00
Gabe Yuan
c2d7668ba7 v1.6.2 2023-09-02 17:26:37 +08:00
Gabe Yuan
aa830f5e20 update readme 2023-09-02 17:22:35 +08:00
Gabe Yuan
b593fa4146 add deepl api 2023-09-02 16:57:09 +08:00
Gabe Yuan
b00b906484 optimize inject script 2023-09-02 15:54:51 +08:00
Gabe Yuan
c1bd6a1be6 use random eventname 2023-09-02 14:14:27 +08:00
Gabe Yuan
36739f04b3 add more shortcut 2023-09-02 13:14:27 +08:00
Gabe Yuan
23eb92853e register a shortcut for userscript 2023-09-02 00:42:15 +08:00
Gabe Yuan
5ab2910dc7 v1.6.1 2023-09-01 22:46:41 +08:00
Gabe Yuan
40d07f6764 fix proxy link 2023-09-01 22:28:28 +08:00
Gabe Yuan
5c8e216169 overwrite subscribe rules 2023-09-01 22:27:25 +08:00
Gabe Yuan
5ba061deda merge yarn 2023-09-01 19:35:48 +08:00
Gabe Yuan
935c83185d fix style text 2023-09-01 17:07:21 +08:00
Gabe Yuan
6327391e65 hide color input when diy style 2023-09-01 17:02:47 +08:00
Gabe Yuan
3d656cf5b0 use debounce to sync setting 2023-09-01 16:39:57 +08:00
Gabe Yuan
d570a0f1a2 replace 'document.documentElement.clientWidth' to 'window.innerWidth' 2023-09-01 16:11:31 +08:00
Gabe Yuan
503a71302c support diy text styles 2023-09-01 15:57:05 +08:00
Gabe Yuan
3e36ceb5b9 num of newline characters 2023-09-01 11:21:06 +08:00
Gabe Yuan
cde7a1d49f add kiss-proxy link 2023-09-01 11:03:53 +08:00
Gabe Yuan
b14a38e4fb sync then reload setting 2023-09-01 10:15:57 +08:00
Gabe Yuan
732a526a8e v1.6.0 2023-08-31 21:24:40 +08:00
Gabe Yuan
2da5ffef44 update userscript download link 2023-08-31 21:21:36 +08:00
Gabe Yuan
2e6e52004f fix firefox bug 2023-08-31 20:57:51 +08:00
Gabe Yuan
4486ad353c dev.....! 2023-08-31 13:38:06 +08:00
Gabe Yuan
aa795e2731 dev...... 2023-08-31 00:18:57 +08:00
Gabe Yuan
c46fe7d1c6 dev... 2023-08-30 18:05:37 +08:00
Gabe Yuan
d7cee8cca6 catch caches is not defined 2023-08-29 21:33:27 +08:00
Gabe Yuan
11f790ace5 catch caches is not defined 2023-08-29 21:24:25 +08:00
Gabe Yuan
13e7c1b754 fix safari webkit 2023-08-29 17:34:37 +08:00
Gabe Yuan
d314d5515f v1.5.8 2023-08-29 16:52:48 +08:00
Gabe Yuan
09b19e3ca0 fix webkit style in safari 2023-08-29 16:48:38 +08:00
Gabe Yuan
687bd11fd1 fix some text 2023-08-29 13:14:12 +08:00
Gabe Yuan
56cb1cd30d fix links 2023-08-29 11:53:02 +08:00
Gabe Yuan
7a3df25521 generate version file to web 2023-08-29 10:41:20 +08:00
Gabe Yuan
ea8919ba07 update readme 2023-08-29 10:30:18 +08:00
Gabe Yuan
3dece4fcdb add version tag to loading page 2023-08-29 01:35:09 +08:00
Gabe Yuan
df950a1bd2 use createElement script 2023-08-29 01:17:22 +08:00
Gabe Yuan
74b9ee31fa eslint-disable-line 2023-08-29 00:52:37 +08:00
Gabe Yuan
64cd55fe58 set no-eval 2023-08-29 00:42:11 +08:00
Gabe Yuan
e80ede14fb v1.5.7 2023-08-29 00:09:09 +08:00
Gabe Yuan
45ba9d3320 use inject-into replace unsafeWindow 2023-08-29 00:06:50 +08:00
Gabe Yuan
47c7048538 injectscript... 2023-08-28 17:59:51 +08:00
Gabe Yuan
f9bfa8101f fix detectLanguage 2023-08-28 11:14:03 +08:00
Gabe Yuan
620ac464eb v1.5.6 2023-08-27 17:59:47 +08:00
Gabe Yuan
62289f8ab8 catch detect lang err 2023-08-27 17:43:27 +08:00
Gabe Yuan
d84594da96 catch global error and display on top of page 2023-08-27 16:45:57 +08:00
Gabe Yuan
e1d74aae6a catch global error and display on top of page 2023-08-27 16:41:14 +08:00
Gabe Yuan
c4980d9eb7 fix rules 2023-08-26 22:12:48 +08:00
Gabe Yuan
882d83c6b7 update helper text 2023-08-26 15:08:21 +08:00
Gabe Yuan
c4a7fd81f8 v1.5.5 2023-08-26 14:47:15 +08:00
Gabe Yuan
0e55799109 fix sync test button 2023-08-26 14:42:50 +08:00
Gabe Yuan
a3cdcb2a1a add sync test button 2023-08-26 14:31:13 +08:00
Gabe Yuan
e0ccc298f9 add foxnews rule 2023-08-26 13:49:44 +08:00
Gabe Yuan
36b49bb577 modify fab opacity to 0.2 2023-08-26 13:45:24 +08:00
Gabe Yuan
2636c24e84 re translate when text changed 2023-08-26 13:10:13 +08:00
Gabe Yuan
6bcf294635 userscript in iframe 2023-08-26 12:11:21 +08:00
Gabe Yuan
c5fa6689a4 content script in iframe 2023-08-26 12:02:16 +08:00
Gabe Yuan
3bf0cb2485 usescript in iframe 2023-08-26 11:43:00 +08:00
Gabe Yuan
19c9335527 shadow root 2023-08-26 00:08:12 +08:00
Gabe Yuan
20da2e1b97 shadow root 2023-08-25 22:48:47 +08:00
Gabe Yuan
9eceb8641d shadow root 2023-08-25 22:48:11 +08:00
Gabe Yuan
86bc915d74 shadow root 2023-08-25 17:07:53 +08:00
Gabe Yuan
6b35525207 run script in iframe 2023-08-24 16:40:42 +08:00
Gabe Yuan
4633bf4fc6 run script in iframe 2023-08-24 16:39:35 +08:00
Gabe Yuan
2665f31d94 fix iframe bug 2023-08-24 16:21:01 +08:00
Gabe Yuan
6c4d3149eb fix shadow dom 2023-08-24 15:07:13 +08:00
Gabe Yuan
a2762e6ce6 fix shadow dom 2023-08-24 14:57:54 +08:00
Gabe Yuan
792a1bfcad Merge branch 'master' into dev 2023-08-24 10:10:26 +08:00
Gabe Yuan
a0eba9d60e update readme 2023-08-24 10:10:00 +08:00
Gabe Yuan
c2e0064253 support shadow dom 2023-08-23 18:01:47 +08:00
Gabe Yuan
f246efc84b support shadow dom 2023-08-23 17:53:46 +08:00
Gabe Yuan
4a3bf7e96c some minor modifications 2023-08-23 10:39:01 +08:00
Gabe Yuan
523b81090d min length & max length can be set 2023-08-22 21:45:23 +08:00
Gabe Yuan
d706c405d9 add shortcut text to pop page 2023-08-22 21:14:33 +08:00
Gabe Yuan
1191791447 v1.5.4 2023-08-22 17:52:12 +08:00
Gabe Yuan
5c510f2df2 add rules filter when add rule 2023-08-22 17:51:40 +08:00
Gabe Yuan
7c0aa23177 add rules filter when add rule 2023-08-22 17:46:57 +08:00
Gabe Yuan
4bc1c26653 add rules filter when add rule 2023-08-22 17:37:42 +08:00
Gabe Yuan
ca1e1148d6 sync subscribe rules when browser start or userscript run 2023-08-22 16:27:09 +08:00
Gabe Yuan
2224455a7f add text description for rules 2023-08-22 10:35:57 +08:00
53 changed files with 2946 additions and 1543 deletions

26
.env
View File

@@ -2,11 +2,25 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=1.5.3
REACT_APP_VERSION=1.6.3
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
REACT_APP_OPTIONSPAGE=https://kiss-translator.rayjar.com/options
REACT_APP_OPTIONSPAGE2=https://fishjar.github.io/kiss-translator/options.html
REACT_APP_OPTIONSPAGE=https://fishjar.github.io/kiss-translator/options.html
REACT_APP_OPTIONSPAGE2=https://kiss-translator.rayjar.com/options
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
REACT_APP_LOGOURL=https://kiss-translator.rayjar.com/images/logo192.png
REACT_APP_USERSCRIPT_DOWNLOADURL=https://kiss-translator.rayjar.com/kiss-translator.user.js
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
REACT_APP_LOGOURL2=https://kiss-translator.rayjar.com/images/logo192.png
REACT_APP_RULESURL=https://fishjar.github.io/kiss-translator/kiss-translator-rules.json
REACT_APP_RULESURL2=https://kiss-translator.rayjar.com/kiss-translator-rules.json
REACT_APP_VERSIONFILE=https://fishjar.github.io/kiss-translator/version.txt
REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt
REACT_APP_USERSCRIPT_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator.user.js
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator-ios-safari.user.js

View File

@@ -1,10 +1,10 @@
## KISS Translator
# KISS Translator
A minimalist [bilingual translation Extension & Greasemonkey Script](https://github.com/fishjar/kiss-translator).
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
### Inspiration
## Inspiration
The inspiration for this project comes from [Immersive Translate](https://github.com/immersive-translate/immersive-translate). After trying it out, I found that it can be used together with the [Webpage Word Translation Extension](https://github.com/fishjar/kiss-dictionary) developed by me earlier, which just forms a very good supplement.
@@ -14,11 +14,11 @@ It just so happens that I am obsessed with translation tools. Based on the conce
If you also like a little more simplicity, welcome to pick it up.
### Features
## Features
- Keep it simple, smart
### Schedule
## Schedule
- [x] Provide trial installation package
- [x] Adapt browser
@@ -31,20 +31,21 @@ If you also like a little more simplicity, welcome to pick it up.
- [x] Google
- [x] Microsoft
- [x] OpenAI
- [ ] DeepL
- [ ] Upload to app Store
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
- [ ] Edge
- [x] [Firefox](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [x] DeepL
- [x] Upload to app Store
- [x] Chrome [Install Link](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
- [x] Edge [Install Link](https://microsoftedge.microsoft.com/addons/detail/kiss-translator/jemckldkclkinpjighnoilpbldbdmmlh)
- [x] Firefox [Install Link](https://addons.mozilla.org/en-US/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] Greasy Fork [Install Link](https://greasyfork.org/en/scripts/472840-kiss-translator)
- [x] Open source
- [x] Data Synchronization Function
- [x] Greasemonkey Script ([link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js))
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox)
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (need test)
- [x] Greasemonkey Script ([Setting Page 1](https://fishjar.github.io/kiss-translator/options.html)、[Setting Page 2](https://kiss-translator.rayjar.com/options))
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- [x] [Violentmonkey](https://violentmonkey.github.io/) (Chrome/Edge/Firefox) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
### Guide
## Guide
```sh
git clone https://github.com/fishjar/kiss-translator.git
@@ -53,10 +54,10 @@ yarn install
yarn build
```
### Data Sync
## Data Sync
Goto: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
### Discussion
## Discussion
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)

View File

@@ -1,10 +1,10 @@
## 简约翻译
# 简约翻译
一个简约的 [双语网页翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
### 缘由
## 缘由
本项目灵感来源于 [Immersive Translate](https://github.com/immersive-translate/immersive-translate),在试用了后,发现搭配本人早前开发的 [网页划词翻译扩展](https://github.com/fishjar/kiss-dictionary) 一起使用,刚好形成很好补充。
@@ -14,11 +14,11 @@
如果你也喜欢简约一点的,欢迎自取。
### 特点
## 特点
- 保持简约
### 进度
## 进度
- [x] 提供试用安装包
- [x] 适配浏览器
@@ -31,20 +31,21 @@
- [x] Google
- [x] Microsoft
- [x] OpenAI
- [ ] DeepL
- [ ] 上架应用市场
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [ ] Edge
- [x] [Firefox](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [x] DeepL
- [x] 上架应用市场
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Edge [安装地址](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] Greasy Fork [安装地址](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] 开放源代码
- [x] 数据同步功能
- [x] 油猴脚本([链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js))
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox)
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (待测)
- [x] 油猴脚本 ([设置页面 1](https://fishjar.github.io/kiss-translator/options.html)、[设置页面 2](https://kiss-translator.rayjar.com/options))
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- [x] [Violentmonkey](https://violentmonkey.github.io/) (Chrome/Edge/Firefox) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
### 指引
## 指引
```sh
git clone https://github.com/fishjar/kiss-translator.git
@@ -53,10 +54,10 @@ yarn install
yarn build
```
### 数据同步
## 数据同步
移步: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
### 交流
## 交流
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)

View File

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

View File

@@ -15,12 +15,63 @@
max-height: 1.2em;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
// (() => {
// var shadow = document.querySelector("#shadow1");
// var root = shadow.attachShadow({ mode: "open" });
// var newLine = document.createElement("p");
// newLine.innerText = "new line";
// root.appendChild(newLine);
// })();
// setTimeout(function () {
// var shadow = document.querySelector("#shadow2");
// var root = shadow.attachShadow({ mode: "open" });
// }, 1000);
// setTimeout(() => {
// var newLine = document.createElement("p");
// newLine.innerText = "new line";
// var shadow = document.querySelector("#shadow2");
// shadow.shadowRoot.appendChild(newLine);
// }, 2000);
// setTimeout(() => {
// var newLine = document.createElement("div");
// newLine.innerHTML = "<p>second line</p><p>third line</p>";
// var shadow = document.querySelector("#shadow2");
// shadow.shadowRoot.appendChild(newLine);
// }, 3000);
// setTimeout(function () {
// var el = document.querySelector("h2");
// el.innerText = "hello world";
// var title = document.querySelector("#addtitle");
// title.innerHTML =
// "<div><p>second title</p><ul><li>second title</li><li><p>second title</p></li></ul></div>";
// }, 1000);
setTimeout(function () {
var el = document.querySelector("h2>p>span");
el.innerText = "hello world";
}, 1000);
});
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<h2>React is a JavaScript library for building user interfaces.</h2>
<h2>
<p><span>React is a JavaScript library for building user interfaces.</span></p>
</h2>
<div id="addtitle"></div>
<h2>Shadow 1</h2>
<div id="shadow1"></div>
<h2>Shadow 2</h2>
<div id="shadow2"></div>
<br />
<br />
<br />
@@ -53,7 +104,16 @@
<br />
<br />
<br />
<h2>React is a JavaScript library for building user interfaces.</h2>
<h2>
React Server Components (or RSC) is a new application architecture
designed by the React team.
</h2>
<iframe
id="iframe1"
width="800px"
height="600px"
src="http://localhost:3000/index.html"
></iframe>
<br />
<br />
<br />
@@ -86,7 +146,10 @@
<br />
<br />
<br />
<h2>React is a JavaScript library for building user interfaces.</h2>
<h2>
Weve first shared our research on RSC in an introductory talk and an
RFC.
</h2>
<br />
<br />
<br />
@@ -119,7 +182,17 @@
<br />
<br />
<br />
<h2>React is a JavaScript library for building user interfaces.</h2>
<h2>
To recap them, we are introducing a new kind of component—Server
Components—that run ahead of time and are excluded from your JavaScript
bundle.
</h2>
<iframe
id="iframe2"
width="800px"
height="600px"
src="https://react.dev/"
></iframe>
<br />
<br />
<br />
@@ -153,175 +226,42 @@
<br />
<br />
<div class="cont cont1">
<h2>React is a JavaScript library for building user interfaces.</h2>
<ul>
<li>
Declarative: React makes it painless to create interactive UIs.
Design simple views for each state in your application, and React
will efficiently update and render just the right components when
your data changes. Declarative views make your code more
predictable, simpler to understand, and easier to debug.
</li>
<li>
Component-Based: Build encapsulated components that manage their own
state, then compose them to make complex UIs. Since component logic
is written in JavaScript instead of templates, you can easily pass
rich data through your app and keep the state out of the DOM.
</li>
<li>
React 使创建交互式 UI
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
能高效更新并渲染合适的组件。
</li>
<li>以声明式编写 UI可以让你的代码更加可靠且方便调试。</li>
</ul>
</div>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<div class="cont cont2">
<h2>React is a JavaScript library for building user interfaces.</h2>
<ul>
<li>
Declarative: React makes it painless to create interactive UIs.
Design simple views for each state in your application, and React
will efficiently update and render just the right components when
your data changes. Declarative views make your code more
predictable, simpler to understand, and easier to debug.
</li>
<li>
Component-Based: Build encapsulated components that manage their own
state, then compose them to make complex UIs. Since component logic
is written in JavaScript instead of templates, you can easily pass
rich data through your app and keep the state out of the DOM.
</li>
<li>
React 使创建交互式 UI
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
能高效更新并渲染合适的组件。
</li>
<li>以声明式编写 UI可以让你的代码更加可靠且方便调试。</li>
</ul>
</div>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<div class="cont cont3">
<h2>React is a JavaScript library for building user interfaces.</h2>
<ul>
<li>
Declarative: React makes it painless to create interactive UIs.
Design simple views for each state in your application, and React
will efficiently update and render just the right components when
your data changes. Declarative views make your code more
predictable, simpler to understand, and easier to debug.
</li>
<li>
Component-Based: Build encapsulated components that manage their own
state, then compose them to make complex UIs. Since component logic
is written in JavaScript instead of templates, you can easily pass
rich data through your app and keep the state out of the DOM.
</li>
<li>
React 使创建交互式 UI
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
能高效更新并渲染合适的组件。
</li>
<li>以声明式编写 UI可以让你的代码更加可靠且方便调试。</li>
</ul>
</div>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<div class="cont cont4">
<h2>
React is a <code>JavaScript</code> <a href="#">library</a> for
building user interfaces.
Server Components can run during the build, letting you read from the
filesystem or fetch static content.
</h2>
<ul>
<li>
Declarative: React makes it painless to create interactive UIs.
Design simple views for each state in your application, and React
will efficiently update and render just the right components when
your data changes. Declarative views make your code more
predictable, simpler to understand, and easier to debug.
</li>
<li>
Component-Based: Build encapsulated components that manage their own
state, then compose them to make complex UIs. Since component logic
is written in JavaScript instead of templates, you can easily pass
rich data through your app and keep the state out of the DOM.
</li>
<li>
React 使创建交互式 UI
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
能高效更新并渲染合适的组件。
They can also run on the server, letting you access your data layer
without having to build an API. You can pass data by props from
Server Components to the interactive Client Components in the
browser.
</li>
<li>以声明式编写 UI可以让你的代码更加可靠且方便调试。</li>
</ul>
</div>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<div class="cont cont5">
<h2>React is a JavaScript library for building user interfaces.</h2>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<div class="cont cont2">
<h2>
Since our last update, we have merged the React Server Components RFC
to ratify the proposal.
</h2>
<ul>
<li>
Declarative: React makes it painless to create interactive UIs.
Design simple views for each state in your application, and React
will efficiently update and render just the right components when
your data changes. Declarative views make your code more
predictable, simpler to understand, and easier to debug.
</li>
<li>
Component-Based: Build encapsulated components that manage their own
state, then compose them to make complex UIs. Since component logic
is written in JavaScript instead of templates, you can easily pass
rich data through your app and keep the state out of the DOM.
RSC combines the simple “request/response” mental model of
server-centric Multi-Page Apps with the seamless interactivity of
client-centric Single-Page Apps, giving you the best of both worlds.
</li>
<li>
React 使创建交互式 UI

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.5.3",
"version": "1.6.3",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -12,10 +12,16 @@
"content_scripts": [
{
"js": ["content.js"],
"matches": ["<all_urls>"]
"matches": ["<all_urls>"],
"all_frames": true
}
],
"commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Alt+K"
}
},
"toggleTranslate": {
"suggested_key": {
"default": "Alt+Q"

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.5.3",
"version": "1.6.3",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -13,10 +13,16 @@
"content_scripts": [
{
"js": ["content.js"],
"matches": ["<all_urls>"]
"matches": ["<all_urls>"],
"all_frames": true
}
],
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Alt+K"
}
},
"toggleTranslate": {
"suggested_key": {
"default": "Alt+Q"

View File

@@ -3,6 +3,7 @@ import { fetchPolyfill } from "../libs/fetch";
import {
OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI,
URL_MICROSOFT_TRANS,
OPT_LANGS_SPECIAL,
@@ -10,7 +11,7 @@ import {
PROMPT_PLACE_TO,
KV_SALT_SYNC,
} from "../config";
import { getSetting, detectLang } from "../libs";
import { tryDetectLang } from "../libs";
import { sha256 } from "../libs/utils";
/**
@@ -20,7 +21,7 @@ import { sha256 } from "../libs/utils";
* @param {*} data
* @returns
*/
export const apiSyncData = async (url, key, data) =>
export const apiSyncData = async (url, key, data, isBg = false) =>
fetchPolyfill(url, {
headers: {
"Content-type": "application/json",
@@ -28,8 +29,18 @@ export const apiSyncData = async (url, key, data) =>
},
method: "POST",
body: JSON.stringify(data),
isBg,
});
/**
* 下载订阅规则
* @param {*} url
* @param {*} isBg
* @returns
*/
export const apiFetchRules = (url, isBg = false) =>
fetchPolyfill(url, { isBg });
/**
* 谷歌翻译
* @param {*} text
@@ -37,7 +48,8 @@ export const apiSyncData = async (url, key, data) =>
* @param {*} from
* @returns
*/
const apiGoogleTranslate = async (translator, text, to, from) => {
const apiGoogleTranslate = async (translator, text, to, from, setting) => {
const { googleUrl } = setting;
const params = {
client: "gtx",
dt: "t",
@@ -47,17 +59,15 @@ const apiGoogleTranslate = async (translator, text, to, from) => {
tl: to,
q: text,
};
const { googleUrl } = await getSetting();
const input = `${googleUrl}?${queryString.stringify(params)}`;
return fetchPolyfill(
input,
{
headers: {
"Content-type": "application/json",
},
return fetchPolyfill(input, {
headers: {
"Content-type": "application/json",
},
{ useCache: true, usePool: true, translator }
);
useCache: true,
usePool: true,
translator,
});
};
/**
@@ -74,17 +84,46 @@ const apiMicrosoftTranslate = (translator, text, to, from) => {
"api-version": "3.0",
};
const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`;
return fetchPolyfill(
input,
{
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify([{ Text: text }]),
return fetchPolyfill(input, {
headers: {
"Content-type": "application/json",
},
{ useCache: true, usePool: true, translator }
);
method: "POST",
body: JSON.stringify([{ Text: text }]),
useCache: true,
usePool: true,
translator,
});
};
/**
* DeepL翻译
* @param {*} text
* @param {*} to
* @param {*} from
* @returns
*/
const apiDeepLTranslate = (translator, text, to, from, setting) => {
const { deeplUrl, deeplKey } = setting;
const data = {
text: [text],
target_lang: to,
split_sentences: "0",
};
if (from) {
data.source_lang = from;
}
return fetchPolyfill(deeplUrl, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
useCache: true,
usePool: true,
translator,
token: deeplKey,
});
};
/**
@@ -94,37 +133,36 @@ const apiMicrosoftTranslate = (translator, text, to, from) => {
* @param {*} from
* @returns
*/
const apiOpenaiTranslate = async (translator, text, to, from) => {
const { openaiUrl, openaiKey, openaiModel, openaiPrompt } =
await getSetting();
const apiOpenaiTranslate = async (translator, text, to, from, setting) => {
const { openaiUrl, openaiKey, openaiModel, openaiPrompt } = setting;
let prompt = openaiPrompt
.replaceAll(PROMPT_PLACE_FROM, from)
.replaceAll(PROMPT_PLACE_TO, to);
return fetchPolyfill(
openaiUrl,
{
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify({
model: openaiModel,
messages: [
{
role: "system",
content: prompt,
},
{
role: "user",
content: text,
},
],
temperature: 0,
max_tokens: 256,
}),
return fetchPolyfill(openaiUrl, {
headers: {
"Content-type": "application/json",
},
{ useCache: true, usePool: true, translator, token: openaiKey }
);
method: "POST",
body: JSON.stringify({
model: openaiModel,
messages: [
{
role: "system",
content: prompt,
},
{
role: "user",
content: text,
},
],
temperature: 0,
max_tokens: 256,
}),
useCache: true,
usePool: true,
translator,
token: openaiKey,
});
};
/**
@@ -132,7 +170,13 @@ const apiOpenaiTranslate = async (translator, text, to, from) => {
* @param {*} param0
* @returns
*/
export const apiTranslate = async ({ translator, q, fromLang, toLang }) => {
export const apiTranslate = async ({
translator,
q,
fromLang,
toLang,
setting,
}) => {
let trText = "";
let isSame = false;
@@ -140,17 +184,23 @@ export const apiTranslate = async ({ translator, q, fromLang, toLang }) => {
let to = OPT_LANGS_SPECIAL?.[translator]?.get(toLang) ?? toLang;
if (translator === OPT_TRANS_GOOGLE) {
const res = await apiGoogleTranslate(translator, q, to, from);
const res = await apiGoogleTranslate(translator, q, to, from, setting);
trText = res.sentences.map((item) => item.trans).join(" ");
isSame = to === res.src;
} else if (translator === OPT_TRANS_MICROSOFT) {
const res = await apiMicrosoftTranslate(translator, q, to, from);
trText = res[0].translations[0].text;
isSame = to === res[0].detectedLanguage.language;
} else if (translator === OPT_TRANS_DEEPL) {
const res = await apiDeepLTranslate(translator, q, to, from, setting);
trText = res.translations.map((item) => item.text).join(" ");
isSame = to === res.translations[0].detected_source_language;
} else if (translator === OPT_TRANS_OPENAI) {
const res = await apiOpenaiTranslate(translator, q, to, from);
const res = await apiOpenaiTranslate(translator, q, to, from, setting);
trText = res?.choices?.[0].message.content;
isSame = (await detectLang(q)) === (await detectLang(trText));
const sLang = await tryDetectLang(q);
const tLang = await tryDetectLang(trText);
isSame = q === trText || (sLang && tLang && sLang === tLang);
}
return [trText, isSame];

View File

@@ -7,29 +7,19 @@ import {
MSG_TRANS_TOGGLE_STYLE,
CMD_TOGGLE_TRANSLATE,
CMD_TOGGLE_STYLE,
DEFAULT_SETTING,
DEFAULT_RULES,
DEFAULT_SYNC,
STOKEY_SETTING,
STOKEY_RULES,
STOKEY_SYNC,
CACHE_NAME,
} from "./config";
import storage from "./libs/storage";
import { getSetting } from "./libs";
import { syncAll } from "./libs/sync";
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
import { trySyncSettingAndRules } from "./libs/sync";
import { fetchData, fetchPool } from "./libs/fetch";
import { sendTabMsg } from "./libs/msg";
import { trySyncAllSubRules } from "./libs/subRules";
import { tryClearCaches } from "./libs";
/**
* 插件安装
*/
browser.runtime.onInstalled.addListener(() => {
console.log("KISS Translator onInstalled");
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES);
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
// todo缓存内置rules
tryInitDefaultData();
});
/**
@@ -39,13 +29,16 @@ browser.runtime.onStartup.addListener(async () => {
console.log("browser onStartup");
// 同步数据
await syncAll();
await trySyncSettingAndRules(true);
// 清除缓存
const { clearCache } = await getSetting();
if (clearCache) {
caches.delete(CACHE_NAME);
const setting = await getSettingWithDefault();
if (setting.clearCache) {
tryClearCaches();
}
// 同步订阅规则
trySyncAllSubRules(setting, true);
});
/**
@@ -55,8 +48,8 @@ browser.runtime.onMessage.addListener(
({ action, args }, sender, sendResponse) => {
switch (action) {
case MSG_FETCH:
const { input, init, opts } = args;
fetchData(input, init, opts)
const { input, opts } = args;
fetchData(input, opts)
.then((data) => {
sendResponse({ data });
})

4
src/config/app.js Normal file
View File

@@ -0,0 +1,4 @@
export const APP_NAME = process.env.REACT_APP_NAME.trim()
.split(/\s+/)
.join("-");
export const APP_LCNAME = APP_NAME.toLowerCase();

View File

@@ -12,6 +12,10 @@ export const I18N = {
zh: `翻译`,
en: `Translate`,
},
translate_alt: {
zh: `翻译 (Alt+Q)`,
en: `Translate (Alt+Q)`,
},
basic_setting: {
zh: `基本设置`,
en: `Basic Setting`,
@@ -41,12 +45,24 @@ export const I18N = {
en: `Interface Language`,
},
fetch_limit: {
zh: `最大请求数量`,
en: `Maximum Number Of Request`,
zh: `最大请求数量 (1-100)`,
en: `Maximum Number Of Request (1-100)`,
},
fetch_interval: {
zh: `请求间隔时间(ms)`,
en: `Request Interval(ms)`,
zh: `请求间隔时间 (0-5000ms)`,
en: `Request Interval (0-5000ms)`,
},
min_translate_length: {
zh: `最小翻译长度 (1-100)`,
en: `Min Translate Length (1-100)`,
},
max_translate_length: {
zh: `最大翻译长度 (100-10000)`,
en: `Max Translate Length (100-10000)`,
},
num_of_newline_characters: {
zh: `换行字符数 (1-1000)`,
en: `Number of Newline Characters (1-1000)`,
},
translate_service: {
zh: `翻译服务`,
@@ -64,10 +80,18 @@ export const I18N = {
zh: `文字样式`,
en: `Text Style`,
},
text_style_alt: {
zh: `文字样式 (Alt+C)`,
en: `Text Style (Alt+C)`,
},
bg_color: {
zh: `样式颜色`,
en: `Style Color`,
},
remain_unchanged: {
zh: `保留不变`,
en: `Remain Unchanged`,
},
google_api: {
zh: `谷歌翻译接口`,
en: `Google Translate API`,
@@ -108,18 +132,30 @@ export const I18N = {
zh: `注入订阅规则`,
en: `Inject Subscribe Rules`,
},
edit_rules: {
zh: `编辑规则`,
en: `Edit Rules`,
personal_rules: {
zh: `个人规则`,
en: `Personal Rules`,
},
subscribe_rules: {
zh: `订阅规则`,
en: `Subscribe Rules`,
},
overwrite_subscribe_rules: {
zh: `覆写订阅规则`,
en: `Overwrite Subscribe Rules`,
},
subscribe_url: {
zh: `订阅地址`,
en: `Subscribe URL`,
},
rules_warn_1: {
zh: `1、“个人规则”一直生效选择“注入订阅规则”后“订阅规则”才会生效。`,
en: `1. The "Personal Rules" are always in effect. After selecting "Inject Subscription Rules", the "Subscription Rules" will take effect.`,
},
rules_warn_2: {
zh: `2、“订阅规则”的注入位置是倒数第二的位置因此除全局规则(*)外,“个人规则”优先级比“订阅规则”高,“个人规则”填写同样的网址会覆盖”订阅规则“的条目。`,
en: `2. The injection position of "Subscription Rules" is the penultimate position. Therefore, except for the global rules (*), the priority of "Personal Rules" is higher than that of "Subscription Rules". Filling in the same url in "Personal Rules" will overwrite "Subscription Rules" entry.`,
},
sync_warn: {
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`,
@@ -128,6 +164,10 @@ export const I18N = {
zh: `查看关于数据同步接口部署`,
en: `View About Data Synchronization Interface Deployment`,
},
about_api_proxy: {
zh: `查看自建一个翻译接口代理`,
en: `Check out the self-built translation interface proxy`,
},
style_none: {
zh: ``,
en: `None`,
@@ -156,6 +196,14 @@ export const I18N = {
zh: `高亮`,
en: `Highlight`,
},
diy_style: {
zh: `自定义样式`,
en: `Custom Style`,
},
diy_style_helper: {
zh: `遵循“styled-components”的语法`,
en: `Follow the syntax of "styled-components"`,
},
setting: {
zh: `设置`,
en: `Setting`,
@@ -169,8 +217,8 @@ export const I18N = {
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`,
},
selector_helper: {
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。`,
en: `1. Follow CSS selector rules. 2. Leave blank to adopt the global setting.`,
zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`,
en: `1. Follow CSS selector syntax. 2. Leave blank to adopt the global setting. 3. Separate multiple CSS selectors with ";". 4. The "shadow root" selector and the internal selector are separated by ">>>".`,
},
translate_switch: {
zh: `开启翻译`,
@@ -212,6 +260,14 @@ export const I18N = {
zh: `请检查url地址是否正确或稍后再试。`,
en: `Please check if the url address is correct or try again later.`,
},
deepl_api: {
zh: `DeepL 接口`,
en: `DeepL API`,
},
deepl_key: {
zh: `DeepL 密钥`,
en: `DeepL Key`,
},
openai_api: {
zh: `OpenAI 接口`,
en: `OpenAI API`,
@@ -248,12 +304,24 @@ export const I18N = {
zh: `数据同步密钥`,
en: `Data Sync Key`,
},
data_sync_test: {
zh: `数据同步测试`,
en: `Data Sync Test`,
},
data_sync_success: {
zh: `数据同步成功!`,
en: `Data Sync Success`,
},
data_sync_error: {
zh: `数据同步失败!`,
en: `Data Sync Error`,
},
error_got_some_wrong: {
zh: "抱歉,出错了!",
en: "Sorry, something went wrong!",
zh: `抱歉,出错了!`,
en: `Sorry, something went wrong!`,
},
error_sync_setting: {
zh: "您的同步设置未填写,无法在线分享。",
en: "Your sync settings are missing and cannot be shared online.",
zh: `您的同步设置未填写,无法在线分享。`,
en: `Your sync settings are missing and cannot be shared online.`,
},
};

View File

@@ -1,15 +1,23 @@
import {
DEFAULT_SELECTOR,
GLOBAL_KEY,
REMAIN_KEY,
SHADOW_KEY,
DEFAULT_RULE,
DEFAULT_OW_RULE,
BUILTIN_RULES,
} from "./rules";
import { APP_NAME, APP_LCNAME } from "./app";
export { I18N, UI_LANGS } from "./i18n";
export { GLOBAL_KEY, DEFAULT_RULE, BUILTIN_RULES };
const APP_NAME = process.env.REACT_APP_NAME.trim().split(/\s+/).join("-");
export const APP_LCNAME = APP_NAME.toLowerCase();
export {
GLOBAL_KEY,
REMAIN_KEY,
SHADOW_KEY,
DEFAULT_RULE,
DEFAULT_OW_RULE,
BUILTIN_RULES,
APP_LCNAME,
};
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
export const STOKEY_SETTING = `${APP_NAME}_setting`;
@@ -45,12 +53,11 @@ export const MSG_TRANS_GETRULE = "trans_getrule";
export const MSG_TRANS_PUTRULE = "trans_putrule";
export const MSG_TRANS_CURRULE = "trans_currule";
export const EVENT_KISS = "kissEvent";
export const THEME_LIGHT = "light";
export const THEME_DARK = "dark";
export const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker";
export const URL_KISS_PROXY = "https://github.com/fishjar/kiss-proxy";
export const URL_RAW_PREFIX =
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
@@ -59,10 +66,12 @@ export const URL_MICROSOFT_TRANS =
export const OPT_TRANS_GOOGLE = "Google";
export const OPT_TRANS_MICROSOFT = "Microsoft";
export const OPT_TRANS_DEEPL = "DeepL";
export const OPT_TRANS_OPENAI = "OpenAI";
export const OPT_TRANS_ALL = [
OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI,
];
@@ -112,6 +121,12 @@ export const OPT_LANGS_SPECIAL = {
["zh-CN", "zh-Hans"],
["zh-TW", "zh-Hant"],
]),
[OPT_TRANS_DEEPL]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
["auto", ""],
["zh-CN", "ZH"],
["zh-TW", "ZH"],
]),
[OPT_TRANS_OPENAI]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
@@ -123,7 +138,8 @@ export const OPT_STYLE_DOTLINE = "dot_line"; // 点状线
export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
export const OPT_STYLE_HIGHTLIGHT = "highlight"; // 高亮
export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
export const OPT_STYLE_DIY = "diy_style"; // 自定义样式
export const OPT_STYLE_ALL = [
OPT_STYLE_NONE,
OPT_STYLE_LINE,
@@ -131,7 +147,15 @@ export const OPT_STYLE_ALL = [
OPT_STYLE_DASHLINE,
OPT_STYLE_WAVYLINE,
OPT_STYLE_FUZZY,
OPT_STYLE_HIGHTLIGHT,
OPT_STYLE_HIGHLIGHT,
OPT_STYLE_DIY,
];
export const OPT_STYLE_USE_COLOR = [
OPT_STYLE_LINE,
OPT_STYLE_DOTLINE,
OPT_STYLE_DASHLINE,
OPT_STYLE_WAVYLINE,
OPT_STYLE_HIGHLIGHT,
];
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
@@ -152,28 +176,40 @@ export const GLOBLA_RULE = {
textStyle: OPT_STYLE_DASHLINE,
transOpen: "false",
bgColor: "",
textDiyStyle: "",
};
// 订阅列表
export const DEFAULT_SUBRULES_LIST = [
{
url: "https://kiss-translator.rayjar.com/kiss-translator-rules.json",
url: process.env.REACT_APP_RULESURL,
selected: true,
},
{
url: "https://fishjar.github.io/kiss-translator/kiss-translator-rules.json",
url: process.env.REACT_APP_RULESURL2,
selected: false,
},
];
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
export const TRANS_NEWLINE_LENGTH = 40; // 换行字符数
export const DEFAULT_SETTING = {
darkMode: false, // 深色模式
uiLang: "en", // 界面语言
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
minLength: TRANS_MIN_LENGTH,
maxLength: TRANS_MAX_LENGTH,
newlineLength: TRANS_NEWLINE_LENGTH,
clearCache: false, // 是否在浏览器下次启动时清除缓存
injectRules: true, // 是否注入订阅规则
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口
deeplUrl: "https://api-free.deepl.com/v2/translate",
deeplKey: "",
openaiUrl: "https://api.openai.com/v1/chat/completions",
openaiKey: "",
openaiModel: "gpt-4",
@@ -182,9 +218,6 @@ export const DEFAULT_SETTING = {
export const DEFAULT_RULES = [GLOBLA_RULE];
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
export const DEFAULT_SYNC = {
syncUrl: "", // 数据同步接口
syncKey: "", // 数据同步密钥
@@ -192,4 +225,5 @@ export const DEFAULT_SYNC = {
settingSyncAt: 0,
rulesUpdateAt: 0,
rulesSyncAt: 0,
subRulesSyncAt: 0, // 订阅规则同步时间
};

View File

@@ -3,6 +3,9 @@ const els = `li, p, h1, h2, h3, h4, h5, h6, dd`;
export const DEFAULT_SELECTOR = `:is(${els})`;
export const GLOBAL_KEY = "*";
export const REMAIN_KEY = "-";
export const SHADOW_KEY = ">>>";
export const DEFAULT_RULE = {
pattern: "",
@@ -13,6 +16,30 @@ export const DEFAULT_RULE = {
textStyle: GLOBAL_KEY,
transOpen: GLOBAL_KEY,
bgColor: "",
textDiyStyle: "",
};
const DEFAULT_DIY_STYLE = `color: #666;
background: linear-gradient(
45deg,
LightGreen 20%,
LightPink 20% 40%,
LightSalmon 40% 60%,
LightSeaGreen 60% 80%,
LightSkyBlue 80%
);
&:hover {
color: #333;
};`;
export const DEFAULT_OW_RULE = {
translator: REMAIN_KEY,
fromLang: REMAIN_KEY,
toLang: REMAIN_KEY,
textStyle: REMAIN_KEY,
transOpen: REMAIN_KEY,
bgColor: "",
textDiyStyle: DEFAULT_DIY_STYLE,
};
const RULES = [
@@ -21,9 +48,13 @@ const RULES = [
selector: `h3, .IsZvec, .VwiC3b`,
},
{
pattern: `https://news.google.com/`,
pattern: `news.google.com`,
selector: `h4`,
},
{
pattern: `www.foxnews.com`,
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
},
{
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,
selector: DEFAULT_SELECTOR,

View File

@@ -5,16 +5,19 @@ import {
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
} from "./config";
import { getSetting, getRules, matchRule } from "./libs";
import { getSettingWithDefault, getRulesWithDefault } from "./libs/storage";
import { Translator } from "./libs/translator";
import { isIframe } from "./libs/iframe";
import { matchRule } from "./libs/rules";
/**
* 入口函数
*/
(async () => {
const setting = await getSetting();
const rules = await getRules();
const rule = await matchRule(rules, document.location.href, setting);
const init = async () => {
const href = isIframe ? document.referrer : document.location.href;
const setting = await getSettingWithDefault();
const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);
// 监听消息
@@ -36,4 +39,15 @@ import { Translator } from "./libs/translator";
}
return { data: translator.rule };
});
};
(async () => {
try {
await init();
} catch (err) {
const $err = document.createElement("div");
$err.innerText = `KISS-Translator: ${err.message}`;
$err.style.cssText = "background:red; color:#fff; z-index:10000;";
document.body.prepend($err);
}
})();

View File

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

View File

@@ -1,22 +1,19 @@
import { useSetting, useSettingUpdate } from "./Setting";
import { useCallback } from "react";
import { useSetting } from "./Setting";
/**
* 深色模式hook
* @returns
*/
export function useDarkMode() {
const setting = useSetting();
return !!setting?.darkMode;
}
const {
setting: { darkMode },
updateSetting,
} = useSetting();
/**
* 切换深色模式
* @returns
*/
export function useDarkModeSwitch() {
const darkMode = useDarkMode();
const updateSetting = useSettingUpdate();
return async () => {
const toggleDarkMode = useCallback(async () => {
await updateSetting({ darkMode: !darkMode });
};
}, [darkMode, updateSetting]);
return { darkMode, toggleDarkMode };
}

View File

@@ -7,7 +7,9 @@ import { useFetch } from "./Fetch";
* @returns
*/
export const useI18n = () => {
const { uiLang } = useSetting() ?? {};
const {
setting: { uiLang },
} = useSetting();
return (key, defaultText = "") => I18N?.[key]?.[uiLang] ?? defaultText;
};

View File

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

View File

@@ -1,28 +1,58 @@
import { STOKEY_SETTING } from "../config";
import storage from "../libs/storage";
import { useStorages } from "./Storage";
import { STOKEY_SETTING, DEFAULT_SETTING } from "../config";
import { useStorage } from "./Storage";
import { useSync } from "./Sync";
import { syncSetting } from "../libs/sync";
import { trySyncSetting } from "../libs/sync";
import { createContext, useCallback, useContext, useMemo } from "react";
import { debounce } from "../libs/utils";
const SettingContext = createContext({
setting: null,
updateSetting: async () => {},
reloadSetting: async () => {},
});
export function SettingProvider({ children }) {
const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
const {
sync: { settingUpdateAt },
updateSync,
} = useSync();
const syncSetting = useMemo(
() =>
debounce(() => {
trySyncSetting();
}, [2000]),
[]
);
const updateSetting = useCallback(
async (obj) => {
const updateAt = settingUpdateAt ? Date.now() : 0;
await update(obj);
await updateSync({ settingUpdateAt: updateAt });
syncSetting();
},
[settingUpdateAt, update, updateSync, syncSetting]
);
return (
<SettingContext.Provider
value={{
setting: data,
updateSetting,
reloadSetting: reload,
}}
>
{children}
</SettingContext.Provider>
);
}
/**
* 设置hook
* 设置 hook
* @returns
*/
export function useSetting() {
const storages = useStorages();
return storages?.[STOKEY_SETTING];
}
/**
* 更新设置
* @returns
*/
export function useSettingUpdate() {
const sync = useSync();
return async (obj) => {
const updateAt = sync.opt?.settingUpdateAt ? Date.now() : 0;
await storage.putObj(STOKEY_SETTING, obj);
await sync.update({ settingUpdateAt: updateAt });
syncSetting();
};
return useContext(SettingContext);
}

View File

@@ -1,91 +1,44 @@
import { createContext, useContext, useEffect, useState } from "react";
import { browser, isExt, isGm, isWeb } from "../libs/browser";
import {
STOKEY_SETTING,
STOKEY_RULES,
STOKEY_SYNC,
DEFAULT_SETTING,
DEFAULT_RULES,
DEFAULT_SYNC,
} from "../config";
import storage from "../libs/storage";
import { useCallback, useEffect, useState } from "react";
import { storage } from "../libs/storage";
/**
* 默认配置
*/
export const defaultStorage = {
[STOKEY_SETTING]: DEFAULT_SETTING,
[STOKEY_RULES]: DEFAULT_RULES,
[STOKEY_SYNC]: DEFAULT_SYNC,
};
export function useStorage(key, defaultVal = null) {
const [data, setData] = useState(defaultVal);
const activeKeys = Object.keys(defaultStorage);
const save = useCallback(
async (val) => {
setData(val);
await storage.setObj(key, val);
},
[key]
);
const StoragesContext = createContext(null);
const update = useCallback(
async (obj) => {
setData((pre) => ({ ...pre, ...obj }));
await storage.putObj(key, obj);
},
[key]
);
export function StoragesProvider({ children }) {
const [storages, setStorages] = useState(null);
const remove = useCallback(async () => {
setData(null);
await storage.del(key);
}, [key]);
const handleChanged = (changes) => {
if (isWeb || isGm) {
const { key, oldValue, newValue } = changes;
changes = {
[key]: {
oldValue,
newValue,
},
};
const reload = useCallback(async () => {
const val = await storage.getObj(key);
if (val) {
setData(val);
} else if (defaultVal) {
await storage.setObj(key, defaultVal);
}
const newStorages = {};
Object.entries(changes)
.filter(
([key, { oldValue, newValue }]) =>
activeKeys.includes(key) && oldValue !== newValue
)
.forEach(([key, { newValue }]) => {
newStorages[key] = JSON.parse(newValue);
});
if (Object.keys(newStorages).length !== 0) {
setStorages((pre) => ({ ...pre, ...newStorages }));
}
};
}, [key, defaultVal]);
useEffect(() => {
// 首次从storage同步配置到内存
(async () => {
const curStorages = {};
for (const key of activeKeys) {
const val = await storage.get(key);
if (val) {
curStorages[key] = JSON.parse(val);
} else {
await storage.setObj(key, defaultStorage[key]);
curStorages[key] = defaultStorage[key];
}
}
setStorages(curStorages);
await reload();
})();
}, [reload]);
// 监听storage并同步到内存中
storage.onChanged(handleChanged);
// 解除监听
return () => {
if (isExt) {
browser.storage.onChanged.removeListener(handleChanged);
} else {
window.removeEventListener("storage", handleChanged);
}
};
}, []);
return (
<StoragesContext.Provider value={storages}>
{children}
</StoragesContext.Provider>
);
}
export function useStorages() {
return useContext(StoragesContext);
return { data, save, update, remove, reload };
}

99
src/hooks/SubRules.js Normal file
View File

@@ -0,0 +1,99 @@
import { DEFAULT_SUBRULES_LIST, DEFAULT_OW_RULE } from "../config";
import { useSetting } from "./Setting";
import { useCallback, useEffect, useMemo, useState } from "react";
import { loadOrFetchSubRules } from "../libs/subRules";
import { delSubRules } from "../libs/storage";
/**
* 订阅规则
* @returns
*/
export function useSubRules() {
const [loading, setLoading] = useState(false);
const [selectedRules, setSelectedRules] = useState([]);
const { setting, updateSetting } = useSetting();
const list = setting?.subrulesList || DEFAULT_SUBRULES_LIST;
const selectedSub = useMemo(() => list.find((item) => item.selected), [list]);
const selectedUrl = selectedSub.url;
const selectSub = useCallback(
async (url) => {
const subrulesList = [...list];
subrulesList.forEach((item) => {
if (item.url === url) {
item.selected = true;
} else {
item.selected = false;
}
});
await updateSetting({ subrulesList });
},
[list, updateSetting]
);
const addSub = useCallback(
async (url) => {
const subrulesList = [...list];
subrulesList.push({ url, selected: false });
await updateSetting({ subrulesList });
},
[list, updateSetting]
);
const delSub = useCallback(
async (url) => {
let subrulesList = [...list];
subrulesList = subrulesList.filter((item) => item.url !== url);
await updateSetting({ subrulesList });
await delSubRules(url);
},
[list, updateSetting]
);
useEffect(() => {
(async () => {
if (selectedUrl) {
try {
setLoading(true);
const rules = await loadOrFetchSubRules(selectedUrl);
setSelectedRules(rules);
} catch (err) {
console.log("[loadOrFetchSubRules]", err);
} finally {
setLoading(false);
}
}
})();
}, [selectedUrl]);
return {
subList: list,
selectSub,
addSub,
delSub,
selectedSub,
selectedUrl,
selectedRules,
setSelectedRules,
loading,
};
}
/**
* 覆写订阅规则
* @returns
*/
export function useOwSubRule() {
const { setting, updateSetting } = useSetting();
const { owSubrule = DEFAULT_OW_RULE } = setting;
const updateOwSubrule = useCallback(
async (obj) => {
await updateSetting({ owSubrule: { ...owSubrule, ...obj } });
},
[owSubrule, updateSetting]
);
return { owSubrule, updateOwSubrule };
}

View File

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

View File

@@ -9,8 +9,8 @@ import { THEME_DARK, THEME_LIGHT } from "../config";
* @param {*} param0
* @returns
*/
export default function MuiThemeProvider({ children, options }) {
const darkMode = useDarkMode();
export default function Theme({ children, options }) {
const { darkMode } = useDarkMode();
const theme = useMemo(() => {
return createTheme({
palette: {

View File

@@ -1,15 +1,16 @@
import { useEffect } from "react";
import { useState } from "react";
import { detectLang } from "../libs";
import { tryDetectLang } from "../libs";
import { apiTranslate } from "../apis";
/**
* 翻译hook
* @param {*} q
* @param {*} rule
* @param {*} setting
* @returns
*/
export function useTranslate(q, rule) {
export function useTranslate(q, rule, setting) {
const [text, setText] = useState("");
const [loading, setLoading] = useState(false);
const [sameLang, setSamelang] = useState(false);
@@ -21,8 +22,8 @@ export function useTranslate(q, rule) {
try {
setLoading(true);
const deLang = await detectLang(q);
if (toLang.includes(deLang)) {
const deLang = await tryDetectLang(q);
if (deLang && toLang.includes(deLang)) {
setSamelang(true);
} else {
const [trText, isSame] = await apiTranslate({
@@ -30,6 +31,7 @@ export function useTranslate(q, rule) {
q,
fromLang,
toLang,
setting,
});
setText(trText);
setSamelang(isSame);
@@ -40,7 +42,7 @@ export function useTranslate(q, rule) {
setLoading(false);
}
})();
}, [q, translator, fromLang, toLang]);
}, [q, translator, fromLang, toLang, setting]);
return { text, sameLang, loading };
}

View File

@@ -1,19 +1,58 @@
import React from "react";
import React, { useState } from "react";
import ReactDOM from "react-dom/client";
import CircularProgress from "@mui/material/CircularProgress";
import Divider from "@mui/material/Divider";
import ReactMarkdown from "react-markdown";
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Button from "@mui/material/Button";
import Link from "@mui/material/Link";
import { useFetch } from "./hooks/Fetch";
import { I18N, URL_RAW_PREFIX } from "./config";
function App() {
const [lang, setLang] = useState("zh");
const [data, loading, error] = useFetch(
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.["zh"]}`
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}`
);
return (
<Paper sx={{ padding: 2, margin: 2 }}>
<Divider>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Divider>
<Stack spacing={2} direction="row" justifyContent="flex-end">
<Button
variant="text"
onClick={() => {
setLang((pre) => (pre === "zh" ? "en" : "zh"));
}}
>
{lang === "zh" ? "ENGLISH" : "中文"}
</Button>
</Stack>
<Divider>
<Link
href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider>
<Stack spacing={2} direction="row" useFlexGap flexWrap="wrap">
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
Install Userscript 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install Userscript 2
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install Userscript Safari 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install Userscript Safari 2
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
Open Options Page 1
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
Open Options Page 2
</Link>
</Stack>
{loading ? (
<center>
<CircularProgress />

View File

@@ -1,5 +1,5 @@
import storage from "./storage";
import { STOKEY_MSAUTH, URL_MICROSOFT_AUTH } from "../config";
import { getMsauth, setMsauth } from "./storage";
import { URL_MICROSOFT_AUTH } from "../config";
import { fetchData } from "./fetch";
const parseMSToken = (token) => {
@@ -26,9 +26,9 @@ const _msAuth = () => {
}
// 查询storage缓存
const res = (await storage.getObj(STOKEY_MSAUTH)) || {};
token = res.token;
exp = res.exp;
const res = await getMsauth();
token = res?.token;
exp = res?.exp;
if (token && exp * 1000 > now + 1000) {
return [token, exp];
}
@@ -36,7 +36,7 @@ const _msAuth = () => {
// 缓存没有或失效,查询接口
token = await fetchData(URL_MICROSOFT_AUTH);
exp = parseMSToken(token);
await storage.setObj(STOKEY_MSAUTH, { token, exp });
await setMsauth({ token, exp });
return [token, exp];
};
};

View File

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

6
src/libs/client.js Normal file
View File

@@ -0,0 +1,6 @@
import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
export const client = process.env.REACT_APP_CLIENT;
export const isExt = CLIENT_EXTS.includes(client);
export const isGm = client === CLIENT_USERSCRIPT;
export const isWeb = client === CLIENT_WEB;

View File

@@ -1,5 +1,5 @@
import { isExt, isGm } from "./browser";
import { sendMsg } from "./msg";
import { isExt, isGm } from "./client";
import { sendBgMsg } from "./msg";
import { taskPool } from "./pool";
import {
MSG_FETCH,
@@ -7,6 +7,7 @@ import {
MSG_FETCH_CLEAR,
CACHE_NAME,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI,
DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT,
@@ -19,7 +20,7 @@ import { msAuth } from "./auth";
* @param {*} init
* @returns
*/
const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method,
@@ -65,20 +66,32 @@ const newCacheReq = async (request) => {
* @param {*} param0
* @returns
*/
const fetchApi = async ({ input, init, translator, token }) => {
const fetchApi = async ({ input, init = {}, translator, token }) => {
if (translator === OPT_TRANS_MICROSOFT) {
init.headers["Authorization"] = `Bearer ${token}`;
init.headers["Authorization"] = `Bearer ${token}`; // Microsoft
} else if (translator === OPT_TRANS_DEEPL) {
init.headers["Authorization"] = `DeepL-Auth-Key ${token}`; // DeepL
} else if (translator === OPT_TRANS_OPENAI) {
init.headers["Authorization"] = `Bearer ${token}`; // // OpenAI
init.headers["Authorization"] = `Bearer ${token}`; // OpenAI
init.headers["api-key"] = token; // Azure OpenAI
}
if (isGm) {
const connects = GM?.info?.script?.connects || [];
let info;
if (window.KISS_GM) {
info = await window.KISS_GM.getInfo();
} else {
info = GM.info;
}
const connects = info?.script?.connects || [];
const url = new URL(input);
const isSafe = connects.find((item) => url.hostname.endsWith(item));
if (isSafe) {
return fetchGM(input, init);
if (window.KISS_GM) {
return window.KISS_GM.fetch(input, init);
} else {
return fetchGM(input, init);
}
}
}
return fetch(input, init);
@@ -103,25 +116,23 @@ export const fetchPool = taskPool(
/**
* 请求数据统一接口
* @param {*} input
* @param {*} init
* @param {*} opts
* @returns
*/
export const fetchData = async (
input,
init,
{ useCache, usePool, translator, token } = {}
{ useCache, usePool, translator, token, ...init } = {}
) => {
const cacheReq = await newCacheReq(new Request(input, init));
const cache = await caches.open(CACHE_NAME);
let res;
// 查询缓存
if (useCache) {
try {
const cache = await caches.open(CACHE_NAME);
res = await cache.match(cacheReq);
} catch (err) {
console.log("[cache match]", err);
console.log("[cache match]", err.message);
}
}
@@ -140,9 +151,10 @@ export const fetchData = async (
// 插入缓存
if (useCache) {
try {
const cache = await caches.open(CACHE_NAME);
await cache.put(cacheReq, res.clone());
} catch (err) {
console.log("[cache put]", err);
console.log("[cache put]", err.message);
}
}
}
@@ -157,22 +169,21 @@ export const fetchData = async (
/**
* fetch 兼容性封装
* @param {*} input
* @param {*} init
* @param {*} opts
* @returns
*/
export const fetchPolyfill = async (input, init, opts) => {
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
// 插件
if (isExt) {
const res = await sendMsg(MSG_FETCH, { input, init, opts });
if (isExt && !isBg) {
const res = await sendBgMsg(MSG_FETCH, { input, opts });
if (res.error) {
throw new Error(res.error);
}
return res.data;
}
// 油猴/网页
return await fetchData(input, init, opts);
// 油猴/网页/BackgroundPage
return await fetchData(input, opts);
};
/**
@@ -180,9 +191,9 @@ export const fetchPolyfill = async (input, init, opts) => {
* @param {*} interval
* @param {*} limit
*/
export const fetchUpdate = async (interval, limit) => {
export const updateFetchPool = async (interval, limit) => {
if (isExt) {
const res = await sendMsg(MSG_FETCH_LIMIT, { interval, limit });
const res = await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit });
if (res.error) {
throw new Error(res.error);
}
@@ -194,9 +205,9 @@ export const fetchUpdate = async (interval, limit) => {
/**
* 清空任务池
*/
export const fetchClear = async () => {
export const clearFetchPool = async () => {
if (isExt) {
const res = await sendMsg(MSG_FETCH_CLEAR);
const res = await sendBgMsg(MSG_FETCH_CLEAR);
if (res.error) {
throw new Error(res.error);
}

102
src/libs/gm.js Normal file
View File

@@ -0,0 +1,102 @@
import { fetchGM } from "./fetch";
import { genEventName } from "./utils";
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
const MSG_GM_setValue = "setValue";
const MSG_GM_getValue = "getValue";
const MSG_GM_deleteValue = "deleteValue";
const MSG_GM_info = "info";
/**
* 注入页面的脚本请求并接受GM接口信息
* @param {*} param0
*/
export const injectScript = (ping) => {
window.APP_INFO = {
name: process.env.REACT_APP_NAME,
version: process.env.REACT_APP_VERSION,
eventName: ping,
};
};
/**
* 适配GM脚本
*/
export const adaptScript = (ping) => {
const promiseGM = (action, args, timeout = 5000) =>
new Promise((resolve, reject) => {
const pong = genEventName();
const handleEvent = (e) => {
window.removeEventListener(pong, handleEvent);
const { data, error } = e.detail;
if (error) {
reject(new Error(error));
} else {
resolve(data);
}
};
window.addEventListener(pong, handleEvent);
window.dispatchEvent(
new CustomEvent(ping, { detail: { action, args, pong } })
);
setTimeout(() => {
window.removeEventListener(pong, handleEvent);
reject(new Error("timeout"));
}, timeout);
});
window.KISS_GM = {
fetch: (input, init) => promiseGM(MSG_GM_xmlHttpRequest, { input, init }),
setValue: (key, val) => promiseGM(MSG_GM_setValue, { key, val }),
getValue: (key) => promiseGM(MSG_GM_getValue, { key }),
deleteValue: (key) => promiseGM(MSG_GM_deleteValue, { key }),
getInfo: async () => {
if (!window.GM_info) {
window.GM_info = await promiseGM(MSG_GM_info);
}
return window.GM_info;
},
};
};
/**
* 监听并回应页面对GM接口的请求
* @param {*} param0
*/
export const handlePing = async (e) => {
const { action, args, pong } = e.detail;
let res;
try {
switch (action) {
case MSG_GM_xmlHttpRequest:
const { input, init } = args;
res = await fetchGM(input, init);
break;
case MSG_GM_setValue:
const { key, val } = args;
await GM.setValue(key, val);
res = val;
break;
case MSG_GM_getValue:
res = await GM.getValue(args.key);
break;
case MSG_GM_deleteValue:
await GM.deleteValue(args.key);
res = "ok";
break;
case MSG_GM_info:
res = GM.info;
break;
default:
throw new Error(`message action is unavailable: ${action}`);
}
window.dispatchEvent(new CustomEvent(pong, { detail: { data: res } }));
} catch (err) {
window.dispatchEvent(
new CustomEvent(pong, { detail: { error: err.message } })
);
}
};

7
src/libs/iframe.js Normal file
View File

@@ -0,0 +1,7 @@
export const isIframe = window.self !== window.top;
export const sendIframeMsg = (action, args) => {
document.querySelectorAll("iframe").forEach((iframe) => {
iframe.contentWindow.postMessage({ action, args }, "*");
});
};

View File

@@ -1,104 +1,15 @@
import storage from "./storage";
import {
DEFAULT_SETTING,
STOKEY_SETTING,
STOKEY_RULES,
STOKEY_FAB,
GLOBLA_RULE,
GLOBAL_KEY,
DEFAULT_SUBRULES_LIST,
} from "../config";
import { CACHE_NAME } from "../config";
import { browser } from "./browser";
import { isMatch } from "./utils";
import { tryLoadRules } from "./rules";
/**
* 获取节点列表并转为数组
* @param {*} selector
* @param {*} el
* @returns
* 清除缓存数据
*/
export const queryEls = (selector, el = document) =>
Array.from(el.querySelectorAll(selector));
/**
* 查询storage中的设置
* @returns
*/
export const getSetting = async () => ({
...DEFAULT_SETTING,
...((await storage.getObj(STOKEY_SETTING)) || {}),
});
/**
* 查询规则列表
* @returns
*/
export const getRules = async () => (await storage.getObj(STOKEY_RULES)) || [];
/**
* 查询fab位置信息
* @returns
*/
export const getFab = async () => (await storage.getObj(STOKEY_FAB)) || {};
/**
* 设置fab位置信息
* @returns
*/
export const setFab = async (obj) => await storage.setObj(STOKEY_FAB, obj);
/**
* 根据href匹配规则
* @param {*} rules
* @param {string} href
* @returns
*/
export const matchRule = async (
rules,
href,
{ injectRules, subrulesList = DEFAULT_SUBRULES_LIST }
) => {
if (injectRules) {
try {
const selectedSub = subrulesList.find((item) => item.selected);
if (selectedSub?.url) {
const subRules = await tryLoadRules(selectedSub.url);
rules.splice(-1, 0, ...subRules);
}
} catch (err) {
console.log("[load injectRules]", err);
}
export const tryClearCaches = async () => {
try {
caches.delete(CACHE_NAME);
} catch (err) {
console.log("[clean caches]", err.message);
}
const rule = rules.find((rule) =>
rule.pattern.split(",").some((p) => isMatch(href, p.trim()))
);
const globalRule =
rules.find((rule) =>
rule.pattern.split(",").some((p) => p.trim() === "*")
) || GLOBLA_RULE;
if (!rule) {
return globalRule;
}
rule.selector =
rule?.selector?.trim() ||
globalRule?.selector?.trim() ||
GLOBLA_RULE.selector;
rule.bgColor = rule?.bgColor?.trim() || globalRule?.bgColor?.trim();
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach(
(key) => {
if (rule[key] === GLOBAL_KEY) {
rule[key] = globalRule[key];
}
}
);
return rule;
};
/**
@@ -106,7 +17,11 @@ export const matchRule = async (
* @param {*} q
* @returns
*/
export const detectLang = async (q) => {
const res = await browser?.i18n.detectLanguage(q);
return res?.languages?.[0]?.language;
export const tryDetectLang = async (q) => {
try {
const res = await browser?.i18n?.detectLanguage(q);
return res?.languages?.[0]?.language;
} catch (err) {
console.log("[detect lang]", err.message);
}
};

View File

@@ -6,8 +6,8 @@ import { browser } from "./browser";
* @param {*} args
* @returns
*/
export const sendMsg = (action, args) =>
browser?.runtime?.sendMessage({ action, args });
export const sendBgMsg = (action, args) =>
browser.runtime.sendMessage({ action, args });
/**
* 发送消息给当前页面
@@ -16,6 +16,6 @@ export const sendMsg = (action, args) =>
* @returns
*/
export const sendTabMsg = async (action, args) => {
const tabs = await browser?.tabs.query({ active: true, currentWindow: true });
return await browser?.tabs.sendMessage(tabs[0].id, { action, args });
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
return browser.tabs.sendMessage(tabs[0].id, { action, args });
};

View File

@@ -1,17 +1,94 @@
import storage from "./storage";
import { fetchPolyfill } from "./fetch";
import { matchValue, type } from "./utils";
import { matchValue, type, isMatch } from "./utils";
import {
STOKEY_RULESCACHE_PREFIX,
GLOBAL_KEY,
REMAIN_KEY,
OPT_TRANS_ALL,
OPT_STYLE_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
GLOBLA_RULE,
DEFAULT_SUBRULES_LIST,
DEFAULT_OW_RULE,
} from "../config";
import { loadOrFetchSubRules } from "./subRules";
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
/**
* 根据href匹配规则
* @param {*} rules
* @param {string} href
* @returns
*/
export const matchRule = async (
rules,
href,
{
injectRules = true,
subrulesList = DEFAULT_SUBRULES_LIST,
owSubrule = DEFAULT_OW_RULE,
}
) => {
rules = [...rules];
if (injectRules) {
try {
const selectedSub = subrulesList.find((item) => item.selected);
if (selectedSub?.url) {
const mixRule = {};
Object.entries(owSubrule)
.filter(([key, val]) => {
if (
owSubrule.textStyle === REMAIN_KEY &&
(key === "bgColor" || key === "textDiyStyle")
) {
return false;
}
return val !== REMAIN_KEY;
})
.forEach(([key, val]) => {
mixRule[key] = val;
});
const subRules = (await loadOrFetchSubRules(selectedSub.url)).map(
(item) => ({ ...item, ...mixRule })
);
rules.splice(-1, 0, ...subRules);
}
} catch (err) {
console.log("[load injectRules]", err);
}
}
const rule = rules.find((r) =>
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
);
const globalRule =
rules.find((r) =>
r.pattern.split(",").some((p) => p.trim() === GLOBAL_KEY)
) || GLOBLA_RULE;
if (!rule) {
return globalRule;
}
rule.selector =
rule?.selector?.trim() ||
globalRule?.selector?.trim() ||
GLOBLA_RULE.selector;
rule.bgColor = rule?.bgColor?.trim() || globalRule?.bgColor?.trim();
rule.textDiyStyle =
rule?.textDiyStyle?.trim() || globalRule?.textDiyStyle?.trim();
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach(
(key) => {
if (rule[key] === GLOBAL_KEY) {
rule[key] = globalRule[key];
}
}
);
return rule;
};
/**
* 检查过滤rules
@@ -26,6 +103,8 @@ export const checkRules = (rules) => {
throw new Error("data error");
}
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
const patternSet = new Set();
rules = rules
.filter((rule) => type(rule) === "object")
@@ -46,10 +125,12 @@ export const checkRules = (rules) => {
textStyle,
transOpen,
bgColor,
textDiyStyle,
}) => ({
pattern: pattern.trim(),
selector: type(selector) === "string" ? selector : "",
bgColor: type(bgColor) === "string" ? bgColor : "",
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
@@ -60,40 +141,3 @@ export const checkRules = (rules) => {
return rules;
};
/**
* 本地rules缓存
*/
export const rulesCache = {
fetch: async (url) => {
const res = await fetchPolyfill(url);
const rules = checkRules(res).filter(
(rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== ""
);
return rules;
},
set: async (url, rules) => {
await storage.setObj(`${STOKEY_RULESCACHE_PREFIX}${url}`, rules);
},
get: async (url) => {
return await storage.getObj(`${STOKEY_RULESCACHE_PREFIX}${url}`);
},
del: async (url) => {
await storage.del(`${STOKEY_RULESCACHE_PREFIX}${url}`);
},
};
/**
* 从缓存或远程加载订阅的rules
* @param {*} url
* @returns
*/
export const tryLoadRules = async (url) => {
let rules = await rulesCache.get(url);
if (rules?.length) {
return rules;
}
rules = await rulesCache.fetch(url);
await rulesCache.set(url, rules);
return rules;
};

View File

@@ -1,28 +1,25 @@
import { browser, isExt, isGm } from "./browser";
import {
STOKEY_SETTING,
STOKEY_RULES,
STOKEY_FAB,
STOKEY_SYNC,
STOKEY_MSAUTH,
STOKEY_RULESCACHE_PREFIX,
DEFAULT_SETTING,
DEFAULT_RULES,
DEFAULT_SYNC,
BUILTIN_RULES,
} from "../config";
import { isExt, isGm } from "./client";
import { browser } from "./browser";
async function set(key, val) {
if (isExt) {
await browser.storage.local.set({ [key]: val });
} else if (isGm) {
const oldValue = await GM.getValue(key);
await GM.setValue(key, val);
window.dispatchEvent(
new StorageEvent("storage", {
key,
oldValue,
newValue: val,
})
);
await (window.KISS_GM || GM).setValue(key, val);
} else {
const oldValue = window.localStorage.getItem(key);
window.localStorage.setItem(key, val);
window.dispatchEvent(
new StorageEvent("storage", {
key,
oldValue,
newValue: val,
})
);
}
}
@@ -31,7 +28,7 @@ async function get(key) {
const val = await browser.storage.local.get([key]);
return val[key];
} else if (isGm) {
const val = await GM.getValue(key);
const val = await (window.KISS_GM || GM).getValue(key);
return val;
}
return window.localStorage.getItem(key);
@@ -41,25 +38,9 @@ async function del(key) {
if (isExt) {
await browser.storage.local.remove([key]);
} else if (isGm) {
const oldValue = await GM.getValue(key);
await GM.deleteValue(key);
window.dispatchEvent(
new StorageEvent("storage", {
key,
oldValue,
newValue: null,
})
);
await (window.KISS_GM || GM).deleteValue(key);
} else {
const oldValue = window.localStorage.getItem(key);
window.localStorage.removeItem(key);
window.dispatchEvent(
new StorageEvent("storage", {
key,
oldValue,
newValue: null,
})
);
}
}
@@ -83,22 +64,10 @@ async function putObj(key, obj) {
await setObj(key, { ...cur, ...obj });
}
/**
* 监听storage事件
* @param {*} handleChanged
*/
function onChanged(handleChanged) {
if (isExt) {
browser.storage.onChanged.addListener(handleChanged);
} else {
window.addEventListener("storage", handleChanged);
}
}
/**
* 对storage的封装
*/
const storage = {
export const storage = {
get,
set,
del,
@@ -106,7 +75,70 @@ const storage = {
trySetObj,
getObj,
putObj,
onChanged,
// onChanged,
};
export default storage;
/**
* 设置信息
*/
export const getSetting = () => getObj(STOKEY_SETTING);
export const getSettingWithDefault = async () => ({
...DEFAULT_SETTING,
...((await getSetting()) || {}),
});
export const setSetting = (val) => setObj(STOKEY_SETTING, val);
export const updateSetting = (obj) => putObj(STOKEY_SETTING, obj);
/**
* 规则列表
*/
export const getRules = () => getObj(STOKEY_RULES);
export const getRulesWithDefault = async () =>
(await getRules()) || DEFAULT_RULES;
export const setRules = (val) => setObj(STOKEY_RULES, val);
/**
* 订阅规则
*/
export const getSubRules = (url) => getObj(STOKEY_RULESCACHE_PREFIX + url);
export const getSubRulesWithDefault = async () => (await getSubRules()) || [];
export const delSubRules = (url) => del(STOKEY_RULESCACHE_PREFIX + url);
export const setSubRules = (url, val) =>
setObj(STOKEY_RULESCACHE_PREFIX + url, val);
/**
* fab位置
*/
export const getFab = () => getObj(STOKEY_FAB);
export const getFabWithDefault = async () => (await getFab()) || {};
export const setFab = (obj) => setObj(STOKEY_FAB, obj);
/**
* 数据同步
*/
export const getSync = () => getObj(STOKEY_SYNC);
export const getSyncWithDefault = async () => (await getSync()) || DEFAULT_SYNC;
export const updateSync = (obj) => putObj(STOKEY_SYNC, obj);
/**
* ms auth
*/
export const getMsauth = () => getObj(STOKEY_MSAUTH);
export const setMsauth = (val) => setObj(STOKEY_MSAUTH, val);
/**
* 存入默认数据
*/
export const tryInitDefaultData = async () => {
try {
await trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
await trySetObj(STOKEY_RULES, DEFAULT_RULES);
await trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
await trySetObj(
`${STOKEY_RULESCACHE_PREFIX}${process.env.REACT_APP_RULESURL}`,
BUILTIN_RULES
);
} catch (err) {
console.log("[init default]", err);
}
};

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

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

View File

@@ -1,81 +1,109 @@
import {
STOKEY_SYNC,
DEFAULT_SYNC,
KV_SETTING_KEY,
KV_RULES_KEY,
KV_RULES_SHARE_KEY,
STOKEY_SETTING,
STOKEY_RULES,
KV_SALT_SHARE,
} from "../config";
import storage from "../libs/storage";
import { getSetting, getRules } from ".";
import {
getSyncWithDefault,
updateSync,
getSettingWithDefault,
getRulesWithDefault,
setSetting,
setRules,
} from "./storage";
import { apiSyncData } from "../apis";
import { sha256 } from "./utils";
export const loadSyncOpt = async () =>
(await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC;
/**
* 同步设置
* @returns
*/
const syncSetting = async (isBg = false) => {
const { syncUrl, syncKey, settingUpdateAt } = await getSyncWithDefault();
if (!syncUrl || !syncKey) {
return;
}
export const syncSetting = async () => {
try {
const { syncUrl, syncKey, settingUpdateAt } = await loadSyncOpt();
if (!syncUrl || !syncKey) {
return;
}
const setting = await getSetting();
const res = await apiSyncData(syncUrl, syncKey, {
const setting = await getSettingWithDefault();
const res = await apiSyncData(
syncUrl,
syncKey,
{
key: KV_SETTING_KEY,
value: setting,
updateAt: settingUpdateAt,
});
},
isBg
);
if (res && res.updateAt > settingUpdateAt) {
await storage.putObj(STOKEY_SYNC, {
settingUpdateAt: res.updateAt,
settingSyncAt: res.updateAt,
});
await storage.setObj(STOKEY_SETTING, res.value);
} else {
await storage.putObj(STOKEY_SYNC, {
settingSyncAt: res.updateAt,
});
}
if (res && res.updateAt > settingUpdateAt) {
await updateSync({
settingUpdateAt: res.updateAt,
settingSyncAt: res.updateAt,
});
await setSetting(res.value);
return res.value;
} else {
await updateSync({ settingSyncAt: res.updateAt });
}
};
export const trySyncSetting = async (isBg = false) => {
try {
return await syncSetting(isBg);
} catch (err) {
console.log("[sync setting]", err);
}
};
export const syncRules = async () => {
try {
const { syncUrl, syncKey, rulesUpdateAt } = await loadSyncOpt();
if (!syncUrl || !syncKey) {
return;
}
/**
* 同步规则
* @returns
*/
const syncRules = async (isBg = false) => {
const { syncUrl, syncKey, rulesUpdateAt } = await getSyncWithDefault();
if (!syncUrl || !syncKey) {
return;
}
const rules = await getRules();
const res = await apiSyncData(syncUrl, syncKey, {
const rules = await getRulesWithDefault();
const res = await apiSyncData(
syncUrl,
syncKey,
{
key: KV_RULES_KEY,
value: rules,
updateAt: rulesUpdateAt,
});
},
isBg
);
if (res && res.updateAt > rulesUpdateAt) {
await storage.putObj(STOKEY_SYNC, {
rulesUpdateAt: res.updateAt,
rulesSyncAt: res.updateAt,
});
await storage.setObj(STOKEY_RULES, res.value);
} else {
await storage.putObj(STOKEY_SYNC, {
rulesSyncAt: res.updateAt,
});
}
if (res && res.updateAt > rulesUpdateAt) {
await updateSync({
rulesUpdateAt: res.updateAt,
rulesSyncAt: res.updateAt,
});
await setRules(res.value);
return res.value;
} else {
await updateSync({ rulesSyncAt: res.updateAt });
}
};
export const trySyncRules = async (isBg = false) => {
try {
return await syncRules(isBg);
} catch (err) {
console.log("[sync user rules]", err);
}
};
/**
* 同步分享规则
* @param {*} param0
* @returns
*/
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
await apiSyncData(syncUrl, syncKey, {
key: KV_RULES_SHARE_KEY,
@@ -87,7 +115,14 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
return shareUrl;
};
export const syncAll = async () => {
await syncSetting();
await syncRules();
/**
* 同步个人设置和规则
* @returns
*/
export const syncSettingAndRules = async (isBg = false) => {
return [await syncSetting(isBg), await syncRules(isBg)];
};
export const trySyncSettingAndRules = async (isBg = false) => {
return [await trySyncSetting(isBg), await trySyncRules(isBg)];
};

View File

@@ -3,21 +3,42 @@ import {
APP_LCNAME,
TRANS_MIN_LENGTH,
TRANS_MAX_LENGTH,
EVENT_KISS,
MSG_TRANS_CURRULE,
OPT_STYLE_DASHLINE,
OPT_STYLE_FUZZY,
SHADOW_KEY,
} from "../config";
import { queryEls } from ".";
import Content from "../views/Content";
import { fetchUpdate, fetchClear } from "./fetch";
import { updateFetchPool, clearFetchPool } from "./fetch";
import { debounce, genEventName } from "./utils";
/**
* 翻译类
*/
export class Translator {
_rule = {};
_setting = {};
_rootNodes = new Set();
_tranNodes = new Map();
_skipNodeNames = [
APP_LCNAME,
"style",
"svg",
"img",
"audio",
"video",
"textarea",
"input",
"button",
"select",
"option",
"head",
"script",
"iframe",
];
_eventName = genEventName();
// 显示
_interseObserver = new IntersectionObserver(
(intersections) => {
intersections.forEach((intersection) => {
@@ -32,28 +53,62 @@ export class Translator {
}
);
// 变化
_mutaObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
try {
queryEls(this.rule.selector, node).forEach((el) => {
this._interseObserver.observe(el);
});
} catch (err) {
//
if (
!this._skipNodeNames.includes(mutation.target.localName) &&
mutation.addedNodes.length > 0
) {
const nodes = Array.from(mutation.addedNodes).filter((node) => {
if (
this._skipNodeNames.includes(node.localName) ||
node.id === APP_LCNAME
) {
return false;
}
return true;
});
if (nodes.length > 0) {
// const rootNode = mutation.target.getRootNode();
// todo
this._reTranslate();
}
});
}
});
});
constructor(rule, { fetchInterval, fetchLimit }) {
fetchUpdate(fetchInterval, fetchLimit);
this.rule = rule;
// 插入 shadowroot
_overrideAttachShadow = () => {
const _this = this;
const _attachShadow = HTMLElement.prototype.attachShadow;
HTMLElement.prototype.attachShadow = function () {
_this._reTranslate();
return _attachShadow.apply(this, arguments);
};
};
constructor(rule, setting) {
const { fetchInterval, fetchLimit } = setting;
updateFetchPool(fetchInterval, fetchLimit);
this._overrideAttachShadow();
this._setting = setting;
this._rule = rule;
if (rule.transOpen === "true") {
this._register();
}
}
get setting() {
return this._setting;
}
get eventName() {
return this._eventName;
}
get rule() {
// console.log("get rule", this._rule);
return this._rule;
@@ -64,8 +119,9 @@ export class Translator {
this._rule = rule;
// 广播消息
const eventName = this._eventName;
window.dispatchEvent(
new CustomEvent(EVENT_KISS, {
new CustomEvent(eventName, {
detail: {
action: MSG_TRANS_CURRULE,
args: rule,
@@ -96,16 +152,80 @@ export class Translator {
this.rule = { ...this.rule, textStyle };
};
_querySelectorAll = (selector, node) => {
try {
return Array.from(node.querySelectorAll(selector));
} catch (err) {
console.log(`[querySelectorAll err]: ${selector}`);
}
return [];
};
_queryFilter = (selector, rootNode) => {
return this._querySelectorAll(selector, rootNode).filter(
(node) => this._queryFilter(selector, node).length === 0
);
};
_queryNodes = (rootNode = document) => {
// const childRoots = Array.from(rootNode.querySelectorAll("*"))
// .map((item) => item.shadowRoot)
// .filter(Boolean);
// const childNodes = childRoots.map((item) => this._queryNodes(item));
// const nodes = Array.from(rootNode.querySelectorAll(this.rule.selector));
// return nodes.concat(childNodes).flat();
this._rootNodes.add(rootNode);
this._rule.selector
.split(";")
.map((item) => item.trim())
.filter(Boolean)
.forEach((selector) => {
if (selector.includes(SHADOW_KEY)) {
const [outSelector, inSelector] = selector
.split(SHADOW_KEY)
.map((item) => item.trim());
if (outSelector && inSelector) {
const outNodes = this._querySelectorAll(outSelector, rootNode);
outNodes.forEach((outNode) => {
if (outNode.shadowRoot) {
this._rootNodes.add(outNode.shadowRoot);
this._queryFilter(inSelector, outNode.shadowRoot).forEach(
(item) => {
if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, "");
}
}
);
}
});
}
} else {
this._queryFilter(selector, rootNode).forEach((item) => {
if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, "");
}
});
}
});
};
_register = () => {
// 监听节点变化
this._mutaObserver.observe(document, {
childList: true,
subtree: true,
// 搜索节点
this._queryNodes();
this._rootNodes.forEach((node) => {
// 监听节点变化;
this._mutaObserver.observe(node, {
childList: true,
subtree: true,
// characterData: true,
});
});
// 监听节点显示
queryEls(this.rule.selector).forEach((el) => {
this._interseObserver.observe(el);
this._tranNodes.forEach((_, node) => {
// 监听节点显示
this._interseObserver.observe(node);
});
};
@@ -114,45 +234,71 @@ export class Translator {
this._mutaObserver.disconnect();
// 解除节点显示监听
queryEls(this.rule.selector).forEach((el) =>
this._interseObserver.unobserve(el)
);
this._interseObserver.disconnect();
// 移除已插入元素
queryEls(APP_LCNAME).forEach((el) => el.remove());
this._tranNodes.forEach((_, node) => {
node.querySelector(APP_LCNAME)?.remove();
});
// 清空节点集合
this._rootNodes.clear();
this._tranNodes.clear();
// 清空任务池
fetchClear();
clearFetchPool();
};
_render = (el) => {
// 含子元素
if (el.querySelector(this.rule.selector)) {
return;
_reTranslate = debounce(() => {
if (this._rule.transOpen === "true") {
this._register();
}
}, 500);
_render = (el) => {
let traEl = el.querySelector(APP_LCNAME);
// 已翻译
if (el.querySelector(APP_LCNAME)) {
return;
if (traEl) {
const preText = this._tranNodes.get(el);
const curText = el.innerText.trim();
// const traText = traEl.innerText.trim();
// todo
// 1. traText when loading
// 2. replace startsWith
if (curText.startsWith(preText)) {
return;
}
traEl.remove();
}
// 太长或太短
const q = el.innerText.trim();
if (!q || q.length < TRANS_MIN_LENGTH || q.length > TRANS_MAX_LENGTH) {
this._tranNodes.set(el, q);
// 太长或太短
if (
!q ||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH)
) {
return;
}
// console.log("---> ", q);
const span = document.createElement(APP_LCNAME);
span.style.visibility = "visible";
el.appendChild(span);
traEl = document.createElement(APP_LCNAME);
traEl.style.visibility = "visible";
el.appendChild(traEl);
el.style.cssText +=
"-webkit-line-clamp: unset; max-height: none; height: auto;";
el.parentElement.style.cssText +=
"-webkit-line-clamp: unset; max-height: none; height: auto;";
if (el.parentElement) {
el.parentElement.style.cssText +=
"-webkit-line-clamp: unset; max-height: none; height: auto;";
}
const root = createRoot(span);
const root = createRoot(traEl);
root.render(<Content q={q} translator={this} />);
};
}

View File

@@ -34,7 +34,12 @@ export const matchValue = (arr, val) => {
* @returns
*/
export const sleep = (delay) =>
new Promise((resolve) => setTimeout(resolve, delay));
new Promise((resolve) => {
const timer = setTimeout(() => {
clearTimeout(timer);
resolve();
}, delay);
});
/**
* 防抖函数
@@ -111,3 +116,9 @@ export const sha256 = async (text, salt) => {
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
/**
* 生成随机事件名称
* @returns
*/
export const genEventName = () => btoa(Math.random()).slice(3, 11);

View File

@@ -1,16 +1,16 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { StoragesProvider } from "./hooks/Storage";
import { SettingProvider } from "./hooks/Setting";
import ThemeProvider from "./hooks/Theme";
import Popup from "./views/Popup";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<StoragesProvider>
<SettingProvider>
<ThemeProvider>
<Popup />
</ThemeProvider>
</StoragesProvider>
</SettingProvider>
</React.StrictMode>
);

View File

@@ -3,6 +3,7 @@ import path from "path";
import { BUILTIN_RULES } from "./config/rules";
(() => {
// rules
try {
const data = JSON.stringify(BUILTIN_RULES, null, " ");
const file = path.resolve(
@@ -14,4 +15,14 @@ import { BUILTIN_RULES } from "./config/rules";
} catch (err) {
console.error(err);
}
// version
try {
var pjson = require("../package.json");
const file = path.resolve(__dirname, "../build/web/version.txt");
fs.writeFileSync(file, pjson.version);
console.info(`Version file generated: ${file}`);
} catch (err) {
console.error(err);
}
})();

View File

@@ -3,41 +3,77 @@ import ReactDOM from "react-dom/client";
import Action from "./views/Action";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { getSetting, getRules, matchRule, getFab } from "./libs";
import {
getSettingWithDefault,
getRulesWithDefault,
getFabWithDefault,
} from "./libs/storage";
import { Translator } from "./libs/translator";
import { trySyncAllSubRules } from "./libs/subRules";
import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config";
import { isIframe } from "./libs/iframe";
import { handlePing, injectScript } from "./libs/gm";
import { matchRule } from "./libs/rules";
import { genEventName } from "./libs/utils";
/**
* 入口函数
*/
(async () => {
const init = async () => {
// 设置页面
if (
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE) ||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE2)
) {
unsafeWindow.GM = GM;
unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
return;
}
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
unsafeWindow.GM = GM;
unsafeWindow.APP_INFO = {
name: process.env.REACT_APP_NAME,
version: process.env.REACT_APP_VERSION,
};
} else {
const ping = genEventName();
window.addEventListener(ping, handlePing);
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
const script = document.createElement("script");
script.textContent = `(${injectScript})("${ping}")`;
document.head.append(script);
}
// skip iframe
if (window.self !== window.top) {
return;
}
// 翻译页面
const setting = await getSetting();
const rules = await getRules();
const rule = await matchRule(rules, document.location.href, setting);
const href = isIframe ? document.referrer : document.location.href;
const setting = await getSettingWithDefault();
const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);
if (isIframe) {
// iframe
window.addEventListener("message", (e) => {
const action = e?.data?.action;
switch (action) {
case MSG_TRANS_TOGGLE:
translator.toggle();
break;
case MSG_TRANS_PUTRULE:
translator.updateRule(e.data.args || {});
break;
default:
}
});
return;
}
// 浮球按钮
const fab = await getFab();
const fab = await getFabWithDefault();
const $action = document.createElement("div");
$action.setAttribute("id", "kiss-translator");
document.body.parentElement.appendChild($action);
const shadowContainer = $action.attachShadow({ mode: "open" });
const shadowContainer = $action.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style");
const shadowRootElement = document.createElement("div");
shadowContainer.appendChild(emotionRoot);
@@ -55,19 +91,17 @@ import { Translator } from "./libs/translator";
</React.StrictMode>
);
// 注册菜单
GM.registerMenuCommand(
"Toggle Translate",
(event) => {
translator.toggle();
},
"Q"
);
GM.registerMenuCommand(
"Toggle Style",
(event) => {
translator.toggleStyle();
},
"C"
);
// 同步订阅规则
trySyncAllSubRules(setting);
};
(async () => {
try {
await init();
} catch (err) {
const $err = document.createElement("div");
$err.innerText = `KISS-Translator: ${err.message}`;
$err.style.cssText = "background:red; color:#fff; z-index:10000;";
document.body.prepend($err);
}
})();

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { limitNumber } from "../../libs/utils";
import { isMobile } from "../../libs/mobile";
import { setFab } from "../../libs";
import { setFab } from "../../libs/storage";
const getEdgePosition = (
{ x: left, y: top, edge },
@@ -159,11 +159,11 @@ export default function Draggable({
y: position.y,
});
}
}, [position]);
}, [position.x, position.y, position.hide]);
const opacity = useMemo(() => {
if (snapEdge) {
return position.hide ? 0.1 : 1;
return position.hide ? 0.2 : 1;
}
return origin ? 0.8 : 1;
}, [origin, snapEdge, position.hide]);

View File

@@ -8,16 +8,18 @@ import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import Stack from "@mui/material/Stack";
import { useEffect, useState, useMemo, useCallback } from "react";
import { StoragesProvider } from "../../hooks/Storage";
import { SettingProvider } from "../../hooks/Setting";
import Popup from "../Popup";
import { debounce } from "../../libs/utils";
import * as shortcut from "@violentmonkey/shortcut";
import { isGm } from "../../libs/client";
export default function Action({ translator, fab }) {
const fabWidth = 40;
const [showPopup, setShowPopup] = useState(false);
const [windowSize, setWindowSize] = useState({
w: document.documentElement.clientWidth,
h: document.documentElement.clientHeight,
w: window.innerWidth,
h: window.innerHeight,
});
const [moved, setMoved] = useState(false);
@@ -25,8 +27,8 @@ export default function Action({ translator, fab }) {
() =>
debounce(() => {
setWindowSize({
w: document.documentElement.clientWidth,
h: document.documentElement.clientHeight,
w: window.innerWidth,
h: window.innerHeight,
});
}),
[]
@@ -44,6 +46,73 @@ export default function Action({ translator, fab }) {
setMoved(true);
}, []);
useEffect(() => {
// 注册快捷键
shortcut.register("a-q", () => {
translator.toggle();
setShowPopup(false);
});
shortcut.register("a-c", () => {
translator.toggleStyle();
setShowPopup(false);
});
shortcut.register("a-k", () => {
setShowPopup((pre) => !pre);
});
return () => {
shortcut.disable();
};
}, [translator]);
useEffect(() => {
// 注册菜单
const menuCommandIds = [];
if (isGm) {
try {
menuCommandIds.push(
GM.registerMenuCommand(
"Toggle Translate",
(event) => {
translator.toggle();
setShowPopup(false);
},
"Q"
),
GM.registerMenuCommand(
"Toggle Style",
(event) => {
translator.toggleStyle();
setShowPopup(false);
},
"C"
),
GM.registerMenuCommand(
"Open Menu",
(event) => {
setShowPopup((pre) => !pre);
},
"K"
)
);
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}
return () => {
if (isGm) {
try {
menuCommandIds.forEach((id) => {
GM.unregisterMenuCommand(id);
});
} catch (err) {
//
}
}
};
}, [translator]);
useEffect(() => {
window.addEventListener("resize", handleWindowResize);
return () => {
@@ -53,6 +122,7 @@ export default function Action({ translator, fab }) {
useEffect(() => {
window.addEventListener("click", handleWindowClick);
return () => {
window.removeEventListener("click", handleWindowClick);
};
@@ -81,7 +151,7 @@ export default function Action({ translator, fab }) {
};
return (
<StoragesProvider>
<SettingProvider>
<ThemeProvider>
<Draggable
key="pop"
@@ -139,6 +209,6 @@ export default function Action({ translator, fab }) {
}
/>
</ThemeProvider>
</StoragesProvider>
</SettingProvider>
);
}

View File

@@ -1,4 +1,4 @@
import { useMemo, useState, useEffect } from "react";
import { useState, useEffect } from "react";
import LoadingIcon from "./LoadingIcon";
import {
OPT_STYLE_LINE,
@@ -6,26 +6,99 @@ import {
OPT_STYLE_DASHLINE,
OPT_STYLE_WAVYLINE,
OPT_STYLE_FUZZY,
OPT_STYLE_HIGHTLIGHT,
OPT_STYLE_HIGHLIGHT,
OPT_STYLE_DIY,
DEFAULT_COLOR,
EVENT_KISS,
MSG_TRANS_CURRULE,
TRANS_NEWLINE_LENGTH,
} from "../../config";
import { useTranslate } from "../../hooks/Translate";
import styled from "styled-components";
const LineSpan = styled.span`
opacity: 0.6;
text-decoration-line: underline;
text-decoration-style: ${(props) => props.$lineStyle};
text-decoration-color: ${(props) => props.$lineColor};
text-decoration-thickness: 2px;
text-underline-offset: 0.3em;
-webkit-text-decoration-line: underline;
-webkit-text-decoration-style: ${(props) => props.$lineStyle};
-webkit-text-decoration-color: ${(props) => props.$lineColor};
-webkit-text-decoration-thickness: 2px;
-webkit-text-underline-offset: 0.3em;
&:hover {
opacity: 1;
}
`;
const FuzzySpan = styled.span`
filter: blur(5px);
transition: filter 0.2s ease-in-out;
&hover: {
filter: none;
}
`;
const HighlightSpan = styled.span`
color: #fff;
background-color: ${(props) => props.$bgColor};
&hover: {
filter: none;
}
`;
const DiySpan = styled.span`
${(props) => props.$diyStyle}
`;
function StyledSpan({ textStyle, textDiyStyle, bgColor, children }) {
switch (textStyle) {
case OPT_STYLE_LINE: // 下划线
return (
<LineSpan $lineStyle="solid" $lineColor={bgColor}>
{children}
</LineSpan>
);
case OPT_STYLE_DOTLINE: // 点状线
return (
<LineSpan $lineStyle="dotted" $lineColor={bgColor}>
{children}
</LineSpan>
);
case OPT_STYLE_DASHLINE: // 虚线
return (
<LineSpan $lineStyle="dashed" $lineColor={bgColor}>
{children}
</LineSpan>
);
case OPT_STYLE_WAVYLINE: // 波浪线
return (
<LineSpan $lineStyle="wavy" $lineColor={bgColor}>
{children}
</LineSpan>
);
case OPT_STYLE_FUZZY: // 模糊
return <FuzzySpan>{children}</FuzzySpan>;
case OPT_STYLE_HIGHLIGHT: // 高亮
return (
<HighlightSpan $bgColor={bgColor || DEFAULT_COLOR}>
{children}
</HighlightSpan>
);
case OPT_STYLE_DIY: // 自定义
return <DiySpan $diyStyle={textDiyStyle}>{children}</DiySpan>;
default:
return <span>{children}</span>;
}
}
export default function Content({ q, translator }) {
const [rule, setRule] = useState(translator.rule);
const [hover, setHover] = useState(false);
const { text, sameLang, loading } = useTranslate(q, rule);
const { textStyle, bgColor } = rule;
const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
const { textStyle, bgColor = "", textDiyStyle = "" } = rule;
const handleMouseEnter = () => {
setHover(true);
};
const handleMouseLeave = () => {
setHover(false);
};
const { newlineLength = TRANS_NEWLINE_LENGTH } = translator.setting;
const handleKissEvent = (e) => {
const { action, args } = e.detail;
@@ -34,63 +107,20 @@ export default function Content({ q, translator }) {
setRule(args);
break;
default:
// console.log(`[popup] kissEvent action skip: ${action}`);
}
};
useEffect(() => {
window.addEventListener(EVENT_KISS, handleKissEvent);
window.addEventListener(translator.eventName, handleKissEvent);
return () => {
window.removeEventListener(EVENT_KISS, handleKissEvent);
window.removeEventListener(translator.eventName, handleKissEvent);
};
}, []);
const style = useMemo(() => {
const lineColor = bgColor || "";
switch (textStyle) {
case OPT_STYLE_LINE: // 下划线
return {
opacity: hover ? 1 : 0.6,
textDecoration: `underline 2px ${lineColor}`,
textUnderlineOffset: "0.3em",
};
case OPT_STYLE_DOTLINE: // 点状线
return {
opacity: hover ? 1 : 0.6,
textDecoration: `dotted underline 2px ${lineColor}`,
textUnderlineOffset: "0.3em",
};
case OPT_STYLE_DASHLINE: // 虚线
return {
opacity: hover ? 1 : 0.6,
textDecoration: `dashed underline 2px ${lineColor}`,
textUnderlineOffset: "0.3em",
};
case OPT_STYLE_WAVYLINE: // 波浪线
return {
opacity: hover ? 1 : 0.6,
textDecoration: `wavy underline 2px ${lineColor}`,
textUnderlineOffset: "0.3em",
};
case OPT_STYLE_FUZZY: // 模糊
return {
filter: hover ? "none" : "blur(5px)",
transition: "filter 0.2s ease-in-out",
};
case OPT_STYLE_HIGHTLIGHT: // 高亮
return {
color: "#FFF",
backgroundColor: bgColor || DEFAULT_COLOR,
};
default:
return {};
}
}, [textStyle, hover, bgColor]);
}, [translator.eventName]);
if (loading) {
return (
<>
{q.length > 40 ? <br /> : " "}
{q.length > newlineLength ? <br /> : " "}
<LoadingIcon />
</>
);
@@ -99,14 +129,14 @@ export default function Content({ q, translator }) {
if (text && !sameLang) {
return (
<>
{q.length > 40 ? <br /> : " "}
<span
style={style}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{q.length > newlineLength ? <br /> : " "}
<StyledSpan
textStyle={textStyle}
textDiyStyle={textDiyStyle}
bgColor={bgColor}
>
{text}
</span>
</StyledSpan>
</>
);
}

View File

@@ -4,17 +4,16 @@ import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu";
import Toolbar from "@mui/material/Toolbar";
import Box from "@mui/material/Box";
import { useDarkModeSwitch } from "../../hooks/ColorMode";
import { useDarkMode } from "../../hooks/ColorMode";
import LightModeIcon from "@mui/icons-material/LightMode";
import DarkModeIcon from "@mui/icons-material/DarkMode";
import Link from "@mui/material/Link";
import { useI18n } from "../../hooks/I18n";
function Header(props) {
const i18n = useI18n();
const { onDrawerToggle } = props;
const switchColorMode = useDarkModeSwitch();
const darkMode = useDarkMode();
const { darkMode, toggleDarkMode } = useDarkMode();
return (
<AppBar
@@ -35,10 +34,14 @@ function Header(props) {
<MenuIcon />
</IconButton>
</Box>
<Box sx={{ flexGrow: 1 }}>{`${i18n("app_name")} v${
process.env.REACT_APP_VERSION
}`}</Box>
<IconButton onClick={switchColorMode} color="inherit">
<Box sx={{ flexGrow: 1 }}>
<Link
underline="none"
color="inherit"
href={process.env.REACT_APP_HOMEPAGE}
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
</Box>
<IconButton onClick={toggleDarkMode} color="inherit">
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
</IconButton>
</Toolbar>

View File

@@ -0,0 +1,175 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import {
GLOBAL_KEY,
REMAIN_KEY,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_TRANS_ALL,
OPT_STYLE_ALL,
OPT_STYLE_DIY,
OPT_STYLE_USE_COLOR,
} from "../../config";
import { useI18n } from "../../hooks/I18n";
import MenuItem from "@mui/material/MenuItem";
import Grid from "@mui/material/Grid";
import { useOwSubRule } from "../../hooks/SubRules";
export default function OwSubRule() {
const i18n = useI18n();
const { owSubrule, updateOwSubrule } = useOwSubRule();
const handleChange = (e) => {
e.preventDefault();
const { name, value } = e.target;
updateOwSubrule({ [name]: value });
};
const {
translator,
fromLang,
toLang,
textStyle,
transOpen,
bgColor,
textDiyStyle,
} = owSubrule;
const RemainItem = (
<MenuItem key={REMAIN_KEY} value={REMAIN_KEY}>
{i18n("remain_unchanged")}
</MenuItem>
);
const GlobalItem = (
<MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}>
{GLOBAL_KEY}
</MenuItem>
);
return (
<Stack spacing={2}>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transOpen"
value={transOpen}
label={i18n("translate_switch")}
onChange={handleChange}
>
{RemainItem}
{GlobalItem}
<MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem>
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="translator"
value={translator}
label={i18n("translate_service")}
onChange={handleChange}
>
{RemainItem}
{GlobalItem}
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="fromLang"
value={fromLang}
label={i18n("from_lang")}
onChange={handleChange}
>
{RemainItem}
{GlobalItem}
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="toLang"
value={toLang}
label={i18n("to_lang")}
onChange={handleChange}
>
{RemainItem}
{GlobalItem}
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="textStyle"
value={textStyle}
label={i18n("text_style")}
onChange={handleChange}
>
{RemainItem}
{GlobalItem}
{OPT_STYLE_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</TextField>
</Grid>
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
size="small"
fullWidth
name="bgColor"
value={bgColor}
label={i18n("bg_color")}
onChange={handleChange}
/>
</Grid>
)}
</Grid>
</Box>
{textStyle === OPT_STYLE_DIY && (
<TextField
size="small"
label={i18n("diy_style")}
helperText={i18n("diy_style_helper")}
name="textDiyStyle"
value={textDiyStyle}
onChange={handleChange}
multiline
/>
)}
</Stack>
);
}

View File

@@ -3,6 +3,7 @@ import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import CircularProgress from "@mui/material/CircularProgress";
import Alert from "@mui/material/Alert";
import {
GLOBAL_KEY,
DEFAULT_RULE,
@@ -10,8 +11,10 @@ import {
OPT_LANGS_TO,
OPT_TRANS_ALL,
OPT_STYLE_ALL,
OPT_STYLE_DIY,
OPT_STYLE_USE_COLOR,
} from "../../config";
import { useState, useRef, useEffect } from "react";
import { useState, useRef, useEffect, useMemo } from "react";
import { useI18n } from "../../hooks/I18n";
import Typography from "@mui/material/Typography";
import Accordion from "@mui/material/Accordion";
@@ -23,7 +26,7 @@ import MenuItem from "@mui/material/MenuItem";
import Grid from "@mui/material/Grid";
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import FileUploadIcon from "@mui/icons-material/FileUpload";
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
import { useSetting } from "../../hooks/Setting";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Tabs from "@mui/material/Tabs";
@@ -34,13 +37,20 @@ import DeleteIcon from "@mui/icons-material/Delete";
import IconButton from "@mui/material/IconButton";
import ShareIcon from "@mui/icons-material/Share";
import SyncIcon from "@mui/icons-material/Sync";
import { useSubrules } from "../../hooks/Rules";
import { rulesCache, tryLoadRules } from "../../libs/rules";
import { useSubRules } from "../../hooks/SubRules";
import { syncSubRules } from "../../libs/subRules";
import { loadOrFetchSubRules } from "../../libs/subRules";
import { useAlert } from "../../hooks/Alert";
import { loadSyncOpt, syncShareRules } from "../../libs/sync";
import { syncShareRules } from "../../libs/sync";
import { debounce } from "../../libs/utils";
import { delSubRules, getSyncWithDefault } from "../../libs/storage";
import OwSubRule from "./OwSubRule";
function RuleFields({ rule, rules, setShow }) {
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
function RuleFields({ rule, rules, setShow, setKeyword }) {
const initFormValues = rule || {
...DEFAULT_RULE,
transOpen: "true",
};
const editMode = !!rule;
const i18n = useI18n();
@@ -56,6 +66,7 @@ function RuleFields({ rule, rules, setShow }) {
textStyle,
transOpen,
bgColor,
textDiyStyle,
} = formValues;
const hasSamePattern = (str) => {
@@ -73,10 +84,21 @@ function RuleFields({ rule, rules, setShow }) {
setErrors((pre) => ({ ...pre, [name]: "" }));
};
const handlePatternChange = useMemo(
() =>
debounce(async (patterns) => {
setKeyword(patterns.trim());
}, 500),
[setKeyword]
);
const handleChange = (e) => {
e.preventDefault();
const { name, value } = e.target;
setFormValues((pre) => ({ ...pre, [name]: value }));
if (name === "pattern" && !editMode) {
handlePatternChange(value);
}
};
const handleCancel = (e) => {
@@ -119,7 +141,7 @@ function RuleFields({ rule, rules, setShow }) {
}
};
const globalItem = rule?.pattern !== "*" && (
const GlobalItem = rule?.pattern !== "*" && (
<MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}>
{GLOBAL_KEY}
</MenuItem>
@@ -166,7 +188,7 @@ function RuleFields({ rule, rules, setShow }) {
disabled={disabled}
onChange={handleChange}
>
{globalItem}
{GlobalItem}
<MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem>
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
</TextField>
@@ -182,7 +204,7 @@ function RuleFields({ rule, rules, setShow }) {
disabled={disabled}
onChange={handleChange}
>
{globalItem}
{GlobalItem}
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
@@ -201,7 +223,7 @@ function RuleFields({ rule, rules, setShow }) {
disabled={disabled}
onChange={handleChange}
>
{globalItem}
{GlobalItem}
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
@@ -220,7 +242,7 @@ function RuleFields({ rule, rules, setShow }) {
disabled={disabled}
onChange={handleChange}
>
{globalItem}
{GlobalItem}
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
@@ -239,7 +261,7 @@ function RuleFields({ rule, rules, setShow }) {
disabled={disabled}
onChange={handleChange}
>
{globalItem}
{GlobalItem}
{OPT_STYLE_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
@@ -247,20 +269,35 @@ function RuleFields({ rule, rules, setShow }) {
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
size="small"
fullWidth
name="bgColor"
value={bgColor}
label={i18n("bg_color")}
disabled={disabled}
onChange={handleChange}
/>
</Grid>
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
size="small"
fullWidth
name="bgColor"
value={bgColor}
label={i18n("bg_color")}
disabled={disabled}
onChange={handleChange}
/>
</Grid>
)}
</Grid>
</Box>
{textStyle === OPT_STYLE_DIY && (
<TextField
size="small"
label={i18n("diy_style")}
helperText={i18n("diy_style_helper")}
name="textDiyStyle"
value={textDiyStyle}
disabled={disabled}
onChange={handleChange}
multiline
/>
)}
{rules &&
(editMode ? (
// 编辑
@@ -396,12 +433,12 @@ function UploadButton({ onChange, text }) {
);
}
function ShareButton({ rules, injectRules, selectedSub }) {
function ShareButton({ rules, injectRules, selectedUrl }) {
const alert = useAlert();
const i18n = useI18n();
const handleClick = async () => {
try {
const { syncUrl, syncKey } = await loadSyncOpt();
const { syncUrl, syncKey } = await getSyncWithDefault();
if (!syncUrl || !syncKey) {
alert.warning(i18n("error_sync_setting"));
return;
@@ -409,7 +446,7 @@ function ShareButton({ rules, injectRules, selectedSub }) {
const shareRules = [...rules.list];
if (injectRules) {
const subRules = await tryLoadRules(selectedSub?.url);
const subRules = await loadOrFetchSubRules(selectedUrl);
shareRules.splice(-1, 0, ...subRules);
}
@@ -438,16 +475,15 @@ function ShareButton({ rules, injectRules, selectedSub }) {
);
}
function UserRules() {
function UserRules({ subRules }) {
const i18n = useI18n();
const rules = useRules();
const [showAdd, setShowAdd] = useState(false);
const setting = useSetting();
const updateSetting = useSettingUpdate();
const subrules = useSubrules();
const selectedSub = subrules.list.find((item) => item.selected);
const { setting, updateSetting } = useSetting();
const [keyword, setKeyword] = useState("");
const injectRules = !!setting?.injectRules;
const { selectedUrl, selectedRules } = subRules;
const handleImport = (e) => {
const file = e.target.files[0];
@@ -477,9 +513,21 @@ function UserRules() {
});
};
useEffect(() => {
if (!showAdd) {
setKeyword("");
}
}, [showAdd]);
return (
<Stack spacing={3}>
<Stack direction="row" spacing={2} useFlexGap flexWrap="wrap">
<Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
<Button
size="small"
variant="contained"
@@ -501,7 +549,7 @@ function UserRules() {
<ShareButton
rules={rules}
injectRules={injectRules}
selectedSub={selectedSub}
selectedUrl={selectedUrl}
/>
<FormControlLabel
@@ -516,24 +564,48 @@ function UserRules() {
/>
</Stack>
{showAdd && <RuleFields rules={rules} setShow={setShowAdd} />}
{showAdd && (
<RuleFields
rules={rules}
setShow={setShowAdd}
setKeyword={setKeyword}
/>
)}
<Box>
{rules.list.map((rule) => (
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} />
))}
{rules.list
.filter(
(rule) =>
rule.pattern.includes(keyword) || keyword.includes(rule.pattern)
)
.map((rule) => (
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} />
))}
</Box>
{injectRules && (
<Box>
{selectedRules
.filter(
(rule) =>
rule.pattern.includes(keyword) || keyword.includes(rule.pattern)
)
.map((rule) => (
<RuleAccordion key={rule.pattern} rule={rule} />
))}
</Box>
)}
</Stack>
);
}
function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
const [loading, setLoading] = useState(false);
const handleDel = async () => {
try {
await subrules.del(url);
await rulesCache.del(url);
await delSub(url);
await delSubRules(url);
} catch (err) {
console.log("[del subrules]", err);
}
@@ -542,10 +614,9 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
const handleSync = async () => {
try {
setLoading(true);
const rules = await rulesCache.fetch(url);
await rulesCache.set(url, rules);
if (url === selectedUrl) {
setRules(rules);
const rules = await syncSubRules(url);
if (rules.length > 0 && url === selectedUrl) {
setSelectedRules(rules);
}
} catch (err) {
console.log("[sync sub rules]", err);
@@ -575,11 +646,12 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
);
}
function SubRulesEdit({ subrules }) {
function SubRulesEdit({ subList, addSub }) {
const i18n = useI18n();
const [inputText, setInputText] = useState("");
const [inputError, setInputError] = useState("");
const [showInput, setShowInput] = useState(false);
const [loading, setLoading] = useState(false);
const handleCancel = (e) => {
e.preventDefault();
@@ -597,23 +669,25 @@ function SubRulesEdit({ subrules }) {
return;
}
if (subrules.list.find((item) => item.url === url)) {
if (subList.find((item) => item.url === url)) {
setInputError(i18n("error_duplicate_values"));
return;
}
try {
const rules = await rulesCache.fetch(url);
setLoading(true);
const rules = await syncSubRules(url);
if (rules.length === 0) {
throw new Error("empty rules");
}
await rulesCache.set(url, rules);
await subrules.add(url);
await addSub(url);
setShowInput(false);
setInputText("");
} catch (err) {
console.log("[fetch rules]", err);
setInputError(i18n("error_fetch_url"));
} finally {
setLoading(false);
}
};
@@ -656,7 +730,12 @@ function SubRulesEdit({ subrules }) {
/>
<Stack direction="row" alignItems="center" spacing={2}>
<Button size="small" variant="contained" onClick={handleSave}>
<Button
size="small"
variant="contained"
onClick={handleSave}
disabled={loading}
>
{i18n("save")}
</Button>
<Button size="small" variant="outlined" onClick={handleCancel}>
@@ -669,47 +748,36 @@ function SubRulesEdit({ subrules }) {
);
}
function SubRules() {
const [loading, setLoading] = useState(false);
const [rules, setRules] = useState([]);
const subrules = useSubrules();
const selectedSub = subrules.list.find((item) => item.selected);
function SubRules({ subRules }) {
const {
subList,
selectSub,
addSub,
delSub,
selectedUrl,
selectedRules,
setSelectedRules,
loading,
} = subRules;
const handleSelect = (e) => {
const url = e.target.value;
subrules.select(url);
selectSub(url);
};
useEffect(() => {
(async () => {
if (selectedSub?.url) {
try {
setLoading(true);
const rules = await tryLoadRules(selectedSub?.url);
setRules(rules);
} catch (err) {
console.log("[load rules]", err);
} finally {
setLoading(false);
}
}
})();
}, [selectedSub?.url]);
return (
<Stack spacing={3}>
<SubRulesEdit subrules={subrules} />
<SubRulesEdit subList={subList} addSub={addSub} />
<RadioGroup value={selectedSub?.url} onChange={handleSelect}>
{subrules.list.map((item, index) => (
<RadioGroup value={selectedUrl} onChange={handleSelect}>
{subList.map((item, index) => (
<SubRulesItem
key={item.url}
url={item.url}
index={index}
selectedUrl={selectedSub?.url}
subrules={subrules}
setRules={setRules}
selectedUrl={selectedUrl}
delSub={delSub}
setSelectedRules={setSelectedRules}
/>
))}
</RadioGroup>
@@ -720,7 +788,9 @@ function SubRules() {
<CircularProgress />
</center>
) : (
rules.map((rule) => <RuleAccordion key={rule.pattern} rule={rule} />)
selectedRules.map((rule) => (
<RuleAccordion key={rule.pattern} rule={rule} />
))
)}
</Box>
</Stack>
@@ -730,6 +800,7 @@ function SubRules() {
export default function Rules() {
const i18n = useI18n();
const [activeTab, setActiveTab] = useState(0);
const subRules = useSubRules();
const handleTabChange = (e, newValue) => {
setActiveTab(newValue);
@@ -738,14 +809,26 @@ export default function Rules() {
return (
<Box>
<Stack spacing={3}>
<Alert severity="info">
{i18n("rules_warn_1")}
<br />
{i18n("rules_warn_2")}
</Alert>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs value={activeTab} onChange={handleTabChange}>
<Tab label={i18n("edit_rules")} />
<Tab label={i18n("personal_rules")} />
<Tab label={i18n("subscribe_rules")} />
<Tab label={i18n("overwrite_subscribe_rules")} />
</Tabs>
</Box>
<div hidden={activeTab !== 0}>{activeTab === 0 && <UserRules />}</div>
<div hidden={activeTab !== 1}>{activeTab === 1 && <SubRules />}</div>
<div hidden={activeTab !== 0}>
{activeTab === 0 && <UserRules subRules={subRules} />}
</div>
<div hidden={activeTab !== 1}>
{activeTab === 1 && <SubRules subRules={subRules} />}
</div>
<div hidden={activeTab !== 2}>{activeTab === 2 && <OwSubRule />}</div>
</Stack>
</Box>
);

View File

@@ -5,52 +5,57 @@ import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
import { limitNumber, debounce } from "../../libs/utils";
import Link from "@mui/material/Link";
import { useSetting } from "../../hooks/Setting";
import { limitNumber } from "../../libs/utils";
import { useI18n } from "../../hooks/I18n";
import { UI_LANGS } from "../../config";
import { useMemo } from "react";
import { UI_LANGS, URL_KISS_PROXY, TRANS_NEWLINE_LENGTH } from "../../config";
export default function Settings() {
const i18n = useI18n();
const setting = useSetting();
const updateSetting = useSettingUpdate();
const { setting, updateSetting } = useSetting();
const handleChange = useMemo(
() =>
debounce((e) => {
e.preventDefault();
let { name, value } = e.target;
switch (name) {
case "fetchLimit":
value = limitNumber(value, 1, 100);
break;
case "fetchInterval":
value = limitNumber(value, 0, 5000);
break;
default:
}
updateSetting({
[name]: value,
});
}, 500),
[updateSetting]
);
if (!setting) {
return;
}
const handleChange = (e) => {
e.preventDefault();
let { name, value } = e.target;
switch (name) {
case "fetchLimit":
value = limitNumber(value, 1, 100);
break;
case "fetchInterval":
value = limitNumber(value, 0, 5000);
break;
case "minLength":
value = limitNumber(value, 1, 100);
break;
case "maxLength":
value = limitNumber(value, 100, 10000);
break;
case "newlineLength":
value = limitNumber(value, 1, 1000);
break;
default:
}
updateSetting({
[name]: value,
});
};
const {
uiLang,
googleUrl,
fetchLimit,
fetchInterval,
minLength,
maxLength,
openaiUrl,
deeplUrl = "",
deeplKey = "",
openaiKey,
openaiModel,
openaiPrompt,
clearCache,
newlineLength = TRANS_NEWLINE_LENGTH,
} = setting;
return (
@@ -77,7 +82,7 @@ export default function Settings() {
label={i18n("fetch_limit")}
type="number"
name="fetchLimit"
defaultValue={fetchLimit}
value={fetchLimit}
onChange={handleChange}
/>
@@ -86,7 +91,34 @@ export default function Settings() {
label={i18n("fetch_interval")}
type="number"
name="fetchInterval"
defaultValue={fetchInterval}
value={fetchInterval}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("min_translate_length")}
type="number"
name="minLength"
value={minLength}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("max_translate_length")}
type="number"
name="maxLength"
value={maxLength}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("num_of_newline_characters")}
type="number"
name="newlineLength"
value={newlineLength}
onChange={handleChange}
/>
@@ -107,7 +139,26 @@ export default function Settings() {
size="small"
label={i18n("google_api")}
name="googleUrl"
defaultValue={googleUrl}
value={googleUrl}
onChange={handleChange}
helperText={
<Link href={URL_KISS_PROXY}>{i18n("about_api_proxy")}</Link>
}
/>
<TextField
size="small"
label={i18n("deepl_api")}
name="deeplUrl"
value={deeplUrl}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("deepl_key")}
name="deeplKey"
value={deeplKey}
onChange={handleChange}
/>
@@ -115,8 +166,11 @@ export default function Settings() {
size="small"
label={i18n("openai_api")}
name="openaiUrl"
defaultValue={openaiUrl}
value={openaiUrl}
onChange={handleChange}
helperText={
<Link href={URL_KISS_PROXY}>{i18n("about_api_proxy")}</Link>
}
/>
<TextField
@@ -124,7 +178,7 @@ export default function Settings() {
type="password"
label={i18n("openai_key")}
name="openaiKey"
defaultValue={openaiKey}
value={openaiKey}
onChange={handleChange}
/>
@@ -132,7 +186,7 @@ export default function Settings() {
size="small"
label={i18n("openai_model")}
name="openaiModel"
defaultValue={openaiModel}
value={openaiModel}
onChange={handleChange}
/>
@@ -140,7 +194,7 @@ export default function Settings() {
size="small"
label={i18n("openai_prompt")}
name="openaiPrompt"
defaultValue={openaiPrompt}
value={openaiPrompt}
onChange={handleChange}
multiline
/>

View File

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

View File

@@ -4,17 +4,22 @@ import Rules from "./Rules";
import Setting from "./Setting";
import Layout from "./Layout";
import SyncSetting from "./SyncSetting";
import { StoragesProvider } from "../../hooks/Storage";
import { SettingProvider } from "../../hooks/Setting";
import ThemeProvider from "../../hooks/Theme";
import { useEffect, useState } from "react";
import { isGm } from "../../libs/browser";
import { isGm } from "../../libs/client";
import { sleep } from "../../libs/utils";
import CircularProgress from "@mui/material/CircularProgress";
import { syncAll } from "../../libs/sync";
import { trySyncSettingAndRules } from "../../libs/sync";
import { AlertProvider } from "../../hooks/Alert";
import Link from "@mui/material/Link";
import Divider from "@mui/material/Divider";
import Stack from "@mui/material/Stack";
import { adaptScript } from "../../libs/gm";
import Alert from "@mui/material/Alert";
export default function Options() {
const [error, setError] = useState(false);
const [error, setError] = useState("");
const [ready, setReady] = useState(false);
useEffect(() => {
@@ -23,52 +28,95 @@ export default function Options() {
// 等待GM注入
let i = 0;
for (;;) {
if (window.APP_NAME === process.env.REACT_APP_NAME) {
if (window?.APP_INFO?.name === process.env.REACT_APP_NAME) {
const { version, eventName } = window.APP_INFO;
// 检查版本是否一致
if (version !== process.env.REACT_APP_VERSION) {
setError(
`The version of the script(v${version}) and this page(v${process.env.REACT_APP_VERSION}) are inconsistent.`
);
break;
}
if (eventName) {
// 注入GM接口
adaptScript(eventName);
}
// 同步数据
await trySyncSettingAndRules();
setReady(true);
break;
}
if (++i > 8) {
setError(true);
setError("Time out.");
break;
}
await sleep(1000);
}
} else {
// 同步数据
await trySyncSettingAndRules();
setReady(true);
}
// 同步数据
syncAll();
})();
}, []);
if (error) {
return (
<center>
<Alert severity="error">{error}</Alert>
<Divider>
<Link
href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider>
<h2>
Please confirm whether to install or enable{" "}
<a href={process.env.REACT_APP_HOMEPAGE}>KISS Translator</a>{" "}
Please confirm whether to install or enable KISS Translator
GreaseMonkey script?
</h2>
<h2>
<a href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>Click here</a>{" "}
to install, or <a href={process.env.REACT_APP_HOMEPAGE}>click here</a>{" "}
for help.
</h2>
<Stack spacing={2}>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
Install Userscript 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install Userscript 2
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install Userscript Safari 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install Userscript Safari 2
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
Open Options Page 1
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
Open Options Page 2
</Link>
</Stack>
</center>
);
}
if (isGm && !ready) {
if (!ready) {
return (
<center>
<Divider>
<Link
href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider>
<CircularProgress />
</center>
);
}
return (
<StoragesProvider>
<SettingProvider>
<ThemeProvider>
<AlertProvider>
<HashRouter>
@@ -83,6 +131,6 @@ export default function Options() {
</HashRouter>
</AlertProvider>
</ThemeProvider>
</StoragesProvider>
</SettingProvider>
);
}

View File

@@ -6,7 +6,8 @@ import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Button from "@mui/material/Button";
import { sendTabMsg } from "../../libs/msg";
import { browser, isExt } from "../../libs/browser";
import { browser } from "../../libs/browser";
import { isExt } from "../../libs/client";
import { useI18n } from "../../hooks/I18n";
import TextField from "@mui/material/TextField";
import {
@@ -17,7 +18,9 @@ import {
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_STYLE_ALL,
OPT_STYLE_USE_COLOR,
} from "../../config";
import { sendIframeMsg } from "../../libs/iframe";
export default function Popup({ setShowPopup, translator: tran }) {
const i18n = useI18n();
@@ -40,6 +43,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
await sendTabMsg(MSG_TRANS_TOGGLE);
} else {
tran.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
}
} catch (err) {
console.log("[toggle trans]", err);
@@ -55,6 +59,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
} else {
tran.updateRule({ [name]: value });
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
}
} catch (err) {
console.log("[update rule]", err);
@@ -101,7 +106,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
onChange={handleTransToggle}
/>
}
label={i18n("translate")}
label={i18n("translate_alt")}
/>
<TextField
@@ -158,7 +163,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
size="small"
value={textStyle}
name="textStyle"
label={i18n("text_style")}
label={i18n("text_style_alt")}
onChange={handleChange}
>
{OPT_STYLE_ALL.map((item) => (
@@ -168,13 +173,15 @@ export default function Popup({ setShowPopup, translator: tran }) {
))}
</TextField>
<TextField
size="small"
name="bgColor"
value={bgColor}
label={i18n("bg_color")}
onChange={handleChange}
/>
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
<TextField
size="small"
name="bgColor"
value={bgColor}
label={i18n("bg_color")}
onChange={handleChange}
/>
)}
<Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")}

979
yarn.lock

File diff suppressed because it is too large Load Diff