Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bb742f828 | ||
|
|
72742e5e12 | ||
|
|
3667e0a509 | ||
|
|
c2d7668ba7 | ||
|
|
aa830f5e20 | ||
|
|
b593fa4146 | ||
|
|
b00b906484 | ||
|
|
c1bd6a1be6 | ||
|
|
36739f04b3 | ||
|
|
23eb92853e | ||
|
|
5ab2910dc7 | ||
|
|
40d07f6764 | ||
|
|
5c8e216169 | ||
|
|
5ba061deda | ||
|
|
935c83185d | ||
|
|
6327391e65 | ||
|
|
3d656cf5b0 | ||
|
|
d570a0f1a2 | ||
|
|
503a71302c | ||
|
|
3e36ceb5b9 | ||
|
|
cde7a1d49f | ||
|
|
b14a38e4fb | ||
|
|
732a526a8e | ||
|
|
2da5ffef44 | ||
|
|
2e6e52004f | ||
|
|
4486ad353c | ||
|
|
aa795e2731 | ||
|
|
c46fe7d1c6 | ||
|
|
d7cee8cca6 | ||
|
|
11f790ace5 | ||
|
|
13e7c1b754 | ||
|
|
d314d5515f | ||
|
|
09b19e3ca0 | ||
|
|
687bd11fd1 | ||
|
|
56cb1cd30d | ||
|
|
7a3df25521 | ||
|
|
ea8919ba07 | ||
|
|
3dece4fcdb | ||
|
|
df950a1bd2 | ||
|
|
74b9ee31fa | ||
|
|
64cd55fe58 | ||
|
|
e80ede14fb | ||
|
|
45ba9d3320 | ||
|
|
47c7048538 | ||
|
|
f9bfa8101f | ||
|
|
620ac464eb | ||
|
|
62289f8ab8 | ||
|
|
d84594da96 | ||
|
|
e1d74aae6a | ||
|
|
c4980d9eb7 | ||
|
|
882d83c6b7 | ||
|
|
c4a7fd81f8 | ||
|
|
0e55799109 | ||
|
|
a3cdcb2a1a | ||
|
|
e0ccc298f9 | ||
|
|
36b49bb577 | ||
|
|
2636c24e84 | ||
|
|
6bcf294635 | ||
|
|
c5fa6689a4 | ||
|
|
3bf0cb2485 | ||
|
|
19c9335527 | ||
|
|
20da2e1b97 | ||
|
|
9eceb8641d | ||
|
|
86bc915d74 | ||
|
|
6b35525207 | ||
|
|
4633bf4fc6 | ||
|
|
2665f31d94 | ||
|
|
6c4d3149eb | ||
|
|
a2762e6ce6 | ||
|
|
792a1bfcad | ||
|
|
a0eba9d60e | ||
|
|
c2e0064253 | ||
|
|
f246efc84b | ||
|
|
4a3bf7e96c | ||
|
|
523b81090d | ||
|
|
d706c405d9 | ||
|
|
1191791447 | ||
|
|
5c510f2df2 | ||
|
|
7c0aa23177 | ||
|
|
4bc1c26653 | ||
|
|
ca1e1148d6 | ||
|
|
2224455a7f | ||
|
|
f463f3ce08 | ||
|
|
c0872db98c | ||
|
|
d3a5d91f01 | ||
|
|
3e9338be0e | ||
|
|
ef7f1ad638 | ||
|
|
1f10ebe404 | ||
|
|
f4a8251c61 | ||
|
|
f585a43480 | ||
|
|
3a11465c24 | ||
|
|
3c3ebdf96c | ||
|
|
6b30f443e1 | ||
|
|
232e9a47a2 | ||
|
|
7ec43a1d3f | ||
|
|
a8caa34bbe | ||
|
|
c2fd1fe9e0 | ||
|
|
2773a76af8 | ||
|
|
1dc7026e8f | ||
|
|
b36ede7393 | ||
|
|
b18721a4e5 | ||
|
|
01676bc682 |
26
.env
26
.env
@@ -2,11 +2,25 @@ GENERATE_SOURCEMAP=false
|
||||
|
||||
REACT_APP_NAME=KISS Translator
|
||||
REACT_APP_NAME_CN=简约翻译
|
||||
REACT_APP_VERSION=1.4.5
|
||||
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
|
||||
|
||||
33
README.en.md
33
README.en.md
@@ -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)
|
||||
|
||||
33
README.md
33
README.md
@@ -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)
|
||||
|
||||
@@ -83,14 +83,12 @@ const userscriptWebpack = (config, env) => {
|
||||
// @icon ${process.env.REACT_APP_LOGOURL}
|
||||
// @downloadURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||
// @grant GM_xmlhttpRequest
|
||||
// @grant GM.xmlhttpRequest
|
||||
// @grant GM_setValue
|
||||
// @grant GM.xmlHttpRequest
|
||||
// @grant GM.registerMenuCommand
|
||||
// @grant GM.setValue
|
||||
// @grant GM_getValue
|
||||
// @grant GM.getValue
|
||||
// @grant GM_deleteValue
|
||||
// @grant GM.deleteValue
|
||||
// @grant GM.info
|
||||
// @grant unsafeWindow
|
||||
// @connect translate.googleapis.com
|
||||
// @connect api-edge.cognitive.microsofttranslator.com
|
||||
@@ -98,6 +96,10 @@ const userscriptWebpack = (config, env) => {
|
||||
// @connect api.openai.com
|
||||
// @connect openai.azure.com
|
||||
// @connect workers.dev
|
||||
// @connect github.io
|
||||
// @connect githubusercontent.com
|
||||
// @connect kiss-translator.rayjar.com
|
||||
// @connect ghproxy.com
|
||||
// @run-at document-end
|
||||
// ==/UserScript==
|
||||
|
||||
|
||||
14
package.json
14
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kiss-translator",
|
||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||
"version": "1.4.5",
|
||||
"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,8 +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": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript",
|
||||
"build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
|
||||
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
|
||||
"build:rules": "babel-node src/rules.js",
|
||||
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript-ios && yarn build:userscript && yarn build:rules",
|
||||
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject"
|
||||
@@ -53,6 +57,10 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
"@babel/node": "^7.22.10",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"wrangler": "^3.4.0"
|
||||
}
|
||||
|
||||
@@ -4,5 +4,11 @@
|
||||
},
|
||||
"app_description": {
|
||||
"message": "A minimalist bilingual translation Extension & Greasemonkey Script"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "Toggle Translate"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "Toggle Style"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,11 @@
|
||||
},
|
||||
"app_description": {
|
||||
"message": "一个简约的双语网页翻译扩展 & 油猴脚本"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "切换翻译"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "切换样式"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
We’ve 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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.4.5",
|
||||
"version": "1.6.3",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -12,9 +12,29 @@
|
||||
"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"
|
||||
},
|
||||
"description": "__MSG_toggle_translate__"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
}
|
||||
},
|
||||
"permissions": ["<all_urls>", "storage"],
|
||||
"icons": {
|
||||
"16": "images/logo16.png",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.4.5",
|
||||
"version": "1.6.3",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -13,9 +13,29 @@
|
||||
"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"
|
||||
},
|
||||
"description": "__MSG_toggle_translate__"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
}
|
||||
},
|
||||
"permissions": ["storage"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"icons": {
|
||||
|
||||
@@ -3,14 +3,16 @@ import { fetchPolyfill } from "../libs/fetch";
|
||||
import {
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
URL_MICROSOFT_TRANS,
|
||||
OPT_LANGS_SPECIAL,
|
||||
PROMPT_PLACE_FROM,
|
||||
PROMPT_PLACE_TO,
|
||||
KV_HEADER_KEY,
|
||||
KV_SALT_SYNC,
|
||||
} from "../config";
|
||||
import { getSetting, detectLang } from "../libs";
|
||||
import { tryDetectLang } from "../libs";
|
||||
import { sha256 } from "../libs/utils";
|
||||
|
||||
/**
|
||||
* 同步数据
|
||||
@@ -19,19 +21,25 @@ import { getSetting, detectLang } from "../libs";
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
export const apiSyncData = async (url, key, data) =>
|
||||
fetchPolyfill(
|
||||
url,
|
||||
{
|
||||
export const apiSyncData = async (url, key, data, isBg = false) =>
|
||||
fetchPolyfill(url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
[KV_HEADER_KEY]: key,
|
||||
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
{ useUnsafe: true }
|
||||
);
|
||||
isBg,
|
||||
});
|
||||
|
||||
/**
|
||||
* 下载订阅规则
|
||||
* @param {*} url
|
||||
* @param {*} isBg
|
||||
* @returns
|
||||
*/
|
||||
export const apiFetchRules = (url, isBg = false) =>
|
||||
fetchPolyfill(url, { isBg });
|
||||
|
||||
/**
|
||||
* 谷歌翻译
|
||||
@@ -40,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",
|
||||
@@ -50,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,
|
||||
{
|
||||
return fetchPolyfill(input, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
},
|
||||
{ useCache: true, usePool: true, translator }
|
||||
);
|
||||
useCache: true,
|
||||
usePool: true,
|
||||
translator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -77,17 +84,46 @@ const apiMicrosoftTranslate = (translator, text, to, from) => {
|
||||
"api-version": "3.0",
|
||||
};
|
||||
const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`;
|
||||
return fetchPolyfill(
|
||||
input,
|
||||
{
|
||||
return fetchPolyfill(input, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
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",
|
||||
},
|
||||
{ useCache: true, usePool: true, translator }
|
||||
);
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
useCache: true,
|
||||
usePool: true,
|
||||
translator,
|
||||
token: deeplKey,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -97,15 +133,12 @@ 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,
|
||||
{
|
||||
return fetchPolyfill(openaiUrl, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
@@ -125,9 +158,11 @@ const apiOpenaiTranslate = async (translator, text, to, from) => {
|
||||
temperature: 0,
|
||||
max_tokens: 256,
|
||||
}),
|
||||
},
|
||||
{ useCache: true, usePool: true, translator, token: openaiKey }
|
||||
);
|
||||
useCache: true,
|
||||
usePool: true,
|
||||
translator,
|
||||
token: openaiKey,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -135,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;
|
||||
|
||||
@@ -143,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];
|
||||
|
||||
@@ -3,27 +3,23 @@ import {
|
||||
MSG_FETCH,
|
||||
MSG_FETCH_LIMIT,
|
||||
MSG_FETCH_CLEAR,
|
||||
DEFAULT_SETTING,
|
||||
DEFAULT_RULES,
|
||||
DEFAULT_SYNC,
|
||||
STOKEY_SETTING,
|
||||
STOKEY_RULES,
|
||||
STOKEY_SYNC,
|
||||
CACHE_NAME,
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
CMD_TOGGLE_TRANSLATE,
|
||||
CMD_TOGGLE_STYLE,
|
||||
} 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);
|
||||
tryInitDefaultData();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -33,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);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -49,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 });
|
||||
})
|
||||
@@ -73,3 +72,19 @@ browser.runtime.onMessage.addListener(
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 监听快捷键
|
||||
*/
|
||||
browser.commands.onCommand.addListener((command) => {
|
||||
// console.log(`Command: ${command}`);
|
||||
switch (command) {
|
||||
case CMD_TOGGLE_TRANSLATE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
break;
|
||||
case CMD_TOGGLE_STYLE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
4
src/config/app.js
Normal file
4
src/config/app.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const APP_NAME = process.env.REACT_APP_NAME.trim()
|
||||
.split(/\s+/)
|
||||
.join("-");
|
||||
export const APP_LCNAME = APP_NAME.toLowerCase();
|
||||
@@ -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`,
|
||||
@@ -105,8 +129,32 @@ export const I18N = {
|
||||
en: `Add`,
|
||||
},
|
||||
inject_rules: {
|
||||
zh: `注入内置规则`,
|
||||
en: `Inject Built-in Rules`,
|
||||
zh: `注入订阅规则`,
|
||||
en: `Inject Subscribe 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: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
|
||||
@@ -116,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`,
|
||||
@@ -144,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`,
|
||||
@@ -153,12 +213,12 @@ export const I18N = {
|
||||
en: `URL pattern`,
|
||||
},
|
||||
pattern_helper: {
|
||||
zh: `多个URL支持英文逗号“,”分隔`,
|
||||
en: `Multiple URLs can be separated by English commas ","`,
|
||||
zh: `1、支持星号(*)通配符。2、多个URL支持英文逗号“,”分隔。`,
|
||||
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: `开启翻译`,
|
||||
@@ -196,6 +256,18 @@ export const I18N = {
|
||||
zh: `错误的文件类型`,
|
||||
en: `Wrong file type`,
|
||||
},
|
||||
error_fetch_url: {
|
||||
zh: `请检查url地址是否正确或稍后再试。`,
|
||||
en: `Please check if the url address is correct or try again later.`,
|
||||
},
|
||||
deepl_api: {
|
||||
zh: `DeepL 接口`,
|
||||
en: `DeepL API`,
|
||||
},
|
||||
deepl_key: {
|
||||
zh: `DeepL 密钥`,
|
||||
en: `DeepL Key`,
|
||||
},
|
||||
openai_api: {
|
||||
zh: `OpenAI 接口`,
|
||||
en: `OpenAI API`,
|
||||
@@ -232,4 +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!`,
|
||||
},
|
||||
error_sync_setting: {
|
||||
zh: `您的同步设置未填写,无法在线分享。`,
|
||||
en: `Your sync settings are missing and cannot be shared online.`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
import { DEFAULT_SELECTOR, RULES } from "./rules";
|
||||
import {
|
||||
DEFAULT_SELECTOR,
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
SHADOW_KEY,
|
||||
DEFAULT_RULE,
|
||||
DEFAULT_OW_RULE,
|
||||
BUILTIN_RULES,
|
||||
} from "./rules";
|
||||
import { APP_NAME, APP_LCNAME } from "./app";
|
||||
export { I18N, UI_LANGS } from "./i18n";
|
||||
|
||||
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`;
|
||||
export const STOKEY_RULES = `${APP_NAME}_rules`;
|
||||
export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
||||
export const STOKEY_FAB = `${APP_NAME}_fab`;
|
||||
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
|
||||
|
||||
export const GLOBAL_KEY = "*";
|
||||
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
|
||||
export const CMD_TOGGLE_STYLE = "toggleStyle";
|
||||
|
||||
export const CLIENT_WEB = "web";
|
||||
export const CLIENT_CHROME = "chrome";
|
||||
@@ -20,9 +36,11 @@ export const CLIENT_FIREFOX = "firefox";
|
||||
export const CLIENT_USERSCRIPT = "userscript";
|
||||
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
|
||||
|
||||
export const KV_HEADER_KEY = "X-KISS-PSK";
|
||||
export const KV_RULES_KEY = "KT_RULES";
|
||||
export const KV_RULES_SHARE_KEY = "KT_RULES_SHARE";
|
||||
export const KV_SETTING_KEY = "KT_SETTING";
|
||||
export const KV_SALT_SYNC = "KISS-Translator-SYNC";
|
||||
export const KV_SALT_SHARE = "KISS-Translator-SHARE";
|
||||
|
||||
export const CACHE_NAME = `${APP_NAME}_cache`;
|
||||
|
||||
@@ -30,16 +48,16 @@ export const MSG_FETCH = "fetch";
|
||||
export const MSG_FETCH_LIMIT = "fetch_limit";
|
||||
export const MSG_FETCH_CLEAR = "fetch_clear";
|
||||
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
||||
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
|
||||
export const MSG_TRANS_GETRULE = "trans_getrule";
|
||||
export const MSG_TRANS_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";
|
||||
@@ -48,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,
|
||||
];
|
||||
|
||||
@@ -101,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]])
|
||||
),
|
||||
@@ -112,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,
|
||||
@@ -120,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; // 默认最大任务数量
|
||||
@@ -141,51 +176,47 @@ export const GLOBLA_RULE = {
|
||||
textStyle: OPT_STYLE_DASHLINE,
|
||||
transOpen: "false",
|
||||
bgColor: "",
|
||||
textDiyStyle: "",
|
||||
};
|
||||
|
||||
// 默认规则
|
||||
export const DEFAULT_RULE = {
|
||||
pattern: "",
|
||||
selector: "",
|
||||
translator: GLOBAL_KEY,
|
||||
fromLang: GLOBAL_KEY,
|
||||
toLang: GLOBAL_KEY,
|
||||
textStyle: GLOBAL_KEY,
|
||||
transOpen: GLOBAL_KEY,
|
||||
bgColor: "",
|
||||
};
|
||||
// 订阅列表
|
||||
export const DEFAULT_SUBRULES_LIST = [
|
||||
{
|
||||
url: process.env.REACT_APP_RULESURL,
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
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, // 是否注入内置规则
|
||||
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",
|
||||
openaiPrompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
|
||||
};
|
||||
|
||||
export const DEFAULT_RULES = [
|
||||
{
|
||||
...DEFAULT_RULE,
|
||||
...RULES[0],
|
||||
transOpen: "true",
|
||||
},
|
||||
GLOBLA_RULE,
|
||||
];
|
||||
|
||||
export const BUILTIN_RULES = RULES.map((item) => ({
|
||||
...DEFAULT_RULE,
|
||||
...item,
|
||||
transOpen: "true",
|
||||
}));
|
||||
|
||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||
|
||||
export const DEFAULT_SYNC = {
|
||||
syncUrl: "", // 数据同步接口
|
||||
@@ -194,4 +225,5 @@ export const DEFAULT_SYNC = {
|
||||
settingSyncAt: 0,
|
||||
rulesUpdateAt: 0,
|
||||
rulesSyncAt: 0,
|
||||
subRulesSyncAt: 0, // 订阅规则同步时间
|
||||
};
|
||||
|
||||
@@ -2,15 +2,59 @@ const els = `li, p, h1, h2, h3, h4, h5, h6, dd`;
|
||||
|
||||
export const DEFAULT_SELECTOR = `:is(${els})`;
|
||||
|
||||
export const RULES = [
|
||||
export const GLOBAL_KEY = "*";
|
||||
export const REMAIN_KEY = "-";
|
||||
|
||||
export const SHADOW_KEY = ">>>";
|
||||
|
||||
export const DEFAULT_RULE = {
|
||||
pattern: "",
|
||||
selector: "",
|
||||
translator: GLOBAL_KEY,
|
||||
fromLang: GLOBAL_KEY,
|
||||
toLang: GLOBAL_KEY,
|
||||
textStyle: GLOBAL_KEY,
|
||||
transOpen: GLOBAL_KEY,
|
||||
bgColor: "",
|
||||
textDiyStyle: "",
|
||||
};
|
||||
|
||||
const DEFAULT_DIY_STYLE = `color: #666;
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
LightGreen 20%,
|
||||
LightPink 20% 40%,
|
||||
LightSalmon 40% 60%,
|
||||
LightSeaGreen 60% 80%,
|
||||
LightSkyBlue 80%
|
||||
);
|
||||
&:hover {
|
||||
color: #333;
|
||||
};`;
|
||||
|
||||
export const DEFAULT_OW_RULE = {
|
||||
translator: REMAIN_KEY,
|
||||
fromLang: REMAIN_KEY,
|
||||
toLang: REMAIN_KEY,
|
||||
textStyle: REMAIN_KEY,
|
||||
transOpen: REMAIN_KEY,
|
||||
bgColor: "",
|
||||
textDiyStyle: DEFAULT_DIY_STYLE,
|
||||
};
|
||||
|
||||
const RULES = [
|
||||
{
|
||||
pattern: `www.google.com/search`,
|
||||
selector: `h3, .IsZvec, .VwiC3b`,
|
||||
},
|
||||
{
|
||||
pattern: `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,
|
||||
@@ -132,3 +176,9 @@ export const RULES = [
|
||||
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`,
|
||||
},
|
||||
];
|
||||
|
||||
export const BUILTIN_RULES = RULES.map((item) => ({
|
||||
...DEFAULT_RULE,
|
||||
...item,
|
||||
transOpen: "true",
|
||||
}));
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { browser } from "./libs/browser";
|
||||
import {
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
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 = 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);
|
||||
|
||||
// 监听消息
|
||||
@@ -22,6 +26,9 @@ import { Translator } from "./libs/translator";
|
||||
case MSG_TRANS_TOGGLE:
|
||||
translator.toggle();
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
translator.toggleStyle();
|
||||
break;
|
||||
case MSG_TRANS_GETRULE:
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
@@ -32,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);
|
||||
}
|
||||
})();
|
||||
|
||||
60
src/hooks/Alert.js
Normal file
60
src/hooks/Alert.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { createContext, useContext, useState, forwardRef } from "react";
|
||||
import Snackbar from "@mui/material/Snackbar";
|
||||
import MuiAlert from "@mui/material/Alert";
|
||||
|
||||
const Alert = forwardRef(function Alert(props, ref) {
|
||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
|
||||
});
|
||||
|
||||
const AlertContext = createContext(null);
|
||||
|
||||
/**
|
||||
* 左下角提示,注入context后,方便全局调用
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export function AlertProvider({ children }) {
|
||||
const vertical = "top";
|
||||
const horizontal = "center";
|
||||
const [open, setOpen] = useState(false);
|
||||
const [severity, setSeverity] = useState("info");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const showAlert = (msg, type) => {
|
||||
setOpen(true);
|
||||
setMessage(msg);
|
||||
setSeverity(type);
|
||||
};
|
||||
|
||||
const handleClose = (_, reason) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const error = (msg) => showAlert(msg, "error");
|
||||
const warning = (msg) => showAlert(msg, "warning");
|
||||
const info = (msg) => showAlert(msg, "info");
|
||||
const success = (msg) => showAlert(msg, "success");
|
||||
|
||||
return (
|
||||
<AlertContext.Provider value={{ error, warning, info, success }}>
|
||||
{children}
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={3000}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical, horizontal }}
|
||||
>
|
||||
<Alert onClose={handleClose} severity={severity} sx={{ width: "100%" }}>
|
||||
{message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</AlertContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAlert() {
|
||||
return useContext(AlertContext);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
import {
|
||||
STOKEY_RULES,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
GLOBAL_KEY,
|
||||
} from "../config";
|
||||
import storage from "../libs/storage";
|
||||
import { useStorages } from "./Storage";
|
||||
import { matchValue } from "../libs/utils";
|
||||
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 { 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 add = useCallback(
|
||||
async (rule) => {
|
||||
const rules = [...list];
|
||||
if (rule.pattern === "*") {
|
||||
return;
|
||||
@@ -37,56 +36,41 @@ export function useRules() {
|
||||
return;
|
||||
}
|
||||
rules.unshift(rule);
|
||||
await update(rules);
|
||||
};
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
const del = async (pattern) => {
|
||||
const del = useCallback(
|
||||
async (pattern) => {
|
||||
let rules = [...list];
|
||||
if (pattern === "*") {
|
||||
return;
|
||||
}
|
||||
rules = rules.filter((item) => item.pattern !== pattern);
|
||||
await update(rules);
|
||||
};
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
const put = async (pattern, obj) => {
|
||||
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 update(rules);
|
||||
};
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
const merge = async (newRules) => {
|
||||
const merge = useCallback(
|
||||
async (newRules) => {
|
||||
const rules = [...list];
|
||||
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
|
||||
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
|
||||
newRules
|
||||
.filter(({ pattern }) => pattern && typeof pattern === "string")
|
||||
.map(
|
||||
({
|
||||
pattern,
|
||||
selector,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
}) => ({
|
||||
pattern,
|
||||
selector: typeof selector === "string" ? selector : "",
|
||||
bgColor: typeof bgColor === "string" ? bgColor : "",
|
||||
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
|
||||
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
|
||||
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
|
||||
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
|
||||
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
|
||||
})
|
||||
)
|
||||
.forEach((newRule) => {
|
||||
newRules = checkRules(newRules);
|
||||
newRules.forEach((newRule) => {
|
||||
const rule = rules.find(
|
||||
(oldRule) => oldRule.pattern === newRule.pattern
|
||||
);
|
||||
@@ -96,8 +80,10 @@ export function useRules() {
|
||||
rules.unshift(newRule);
|
||||
}
|
||||
});
|
||||
await update(rules);
|
||||
};
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
return { list, add, del, put, merge };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* @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);
|
||||
}
|
||||
|
||||
@@ -1,89 +1,44 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import { browser, isExt, isGm, isWeb } from "../libs/browser";
|
||||
import {
|
||||
STOKEY_SETTING,
|
||||
STOKEY_RULES,
|
||||
STOKEY_MSAUTH,
|
||||
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_MSAUTH]: null,
|
||||
[STOKEY_SETTING]: DEFAULT_SETTING,
|
||||
[STOKEY_RULES]: DEFAULT_RULES,
|
||||
[STOKEY_SYNC]: DEFAULT_SYNC,
|
||||
};
|
||||
export function useStorage(key, defaultVal = null) {
|
||||
const [data, setData] = useState(defaultVal);
|
||||
|
||||
const StoragesContext = createContext(null);
|
||||
|
||||
export function StoragesProvider({ children }) {
|
||||
const [storages, setStorages] = useState(null);
|
||||
|
||||
const handleChanged = (changes) => {
|
||||
if (isWeb || isGm) {
|
||||
const { key, oldValue, newValue } = changes;
|
||||
changes = {
|
||||
[key]: {
|
||||
oldValue,
|
||||
newValue,
|
||||
const save = useCallback(
|
||||
async (val) => {
|
||||
setData(val);
|
||||
await storage.setObj(key, val);
|
||||
},
|
||||
};
|
||||
[key]
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
async (obj) => {
|
||||
setData((pre) => ({ ...pre, ...obj }));
|
||||
await storage.putObj(key, obj);
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
||||
const remove = useCallback(async () => {
|
||||
setData(null);
|
||||
await storage.del(key);
|
||||
}, [key]);
|
||||
|
||||
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(([_, { oldValue, newValue }]) => 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 = {};
|
||||
const keys = Object.keys(defaultStorage);
|
||||
for (const key of keys) {
|
||||
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
99
src/hooks/SubRules.js
Normal 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 };
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
45
src/index.js
45
src/index.js
@@ -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 />
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
6
src/libs/client.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
|
||||
|
||||
export const client = process.env.REACT_APP_CLIENT;
|
||||
export const isExt = CLIENT_EXTS.includes(client);
|
||||
export const isGm = client === CLIENT_USERSCRIPT;
|
||||
export const isWeb = client === CLIENT_WEB;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isExt, isGm } from "./browser";
|
||||
import { 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,17 +66,34 @@ const newCacheReq = async (request) => {
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const fetchApi = async ({ input, init, useUnsafe, 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 && !useUnsafe) {
|
||||
if (isGm) {
|
||||
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) {
|
||||
if (window.KISS_GM) {
|
||||
return window.KISS_GM.fetch(input, init);
|
||||
} else {
|
||||
return fetchGM(input, init);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fetch(input, init);
|
||||
};
|
||||
|
||||
@@ -98,34 +116,32 @@ export const fetchPool = taskPool(
|
||||
/**
|
||||
* 请求数据统一接口
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @param {*} opts
|
||||
* @returns
|
||||
*/
|
||||
export const fetchData = async (
|
||||
input,
|
||||
init,
|
||||
{ useCache, usePool, translator, useUnsafe, 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);
|
||||
}
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
// 发送请求
|
||||
if (usePool) {
|
||||
res = await fetchPool.push({ input, init, useUnsafe, translator, token });
|
||||
res = await fetchPool.push({ input, init, translator, token });
|
||||
} else {
|
||||
res = await fetchApi({ input, init, useUnsafe, translator, token });
|
||||
res = await fetchApi({ input, init, translator, token });
|
||||
}
|
||||
|
||||
if (!res?.ok) {
|
||||
@@ -135,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,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);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -175,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);
|
||||
}
|
||||
@@ -189,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
102
src/libs/gm.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { fetchGM } from "./fetch";
|
||||
import { genEventName } from "./utils";
|
||||
|
||||
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
|
||||
const MSG_GM_setValue = "setValue";
|
||||
const MSG_GM_getValue = "getValue";
|
||||
const MSG_GM_deleteValue = "deleteValue";
|
||||
const MSG_GM_info = "info";
|
||||
|
||||
/**
|
||||
* 注入页面的脚本,请求并接受GM接口信息
|
||||
* @param {*} param0
|
||||
*/
|
||||
export const injectScript = (ping) => {
|
||||
window.APP_INFO = {
|
||||
name: process.env.REACT_APP_NAME,
|
||||
version: process.env.REACT_APP_VERSION,
|
||||
eventName: ping,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 适配GM脚本
|
||||
*/
|
||||
export const adaptScript = (ping) => {
|
||||
const promiseGM = (action, args, timeout = 5000) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const pong = genEventName();
|
||||
const handleEvent = (e) => {
|
||||
window.removeEventListener(pong, handleEvent);
|
||||
const { data, error } = e.detail;
|
||||
if (error) {
|
||||
reject(new Error(error));
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(pong, handleEvent);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(ping, { detail: { action, args, pong } })
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
window.removeEventListener(pong, handleEvent);
|
||||
reject(new Error("timeout"));
|
||||
}, timeout);
|
||||
});
|
||||
|
||||
window.KISS_GM = {
|
||||
fetch: (input, init) => promiseGM(MSG_GM_xmlHttpRequest, { input, init }),
|
||||
setValue: (key, val) => promiseGM(MSG_GM_setValue, { key, val }),
|
||||
getValue: (key) => promiseGM(MSG_GM_getValue, { key }),
|
||||
deleteValue: (key) => promiseGM(MSG_GM_deleteValue, { key }),
|
||||
getInfo: async () => {
|
||||
if (!window.GM_info) {
|
||||
window.GM_info = await promiseGM(MSG_GM_info);
|
||||
}
|
||||
return window.GM_info;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听并回应页面对GM接口的请求
|
||||
* @param {*} param0
|
||||
*/
|
||||
export const handlePing = async (e) => {
|
||||
const { action, args, pong } = e.detail;
|
||||
let res;
|
||||
try {
|
||||
switch (action) {
|
||||
case MSG_GM_xmlHttpRequest:
|
||||
const { input, init } = args;
|
||||
res = await fetchGM(input, init);
|
||||
break;
|
||||
case MSG_GM_setValue:
|
||||
const { key, val } = args;
|
||||
await GM.setValue(key, val);
|
||||
res = val;
|
||||
break;
|
||||
case MSG_GM_getValue:
|
||||
res = await GM.getValue(args.key);
|
||||
break;
|
||||
case MSG_GM_deleteValue:
|
||||
await GM.deleteValue(args.key);
|
||||
res = "ok";
|
||||
break;
|
||||
case MSG_GM_info:
|
||||
res = GM.info;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`message action is unavailable: ${action}`);
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent(pong, { detail: { data: res } }));
|
||||
} catch (err) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(pong, { detail: { error: err.message } })
|
||||
);
|
||||
}
|
||||
};
|
||||
7
src/libs/iframe.js
Normal file
7
src/libs/iframe.js
Normal 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 }, "*");
|
||||
});
|
||||
};
|
||||
@@ -1,91 +1,15 @@
|
||||
import storage from "./storage";
|
||||
import {
|
||||
DEFAULT_SETTING,
|
||||
STOKEY_SETTING,
|
||||
STOKEY_RULES,
|
||||
STOKEY_FAB,
|
||||
GLOBLA_RULE,
|
||||
GLOBAL_KEY,
|
||||
BUILTIN_RULES,
|
||||
} from "../config";
|
||||
import { CACHE_NAME } from "../config";
|
||||
import { browser } from "./browser";
|
||||
|
||||
/**
|
||||
* 获取节点列表并转为数组
|
||||
* @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匹配规则
|
||||
* TODO: 支持通配符(*)匹配
|
||||
* @param {*} rules
|
||||
* @param {string} href
|
||||
* @returns
|
||||
*/
|
||||
export const matchRule = (rules, href, { injectRules }) => {
|
||||
if (injectRules) {
|
||||
rules.splice(-1, 0, ...BUILTIN_RULES);
|
||||
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) => href.includes(p.trim()))
|
||||
);
|
||||
const globalRule =
|
||||
rules.find((rule) =>
|
||||
rule.pattern.split(",").some((p) => p.trim() === "*")
|
||||
) || GLOBLA_RULE;
|
||||
|
||||
if (!rule) {
|
||||
return globalRule;
|
||||
}
|
||||
|
||||
rule.selector =
|
||||
rule?.selector?.trim() ||
|
||||
globalRule?.selector?.trim() ||
|
||||
GLOBLA_RULE.selector;
|
||||
|
||||
rule.bgColor = rule?.bgColor?.trim() || globalRule?.bgColor?.trim();
|
||||
|
||||
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach(
|
||||
(key) => {
|
||||
if (rule[key] === GLOBAL_KEY) {
|
||||
rule[key] = globalRule[key];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return rule;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -93,7 +17,11 @@ export const matchRule = (rules, href, { injectRules }) => {
|
||||
* @param {*} q
|
||||
* @returns
|
||||
*/
|
||||
export const detectLang = async (q) => {
|
||||
const res = await browser?.i18n.detectLanguage(q);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* 任务池
|
||||
* @param {*} fn
|
||||
* @param {*} preFn
|
||||
* @param {*} _interval
|
||||
* @param {*} _limit
|
||||
* @returns
|
||||
*/
|
||||
export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
||||
const pool = [];
|
||||
const maxRetry = 2; // 最大重试次数
|
||||
@@ -6,11 +14,6 @@ export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
||||
let interval = _interval; // 间隔时间
|
||||
let timer = null;
|
||||
|
||||
/**
|
||||
* 任务池
|
||||
* @param {*} item
|
||||
* @param {*} preArgs
|
||||
*/
|
||||
const handleTask = async (item, preArgs) => {
|
||||
curCount++;
|
||||
const { args, resolve, reject, retry } = item;
|
||||
|
||||
143
src/libs/rules.js
Normal file
143
src/libs/rules.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import { matchValue, type, isMatch } from "./utils";
|
||||
import {
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
GLOBLA_RULE,
|
||||
DEFAULT_SUBRULES_LIST,
|
||||
DEFAULT_OW_RULE,
|
||||
} from "../config";
|
||||
import { loadOrFetchSubRules } from "./subRules";
|
||||
|
||||
/**
|
||||
* 根据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
|
||||
* @param {*} rules
|
||||
* @returns
|
||||
*/
|
||||
export const checkRules = (rules) => {
|
||||
if (type(rules) === "string") {
|
||||
rules = JSON.parse(rules);
|
||||
}
|
||||
if (type(rules) !== "array") {
|
||||
throw new Error("data error");
|
||||
}
|
||||
|
||||
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
|
||||
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
|
||||
const patternSet = new Set();
|
||||
rules = rules
|
||||
.filter((rule) => type(rule) === "object")
|
||||
.filter(({ pattern }) => {
|
||||
if (type(pattern) !== "string" || patternSet.has(pattern.trim())) {
|
||||
return false;
|
||||
}
|
||||
patternSet.add(pattern.trim());
|
||||
return true;
|
||||
})
|
||||
.map(
|
||||
({
|
||||
pattern,
|
||||
selector,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
}) => ({
|
||||
pattern: pattern.trim(),
|
||||
selector: type(selector) === "string" ? selector : "",
|
||||
bgColor: type(bgColor) === "string" ? bgColor : "",
|
||||
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
|
||||
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
|
||||
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
|
||||
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
|
||||
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
|
||||
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
|
||||
})
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
@@ -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
72
src/libs/subRules.js
Normal 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);
|
||||
};
|
||||
122
src/libs/sync.js
122
src/libs/sync.js
@@ -1,78 +1,128 @@
|
||||
import {
|
||||
STOKEY_SYNC,
|
||||
DEFAULT_SYNC,
|
||||
KV_SETTING_KEY,
|
||||
KV_RULES_KEY,
|
||||
STOKEY_SETTING,
|
||||
STOKEY_RULES,
|
||||
KV_RULES_SHARE_KEY,
|
||||
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";
|
||||
|
||||
const loadOpt = async () => (await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC;
|
||||
|
||||
export const syncSetting = async () => {
|
||||
try {
|
||||
const { syncUrl, syncKey, settingUpdateAt } = await loadOpt();
|
||||
/**
|
||||
* 同步设置
|
||||
* @returns
|
||||
*/
|
||||
const syncSetting = async (isBg = false) => {
|
||||
const { syncUrl, syncKey, settingUpdateAt } = await getSyncWithDefault();
|
||||
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, {
|
||||
await updateSync({
|
||||
settingUpdateAt: res.updateAt,
|
||||
settingSyncAt: res.updateAt,
|
||||
});
|
||||
await storage.setObj(STOKEY_SETTING, res.value);
|
||||
await setSetting(res.value);
|
||||
return res.value;
|
||||
} else {
|
||||
await storage.putObj(STOKEY_SYNC, {
|
||||
settingSyncAt: res.updateAt,
|
||||
});
|
||||
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 loadOpt();
|
||||
/**
|
||||
* 同步规则
|
||||
* @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, {
|
||||
await updateSync({
|
||||
rulesUpdateAt: res.updateAt,
|
||||
rulesSyncAt: res.updateAt,
|
||||
});
|
||||
await storage.setObj(STOKEY_RULES, res.value);
|
||||
await setRules(res.value);
|
||||
return res.value;
|
||||
} else {
|
||||
await storage.putObj(STOKEY_SYNC, {
|
||||
rulesSyncAt: res.updateAt,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[sync rules]", err);
|
||||
await updateSync({ rulesSyncAt: res.updateAt });
|
||||
}
|
||||
};
|
||||
|
||||
export const syncAll = async () => {
|
||||
await syncSetting();
|
||||
await syncRules();
|
||||
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,
|
||||
value: rules,
|
||||
updateAt: Date.now(),
|
||||
});
|
||||
const psk = await sha256(syncKey, KV_SALT_SHARE);
|
||||
const shareUrl = `${syncUrl}?psk=${psk}`;
|
||||
return shareUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步个人设置和规则
|
||||
* @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)];
|
||||
};
|
||||
|
||||
@@ -3,20 +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 { StoragesProvider } from "../hooks/Storage";
|
||||
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) => {
|
||||
@@ -31,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;
|
||||
@@ -63,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,
|
||||
@@ -87,16 +144,88 @@ export class Translator {
|
||||
}
|
||||
};
|
||||
|
||||
toggleStyle = () => {
|
||||
const textStyle =
|
||||
this.rule.textStyle === OPT_STYLE_FUZZY
|
||||
? OPT_STYLE_DASHLINE
|
||||
: OPT_STYLE_FUZZY;
|
||||
this.rule = { ...this.rule, textStyle };
|
||||
};
|
||||
|
||||
_querySelectorAll = (selector, node) => {
|
||||
try {
|
||||
return Array.from(node.querySelectorAll(selector));
|
||||
} catch (err) {
|
||||
console.log(`[querySelectorAll err]: ${selector}`);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
_queryFilter = (selector, rootNode) => {
|
||||
return this._querySelectorAll(selector, rootNode).filter(
|
||||
(node) => this._queryFilter(selector, node).length === 0
|
||||
);
|
||||
};
|
||||
|
||||
_queryNodes = (rootNode = document) => {
|
||||
// const childRoots = Array.from(rootNode.querySelectorAll("*"))
|
||||
// .map((item) => item.shadowRoot)
|
||||
// .filter(Boolean);
|
||||
// const childNodes = childRoots.map((item) => this._queryNodes(item));
|
||||
// const nodes = Array.from(rootNode.querySelectorAll(this.rule.selector));
|
||||
// return nodes.concat(childNodes).flat();
|
||||
|
||||
this._rootNodes.add(rootNode);
|
||||
this._rule.selector
|
||||
.split(";")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((selector) => {
|
||||
if (selector.includes(SHADOW_KEY)) {
|
||||
const [outSelector, inSelector] = selector
|
||||
.split(SHADOW_KEY)
|
||||
.map((item) => item.trim());
|
||||
if (outSelector && inSelector) {
|
||||
const outNodes = this._querySelectorAll(outSelector, rootNode);
|
||||
outNodes.forEach((outNode) => {
|
||||
if (outNode.shadowRoot) {
|
||||
this._rootNodes.add(outNode.shadowRoot);
|
||||
this._queryFilter(inSelector, outNode.shadowRoot).forEach(
|
||||
(item) => {
|
||||
if (!this._tranNodes.has(item)) {
|
||||
this._tranNodes.set(item, "");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this._queryFilter(selector, rootNode).forEach((item) => {
|
||||
if (!this._tranNodes.has(item)) {
|
||||
this._tranNodes.set(item, "");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
_register = () => {
|
||||
// 监听节点变化
|
||||
this._mutaObserver.observe(document, {
|
||||
// 搜索节点
|
||||
this._queryNodes();
|
||||
|
||||
this._rootNodes.forEach((node) => {
|
||||
// 监听节点变化;
|
||||
this._mutaObserver.observe(node, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
// characterData: true,
|
||||
});
|
||||
});
|
||||
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
// 监听节点显示
|
||||
queryEls(this.rule.selector).forEach((el) => {
|
||||
this._interseObserver.observe(el);
|
||||
this._interseObserver.observe(node);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -105,49 +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)) {
|
||||
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;";
|
||||
if (el.parentElement) {
|
||||
el.parentElement.style.cssText +=
|
||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||
}
|
||||
|
||||
const root = createRoot(span);
|
||||
root.render(
|
||||
<StoragesProvider>
|
||||
<Content q={q} translator={this} />
|
||||
</StoragesProvider>
|
||||
);
|
||||
const root = createRoot(traEl);
|
||||
root.render(<Content q={q} translator={this} />);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
@@ -51,3 +56,69 @@ export const debounce = (func, delay = 200) => {
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 字符串通配符(*)匹配
|
||||
* @param {*} s
|
||||
* @param {*} p
|
||||
* @returns
|
||||
*/
|
||||
export const isMatch = (s, p) => {
|
||||
if (s.length === 0 || p.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
p = `*${p}*`;
|
||||
|
||||
let [sIndex, pIndex] = [0, 0];
|
||||
let [sRecord, pRecord] = [-1, -1];
|
||||
while (sIndex < s.length && pRecord < p.length) {
|
||||
if (p[pIndex] === "*") {
|
||||
pIndex++;
|
||||
[sRecord, pRecord] = [sIndex, pIndex];
|
||||
} else if (s[sIndex] === p[pIndex]) {
|
||||
sIndex++;
|
||||
pIndex++;
|
||||
} else if (sRecord + 1 < s.length) {
|
||||
sRecord++;
|
||||
[sIndex, pIndex] = [sRecord, pRecord];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (p.length === pIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return p.slice(pIndex).replaceAll("*", "") === "";
|
||||
};
|
||||
|
||||
/**
|
||||
* 类型检查
|
||||
* @param {*} o
|
||||
* @returns
|
||||
*/
|
||||
export const type = (o) => {
|
||||
const s = Object.prototype.toString.call(o);
|
||||
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
|
||||
};
|
||||
|
||||
/**
|
||||
* sha256
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const sha256 = async (text, salt) => {
|
||||
const data = new TextEncoder().encode(text + salt);
|
||||
const digest = await crypto.subtle.digest({ name: "SHA-256" }, data);
|
||||
return [...new Uint8Array(digest)]
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机事件名称
|
||||
* @returns
|
||||
*/
|
||||
export const genEventName = () => btoa(Math.random()).slice(3, 11);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
28
src/rules.js
Normal file
28
src/rules.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { BUILTIN_RULES } from "./config/rules";
|
||||
|
||||
(() => {
|
||||
// rules
|
||||
try {
|
||||
const data = JSON.stringify(BUILTIN_RULES, null, " ");
|
||||
const file = path.resolve(
|
||||
__dirname,
|
||||
"../build/web/kiss-translator-rules.json"
|
||||
);
|
||||
fs.writeFileSync(file, data);
|
||||
console.info(`Built-in rules generated: ${file}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
// version
|
||||
try {
|
||||
var pjson = require("../package.json");
|
||||
const file = path.resolve(__dirname, "../build/web/version.txt");
|
||||
fs.writeFileSync(file, pjson.version);
|
||||
console.info(`Version file generated: ${file}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
@@ -3,41 +3,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)
|
||||
) {
|
||||
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
|
||||
unsafeWindow.GM = GM;
|
||||
unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
|
||||
return;
|
||||
unsafeWindow.APP_INFO = {
|
||||
name: process.env.REACT_APP_NAME,
|
||||
version: process.env.REACT_APP_VERSION,
|
||||
};
|
||||
} else {
|
||||
const ping = genEventName();
|
||||
window.addEventListener(ping, handlePing);
|
||||
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
|
||||
const script = document.createElement("script");
|
||||
script.textContent = `(${injectScript})("${ping}")`;
|
||||
document.head.append(script);
|
||||
}
|
||||
|
||||
// skip iframe
|
||||
if (window.self !== window.top) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 翻译页面
|
||||
const setting = await getSetting();
|
||||
const rules = await getRules();
|
||||
const rule = 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);
|
||||
@@ -54,4 +90,18 @@ import { Translator } from "./libs/translator";
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// 同步订阅规则
|
||||
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);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -76,12 +146,12 @@ export default function Action({ translator, fab }) {
|
||||
windowSize,
|
||||
width: fabWidth,
|
||||
height: fabWidth,
|
||||
left: fab.x ?? windowSize.w - fabWidth,
|
||||
left: fab.x ?? 0,
|
||||
top: fab.y ?? windowSize.h / 2,
|
||||
};
|
||||
|
||||
return (
|
||||
<StoragesProvider>
|
||||
<SettingProvider>
|
||||
<ThemeProvider>
|
||||
<Draggable
|
||||
key="pop"
|
||||
@@ -112,7 +182,9 @@ export default function Action({ translator, fab }) {
|
||||
}
|
||||
>
|
||||
<Paper>
|
||||
{showPopup && (
|
||||
<Popup setShowPopup={setShowPopup} translator={translator} />
|
||||
)}
|
||||
</Paper>
|
||||
</Draggable>
|
||||
<Draggable
|
||||
@@ -137,6 +209,6 @@ export default function Action({ translator, fab }) {
|
||||
}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</StoragesProvider>
|
||||
</SettingProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
175
src/views/Options/OwSubRule.js
Normal file
175
src/views/Options/OwSubRule.js
Normal file
@@ -0,0 +1,175 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_STYLE_DIY,
|
||||
OPT_STYLE_USE_COLOR,
|
||||
} from "../../config";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { useOwSubRule } from "../../hooks/SubRules";
|
||||
|
||||
export default function OwSubRule() {
|
||||
const i18n = useI18n();
|
||||
const { owSubrule, updateOwSubrule } = useOwSubRule();
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
const { name, value } = e.target;
|
||||
updateOwSubrule({ [name]: value });
|
||||
};
|
||||
|
||||
const {
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
} = owSubrule;
|
||||
|
||||
const RemainItem = (
|
||||
<MenuItem key={REMAIN_KEY} value={REMAIN_KEY}>
|
||||
{i18n("remain_unchanged")}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const GlobalItem = (
|
||||
<MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}>
|
||||
{GLOBAL_KEY}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={12}>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transOpen"
|
||||
value={transOpen}
|
||||
label={i18n("translate_switch")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
<MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem>
|
||||
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="translator"
|
||||
value={translator}
|
||||
label={i18n("translate_service")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_TRANS_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="fromLang"
|
||||
value={fromLang}
|
||||
label={i18n("from_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="toLang"
|
||||
value={toLang}
|
||||
label={i18n("to_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="textStyle"
|
||||
value={textStyle}
|
||||
label={i18n("text_style")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
name="bgColor"
|
||||
value={bgColor}
|
||||
label={i18n("bg_color")}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{textStyle === OPT_STYLE_DIY && (
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("diy_style")}
|
||||
helperText={i18n("diy_style_helper")}
|
||||
name="textDiyStyle"
|
||||
value={textDiyStyle}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import 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,
|
||||
@@ -9,9 +11,10 @@ import {
|
||||
OPT_LANGS_TO,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
BUILTIN_RULES,
|
||||
OPT_STYLE_DIY,
|
||||
OPT_STYLE_USE_COLOR,
|
||||
} from "../../config";
|
||||
import { useState, useRef } 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,12 +26,31 @@ 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";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import Radio from "@mui/material/Radio";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ShareIcon from "@mui/icons-material/Share";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import { useSubRules } from "../../hooks/SubRules";
|
||||
import { syncSubRules } from "../../libs/subRules";
|
||||
import { loadOrFetchSubRules } from "../../libs/subRules";
|
||||
import { useAlert } from "../../hooks/Alert";
|
||||
import { syncShareRules } from "../../libs/sync";
|
||||
import { debounce } from "../../libs/utils";
|
||||
import { delSubRules, getSyncWithDefault } from "../../libs/storage";
|
||||
import OwSubRule from "./OwSubRule";
|
||||
|
||||
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();
|
||||
@@ -44,6 +66,7 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
} = formValues;
|
||||
|
||||
const hasSamePattern = (str) => {
|
||||
@@ -61,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) => {
|
||||
@@ -107,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>
|
||||
@@ -154,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>
|
||||
@@ -170,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}
|
||||
@@ -189,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}
|
||||
@@ -208,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}
|
||||
@@ -227,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)}
|
||||
@@ -235,6 +269,7 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
size="small"
|
||||
@@ -246,9 +281,23 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
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 ? (
|
||||
// 编辑
|
||||
@@ -384,13 +433,57 @@ function UploadButton({ onChange, text }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function Rules() {
|
||||
function ShareButton({ rules, injectRules, selectedUrl }) {
|
||||
const alert = useAlert();
|
||||
const i18n = useI18n();
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
const { syncUrl, syncKey } = await getSyncWithDefault();
|
||||
if (!syncUrl || !syncKey) {
|
||||
alert.warning(i18n("error_sync_setting"));
|
||||
return;
|
||||
}
|
||||
|
||||
const shareRules = [...rules.list];
|
||||
if (injectRules) {
|
||||
const subRules = await loadOrFetchSubRules(selectedUrl);
|
||||
shareRules.splice(-1, 0, ...subRules);
|
||||
}
|
||||
|
||||
const url = await syncShareRules({
|
||||
rules: shareRules,
|
||||
syncUrl,
|
||||
syncKey,
|
||||
});
|
||||
|
||||
window.open(url, "_blank");
|
||||
} catch (err) {
|
||||
alert.warning(i18n("error_got_some_wrong"));
|
||||
console.log("[share rules]", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleClick}
|
||||
startIcon={<ShareIcon />}
|
||||
>
|
||||
{"分享"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function UserRules({ subRules }) {
|
||||
const i18n = useI18n();
|
||||
const rules = useRules();
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const setting = useSetting();
|
||||
const updateSetting = useSettingUpdate();
|
||||
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];
|
||||
@@ -420,10 +513,21 @@ export default function Rules() {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!showAdd) {
|
||||
setKeyword("");
|
||||
}
|
||||
}, [showAdd]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
useFlexGap
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
@@ -442,6 +546,12 @@ export default function Rules() {
|
||||
text={i18n("export")}
|
||||
/>
|
||||
|
||||
<ShareButton
|
||||
rules={rules}
|
||||
injectRules={injectRules}
|
||||
selectedUrl={selectedUrl}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
@@ -454,22 +564,272 @@ export default function Rules() {
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{showAdd && <RuleFields rules={rules} setShow={setShowAdd} />}
|
||||
{showAdd && (
|
||||
<RuleFields
|
||||
rules={rules}
|
||||
setShow={setShowAdd}
|
||||
setKeyword={setKeyword}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
{rules.list.map((rule) => (
|
||||
{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>
|
||||
{BUILTIN_RULES.map((rule) => (
|
||||
{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, delSub, setSelectedRules }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleDel = async () => {
|
||||
try {
|
||||
await delSub(url);
|
||||
await delSubRules(url);
|
||||
} catch (err) {
|
||||
console.log("[del subrules]", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const rules = await syncSubRules(url);
|
||||
if (rules.length > 0 && url === selectedUrl) {
|
||||
setSelectedRules(rules);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[sync sub rules]", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<FormControlLabel value={url} control={<Radio />} label={url} />
|
||||
|
||||
{loading ? (
|
||||
<CircularProgress size={16} />
|
||||
) : (
|
||||
<IconButton size="small" onClick={handleSync}>
|
||||
<SyncIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{index !== 0 && selectedUrl !== url && (
|
||||
<IconButton size="small" onClick={handleDel}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function SubRulesEdit({ subList, addSub }) {
|
||||
const i18n = useI18n();
|
||||
const [inputText, setInputText] = useState("");
|
||||
const [inputError, setInputError] = useState("");
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleCancel = (e) => {
|
||||
e.preventDefault();
|
||||
setShowInput(false);
|
||||
setInputText("");
|
||||
setInputError("");
|
||||
};
|
||||
|
||||
const handleSave = async (e) => {
|
||||
e.preventDefault();
|
||||
const url = inputText.trim();
|
||||
|
||||
if (!url) {
|
||||
setInputError(i18n("error_cant_be_blank"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (subList.find((item) => item.url === url)) {
|
||||
setInputError(i18n("error_duplicate_values"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const rules = await syncSubRules(url);
|
||||
if (rules.length === 0) {
|
||||
throw new Error("empty rules");
|
||||
}
|
||||
await addSub(url);
|
||||
setShowInput(false);
|
||||
setInputText("");
|
||||
} catch (err) {
|
||||
console.log("[fetch rules]", err);
|
||||
setInputError(i18n("error_fetch_url"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (e) => {
|
||||
e.preventDefault();
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
|
||||
const handleFocus = (e) => {
|
||||
e.preventDefault();
|
||||
setInputError("");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
disabled={showInput}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowInput(true);
|
||||
}}
|
||||
>
|
||||
{i18n("add")}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{showInput && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
value={inputText}
|
||||
error={!!inputError}
|
||||
helperText={inputError}
|
||||
onChange={handleInput}
|
||||
onFocus={handleFocus}
|
||||
label={i18n("subscribe_url")}
|
||||
/>
|
||||
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
>
|
||||
{i18n("save")}
|
||||
</Button>
|
||||
<Button size="small" variant="outlined" onClick={handleCancel}>
|
||||
{i18n("cancel")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SubRules({ subRules }) {
|
||||
const {
|
||||
subList,
|
||||
selectSub,
|
||||
addSub,
|
||||
delSub,
|
||||
selectedUrl,
|
||||
selectedRules,
|
||||
setSelectedRules,
|
||||
loading,
|
||||
} = subRules;
|
||||
|
||||
const handleSelect = (e) => {
|
||||
const url = e.target.value;
|
||||
selectSub(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<SubRulesEdit subList={subList} addSub={addSub} />
|
||||
|
||||
<RadioGroup value={selectedUrl} onChange={handleSelect}>
|
||||
{subList.map((item, index) => (
|
||||
<SubRulesItem
|
||||
key={item.url}
|
||||
url={item.url}
|
||||
index={index}
|
||||
selectedUrl={selectedUrl}
|
||||
delSub={delSub}
|
||||
setSelectedRules={setSelectedRules}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
|
||||
<Box>
|
||||
{loading ? (
|
||||
<center>
|
||||
<CircularProgress />
|
||||
</center>
|
||||
) : (
|
||||
selectedRules.map((rule) => (
|
||||
<RuleAccordion key={rule.pattern} rule={rule} />
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Rules() {
|
||||
const i18n = useI18n();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const subRules = useSubRules();
|
||||
|
||||
const handleTabChange = (e, newValue) => {
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<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("personal_rules")} />
|
||||
<Tab label={i18n("subscribe_rules")} />
|
||||
<Tab label={i18n("overwrite_subscribe_rules")} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<div hidden={activeTab !== 0}>
|
||||
{activeTab === 0 && <UserRules subRules={subRules} />}
|
||||
</div>
|
||||
<div hidden={activeTab !== 1}>
|
||||
{activeTab === 1 && <SubRules subRules={subRules} />}
|
||||
</div>
|
||||
<div hidden={activeTab !== 2}>{activeTab === 2 && <OwSubRule />}</div>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,20 +5,17 @@ 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) => {
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
let { name, value } = e.target;
|
||||
switch (name) {
|
||||
@@ -28,29 +25,37 @@ export default function Settings() {
|
||||
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,
|
||||
});
|
||||
}, 500),
|
||||
[updateSetting]
|
||||
);
|
||||
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
/>
|
||||
|
||||
@@ -6,30 +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 { 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((e) => {
|
||||
const handleChange = async (e) => {
|
||||
e.preventDefault();
|
||||
const { name, value } = e.target;
|
||||
sync.update({
|
||||
await updateSync({
|
||||
[name]: value,
|
||||
});
|
||||
}, 500),
|
||||
[sync]
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
@@ -40,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>
|
||||
@@ -52,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>
|
||||
);
|
||||
|
||||
@@ -4,16 +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(() => {
|
||||
@@ -22,53 +28,97 @@ 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 {
|
||||
// 同步数据
|
||||
syncAll();
|
||||
await trySyncSettingAndRules();
|
||||
setReady(true);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
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>
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
@@ -79,7 +129,8 @@ export default function Options() {
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</AlertProvider>
|
||||
</ThemeProvider>
|
||||
</StoragesProvider>
|
||||
</SettingProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,6 +173,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||
<TextField
|
||||
size="small"
|
||||
name="bgColor"
|
||||
@@ -175,6 +181,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
label={i18n("bg_color")}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button variant="text" onClick={handleOpenSetting}>
|
||||
{i18n("setting")}
|
||||
|
||||
Reference in New Issue
Block a user