Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
26
.env
26
.env
@@ -2,11 +2,25 @@ GENERATE_SOURCEMAP=false
|
|||||||
|
|
||||||
REACT_APP_NAME=KISS Translator
|
REACT_APP_NAME=KISS Translator
|
||||||
REACT_APP_NAME_CN=简约翻译
|
REACT_APP_NAME_CN=简约翻译
|
||||||
REACT_APP_VERSION=1.5.2
|
REACT_APP_VERSION=1.6.0
|
||||||
|
|
||||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||||
REACT_APP_OPTIONSPAGE=https://kiss-translator.rayjar.com/options
|
|
||||||
REACT_APP_OPTIONSPAGE2=https://fishjar.github.io/kiss-translator/options.html
|
REACT_APP_OPTIONSPAGE=https://fishjar.github.io/kiss-translator/options.html
|
||||||
|
REACT_APP_OPTIONSPAGE2=https://kiss-translator.rayjar.com/options
|
||||||
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
|
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
|
||||||
REACT_APP_LOGOURL=https://kiss-translator.rayjar.com/images/logo192.png
|
|
||||||
REACT_APP_USERSCRIPT_DOWNLOADURL=https://kiss-translator.rayjar.com/kiss-translator.user.js
|
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
|
||||||
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
|
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
|
||||||
|
|||||||
16
README.en.md
16
README.en.md
@@ -32,17 +32,17 @@ If you also like a little more simplicity, welcome to pick it up.
|
|||||||
- [x] Microsoft
|
- [x] Microsoft
|
||||||
- [x] OpenAI
|
- [x] OpenAI
|
||||||
- [ ] DeepL
|
- [ ] DeepL
|
||||||
- [ ] Upload to app Store
|
- [x] Upload to app Store
|
||||||
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
|
- [x] Chrome [Install Link](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
|
||||||
- [ ] Edge
|
- [x] Edge [Install Link](https://microsoftedge.microsoft.com/addons/detail/kiss-translator/jemckldkclkinpjighnoilpbldbdmmlh)
|
||||||
- [x] [Firefox](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
|
- [x] Firefox [Install Link](https://addons.mozilla.org/en-US/firefox/addon/kiss-translator/)
|
||||||
- [ ] Safari
|
- [ ] 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] Open source
|
||||||
- [x] Data Synchronization Function
|
- [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] 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)
|
- [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)
|
||||||
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (need test)
|
- [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
|
||||||
|
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -32,17 +32,17 @@
|
|||||||
- [x] Microsoft
|
- [x] Microsoft
|
||||||
- [x] OpenAI
|
- [x] OpenAI
|
||||||
- [ ] DeepL
|
- [ ] DeepL
|
||||||
- [ ] 上架应用市场
|
- [x] 上架应用市场
|
||||||
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
||||||
- [ ] Edge
|
- [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/)
|
- [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
|
||||||
- [ ] Safari
|
- [ ] 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] 数据同步功能
|
- [x] 数据同步功能
|
||||||
- [x] 油猴脚本([链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js))
|
- [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)
|
- [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)
|
||||||
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (待测)
|
- [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)
|
||||||
|
|
||||||
### 指引
|
### 指引
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ const userscriptWebpack = (config, env) => {
|
|||||||
// @grant GM.setValue
|
// @grant GM.setValue
|
||||||
// @grant GM.getValue
|
// @grant GM.getValue
|
||||||
// @grant GM.deleteValue
|
// @grant GM.deleteValue
|
||||||
|
// @grant GM.info
|
||||||
// @grant unsafeWindow
|
// @grant unsafeWindow
|
||||||
// @connect translate.googleapis.com
|
// @connect translate.googleapis.com
|
||||||
// @connect api-edge.cognitive.microsofttranslator.com
|
// @connect api-edge.cognitive.microsofttranslator.com
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kiss-translator",
|
"name": "kiss-translator",
|
||||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||||
"version": "1.5.2",
|
"version": "1.6.0",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -25,8 +25,9 @@
|
|||||||
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json",
|
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json",
|
||||||
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
|
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
|
||||||
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/kiss-translator.user.js build/userscript/kiss-translator.user.js",
|
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/kiss-translator.user.js build/userscript/kiss-translator.user.js",
|
||||||
|
"build:userscript-ios": "file1=build/userscript/kiss-translator.user.js file2=build/userscript/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
|
||||||
"build:rules": "babel-node src/rules.js",
|
"build:rules": "babel-node src/rules.js",
|
||||||
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript && yarn build:rules",
|
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript && yarn build:userscript-ios && yarn build:rules",
|
||||||
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
|
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
|
||||||
"test": "react-app-rewired test",
|
"test": "react-app-rewired test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
|
|||||||
@@ -15,12 +15,63 @@
|
|||||||
max-height: 1.2em;
|
max-height: 1.2em;
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root">
|
<div id="root">
|
||||||
<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 />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@@ -53,7 +104,16 @@
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<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 />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@@ -86,7 +146,10 @@
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<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 />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@@ -119,7 +182,17 @@
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<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 />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@@ -153,175 +226,42 @@
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<div class="cont cont1">
|
<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>
|
<h2>
|
||||||
React is a <code>JavaScript</code> <a href="#">library</a> for
|
Server Components can run during the build, letting you read from the
|
||||||
building user interfaces.
|
filesystem or fetch static content.
|
||||||
</h2>
|
</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Declarative: React makes it painless to create interactive UIs.
|
They can also run on the server, letting you access your data layer
|
||||||
Design simple views for each state in your application, and React
|
without having to build an API. You can pass data by props from
|
||||||
will efficiently update and render just the right components when
|
Server Components to the interactive Client Components in the
|
||||||
your data changes. Declarative views make your code more
|
browser.
|
||||||
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>
|
||||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<p></p>
|
<br />
|
||||||
<div class="cont cont5">
|
<div class="cont cont2">
|
||||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
<h2>
|
||||||
|
Since our last update, we have merged the React Server Components RFC
|
||||||
|
to ratify the proposal.
|
||||||
|
</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Declarative: React makes it painless to create interactive UIs.
|
RSC combines the simple “request/response” mental model of
|
||||||
Design simple views for each state in your application, and React
|
server-centric Multi-Page Apps with the seamless interactivity of
|
||||||
will efficiently update and render just the right components when
|
client-centric Single-Page Apps, giving you the best of both worlds.
|
||||||
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>
|
||||||
<li>
|
<li>
|
||||||
React 使创建交互式 UI
|
React 使创建交互式 UI
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "1.5.2",
|
"version": "1.6.0",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
@@ -12,7 +12,8 @@
|
|||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"js": ["content.js"],
|
"js": ["content.js"],
|
||||||
"matches": ["<all_urls>"]
|
"matches": ["<all_urls>"],
|
||||||
|
"all_frames": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"commands": {
|
"commands": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "1.5.2",
|
"version": "1.6.0",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"js": ["content.js"],
|
"js": ["content.js"],
|
||||||
"matches": ["<all_urls>"]
|
"matches": ["<all_urls>"],
|
||||||
|
"all_frames": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"commands": {
|
"commands": {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
PROMPT_PLACE_TO,
|
PROMPT_PLACE_TO,
|
||||||
KV_SALT_SYNC,
|
KV_SALT_SYNC,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { getSetting, detectLang } from "../libs";
|
import { tryDetectLang } from "../libs";
|
||||||
import { sha256 } from "../libs/utils";
|
import { sha256 } from "../libs/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,19 +20,25 @@ import { sha256 } from "../libs/utils";
|
|||||||
* @param {*} data
|
* @param {*} data
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiSyncData = async (url, key, data) =>
|
export const apiSyncData = async (url, key, data, isBg = false) =>
|
||||||
fetchPolyfill(
|
fetchPolyfill(url, {
|
||||||
url,
|
|
||||||
{
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
||||||
},
|
},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
},
|
isBg,
|
||||||
{ useUnsafe: true }
|
});
|
||||||
);
|
|
||||||
|
/**
|
||||||
|
* 下载订阅规则
|
||||||
|
* @param {*} url
|
||||||
|
* @param {*} isBg
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiFetchRules = (url, isBg = false) =>
|
||||||
|
fetchPolyfill(url, { isBg });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 谷歌翻译
|
* 谷歌翻译
|
||||||
@@ -41,7 +47,8 @@ export const apiSyncData = async (url, key, data) =>
|
|||||||
* @param {*} from
|
* @param {*} from
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const apiGoogleTranslate = async (translator, text, to, from) => {
|
const apiGoogleTranslate = async (translator, text, to, from, setting) => {
|
||||||
|
const { googleUrl } = setting;
|
||||||
const params = {
|
const params = {
|
||||||
client: "gtx",
|
client: "gtx",
|
||||||
dt: "t",
|
dt: "t",
|
||||||
@@ -51,17 +58,15 @@ const apiGoogleTranslate = async (translator, text, to, from) => {
|
|||||||
tl: to,
|
tl: to,
|
||||||
q: text,
|
q: text,
|
||||||
};
|
};
|
||||||
const { googleUrl } = await getSetting();
|
|
||||||
const input = `${googleUrl}?${queryString.stringify(params)}`;
|
const input = `${googleUrl}?${queryString.stringify(params)}`;
|
||||||
return fetchPolyfill(
|
return fetchPolyfill(input, {
|
||||||
input,
|
|
||||||
{
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
},
|
},
|
||||||
},
|
useCache: true,
|
||||||
{ useCache: true, usePool: true, translator }
|
usePool: true,
|
||||||
);
|
translator,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,17 +83,16 @@ const apiMicrosoftTranslate = (translator, text, to, from) => {
|
|||||||
"api-version": "3.0",
|
"api-version": "3.0",
|
||||||
};
|
};
|
||||||
const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`;
|
const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`;
|
||||||
return fetchPolyfill(
|
return fetchPolyfill(input, {
|
||||||
input,
|
|
||||||
{
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
},
|
},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify([{ Text: text }]),
|
body: JSON.stringify([{ Text: text }]),
|
||||||
},
|
useCache: true,
|
||||||
{ useCache: true, usePool: true, translator }
|
usePool: true,
|
||||||
);
|
translator,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,15 +102,12 @@ const apiMicrosoftTranslate = (translator, text, to, from) => {
|
|||||||
* @param {*} from
|
* @param {*} from
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const apiOpenaiTranslate = async (translator, text, to, from) => {
|
const apiOpenaiTranslate = async (translator, text, to, from, setting) => {
|
||||||
const { openaiUrl, openaiKey, openaiModel, openaiPrompt } =
|
const { openaiUrl, openaiKey, openaiModel, openaiPrompt } = setting;
|
||||||
await getSetting();
|
|
||||||
let prompt = openaiPrompt
|
let prompt = openaiPrompt
|
||||||
.replaceAll(PROMPT_PLACE_FROM, from)
|
.replaceAll(PROMPT_PLACE_FROM, from)
|
||||||
.replaceAll(PROMPT_PLACE_TO, to);
|
.replaceAll(PROMPT_PLACE_TO, to);
|
||||||
return fetchPolyfill(
|
return fetchPolyfill(openaiUrl, {
|
||||||
openaiUrl,
|
|
||||||
{
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
},
|
},
|
||||||
@@ -126,9 +127,11 @@ const apiOpenaiTranslate = async (translator, text, to, from) => {
|
|||||||
temperature: 0,
|
temperature: 0,
|
||||||
max_tokens: 256,
|
max_tokens: 256,
|
||||||
}),
|
}),
|
||||||
},
|
useCache: true,
|
||||||
{ useCache: true, usePool: true, translator, token: openaiKey }
|
usePool: true,
|
||||||
);
|
translator,
|
||||||
|
token: openaiKey,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,7 +139,13 @@ const apiOpenaiTranslate = async (translator, text, to, from) => {
|
|||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiTranslate = async ({ translator, q, fromLang, toLang }) => {
|
export const apiTranslate = async ({
|
||||||
|
translator,
|
||||||
|
q,
|
||||||
|
fromLang,
|
||||||
|
toLang,
|
||||||
|
setting,
|
||||||
|
}) => {
|
||||||
let trText = "";
|
let trText = "";
|
||||||
let isSame = false;
|
let isSame = false;
|
||||||
|
|
||||||
@@ -144,7 +153,7 @@ export const apiTranslate = async ({ translator, q, fromLang, toLang }) => {
|
|||||||
let to = OPT_LANGS_SPECIAL?.[translator]?.get(toLang) ?? toLang;
|
let to = OPT_LANGS_SPECIAL?.[translator]?.get(toLang) ?? toLang;
|
||||||
|
|
||||||
if (translator === OPT_TRANS_GOOGLE) {
|
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(" ");
|
trText = res.sentences.map((item) => item.trans).join(" ");
|
||||||
isSame = to === res.src;
|
isSame = to === res.src;
|
||||||
} else if (translator === OPT_TRANS_MICROSOFT) {
|
} else if (translator === OPT_TRANS_MICROSOFT) {
|
||||||
@@ -152,9 +161,11 @@ export const apiTranslate = async ({ translator, q, fromLang, toLang }) => {
|
|||||||
trText = res[0].translations[0].text;
|
trText = res[0].translations[0].text;
|
||||||
isSame = to === res[0].detectedLanguage.language;
|
isSame = to === res[0].detectedLanguage.language;
|
||||||
} else if (translator === OPT_TRANS_OPENAI) {
|
} 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;
|
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];
|
return [trText, isSame];
|
||||||
|
|||||||
@@ -7,29 +7,19 @@ import {
|
|||||||
MSG_TRANS_TOGGLE_STYLE,
|
MSG_TRANS_TOGGLE_STYLE,
|
||||||
CMD_TOGGLE_TRANSLATE,
|
CMD_TOGGLE_TRANSLATE,
|
||||||
CMD_TOGGLE_STYLE,
|
CMD_TOGGLE_STYLE,
|
||||||
DEFAULT_SETTING,
|
|
||||||
DEFAULT_RULES,
|
|
||||||
DEFAULT_SYNC,
|
|
||||||
STOKEY_SETTING,
|
|
||||||
STOKEY_RULES,
|
|
||||||
STOKEY_SYNC,
|
|
||||||
CACHE_NAME,
|
|
||||||
} from "./config";
|
} from "./config";
|
||||||
import storage from "./libs/storage";
|
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
|
||||||
import { getSetting } from "./libs";
|
import { trySyncSettingAndRules } from "./libs/sync";
|
||||||
import { syncAll } from "./libs/sync";
|
|
||||||
import { fetchData, fetchPool } from "./libs/fetch";
|
import { fetchData, fetchPool } from "./libs/fetch";
|
||||||
import { sendTabMsg } from "./libs/msg";
|
import { sendTabMsg } from "./libs/msg";
|
||||||
|
import { trySyncAllSubRules } from "./libs/subRules";
|
||||||
|
import { tryClearCaches } from "./libs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件安装
|
* 插件安装
|
||||||
*/
|
*/
|
||||||
browser.runtime.onInstalled.addListener(() => {
|
browser.runtime.onInstalled.addListener(() => {
|
||||||
console.log("KISS Translator onInstalled");
|
tryInitDefaultData();
|
||||||
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
|
|
||||||
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES);
|
|
||||||
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
|
|
||||||
// todo:缓存内置rules
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,13 +29,16 @@ browser.runtime.onStartup.addListener(async () => {
|
|||||||
console.log("browser onStartup");
|
console.log("browser onStartup");
|
||||||
|
|
||||||
// 同步数据
|
// 同步数据
|
||||||
await syncAll();
|
await trySyncSettingAndRules(true);
|
||||||
|
|
||||||
// 清除缓存
|
// 清除缓存
|
||||||
const { clearCache } = await getSetting();
|
const setting = await getSettingWithDefault();
|
||||||
if (clearCache) {
|
if (setting.clearCache) {
|
||||||
caches.delete(CACHE_NAME);
|
tryClearCaches();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步订阅规则
|
||||||
|
trySyncAllSubRules(setting, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,8 +48,8 @@ browser.runtime.onMessage.addListener(
|
|||||||
({ action, args }, sender, sendResponse) => {
|
({ action, args }, sender, sendResponse) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MSG_FETCH:
|
case MSG_FETCH:
|
||||||
const { input, init, opts } = args;
|
const { input, opts } = args;
|
||||||
fetchData(input, init, opts)
|
fetchData(input, opts)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
sendResponse({ data });
|
sendResponse({ data });
|
||||||
})
|
})
|
||||||
|
|||||||
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: `翻译`,
|
zh: `翻译`,
|
||||||
en: `Translate`,
|
en: `Translate`,
|
||||||
},
|
},
|
||||||
|
translate_alt: {
|
||||||
|
zh: `翻译 (Alt+Q)`,
|
||||||
|
en: `Translate (Alt+Q)`,
|
||||||
|
},
|
||||||
basic_setting: {
|
basic_setting: {
|
||||||
zh: `基本设置`,
|
zh: `基本设置`,
|
||||||
en: `Basic Setting`,
|
en: `Basic Setting`,
|
||||||
@@ -41,12 +45,20 @@ export const I18N = {
|
|||||||
en: `Interface Language`,
|
en: `Interface Language`,
|
||||||
},
|
},
|
||||||
fetch_limit: {
|
fetch_limit: {
|
||||||
zh: `最大请求数量`,
|
zh: `最大请求数量 (1-100)`,
|
||||||
en: `Maximum Number Of Request`,
|
en: `Maximum Number Of Request (1-100)`,
|
||||||
},
|
},
|
||||||
fetch_interval: {
|
fetch_interval: {
|
||||||
zh: `请求间隔时间(ms)`,
|
zh: `请求间隔时间 (0-5000ms)`,
|
||||||
en: `Request Interval(ms)`,
|
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)`,
|
||||||
},
|
},
|
||||||
translate_service: {
|
translate_service: {
|
||||||
zh: `翻译服务`,
|
zh: `翻译服务`,
|
||||||
@@ -64,6 +76,10 @@ export const I18N = {
|
|||||||
zh: `文字样式`,
|
zh: `文字样式`,
|
||||||
en: `Text Style`,
|
en: `Text Style`,
|
||||||
},
|
},
|
||||||
|
text_style_alt: {
|
||||||
|
zh: `文字样式 (Alt+C)`,
|
||||||
|
en: `Text Style (Alt+C)`,
|
||||||
|
},
|
||||||
bg_color: {
|
bg_color: {
|
||||||
zh: `样式颜色`,
|
zh: `样式颜色`,
|
||||||
en: `Style Color`,
|
en: `Style Color`,
|
||||||
@@ -108,9 +124,9 @@ export const I18N = {
|
|||||||
zh: `注入订阅规则`,
|
zh: `注入订阅规则`,
|
||||||
en: `Inject Subscribe Rules`,
|
en: `Inject Subscribe Rules`,
|
||||||
},
|
},
|
||||||
edit_rules: {
|
personal_rules: {
|
||||||
zh: `编辑规则`,
|
zh: `个人规则`,
|
||||||
en: `Edit Rules`,
|
en: `Personal Rules`,
|
||||||
},
|
},
|
||||||
subscribe_rules: {
|
subscribe_rules: {
|
||||||
zh: `订阅规则`,
|
zh: `订阅规则`,
|
||||||
@@ -120,6 +136,14 @@ export const I18N = {
|
|||||||
zh: `订阅地址`,
|
zh: `订阅地址`,
|
||||||
en: `Subscribe URL`,
|
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: {
|
sync_warn: {
|
||||||
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
|
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
|
||||||
en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`,
|
en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`,
|
||||||
@@ -169,8 +193,8 @@ export const I18N = {
|
|||||||
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`,
|
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`,
|
||||||
},
|
},
|
||||||
selector_helper: {
|
selector_helper: {
|
||||||
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。`,
|
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`,
|
||||||
en: `1. Follow CSS selector rules. 2. Leave blank to adopt the global setting.`,
|
en: `1. Follow the CSS selector rules. 2. Leave blank to adopt the global setting. 3. Separate multiple CSS selectors with ";". 4. The "shadow root" selector and the internal selector are separated by ">>>".`,
|
||||||
},
|
},
|
||||||
translate_switch: {
|
translate_switch: {
|
||||||
zh: `开启翻译`,
|
zh: `开启翻译`,
|
||||||
@@ -248,12 +272,24 @@ export const I18N = {
|
|||||||
zh: `数据同步密钥`,
|
zh: `数据同步密钥`,
|
||||||
en: `Data Sync Key`,
|
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: {
|
error_got_some_wrong: {
|
||||||
zh: "抱歉,出错了!",
|
zh: `抱歉,出错了!`,
|
||||||
en: "Sorry, something went wrong!",
|
en: `Sorry, something went wrong!`,
|
||||||
},
|
},
|
||||||
error_sync_setting: {
|
error_sync_setting: {
|
||||||
zh: "您的同步设置未填写,无法在线分享。",
|
zh: `您的同步设置未填写,无法在线分享。`,
|
||||||
en: "Your sync settings are missing and cannot be shared online.",
|
en: `Your sync settings are missing and cannot be shared online.`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
DEFAULT_SELECTOR,
|
DEFAULT_SELECTOR,
|
||||||
GLOBAL_KEY,
|
GLOBAL_KEY,
|
||||||
|
SHADOW_KEY,
|
||||||
DEFAULT_RULE,
|
DEFAULT_RULE,
|
||||||
BUILTIN_RULES,
|
BUILTIN_RULES,
|
||||||
} from "./rules";
|
} from "./rules";
|
||||||
|
import { APP_NAME, APP_LCNAME } from "./app";
|
||||||
export { I18N, UI_LANGS } from "./i18n";
|
export { I18N, UI_LANGS } from "./i18n";
|
||||||
export { GLOBAL_KEY, DEFAULT_RULE, BUILTIN_RULES };
|
export { GLOBAL_KEY, SHADOW_KEY, DEFAULT_RULE, BUILTIN_RULES, APP_LCNAME };
|
||||||
|
|
||||||
const APP_NAME = process.env.REACT_APP_NAME.trim().split(/\s+/).join("-");
|
|
||||||
|
|
||||||
export const APP_LCNAME = APP_NAME.toLowerCase();
|
|
||||||
|
|
||||||
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
|
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
|
||||||
export const STOKEY_SETTING = `${APP_NAME}_setting`;
|
export const STOKEY_SETTING = `${APP_NAME}_setting`;
|
||||||
@@ -157,19 +155,25 @@ export const GLOBLA_RULE = {
|
|||||||
// 订阅列表
|
// 订阅列表
|
||||||
export const DEFAULT_SUBRULES_LIST = [
|
export const DEFAULT_SUBRULES_LIST = [
|
||||||
{
|
{
|
||||||
url: "https://kiss-translator.rayjar.com/kiss-translator-rules.json",
|
url: process.env.REACT_APP_RULESURL,
|
||||||
selected: true,
|
selected: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: "https://fishjar.github.io/kiss-translator/kiss-translator-rules.json",
|
url: process.env.REACT_APP_RULESURL2,
|
||||||
|
selected: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
||||||
|
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
||||||
|
|
||||||
export const DEFAULT_SETTING = {
|
export const DEFAULT_SETTING = {
|
||||||
darkMode: false, // 深色模式
|
darkMode: false, // 深色模式
|
||||||
uiLang: "en", // 界面语言
|
uiLang: "en", // 界面语言
|
||||||
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
||||||
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
||||||
|
minLength: TRANS_MIN_LENGTH,
|
||||||
|
maxLength: TRANS_MAX_LENGTH,
|
||||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||||
injectRules: true, // 是否注入订阅规则
|
injectRules: true, // 是否注入订阅规则
|
||||||
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
||||||
@@ -182,9 +186,6 @@ export const DEFAULT_SETTING = {
|
|||||||
|
|
||||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||||
|
|
||||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
|
||||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
|
||||||
|
|
||||||
export const DEFAULT_SYNC = {
|
export const DEFAULT_SYNC = {
|
||||||
syncUrl: "", // 数据同步接口
|
syncUrl: "", // 数据同步接口
|
||||||
syncKey: "", // 数据同步密钥
|
syncKey: "", // 数据同步密钥
|
||||||
@@ -192,4 +193,5 @@ export const DEFAULT_SYNC = {
|
|||||||
settingSyncAt: 0,
|
settingSyncAt: 0,
|
||||||
rulesUpdateAt: 0,
|
rulesUpdateAt: 0,
|
||||||
rulesSyncAt: 0,
|
rulesSyncAt: 0,
|
||||||
|
subRulesSyncAt: 0, // 订阅规则同步时间
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export const DEFAULT_SELECTOR = `:is(${els})`;
|
|||||||
|
|
||||||
export const GLOBAL_KEY = "*";
|
export const GLOBAL_KEY = "*";
|
||||||
|
|
||||||
|
export const SHADOW_KEY = ">>>";
|
||||||
|
|
||||||
export const DEFAULT_RULE = {
|
export const DEFAULT_RULE = {
|
||||||
pattern: "",
|
pattern: "",
|
||||||
selector: "",
|
selector: "",
|
||||||
@@ -21,9 +23,13 @@ const RULES = [
|
|||||||
selector: `h3, .IsZvec, .VwiC3b`,
|
selector: `h3, .IsZvec, .VwiC3b`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: `https://news.google.com/`,
|
pattern: `news.google.com`,
|
||||||
selector: `h4`,
|
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`,
|
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,
|
||||||
selector: DEFAULT_SELECTOR,
|
selector: DEFAULT_SELECTOR,
|
||||||
|
|||||||
@@ -5,16 +5,19 @@ import {
|
|||||||
MSG_TRANS_GETRULE,
|
MSG_TRANS_GETRULE,
|
||||||
MSG_TRANS_PUTRULE,
|
MSG_TRANS_PUTRULE,
|
||||||
} from "./config";
|
} from "./config";
|
||||||
import { getSetting, getRules, matchRule } from "./libs";
|
import { getSettingWithDefault, getRulesWithDefault } from "./libs/storage";
|
||||||
import { Translator } from "./libs/translator";
|
import { Translator } from "./libs/translator";
|
||||||
|
import { isIframe } from "./libs/iframe";
|
||||||
|
import { matchRule } from "./libs/rules";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 入口函数
|
* 入口函数
|
||||||
*/
|
*/
|
||||||
(async () => {
|
const init = async () => {
|
||||||
const setting = await getSetting();
|
const href = isIframe ? document.referrer : document.location.href;
|
||||||
const rules = await getRules();
|
const setting = await getSettingWithDefault();
|
||||||
const rule = await matchRule(rules, document.location.href, setting);
|
const rules = await getRulesWithDefault();
|
||||||
|
const rule = await matchRule(rules, href, setting);
|
||||||
const translator = new Translator(rule, setting);
|
const translator = new Translator(rule, setting);
|
||||||
|
|
||||||
// 监听消息
|
// 监听消息
|
||||||
@@ -36,4 +39,15 @@ import { Translator } from "./libs/translator";
|
|||||||
}
|
}
|
||||||
return { data: translator.rule };
|
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);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -20,11 +20,6 @@ export function AlertProvider({ children }) {
|
|||||||
const [severity, setSeverity] = useState("info");
|
const [severity, setSeverity] = useState("info");
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
|
|
||||||
const error = (msg) => showAlert(msg, "error");
|
|
||||||
const warning = (msg) => showAlert(msg, "warning");
|
|
||||||
const info = (msg) => showAlert(msg, "info");
|
|
||||||
const success = (msg) => showAlert(msg, "success");
|
|
||||||
|
|
||||||
const showAlert = (msg, type) => {
|
const showAlert = (msg, type) => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
setMessage(msg);
|
setMessage(msg);
|
||||||
@@ -38,6 +33,11 @@ export function AlertProvider({ children }) {
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const error = (msg) => showAlert(msg, "error");
|
||||||
|
const warning = (msg) => showAlert(msg, "warning");
|
||||||
|
const info = (msg) => showAlert(msg, "info");
|
||||||
|
const success = (msg) => showAlert(msg, "success");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertContext.Provider value={{ error, warning, info, success }}>
|
<AlertContext.Provider value={{ error, warning, info, success }}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import { useSetting, useSettingUpdate } from "./Setting";
|
import { useCallback } from "react";
|
||||||
|
import { useSetting } from "./Setting";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 深色模式hook
|
* 深色模式hook
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useDarkMode() {
|
export function useDarkMode() {
|
||||||
const setting = useSetting();
|
const {
|
||||||
return !!setting?.darkMode;
|
setting: { darkMode },
|
||||||
}
|
updateSetting,
|
||||||
|
} = useSetting();
|
||||||
|
|
||||||
/**
|
const toggleDarkMode = useCallback(async () => {
|
||||||
* 切换深色模式
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function useDarkModeSwitch() {
|
|
||||||
const darkMode = useDarkMode();
|
|
||||||
const updateSetting = useSettingUpdate();
|
|
||||||
return async () => {
|
|
||||||
await updateSetting({ darkMode: !darkMode });
|
await updateSetting({ darkMode: !darkMode });
|
||||||
};
|
}, [darkMode, updateSetting]);
|
||||||
|
|
||||||
|
return { darkMode, toggleDarkMode };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { useFetch } from "./Fetch";
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const useI18n = () => {
|
export const useI18n = () => {
|
||||||
const { uiLang } = useSetting() ?? {};
|
const {
|
||||||
|
setting: { uiLang },
|
||||||
|
} = useSetting();
|
||||||
return (key, defaultText = "") => I18N?.[key]?.[uiLang] ?? defaultText;
|
return (key, defaultText = "") => I18N?.[key]?.[uiLang] ?? defaultText;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,33 @@
|
|||||||
import { STOKEY_RULES, DEFAULT_SUBRULES_LIST } from "../config";
|
import { STOKEY_RULES, DEFAULT_RULES } from "../config";
|
||||||
import storage from "../libs/storage";
|
import { useStorage } from "./Storage";
|
||||||
import { useStorages } from "./Storage";
|
import { trySyncRules } from "../libs/sync";
|
||||||
import { syncRules } from "../libs/sync";
|
|
||||||
import { useSync } from "./Sync";
|
import { useSync } from "./Sync";
|
||||||
import { useSetting, useSettingUpdate } from "./Setting";
|
|
||||||
import { checkRules } from "../libs/rules";
|
import { checkRules } from "../libs/rules";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 匹配规则增删改查 hook
|
* 规则 hook
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useRules() {
|
export function useRules() {
|
||||||
const storages = useStorages();
|
const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
|
||||||
const list = storages?.[STOKEY_RULES] || [];
|
const {
|
||||||
const sync = useSync();
|
sync: { rulesUpdateAt },
|
||||||
|
updateSync,
|
||||||
|
} = useSync();
|
||||||
|
|
||||||
const update = async (rules) => {
|
const updateRules = useCallback(
|
||||||
const updateAt = sync.opt?.rulesUpdateAt ? Date.now() : 0;
|
async (rules) => {
|
||||||
await storage.setObj(STOKEY_RULES, rules);
|
const updateAt = rulesUpdateAt ? Date.now() : 0;
|
||||||
await sync.update({ rulesUpdateAt: updateAt });
|
await save(rules);
|
||||||
syncRules();
|
await updateSync({ rulesUpdateAt: updateAt });
|
||||||
};
|
trySyncRules();
|
||||||
|
},
|
||||||
|
[rulesUpdateAt, save, updateSync]
|
||||||
|
);
|
||||||
|
|
||||||
const add = async (rule) => {
|
const add = useCallback(
|
||||||
|
async (rule) => {
|
||||||
const rules = [...list];
|
const rules = [...list];
|
||||||
if (rule.pattern === "*") {
|
if (rule.pattern === "*") {
|
||||||
return;
|
return;
|
||||||
@@ -31,77 +36,54 @@ export function useRules() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rules.unshift(rule);
|
rules.unshift(rule);
|
||||||
await update(rules);
|
await updateRules(rules);
|
||||||
};
|
},
|
||||||
|
[list, updateRules]
|
||||||
|
);
|
||||||
|
|
||||||
const del = async (pattern) => {
|
const del = useCallback(
|
||||||
|
async (pattern) => {
|
||||||
let rules = [...list];
|
let rules = [...list];
|
||||||
if (pattern === "*") {
|
if (pattern === "*") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rules = rules.filter((item) => item.pattern !== pattern);
|
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];
|
const rules = [...list];
|
||||||
if (pattern === "*") {
|
if (pattern === "*") {
|
||||||
obj.pattern = "*";
|
obj.pattern = "*";
|
||||||
}
|
}
|
||||||
const rule = rules.find((r) => r.pattern === pattern);
|
const rule = rules.find((r) => r.pattern === pattern);
|
||||||
rule && Object.assign(rule, obj);
|
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 rules = [...list];
|
||||||
newRules = checkRules(newRules);
|
newRules = checkRules(newRules);
|
||||||
newRules.forEach((newRule) => {
|
newRules.forEach((newRule) => {
|
||||||
const rule = rules.find((oldRule) => oldRule.pattern === newRule.pattern);
|
const rule = rules.find(
|
||||||
|
(oldRule) => oldRule.pattern === newRule.pattern
|
||||||
|
);
|
||||||
if (rule) {
|
if (rule) {
|
||||||
Object.assign(rule, newRule);
|
Object.assign(rule, newRule);
|
||||||
} else {
|
} else {
|
||||||
rules.unshift(newRule);
|
rules.unshift(newRule);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await update(rules);
|
await updateRules(rules);
|
||||||
};
|
},
|
||||||
|
[list, updateRules]
|
||||||
|
);
|
||||||
|
|
||||||
return { list, add, del, put, merge };
|
return { list, add, del, put, merge };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 订阅规则
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function useSubrules() {
|
|
||||||
const setting = useSetting();
|
|
||||||
const updateSetting = useSettingUpdate();
|
|
||||||
const list = setting?.subrulesList || DEFAULT_SUBRULES_LIST;
|
|
||||||
|
|
||||||
const select = async (url) => {
|
|
||||||
const subrulesList = [...list];
|
|
||||||
subrulesList.forEach((item) => {
|
|
||||||
if (item.url === url) {
|
|
||||||
item.selected = true;
|
|
||||||
} else {
|
|
||||||
item.selected = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await updateSetting({ subrulesList });
|
|
||||||
};
|
|
||||||
|
|
||||||
const add = async (url) => {
|
|
||||||
const subrulesList = [...list];
|
|
||||||
subrulesList.push({ url });
|
|
||||||
await updateSetting({ subrulesList });
|
|
||||||
};
|
|
||||||
|
|
||||||
const del = async (url) => {
|
|
||||||
let subrulesList = [...list];
|
|
||||||
subrulesList = subrulesList.filter((item) => item.url !== url);
|
|
||||||
await updateSetting({ subrulesList });
|
|
||||||
};
|
|
||||||
|
|
||||||
return { list, select, add, del };
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,28 +1,47 @@
|
|||||||
import { STOKEY_SETTING } from "../config";
|
import { STOKEY_SETTING, DEFAULT_SETTING } from "../config";
|
||||||
import storage from "../libs/storage";
|
import { useStorage } from "./Storage";
|
||||||
import { useStorages } from "./Storage";
|
|
||||||
import { useSync } from "./Sync";
|
import { useSync } from "./Sync";
|
||||||
import { syncSetting } from "../libs/sync";
|
import { trySyncSetting } from "../libs/sync";
|
||||||
|
import { createContext, useCallback, useContext } from "react";
|
||||||
|
|
||||||
|
const SettingContext = createContext({
|
||||||
|
setting: null,
|
||||||
|
updateSetting: async () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function SettingProvider({ children }) {
|
||||||
|
const { data, update } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
|
||||||
|
const {
|
||||||
|
sync: { settingUpdateAt },
|
||||||
|
updateSync,
|
||||||
|
} = useSync();
|
||||||
|
|
||||||
|
const updateSetting = useCallback(
|
||||||
|
async (obj) => {
|
||||||
|
const updateAt = settingUpdateAt ? Date.now() : 0;
|
||||||
|
await update(obj);
|
||||||
|
await updateSync({ settingUpdateAt: updateAt });
|
||||||
|
trySyncSetting();
|
||||||
|
},
|
||||||
|
[settingUpdateAt, update, updateSync]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingContext.Provider
|
||||||
|
value={{
|
||||||
|
setting: data,
|
||||||
|
updateSetting,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SettingContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置hook
|
* 设置 hook
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useSetting() {
|
export function useSetting() {
|
||||||
const storages = useStorages();
|
return useContext(SettingContext);
|
||||||
return storages?.[STOKEY_SETTING];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新设置
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function useSettingUpdate() {
|
|
||||||
const sync = useSync();
|
|
||||||
return async (obj) => {
|
|
||||||
const updateAt = sync.opt?.settingUpdateAt ? Date.now() : 0;
|
|
||||||
await storage.putObj(STOKEY_SETTING, obj);
|
|
||||||
await sync.update({ settingUpdateAt: updateAt });
|
|
||||||
syncSetting();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,40 @@
|
|||||||
import { createContext, useContext, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { browser, isExt, isGm, isWeb } from "../libs/browser";
|
import { storage } from "../libs/storage";
|
||||||
import {
|
|
||||||
STOKEY_SETTING,
|
|
||||||
STOKEY_RULES,
|
|
||||||
STOKEY_SYNC,
|
|
||||||
DEFAULT_SETTING,
|
|
||||||
DEFAULT_RULES,
|
|
||||||
DEFAULT_SYNC,
|
|
||||||
} from "../config";
|
|
||||||
import storage from "../libs/storage";
|
|
||||||
|
|
||||||
/**
|
export function useStorage(key, defaultVal = null) {
|
||||||
* 默认配置
|
const [data, setData] = useState(defaultVal);
|
||||||
*/
|
|
||||||
export const defaultStorage = {
|
|
||||||
[STOKEY_SETTING]: DEFAULT_SETTING,
|
|
||||||
[STOKEY_RULES]: DEFAULT_RULES,
|
|
||||||
[STOKEY_SYNC]: DEFAULT_SYNC,
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeKeys = Object.keys(defaultStorage);
|
const save = useCallback(
|
||||||
|
async (val) => {
|
||||||
const StoragesContext = createContext(null);
|
setData(val);
|
||||||
|
await storage.setObj(key, val);
|
||||||
export function StoragesProvider({ children }) {
|
|
||||||
const [storages, setStorages] = useState(null);
|
|
||||||
|
|
||||||
const handleChanged = (changes) => {
|
|
||||||
if (isWeb || isGm) {
|
|
||||||
const { key, oldValue, newValue } = changes;
|
|
||||||
changes = {
|
|
||||||
[key]: {
|
|
||||||
oldValue,
|
|
||||||
newValue,
|
|
||||||
},
|
},
|
||||||
};
|
[key]
|
||||||
}
|
);
|
||||||
const newStorages = {};
|
|
||||||
Object.entries(changes)
|
const update = useCallback(
|
||||||
.filter(
|
async (obj) => {
|
||||||
([key, { oldValue, newValue }]) =>
|
setData((pre) => ({ ...pre, ...obj }));
|
||||||
activeKeys.includes(key) && oldValue !== newValue
|
await storage.putObj(key, obj);
|
||||||
)
|
},
|
||||||
.forEach(([key, { newValue }]) => {
|
[key]
|
||||||
newStorages[key] = JSON.parse(newValue);
|
);
|
||||||
});
|
|
||||||
if (Object.keys(newStorages).length !== 0) {
|
const remove = useCallback(async () => {
|
||||||
setStorages((pre) => ({ ...pre, ...newStorages }));
|
setData(null);
|
||||||
}
|
await storage.del(key);
|
||||||
};
|
}, [key]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 首次从storage同步配置到内存
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const curStorages = {};
|
const val = await storage.getObj(key);
|
||||||
for (const key of activeKeys) {
|
|
||||||
const val = await storage.get(key);
|
|
||||||
if (val) {
|
if (val) {
|
||||||
curStorages[key] = JSON.parse(val);
|
setData(val);
|
||||||
} else {
|
} else if (defaultVal) {
|
||||||
await storage.setObj(key, defaultStorage[key]);
|
await storage.setObj(key, defaultVal);
|
||||||
curStorages[key] = defaultStorage[key];
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
setStorages(curStorages);
|
|
||||||
})();
|
})();
|
||||||
|
}, [key, defaultVal]);
|
||||||
|
|
||||||
// 监听storage,并同步到内存中
|
return { data, save, update, remove };
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
81
src/hooks/SubRules.js
Normal file
81
src/hooks/SubRules.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { DEFAULT_SUBRULES_LIST } 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,20 +1,11 @@
|
|||||||
import { useCallback } from "react";
|
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
|
||||||
import { STOKEY_SYNC } from "../config";
|
import { useStorage } from "./Storage";
|
||||||
import storage from "../libs/storage";
|
|
||||||
import { useStorages } from "./Storage";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* sync hook
|
* sync hook
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useSync() {
|
export function useSync() {
|
||||||
const storages = useStorages();
|
const { data, update } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
|
||||||
const opt = storages?.[STOKEY_SYNC];
|
return { sync: data, updateSync: update };
|
||||||
const update = useCallback(async (obj) => {
|
|
||||||
await storage.putObj(STOKEY_SYNC, obj);
|
|
||||||
}, []);
|
|
||||||
return {
|
|
||||||
opt,
|
|
||||||
update,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { THEME_DARK, THEME_LIGHT } from "../config";
|
|||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function MuiThemeProvider({ children, options }) {
|
export default function Theme({ children, options }) {
|
||||||
const darkMode = useDarkMode();
|
const { darkMode } = useDarkMode();
|
||||||
const theme = useMemo(() => {
|
const theme = useMemo(() => {
|
||||||
return createTheme({
|
return createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { detectLang } from "../libs";
|
import { tryDetectLang } from "../libs";
|
||||||
import { apiTranslate } from "../apis";
|
import { apiTranslate } from "../apis";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 翻译hook
|
* 翻译hook
|
||||||
* @param {*} q
|
* @param {*} q
|
||||||
* @param {*} rule
|
* @param {*} rule
|
||||||
|
* @param {*} setting
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useTranslate(q, rule) {
|
export function useTranslate(q, rule, setting) {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [sameLang, setSamelang] = useState(false);
|
const [sameLang, setSamelang] = useState(false);
|
||||||
@@ -21,8 +22,8 @@ export function useTranslate(q, rule) {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const deLang = await detectLang(q);
|
const deLang = await tryDetectLang(q);
|
||||||
if (toLang.includes(deLang)) {
|
if (deLang && toLang.includes(deLang)) {
|
||||||
setSamelang(true);
|
setSamelang(true);
|
||||||
} else {
|
} else {
|
||||||
const [trText, isSame] = await apiTranslate({
|
const [trText, isSame] = await apiTranslate({
|
||||||
@@ -30,6 +31,7 @@ export function useTranslate(q, rule) {
|
|||||||
q,
|
q,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
|
setting,
|
||||||
});
|
});
|
||||||
setText(trText);
|
setText(trText);
|
||||||
setSamelang(isSame);
|
setSamelang(isSame);
|
||||||
@@ -40,7 +42,7 @@ export function useTranslate(q, rule) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [q, translator, fromLang, toLang]);
|
}, [q, translator, fromLang, toLang, setting]);
|
||||||
|
|
||||||
return { text, sameLang, loading };
|
return { text, sameLang, loading };
|
||||||
}
|
}
|
||||||
|
|||||||
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 ReactDOM from "react-dom/client";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import Paper from "@mui/material/Paper";
|
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 { useFetch } from "./hooks/Fetch";
|
||||||
import { I18N, URL_RAW_PREFIX } from "./config";
|
import { I18N, URL_RAW_PREFIX } from "./config";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [lang, setLang] = useState("zh");
|
||||||
const [data, loading, error] = useFetch(
|
const [data, loading, error] = useFetch(
|
||||||
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.["zh"]}`
|
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}`
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ padding: 2, margin: 2 }}>
|
<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 ? (
|
{loading ? (
|
||||||
<center>
|
<center>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import storage from "./storage";
|
import { getMsauth, setMsauth } from "./storage";
|
||||||
import { STOKEY_MSAUTH, URL_MICROSOFT_AUTH } from "../config";
|
import { URL_MICROSOFT_AUTH } from "../config";
|
||||||
import { fetchData } from "./fetch";
|
import { fetchData } from "./fetch";
|
||||||
|
|
||||||
const parseMSToken = (token) => {
|
const parseMSToken = (token) => {
|
||||||
@@ -26,9 +26,9 @@ const _msAuth = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查询storage缓存
|
// 查询storage缓存
|
||||||
const res = (await storage.getObj(STOKEY_MSAUTH)) || {};
|
const res = await getMsauth();
|
||||||
token = res.token;
|
token = res?.token;
|
||||||
exp = res.exp;
|
exp = res?.exp;
|
||||||
if (token && exp * 1000 > now + 1000) {
|
if (token && exp * 1000 > now + 1000) {
|
||||||
return [token, exp];
|
return [token, exp];
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ const _msAuth = () => {
|
|||||||
// 缓存没有或失效,查询接口
|
// 缓存没有或失效,查询接口
|
||||||
token = await fetchData(URL_MICROSOFT_AUTH);
|
token = await fetchData(URL_MICROSOFT_AUTH);
|
||||||
exp = parseMSToken(token);
|
exp = parseMSToken(token);
|
||||||
await storage.setObj(STOKEY_MSAUTH, { token, exp });
|
await setMsauth({ token, exp });
|
||||||
return [token, exp];
|
return [token, exp];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
|
// import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 浏览器兼容插件,另可用于判断是插件模式还是网页模式,方便开发
|
* 浏览器兼容插件,另可用于判断是插件模式还是网页模式,方便开发
|
||||||
@@ -13,7 +13,3 @@ function _browser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const browser = _browser();
|
export const browser = _browser();
|
||||||
export const client = process.env.REACT_APP_CLIENT;
|
|
||||||
export const isExt = CLIENT_EXTS.includes(client);
|
|
||||||
export const isGm = client === CLIENT_USERSCRIPT;
|
|
||||||
export const isWeb = client === CLIENT_WEB;
|
|
||||||
|
|||||||
6
src/libs/client.js
Normal file
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 { isExt, isGm } from "./client";
|
||||||
import { sendMsg } from "./msg";
|
import { sendBgMsg } from "./msg";
|
||||||
import { taskPool } from "./pool";
|
import { taskPool } from "./pool";
|
||||||
import {
|
import {
|
||||||
MSG_FETCH,
|
MSG_FETCH,
|
||||||
@@ -19,7 +19,7 @@ import { msAuth } from "./auth";
|
|||||||
* @param {*} init
|
* @param {*} init
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
GM.xmlHttpRequest({
|
GM.xmlHttpRequest({
|
||||||
method,
|
method,
|
||||||
@@ -65,7 +65,7 @@ const newCacheReq = async (request) => {
|
|||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const fetchApi = async ({ input, init, useUnsafe, translator, token }) => {
|
const fetchApi = async ({ input, init = {}, translator, token }) => {
|
||||||
if (translator === OPT_TRANS_MICROSOFT) {
|
if (translator === OPT_TRANS_MICROSOFT) {
|
||||||
init.headers["Authorization"] = `Bearer ${token}`;
|
init.headers["Authorization"] = `Bearer ${token}`;
|
||||||
} else if (translator === OPT_TRANS_OPENAI) {
|
} else if (translator === OPT_TRANS_OPENAI) {
|
||||||
@@ -73,9 +73,24 @@ const fetchApi = async ({ input, init, useUnsafe, translator, token }) => {
|
|||||||
init.headers["api-key"] = token; // Azure 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 fetchGM(input, init);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return fetch(input, init);
|
return fetch(input, init);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,34 +113,32 @@ export const fetchPool = taskPool(
|
|||||||
/**
|
/**
|
||||||
* 请求数据统一接口
|
* 请求数据统一接口
|
||||||
* @param {*} input
|
* @param {*} input
|
||||||
* @param {*} init
|
|
||||||
* @param {*} opts
|
* @param {*} opts
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const fetchData = async (
|
export const fetchData = async (
|
||||||
input,
|
input,
|
||||||
init,
|
{ useCache, usePool, translator, token, ...init } = {}
|
||||||
{ useCache, usePool, translator, useUnsafe, token } = {}
|
|
||||||
) => {
|
) => {
|
||||||
const cacheReq = await newCacheReq(new Request(input, init));
|
const cacheReq = await newCacheReq(new Request(input, init));
|
||||||
const cache = await caches.open(CACHE_NAME);
|
|
||||||
let res;
|
let res;
|
||||||
|
|
||||||
// 查询缓存
|
// 查询缓存
|
||||||
if (useCache) {
|
if (useCache) {
|
||||||
try {
|
try {
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
res = await cache.match(cacheReq);
|
res = await cache.match(cacheReq);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[cache match]", err);
|
console.log("[cache match]", err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
// 发送请求
|
// 发送请求
|
||||||
if (usePool) {
|
if (usePool) {
|
||||||
res = await fetchPool.push({ input, init, useUnsafe, translator, token });
|
res = await fetchPool.push({ input, init, translator, token });
|
||||||
} else {
|
} else {
|
||||||
res = await fetchApi({ input, init, useUnsafe, translator, token });
|
res = await fetchApi({ input, init, translator, token });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res?.ok) {
|
if (!res?.ok) {
|
||||||
@@ -135,9 +148,10 @@ export const fetchData = async (
|
|||||||
// 插入缓存
|
// 插入缓存
|
||||||
if (useCache) {
|
if (useCache) {
|
||||||
try {
|
try {
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
await cache.put(cacheReq, res.clone());
|
await cache.put(cacheReq, res.clone());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[cache put]", err);
|
console.log("[cache put]", err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,22 +166,21 @@ export const fetchData = async (
|
|||||||
/**
|
/**
|
||||||
* fetch 兼容性封装
|
* fetch 兼容性封装
|
||||||
* @param {*} input
|
* @param {*} input
|
||||||
* @param {*} init
|
|
||||||
* @param {*} opts
|
* @param {*} opts
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const fetchPolyfill = async (input, init, opts) => {
|
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
|
||||||
// 插件
|
// 插件
|
||||||
if (isExt) {
|
if (isExt && !isBg) {
|
||||||
const res = await sendMsg(MSG_FETCH, { input, init, opts });
|
const res = await sendBgMsg(MSG_FETCH, { input, opts });
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
throw new Error(res.error);
|
throw new Error(res.error);
|
||||||
}
|
}
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 油猴/网页
|
// 油猴/网页/BackgroundPage
|
||||||
return await fetchData(input, init, opts);
|
return await fetchData(input, opts);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,9 +188,9 @@ export const fetchPolyfill = async (input, init, opts) => {
|
|||||||
* @param {*} interval
|
* @param {*} interval
|
||||||
* @param {*} limit
|
* @param {*} limit
|
||||||
*/
|
*/
|
||||||
export const fetchUpdate = async (interval, limit) => {
|
export const updateFetchPool = async (interval, limit) => {
|
||||||
if (isExt) {
|
if (isExt) {
|
||||||
const res = await sendMsg(MSG_FETCH_LIMIT, { interval, limit });
|
const res = await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit });
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
throw new Error(res.error);
|
throw new Error(res.error);
|
||||||
}
|
}
|
||||||
@@ -189,9 +202,9 @@ export const fetchUpdate = async (interval, limit) => {
|
|||||||
/**
|
/**
|
||||||
* 清空任务池
|
* 清空任务池
|
||||||
*/
|
*/
|
||||||
export const fetchClear = async () => {
|
export const clearFetchPool = async () => {
|
||||||
if (isExt) {
|
if (isExt) {
|
||||||
const res = await sendMsg(MSG_FETCH_CLEAR);
|
const res = await sendBgMsg(MSG_FETCH_CLEAR);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
throw new Error(res.error);
|
throw new Error(res.error);
|
||||||
}
|
}
|
||||||
|
|||||||
97
src/libs/gm.js
Normal file
97
src/libs/gm.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { fetchGM } from "./fetch";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注入页面的脚本,请求并接受GM接口信息
|
||||||
|
* @param {*} param0
|
||||||
|
*/
|
||||||
|
export const injectScript = (ping) => {
|
||||||
|
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";
|
||||||
|
let GM_info;
|
||||||
|
|
||||||
|
const promiseGM = (action, args, timeout = 5000) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const pong = btoa(Math.random()).slice(3, 11);
|
||||||
|
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: () => {
|
||||||
|
if (GM_info) {
|
||||||
|
return GM_info;
|
||||||
|
}
|
||||||
|
return promiseGM(MSG_GM_info);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
window.APP_NAME = process.env.REACT_APP_NAME;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听并回应页面对GM接口的请求
|
||||||
|
* @param {*} param0
|
||||||
|
*/
|
||||||
|
export const handlePing = async (e) => {
|
||||||
|
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
|
||||||
|
const MSG_GM_setValue = "setValue";
|
||||||
|
const MSG_GM_getValue = "getValue";
|
||||||
|
const MSG_GM_deleteValue = "deleteValue";
|
||||||
|
const MSG_GM_info = "info";
|
||||||
|
const { action, args, pong } = e.detail;
|
||||||
|
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,104 +1,15 @@
|
|||||||
import storage from "./storage";
|
import { CACHE_NAME } from "../config";
|
||||||
import {
|
|
||||||
DEFAULT_SETTING,
|
|
||||||
STOKEY_SETTING,
|
|
||||||
STOKEY_RULES,
|
|
||||||
STOKEY_FAB,
|
|
||||||
GLOBLA_RULE,
|
|
||||||
GLOBAL_KEY,
|
|
||||||
DEFAULT_SUBRULES_LIST,
|
|
||||||
} from "../config";
|
|
||||||
import { browser } from "./browser";
|
import { browser } from "./browser";
|
||||||
import { isMatch } from "./utils";
|
|
||||||
import { tryLoadRules } from "./rules";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取节点列表并转为数组
|
* 清除缓存数据
|
||||||
* @param {*} selector
|
|
||||||
* @param {*} el
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export const queryEls = (selector, el = document) =>
|
export const tryClearCaches = async () => {
|
||||||
Array.from(el.querySelectorAll(selector));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询storage中的设置
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getSetting = async () => ({
|
|
||||||
...DEFAULT_SETTING,
|
|
||||||
...((await storage.getObj(STOKEY_SETTING)) || {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询规则列表
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getRules = async () => (await storage.getObj(STOKEY_RULES)) || [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询fab位置信息
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getFab = async () => (await storage.getObj(STOKEY_FAB)) || {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置fab位置信息
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const setFab = async (obj) => await storage.setObj(STOKEY_FAB, obj);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据href匹配规则
|
|
||||||
* @param {*} rules
|
|
||||||
* @param {string} href
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const matchRule = async (
|
|
||||||
rules,
|
|
||||||
href,
|
|
||||||
{ injectRules, subrulesList = DEFAULT_SUBRULES_LIST }
|
|
||||||
) => {
|
|
||||||
if (injectRules) {
|
|
||||||
try {
|
try {
|
||||||
const selectedSub = subrulesList.find((item) => item.selected);
|
caches.delete(CACHE_NAME);
|
||||||
if (selectedSub?.url) {
|
|
||||||
const subRules = await tryLoadRules(selectedSub.url);
|
|
||||||
rules.splice(-1, 0, ...subRules);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[load injectRules]", err);
|
console.log("[clean caches]", err.message);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const rule = rules.find((rule) =>
|
|
||||||
rule.pattern.split(",").some((p) => isMatch(href, p.trim()))
|
|
||||||
);
|
|
||||||
const globalRule =
|
|
||||||
rules.find((rule) =>
|
|
||||||
rule.pattern.split(",").some((p) => p.trim() === "*")
|
|
||||||
) || GLOBLA_RULE;
|
|
||||||
|
|
||||||
if (!rule) {
|
|
||||||
return globalRule;
|
|
||||||
}
|
|
||||||
|
|
||||||
rule.selector =
|
|
||||||
rule?.selector?.trim() ||
|
|
||||||
globalRule?.selector?.trim() ||
|
|
||||||
GLOBLA_RULE.selector;
|
|
||||||
|
|
||||||
rule.bgColor = rule?.bgColor?.trim() || globalRule?.bgColor?.trim();
|
|
||||||
|
|
||||||
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach(
|
|
||||||
(key) => {
|
|
||||||
if (rule[key] === GLOBAL_KEY) {
|
|
||||||
rule[key] = globalRule[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return rule;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,7 +17,11 @@ export const matchRule = async (
|
|||||||
* @param {*} q
|
* @param {*} q
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const detectLang = async (q) => {
|
export const tryDetectLang = async (q) => {
|
||||||
const res = await browser?.i18n.detectLanguage(q);
|
try {
|
||||||
|
const res = await browser?.i18n?.detectLanguage(q);
|
||||||
return res?.languages?.[0]?.language;
|
return res?.languages?.[0]?.language;
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[detect lang]", err.message);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { browser } from "./browser";
|
|||||||
* @param {*} args
|
* @param {*} args
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const sendMsg = (action, args) =>
|
export const sendBgMsg = (action, args) =>
|
||||||
browser?.runtime?.sendMessage({ action, args });
|
browser.runtime.sendMessage({ action, args });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送消息给当前页面
|
* 发送消息给当前页面
|
||||||
@@ -16,6 +16,6 @@ export const sendMsg = (action, args) =>
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const sendTabMsg = async (action, args) => {
|
export const sendTabMsg = async (action, args) => {
|
||||||
const tabs = await browser?.tabs.query({ active: true, currentWindow: true });
|
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||||
return await browser?.tabs.sendMessage(tabs[0].id, { action, args });
|
return browser.tabs.sendMessage(tabs[0].id, { action, args });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,68 @@
|
|||||||
import storage from "./storage";
|
import { matchValue, type, isMatch } from "./utils";
|
||||||
import { fetchPolyfill } from "./fetch";
|
|
||||||
import { matchValue, type } from "./utils";
|
|
||||||
import {
|
import {
|
||||||
STOKEY_RULESCACHE_PREFIX,
|
|
||||||
GLOBAL_KEY,
|
GLOBAL_KEY,
|
||||||
OPT_TRANS_ALL,
|
OPT_TRANS_ALL,
|
||||||
OPT_STYLE_ALL,
|
OPT_STYLE_ALL,
|
||||||
OPT_LANGS_FROM,
|
OPT_LANGS_FROM,
|
||||||
OPT_LANGS_TO,
|
OPT_LANGS_TO,
|
||||||
|
GLOBLA_RULE,
|
||||||
|
DEFAULT_SUBRULES_LIST,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
|
import { loadOrFetchSubRules } from "./subRules";
|
||||||
|
|
||||||
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
|
/**
|
||||||
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
|
* 根据href匹配规则
|
||||||
|
* @param {*} rules
|
||||||
|
* @param {string} href
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const matchRule = async (
|
||||||
|
rules,
|
||||||
|
href,
|
||||||
|
{ injectRules = true, subrulesList = DEFAULT_SUBRULES_LIST }
|
||||||
|
) => {
|
||||||
|
rules = [...rules];
|
||||||
|
if (injectRules) {
|
||||||
|
try {
|
||||||
|
const selectedSub = subrulesList.find((item) => item.selected);
|
||||||
|
if (selectedSub?.url) {
|
||||||
|
const subRules = await loadOrFetchSubRules(selectedSub.url);
|
||||||
|
rules.splice(-1, 0, ...subRules);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[load injectRules]", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rule = rules.find((r) =>
|
||||||
|
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
|
||||||
|
);
|
||||||
|
|
||||||
|
const globalRule =
|
||||||
|
rules.find((r) => r.pattern.split(",").some((p) => p.trim() === "*")) ||
|
||||||
|
GLOBLA_RULE;
|
||||||
|
|
||||||
|
if (!rule) {
|
||||||
|
return globalRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.selector =
|
||||||
|
rule?.selector?.trim() ||
|
||||||
|
globalRule?.selector?.trim() ||
|
||||||
|
GLOBLA_RULE.selector;
|
||||||
|
|
||||||
|
rule.bgColor = rule?.bgColor?.trim() || globalRule?.bgColor?.trim();
|
||||||
|
|
||||||
|
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach(
|
||||||
|
(key) => {
|
||||||
|
if (rule[key] === GLOBAL_KEY) {
|
||||||
|
rule[key] = globalRule[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return rule;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查过滤rules
|
* 检查过滤rules
|
||||||
@@ -26,6 +77,8 @@ export const checkRules = (rules) => {
|
|||||||
throw new Error("data error");
|
throw new Error("data error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
|
||||||
|
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
|
||||||
const patternSet = new Set();
|
const patternSet = new Set();
|
||||||
rules = rules
|
rules = rules
|
||||||
.filter((rule) => type(rule) === "object")
|
.filter((rule) => type(rule) === "object")
|
||||||
@@ -60,40 +113,3 @@ export const checkRules = (rules) => {
|
|||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 本地rules缓存
|
|
||||||
*/
|
|
||||||
export const rulesCache = {
|
|
||||||
fetch: async (url) => {
|
|
||||||
const res = await fetchPolyfill(url, null, { useUnsafe: true });
|
|
||||||
const rules = checkRules(res).filter(
|
|
||||||
(rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== ""
|
|
||||||
);
|
|
||||||
return rules;
|
|
||||||
},
|
|
||||||
set: async (url, rules) => {
|
|
||||||
await storage.setObj(`${STOKEY_RULESCACHE_PREFIX}${url}`, rules);
|
|
||||||
},
|
|
||||||
get: async (url) => {
|
|
||||||
return await storage.getObj(`${STOKEY_RULESCACHE_PREFIX}${url}`);
|
|
||||||
},
|
|
||||||
del: async (url) => {
|
|
||||||
await storage.del(`${STOKEY_RULESCACHE_PREFIX}${url}`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从缓存或远程加载订阅的rules
|
|
||||||
* @param {*} url
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const tryLoadRules = async (url) => {
|
|
||||||
let rules = await rulesCache.get(url);
|
|
||||||
if (rules?.length) {
|
|
||||||
return rules;
|
|
||||||
}
|
|
||||||
rules = await rulesCache.fetch(url);
|
|
||||||
await rulesCache.set(url, rules);
|
|
||||||
return rules;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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) {
|
async function set(key, val) {
|
||||||
if (isExt) {
|
if (isExt) {
|
||||||
await browser.storage.local.set({ [key]: val });
|
await browser.storage.local.set({ [key]: val });
|
||||||
} else if (isGm) {
|
} else if (isGm) {
|
||||||
const oldValue = await GM.getValue(key);
|
await (window.KISS_GM || GM).setValue(key, val);
|
||||||
await GM.setValue(key, val);
|
|
||||||
window.dispatchEvent(
|
|
||||||
new StorageEvent("storage", {
|
|
||||||
key,
|
|
||||||
oldValue,
|
|
||||||
newValue: val,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const oldValue = window.localStorage.getItem(key);
|
|
||||||
window.localStorage.setItem(key, val);
|
window.localStorage.setItem(key, val);
|
||||||
window.dispatchEvent(
|
|
||||||
new StorageEvent("storage", {
|
|
||||||
key,
|
|
||||||
oldValue,
|
|
||||||
newValue: val,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +28,7 @@ async function get(key) {
|
|||||||
const val = await browser.storage.local.get([key]);
|
const val = await browser.storage.local.get([key]);
|
||||||
return val[key];
|
return val[key];
|
||||||
} else if (isGm) {
|
} else if (isGm) {
|
||||||
const val = await GM.getValue(key);
|
const val = await (window.KISS_GM || GM).getValue(key);
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
return window.localStorage.getItem(key);
|
return window.localStorage.getItem(key);
|
||||||
@@ -41,25 +38,9 @@ async function del(key) {
|
|||||||
if (isExt) {
|
if (isExt) {
|
||||||
await browser.storage.local.remove([key]);
|
await browser.storage.local.remove([key]);
|
||||||
} else if (isGm) {
|
} else if (isGm) {
|
||||||
const oldValue = await GM.getValue(key);
|
await (window.KISS_GM || GM).deleteValue(key);
|
||||||
await GM.deleteValue(key);
|
|
||||||
window.dispatchEvent(
|
|
||||||
new StorageEvent("storage", {
|
|
||||||
key,
|
|
||||||
oldValue,
|
|
||||||
newValue: null,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const oldValue = window.localStorage.getItem(key);
|
|
||||||
window.localStorage.removeItem(key);
|
window.localStorage.removeItem(key);
|
||||||
window.dispatchEvent(
|
|
||||||
new StorageEvent("storage", {
|
|
||||||
key,
|
|
||||||
oldValue,
|
|
||||||
newValue: null,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,22 +64,10 @@ async function putObj(key, obj) {
|
|||||||
await setObj(key, { ...cur, ...obj });
|
await setObj(key, { ...cur, ...obj });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听storage事件
|
|
||||||
* @param {*} handleChanged
|
|
||||||
*/
|
|
||||||
function onChanged(handleChanged) {
|
|
||||||
if (isExt) {
|
|
||||||
browser.storage.onChanged.addListener(handleChanged);
|
|
||||||
} else {
|
|
||||||
window.addEventListener("storage", handleChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对storage的封装
|
* 对storage的封装
|
||||||
*/
|
*/
|
||||||
const storage = {
|
export const storage = {
|
||||||
get,
|
get,
|
||||||
set,
|
set,
|
||||||
del,
|
del,
|
||||||
@@ -106,7 +75,70 @@ const storage = {
|
|||||||
trySetObj,
|
trySetObj,
|
||||||
getObj,
|
getObj,
|
||||||
putObj,
|
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);
|
||||||
|
};
|
||||||
103
src/libs/sync.js
103
src/libs/sync.js
@@ -1,81 +1,107 @@
|
|||||||
import {
|
import {
|
||||||
STOKEY_SYNC,
|
|
||||||
DEFAULT_SYNC,
|
|
||||||
KV_SETTING_KEY,
|
KV_SETTING_KEY,
|
||||||
KV_RULES_KEY,
|
KV_RULES_KEY,
|
||||||
KV_RULES_SHARE_KEY,
|
KV_RULES_SHARE_KEY,
|
||||||
STOKEY_SETTING,
|
|
||||||
STOKEY_RULES,
|
|
||||||
KV_SALT_SHARE,
|
KV_SALT_SHARE,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import storage from "../libs/storage";
|
import {
|
||||||
import { getSetting, getRules } from ".";
|
getSyncWithDefault,
|
||||||
|
updateSync,
|
||||||
|
getSettingWithDefault,
|
||||||
|
getRulesWithDefault,
|
||||||
|
setSetting,
|
||||||
|
setRules,
|
||||||
|
} from "./storage";
|
||||||
import { apiSyncData } from "../apis";
|
import { apiSyncData } from "../apis";
|
||||||
import { sha256 } from "./utils";
|
import { sha256 } from "./utils";
|
||||||
|
|
||||||
export const loadSyncOpt = async () =>
|
/**
|
||||||
(await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC;
|
* 同步设置
|
||||||
|
* @returns
|
||||||
export const syncSetting = async () => {
|
*/
|
||||||
try {
|
const syncSetting = async (isBg = false) => {
|
||||||
const { syncUrl, syncKey, settingUpdateAt } = await loadSyncOpt();
|
const { syncUrl, syncKey, settingUpdateAt } = await getSyncWithDefault();
|
||||||
if (!syncUrl || !syncKey) {
|
if (!syncUrl || !syncKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setting = await getSetting();
|
const setting = await getSettingWithDefault();
|
||||||
const res = await apiSyncData(syncUrl, syncKey, {
|
const res = await apiSyncData(
|
||||||
|
syncUrl,
|
||||||
|
syncKey,
|
||||||
|
{
|
||||||
key: KV_SETTING_KEY,
|
key: KV_SETTING_KEY,
|
||||||
value: setting,
|
value: setting,
|
||||||
updateAt: settingUpdateAt,
|
updateAt: settingUpdateAt,
|
||||||
});
|
},
|
||||||
|
isBg
|
||||||
|
);
|
||||||
|
|
||||||
if (res && res.updateAt > settingUpdateAt) {
|
if (res && res.updateAt > settingUpdateAt) {
|
||||||
await storage.putObj(STOKEY_SYNC, {
|
await updateSync({
|
||||||
settingUpdateAt: res.updateAt,
|
settingUpdateAt: res.updateAt,
|
||||||
settingSyncAt: res.updateAt,
|
settingSyncAt: res.updateAt,
|
||||||
});
|
});
|
||||||
await storage.setObj(STOKEY_SETTING, res.value);
|
await setSetting(res.value);
|
||||||
} else {
|
} else {
|
||||||
await storage.putObj(STOKEY_SYNC, {
|
await updateSync({ settingSyncAt: res.updateAt });
|
||||||
settingSyncAt: res.updateAt,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trySyncSetting = async (isBg = false) => {
|
||||||
|
try {
|
||||||
|
await syncSetting(isBg);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[sync setting]", err);
|
console.log("[sync setting]", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const syncRules = async () => {
|
/**
|
||||||
try {
|
* 同步规则
|
||||||
const { syncUrl, syncKey, rulesUpdateAt } = await loadSyncOpt();
|
* @returns
|
||||||
|
*/
|
||||||
|
const syncRules = async (isBg = false) => {
|
||||||
|
const { syncUrl, syncKey, rulesUpdateAt } = await getSyncWithDefault();
|
||||||
if (!syncUrl || !syncKey) {
|
if (!syncUrl || !syncKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rules = await getRules();
|
const rules = await getRulesWithDefault();
|
||||||
const res = await apiSyncData(syncUrl, syncKey, {
|
const res = await apiSyncData(
|
||||||
|
syncUrl,
|
||||||
|
syncKey,
|
||||||
|
{
|
||||||
key: KV_RULES_KEY,
|
key: KV_RULES_KEY,
|
||||||
value: rules,
|
value: rules,
|
||||||
updateAt: rulesUpdateAt,
|
updateAt: rulesUpdateAt,
|
||||||
});
|
},
|
||||||
|
isBg
|
||||||
|
);
|
||||||
|
|
||||||
if (res && res.updateAt > rulesUpdateAt) {
|
if (res && res.updateAt > rulesUpdateAt) {
|
||||||
await storage.putObj(STOKEY_SYNC, {
|
await updateSync({
|
||||||
rulesUpdateAt: res.updateAt,
|
rulesUpdateAt: res.updateAt,
|
||||||
rulesSyncAt: res.updateAt,
|
rulesSyncAt: res.updateAt,
|
||||||
});
|
});
|
||||||
await storage.setObj(STOKEY_RULES, res.value);
|
await setRules(res.value);
|
||||||
} else {
|
} else {
|
||||||
await storage.putObj(STOKEY_SYNC, {
|
await updateSync({ rulesSyncAt: res.updateAt });
|
||||||
rulesSyncAt: res.updateAt,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trySyncRules = async (isBg = false) => {
|
||||||
|
try {
|
||||||
|
await syncRules(isBg);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[sync user rules]", err);
|
console.log("[sync user rules]", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步分享规则
|
||||||
|
* @param {*} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
||||||
await apiSyncData(syncUrl, syncKey, {
|
await apiSyncData(syncUrl, syncKey, {
|
||||||
key: KV_RULES_SHARE_KEY,
|
key: KV_RULES_SHARE_KEY,
|
||||||
@@ -87,7 +113,16 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
|||||||
return shareUrl;
|
return shareUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const syncAll = async () => {
|
/**
|
||||||
await syncSetting();
|
* 同步个人设置和规则
|
||||||
await syncRules();
|
* @returns
|
||||||
|
*/
|
||||||
|
export const syncSettingAndRules = async (isBg = false) => {
|
||||||
|
await syncSetting(isBg);
|
||||||
|
await syncRules(isBg);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trySyncSettingAndRules = async (isBg = false) => {
|
||||||
|
await trySyncSetting(isBg);
|
||||||
|
await trySyncRules(isBg);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,17 +7,38 @@ import {
|
|||||||
MSG_TRANS_CURRULE,
|
MSG_TRANS_CURRULE,
|
||||||
OPT_STYLE_DASHLINE,
|
OPT_STYLE_DASHLINE,
|
||||||
OPT_STYLE_FUZZY,
|
OPT_STYLE_FUZZY,
|
||||||
|
SHADOW_KEY,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { queryEls } from ".";
|
|
||||||
import Content from "../views/Content";
|
import Content from "../views/Content";
|
||||||
import { fetchUpdate, fetchClear } from "./fetch";
|
import { updateFetchPool, clearFetchPool } from "./fetch";
|
||||||
|
import { debounce } from "./utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 翻译类
|
* 翻译类
|
||||||
*/
|
*/
|
||||||
export class Translator {
|
export class Translator {
|
||||||
_rule = {};
|
_rule = {};
|
||||||
|
_setting = {};
|
||||||
|
_rootNodes = new Set();
|
||||||
|
_tranNodes = new Map();
|
||||||
|
_skipNodeNames = [
|
||||||
|
APP_LCNAME,
|
||||||
|
"style",
|
||||||
|
"svg",
|
||||||
|
"img",
|
||||||
|
"audio",
|
||||||
|
"video",
|
||||||
|
"textarea",
|
||||||
|
"input",
|
||||||
|
"button",
|
||||||
|
"select",
|
||||||
|
"option",
|
||||||
|
"head",
|
||||||
|
"script",
|
||||||
|
"iframe",
|
||||||
|
];
|
||||||
|
|
||||||
|
// 显示
|
||||||
_interseObserver = new IntersectionObserver(
|
_interseObserver = new IntersectionObserver(
|
||||||
(intersections) => {
|
(intersections) => {
|
||||||
intersections.forEach((intersection) => {
|
intersections.forEach((intersection) => {
|
||||||
@@ -32,28 +53,58 @@ export class Translator {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 变化
|
||||||
_mutaObserver = new MutationObserver((mutations) => {
|
_mutaObserver = new MutationObserver((mutations) => {
|
||||||
mutations.forEach((mutation) => {
|
mutations.forEach((mutation) => {
|
||||||
mutation.addedNodes.forEach((node) => {
|
if (
|
||||||
try {
|
!this._skipNodeNames.includes(mutation.target.localName) &&
|
||||||
queryEls(this.rule.selector, node).forEach((el) => {
|
mutation.addedNodes.length > 0
|
||||||
this._interseObserver.observe(el);
|
) {
|
||||||
});
|
const nodes = Array.from(mutation.addedNodes).filter((node) => {
|
||||||
} catch (err) {
|
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 }) {
|
// 插入 shadowroot
|
||||||
fetchUpdate(fetchInterval, fetchLimit);
|
_overrideAttachShadow = () => {
|
||||||
this.rule = rule;
|
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") {
|
if (rule.transOpen === "true") {
|
||||||
this._register();
|
this._register();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get setting() {
|
||||||
|
return this._setting;
|
||||||
|
}
|
||||||
|
|
||||||
get rule() {
|
get rule() {
|
||||||
// console.log("get rule", this._rule);
|
// console.log("get rule", this._rule);
|
||||||
return this._rule;
|
return this._rule;
|
||||||
@@ -96,16 +147,80 @@ export class Translator {
|
|||||||
this.rule = { ...this.rule, textStyle };
|
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 = () => {
|
_register = () => {
|
||||||
// 监听节点变化
|
// 搜索节点
|
||||||
this._mutaObserver.observe(document, {
|
this._queryNodes();
|
||||||
|
|
||||||
|
this._rootNodes.forEach((node) => {
|
||||||
|
// 监听节点变化;
|
||||||
|
this._mutaObserver.observe(node, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
|
// characterData: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._tranNodes.forEach((_, node) => {
|
||||||
// 监听节点显示
|
// 监听节点显示
|
||||||
queryEls(this.rule.selector).forEach((el) => {
|
this._interseObserver.observe(node);
|
||||||
this._interseObserver.observe(el);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,45 +229,71 @@ export class Translator {
|
|||||||
this._mutaObserver.disconnect();
|
this._mutaObserver.disconnect();
|
||||||
|
|
||||||
// 解除节点显示监听
|
// 解除节点显示监听
|
||||||
queryEls(this.rule.selector).forEach((el) =>
|
this._interseObserver.disconnect();
|
||||||
this._interseObserver.unobserve(el)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 移除已插入元素
|
// 移除已插入元素
|
||||||
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) => {
|
_reTranslate = debounce(() => {
|
||||||
// 含子元素
|
if (this._rule.transOpen === "true") {
|
||||||
if (el.querySelector(this.rule.selector)) {
|
this._register();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}, 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 太长或太短
|
traEl.remove();
|
||||||
|
}
|
||||||
|
|
||||||
const q = el.innerText.trim();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log("---> ", q);
|
// console.log("---> ", q);
|
||||||
|
|
||||||
const span = document.createElement(APP_LCNAME);
|
traEl = document.createElement(APP_LCNAME);
|
||||||
span.style.visibility = "visible";
|
traEl.style.visibility = "visible";
|
||||||
el.appendChild(span);
|
el.appendChild(traEl);
|
||||||
el.style.cssText +=
|
el.style.cssText +=
|
||||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||||
|
if (el.parentElement) {
|
||||||
el.parentElement.style.cssText +=
|
el.parentElement.style.cssText +=
|
||||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||||
|
}
|
||||||
|
|
||||||
const root = createRoot(span);
|
const root = createRoot(traEl);
|
||||||
root.render(<Content q={q} translator={this} />);
|
root.render(<Content q={q} translator={this} />);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,12 @@ export const matchValue = (arr, val) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const sleep = (delay) =>
|
export const sleep = (delay) =>
|
||||||
new Promise((resolve) => setTimeout(resolve, delay));
|
new Promise((resolve) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve();
|
||||||
|
}, delay);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 防抖函数
|
* 防抖函数
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { StoragesProvider } from "./hooks/Storage";
|
import { SettingProvider } from "./hooks/Setting";
|
||||||
import ThemeProvider from "./hooks/Theme";
|
import ThemeProvider from "./hooks/Theme";
|
||||||
import Popup from "./views/Popup";
|
import Popup from "./views/Popup";
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<StoragesProvider>
|
<SettingProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Popup />
|
<Popup />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StoragesProvider>
|
</SettingProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
11
src/rules.js
11
src/rules.js
@@ -3,6 +3,7 @@ import path from "path";
|
|||||||
import { BUILTIN_RULES } from "./config/rules";
|
import { BUILTIN_RULES } from "./config/rules";
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
// rules
|
||||||
try {
|
try {
|
||||||
const data = JSON.stringify(BUILTIN_RULES, null, " ");
|
const data = JSON.stringify(BUILTIN_RULES, null, " ");
|
||||||
const file = path.resolve(
|
const file = path.resolve(
|
||||||
@@ -14,4 +15,14 @@ import { BUILTIN_RULES } from "./config/rules";
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// version
|
||||||
|
try {
|
||||||
|
var pjson = require("../package.json");
|
||||||
|
const file = path.resolve(__dirname, "../build/web/version.txt");
|
||||||
|
fs.writeFileSync(file, pjson.version);
|
||||||
|
console.info(`Version file generated: ${file}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -3,41 +3,74 @@ import ReactDOM from "react-dom/client";
|
|||||||
import Action from "./views/Action";
|
import Action from "./views/Action";
|
||||||
import createCache from "@emotion/cache";
|
import createCache from "@emotion/cache";
|
||||||
import { CacheProvider } from "@emotion/react";
|
import { CacheProvider } from "@emotion/react";
|
||||||
import { getSetting, getRules, matchRule, getFab } from "./libs";
|
import {
|
||||||
|
getSettingWithDefault,
|
||||||
|
getRulesWithDefault,
|
||||||
|
getFabWithDefault,
|
||||||
|
} from "./libs/storage";
|
||||||
import { Translator } from "./libs/translator";
|
import { Translator } from "./libs/translator";
|
||||||
|
import { trySyncAllSubRules } from "./libs/subRules";
|
||||||
|
import { isGm } from "./libs/client";
|
||||||
|
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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 入口函数
|
* 入口函数
|
||||||
*/
|
*/
|
||||||
(async () => {
|
const init = async () => {
|
||||||
// 设置页面
|
// 设置页面
|
||||||
if (
|
if (
|
||||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
|
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
|
||||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE) ||
|
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE) ||
|
||||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE2)
|
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE2)
|
||||||
) {
|
) {
|
||||||
|
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
|
||||||
unsafeWindow.GM = GM;
|
unsafeWindow.GM = GM;
|
||||||
unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
|
unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
|
||||||
return;
|
} else {
|
||||||
|
const ping = btoa(Math.random()).slice(3, 11);
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 翻译页面
|
// 翻译页面
|
||||||
const setting = await getSetting();
|
const href = isIframe ? document.referrer : document.location.href;
|
||||||
const rules = await getRules();
|
const setting = await getSettingWithDefault();
|
||||||
const rule = await matchRule(rules, document.location.href, setting);
|
const rules = await getRulesWithDefault();
|
||||||
|
const rule = await matchRule(rules, href, setting);
|
||||||
const translator = new Translator(rule, 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");
|
const $action = document.createElement("div");
|
||||||
$action.setAttribute("id", "kiss-translator");
|
$action.setAttribute("id", "kiss-translator");
|
||||||
document.body.parentElement.appendChild($action);
|
document.body.parentElement.appendChild($action);
|
||||||
const shadowContainer = $action.attachShadow({ mode: "open" });
|
const shadowContainer = $action.attachShadow({ mode: "closed" });
|
||||||
const emotionRoot = document.createElement("style");
|
const emotionRoot = document.createElement("style");
|
||||||
const shadowRootElement = document.createElement("div");
|
const shadowRootElement = document.createElement("div");
|
||||||
shadowContainer.appendChild(emotionRoot);
|
shadowContainer.appendChild(emotionRoot);
|
||||||
@@ -56,6 +89,8 @@ import { Translator } from "./libs/translator";
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 注册菜单
|
// 注册菜单
|
||||||
|
if (isGm) {
|
||||||
|
try {
|
||||||
GM.registerMenuCommand(
|
GM.registerMenuCommand(
|
||||||
"Toggle Translate",
|
"Toggle Translate",
|
||||||
(event) => {
|
(event) => {
|
||||||
@@ -70,4 +105,22 @@ import { Translator } from "./libs/translator";
|
|||||||
},
|
},
|
||||||
"C"
|
"C"
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[registerMenuCommand]", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步订阅规则
|
||||||
|
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 { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { limitNumber } from "../../libs/utils";
|
import { limitNumber } from "../../libs/utils";
|
||||||
import { isMobile } from "../../libs/mobile";
|
import { isMobile } from "../../libs/mobile";
|
||||||
import { setFab } from "../../libs";
|
import { setFab } from "../../libs/storage";
|
||||||
|
|
||||||
const getEdgePosition = (
|
const getEdgePosition = (
|
||||||
{ x: left, y: top, edge },
|
{ x: left, y: top, edge },
|
||||||
@@ -159,11 +159,11 @@ export default function Draggable({
|
|||||||
y: position.y,
|
y: position.y,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [position]);
|
}, [position.x, position.y, position.hide]);
|
||||||
|
|
||||||
const opacity = useMemo(() => {
|
const opacity = useMemo(() => {
|
||||||
if (snapEdge) {
|
if (snapEdge) {
|
||||||
return position.hide ? 0.1 : 1;
|
return position.hide ? 0.2 : 1;
|
||||||
}
|
}
|
||||||
return origin ? 0.8 : 1;
|
return origin ? 0.8 : 1;
|
||||||
}, [origin, snapEdge, position.hide]);
|
}, [origin, snapEdge, position.hide]);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import IconButton from "@mui/material/IconButton";
|
|||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||||
import { StoragesProvider } from "../../hooks/Storage";
|
import { SettingProvider } from "../../hooks/Setting";
|
||||||
import Popup from "../Popup";
|
import Popup from "../Popup";
|
||||||
import { debounce } from "../../libs/utils";
|
import { debounce } from "../../libs/utils";
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ export default function Action({ translator, fab }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StoragesProvider>
|
<SettingProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Draggable
|
<Draggable
|
||||||
key="pop"
|
key="pop"
|
||||||
@@ -139,6 +139,6 @@ export default function Action({ translator, fab }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StoragesProvider>
|
</SettingProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { useTranslate } from "../../hooks/Translate";
|
|||||||
export default function Content({ q, translator }) {
|
export default function Content({ q, translator }) {
|
||||||
const [rule, setRule] = useState(translator.rule);
|
const [rule, setRule] = useState(translator.rule);
|
||||||
const [hover, setHover] = useState(false);
|
const [hover, setHover] = useState(false);
|
||||||
const { text, sameLang, loading } = useTranslate(q, rule);
|
const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
|
||||||
const { textStyle, bgColor } = rule;
|
const { textStyle, bgColor } = rule;
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
@@ -47,31 +47,28 @@ export default function Content({ q, translator }) {
|
|||||||
|
|
||||||
const style = useMemo(() => {
|
const style = useMemo(() => {
|
||||||
const lineColor = bgColor || "";
|
const lineColor = bgColor || "";
|
||||||
|
const underlineStyle = (st) => ({
|
||||||
|
opacity: hover ? 1 : 0.6,
|
||||||
|
textDecorationLine: "underline",
|
||||||
|
textDecorationColor: lineColor,
|
||||||
|
textDecorationStyle: st,
|
||||||
|
textDecorationThickness: "2px",
|
||||||
|
textUnderlineOffset: "0.3em",
|
||||||
|
WebkittextDecorationLine: "underline",
|
||||||
|
WebkittextDecorationColor: lineColor,
|
||||||
|
WebkittextDecorationStyle: st,
|
||||||
|
WebkittextDecorationThickness: "2px",
|
||||||
|
WebkittextTextUnderlineOffset: "0.3em",
|
||||||
|
});
|
||||||
switch (textStyle) {
|
switch (textStyle) {
|
||||||
case OPT_STYLE_LINE: // 下划线
|
case OPT_STYLE_LINE: // 下划线
|
||||||
return {
|
return underlineStyle("solid");
|
||||||
opacity: hover ? 1 : 0.6,
|
|
||||||
textDecoration: `underline 2px ${lineColor}`,
|
|
||||||
textUnderlineOffset: "0.3em",
|
|
||||||
};
|
|
||||||
case OPT_STYLE_DOTLINE: // 点状线
|
case OPT_STYLE_DOTLINE: // 点状线
|
||||||
return {
|
return underlineStyle("dotted");
|
||||||
opacity: hover ? 1 : 0.6,
|
|
||||||
textDecoration: `dotted underline 2px ${lineColor}`,
|
|
||||||
textUnderlineOffset: "0.3em",
|
|
||||||
};
|
|
||||||
case OPT_STYLE_DASHLINE: // 虚线
|
case OPT_STYLE_DASHLINE: // 虚线
|
||||||
return {
|
return underlineStyle("dashed");
|
||||||
opacity: hover ? 1 : 0.6,
|
|
||||||
textDecoration: `dashed underline 2px ${lineColor}`,
|
|
||||||
textUnderlineOffset: "0.3em",
|
|
||||||
};
|
|
||||||
case OPT_STYLE_WAVYLINE: // 波浪线
|
case OPT_STYLE_WAVYLINE: // 波浪线
|
||||||
return {
|
return underlineStyle("wavy");
|
||||||
opacity: hover ? 1 : 0.6,
|
|
||||||
textDecoration: `wavy underline 2px ${lineColor}`,
|
|
||||||
textUnderlineOffset: "0.3em",
|
|
||||||
};
|
|
||||||
case OPT_STYLE_FUZZY: // 模糊
|
case OPT_STYLE_FUZZY: // 模糊
|
||||||
return {
|
return {
|
||||||
filter: hover ? "none" : "blur(5px)",
|
filter: hover ? "none" : "blur(5px)",
|
||||||
|
|||||||
@@ -4,17 +4,16 @@ import IconButton from "@mui/material/IconButton";
|
|||||||
import MenuIcon from "@mui/icons-material/Menu";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useDarkModeSwitch } from "../../hooks/ColorMode";
|
|
||||||
import { useDarkMode } from "../../hooks/ColorMode";
|
import { useDarkMode } from "../../hooks/ColorMode";
|
||||||
import LightModeIcon from "@mui/icons-material/LightMode";
|
import LightModeIcon from "@mui/icons-material/LightMode";
|
||||||
import DarkModeIcon from "@mui/icons-material/DarkMode";
|
import DarkModeIcon from "@mui/icons-material/DarkMode";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
|
|
||||||
function Header(props) {
|
function Header(props) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { onDrawerToggle } = props;
|
const { onDrawerToggle } = props;
|
||||||
const switchColorMode = useDarkModeSwitch();
|
const { darkMode, toggleDarkMode } = useDarkMode();
|
||||||
const darkMode = useDarkMode();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
@@ -35,10 +34,14 @@ function Header(props) {
|
|||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ flexGrow: 1 }}>{`${i18n("app_name")} v${
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
process.env.REACT_APP_VERSION
|
<Link
|
||||||
}`}</Box>
|
underline="none"
|
||||||
<IconButton onClick={switchColorMode} color="inherit">
|
color="inherit"
|
||||||
|
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 />}
|
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Stack from "@mui/material/Stack";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
import {
|
import {
|
||||||
GLOBAL_KEY,
|
GLOBAL_KEY,
|
||||||
DEFAULT_RULE,
|
DEFAULT_RULE,
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
OPT_TRANS_ALL,
|
OPT_TRANS_ALL,
|
||||||
OPT_STYLE_ALL,
|
OPT_STYLE_ALL,
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect, useMemo } from "react";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Accordion from "@mui/material/Accordion";
|
import Accordion from "@mui/material/Accordion";
|
||||||
@@ -23,7 +24,7 @@ import MenuItem from "@mui/material/MenuItem";
|
|||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import FileDownloadIcon from "@mui/icons-material/FileDownload";
|
import FileDownloadIcon from "@mui/icons-material/FileDownload";
|
||||||
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
||||||
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
|
import { useSetting } from "../../hooks/Setting";
|
||||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
import Switch from "@mui/material/Switch";
|
import Switch from "@mui/material/Switch";
|
||||||
import Tabs from "@mui/material/Tabs";
|
import Tabs from "@mui/material/Tabs";
|
||||||
@@ -34,12 +35,15 @@ import DeleteIcon from "@mui/icons-material/Delete";
|
|||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import ShareIcon from "@mui/icons-material/Share";
|
import ShareIcon from "@mui/icons-material/Share";
|
||||||
import SyncIcon from "@mui/icons-material/Sync";
|
import SyncIcon from "@mui/icons-material/Sync";
|
||||||
import { useSubrules } from "../../hooks/Rules";
|
import { useSubRules } from "../../hooks/SubRules";
|
||||||
import { rulesCache, tryLoadRules } from "../../libs/rules";
|
import { syncSubRules } from "../../libs/subRules";
|
||||||
|
import { loadOrFetchSubRules } from "../../libs/subRules";
|
||||||
import { useAlert } from "../../hooks/Alert";
|
import { useAlert } from "../../hooks/Alert";
|
||||||
import { loadSyncOpt, syncShareRules } from "../../libs/sync";
|
import { syncShareRules } from "../../libs/sync";
|
||||||
|
import { debounce } from "../../libs/utils";
|
||||||
|
import { delSubRules, getSyncWithDefault } from "../../libs/storage";
|
||||||
|
|
||||||
function RuleFields({ rule, rules, setShow }) {
|
function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||||
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
|
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
|
||||||
const editMode = !!rule;
|
const editMode = !!rule;
|
||||||
|
|
||||||
@@ -73,10 +77,21 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
setErrors((pre) => ({ ...pre, [name]: "" }));
|
setErrors((pre) => ({ ...pre, [name]: "" }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePatternChange = useMemo(
|
||||||
|
() =>
|
||||||
|
debounce(async (patterns) => {
|
||||||
|
setKeyword(patterns.trim());
|
||||||
|
}, 500),
|
||||||
|
[setKeyword]
|
||||||
|
);
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormValues((pre) => ({ ...pre, [name]: value }));
|
setFormValues((pre) => ({ ...pre, [name]: value }));
|
||||||
|
if (name === "pattern" && !editMode) {
|
||||||
|
handlePatternChange(value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = (e) => {
|
const handleCancel = (e) => {
|
||||||
@@ -396,12 +411,12 @@ function UploadButton({ onChange, text }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ShareButton({ rules, injectRules, selectedSub }) {
|
function ShareButton({ rules, injectRules, selectedUrl }) {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
try {
|
try {
|
||||||
const { syncUrl, syncKey } = await loadSyncOpt();
|
const { syncUrl, syncKey } = await getSyncWithDefault();
|
||||||
if (!syncUrl || !syncKey) {
|
if (!syncUrl || !syncKey) {
|
||||||
alert.warning(i18n("error_sync_setting"));
|
alert.warning(i18n("error_sync_setting"));
|
||||||
return;
|
return;
|
||||||
@@ -409,7 +424,7 @@ function ShareButton({ rules, injectRules, selectedSub }) {
|
|||||||
|
|
||||||
const shareRules = [...rules.list];
|
const shareRules = [...rules.list];
|
||||||
if (injectRules) {
|
if (injectRules) {
|
||||||
const subRules = await tryLoadRules(selectedSub?.url);
|
const subRules = await loadOrFetchSubRules(selectedUrl);
|
||||||
shareRules.splice(-1, 0, ...subRules);
|
shareRules.splice(-1, 0, ...subRules);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,16 +453,15 @@ function ShareButton({ rules, injectRules, selectedSub }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserRules() {
|
function UserRules({ subRules }) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const rules = useRules();
|
const rules = useRules();
|
||||||
const [showAdd, setShowAdd] = useState(false);
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
const setting = useSetting();
|
const { setting, updateSetting } = useSetting();
|
||||||
const updateSetting = useSettingUpdate();
|
const [keyword, setKeyword] = useState("");
|
||||||
const subrules = useSubrules();
|
|
||||||
const selectedSub = subrules.list.find((item) => item.selected);
|
|
||||||
|
|
||||||
const injectRules = !!setting?.injectRules;
|
const injectRules = !!setting?.injectRules;
|
||||||
|
const { selectedUrl, selectedRules } = subRules;
|
||||||
|
|
||||||
const handleImport = (e) => {
|
const handleImport = (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
@@ -477,9 +491,21 @@ function UserRules() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showAdd) {
|
||||||
|
setKeyword("");
|
||||||
|
}
|
||||||
|
}, [showAdd]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Stack direction="row" spacing={2} useFlexGap flexWrap="wrap">
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={2}
|
||||||
|
useFlexGap
|
||||||
|
flexWrap="wrap"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -501,7 +527,7 @@ function UserRules() {
|
|||||||
<ShareButton
|
<ShareButton
|
||||||
rules={rules}
|
rules={rules}
|
||||||
injectRules={injectRules}
|
injectRules={injectRules}
|
||||||
selectedSub={selectedSub}
|
selectedUrl={selectedUrl}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
@@ -516,24 +542,48 @@ function UserRules() {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{showAdd && <RuleFields rules={rules} setShow={setShowAdd} />}
|
{showAdd && (
|
||||||
|
<RuleFields
|
||||||
|
rules={rules}
|
||||||
|
setShow={setShowAdd}
|
||||||
|
setKeyword={setKeyword}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box>
|
<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} />
|
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} />
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{injectRules && (
|
||||||
|
<Box>
|
||||||
|
{selectedRules
|
||||||
|
.filter(
|
||||||
|
(rule) =>
|
||||||
|
rule.pattern.includes(keyword) || keyword.includes(rule.pattern)
|
||||||
|
)
|
||||||
|
.map((rule) => (
|
||||||
|
<RuleAccordion key={rule.pattern} rule={rule} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
|
function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleDel = async () => {
|
const handleDel = async () => {
|
||||||
try {
|
try {
|
||||||
await subrules.del(url);
|
await delSub(url);
|
||||||
await rulesCache.del(url);
|
await delSubRules(url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[del subrules]", err);
|
console.log("[del subrules]", err);
|
||||||
}
|
}
|
||||||
@@ -542,10 +592,9 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
|
|||||||
const handleSync = async () => {
|
const handleSync = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const rules = await rulesCache.fetch(url);
|
const rules = await syncSubRules(url);
|
||||||
await rulesCache.set(url, rules);
|
if (rules.length > 0 && url === selectedUrl) {
|
||||||
if (url === selectedUrl) {
|
setSelectedRules(rules);
|
||||||
setRules(rules);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[sync sub rules]", err);
|
console.log("[sync sub rules]", err);
|
||||||
@@ -575,11 +624,12 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubRulesEdit({ subrules }) {
|
function SubRulesEdit({ subList, addSub }) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const [inputText, setInputText] = useState("");
|
const [inputText, setInputText] = useState("");
|
||||||
const [inputError, setInputError] = useState("");
|
const [inputError, setInputError] = useState("");
|
||||||
const [showInput, setShowInput] = useState(false);
|
const [showInput, setShowInput] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleCancel = (e) => {
|
const handleCancel = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -597,23 +647,25 @@ function SubRulesEdit({ subrules }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subrules.list.find((item) => item.url === url)) {
|
if (subList.find((item) => item.url === url)) {
|
||||||
setInputError(i18n("error_duplicate_values"));
|
setInputError(i18n("error_duplicate_values"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rules = await rulesCache.fetch(url);
|
setLoading(true);
|
||||||
|
const rules = await syncSubRules(url);
|
||||||
if (rules.length === 0) {
|
if (rules.length === 0) {
|
||||||
throw new Error("empty rules");
|
throw new Error("empty rules");
|
||||||
}
|
}
|
||||||
await rulesCache.set(url, rules);
|
await addSub(url);
|
||||||
await subrules.add(url);
|
|
||||||
setShowInput(false);
|
setShowInput(false);
|
||||||
setInputText("");
|
setInputText("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[fetch rules]", err);
|
console.log("[fetch rules]", err);
|
||||||
setInputError(i18n("error_fetch_url"));
|
setInputError(i18n("error_fetch_url"));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -656,7 +708,12 @@ function SubRulesEdit({ subrules }) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack direction="row" alignItems="center" spacing={2}>
|
<Stack direction="row" alignItems="center" spacing={2}>
|
||||||
<Button size="small" variant="contained" onClick={handleSave}>
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
{i18n("save")}
|
{i18n("save")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="small" variant="outlined" onClick={handleCancel}>
|
<Button size="small" variant="outlined" onClick={handleCancel}>
|
||||||
@@ -669,47 +726,36 @@ function SubRulesEdit({ subrules }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubRules() {
|
function SubRules({ subRules }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const {
|
||||||
const [rules, setRules] = useState([]);
|
subList,
|
||||||
const subrules = useSubrules();
|
selectSub,
|
||||||
const selectedSub = subrules.list.find((item) => item.selected);
|
addSub,
|
||||||
|
delSub,
|
||||||
|
selectedUrl,
|
||||||
|
selectedRules,
|
||||||
|
setSelectedRules,
|
||||||
|
loading,
|
||||||
|
} = subRules;
|
||||||
|
|
||||||
const handleSelect = (e) => {
|
const handleSelect = (e) => {
|
||||||
const url = e.target.value;
|
const url = e.target.value;
|
||||||
subrules.select(url);
|
selectSub(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
if (selectedSub?.url) {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const rules = await tryLoadRules(selectedSub?.url);
|
|
||||||
setRules(rules);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[load rules]", err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [selectedSub?.url]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<SubRulesEdit subrules={subrules} />
|
<SubRulesEdit subList={subList} addSub={addSub} />
|
||||||
|
|
||||||
<RadioGroup value={selectedSub?.url} onChange={handleSelect}>
|
<RadioGroup value={selectedUrl} onChange={handleSelect}>
|
||||||
{subrules.list.map((item, index) => (
|
{subList.map((item, index) => (
|
||||||
<SubRulesItem
|
<SubRulesItem
|
||||||
key={item.url}
|
key={item.url}
|
||||||
url={item.url}
|
url={item.url}
|
||||||
index={index}
|
index={index}
|
||||||
selectedUrl={selectedSub?.url}
|
selectedUrl={selectedUrl}
|
||||||
subrules={subrules}
|
delSub={delSub}
|
||||||
setRules={setRules}
|
setSelectedRules={setSelectedRules}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
@@ -720,7 +766,9 @@ function SubRules() {
|
|||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</center>
|
</center>
|
||||||
) : (
|
) : (
|
||||||
rules.map((rule) => <RuleAccordion key={rule.pattern} rule={rule} />)
|
selectedRules.map((rule) => (
|
||||||
|
<RuleAccordion key={rule.pattern} rule={rule} />
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -730,6 +778,7 @@ function SubRules() {
|
|||||||
export default function Rules() {
|
export default function Rules() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const subRules = useSubRules();
|
||||||
|
|
||||||
const handleTabChange = (e, newValue) => {
|
const handleTabChange = (e, newValue) => {
|
||||||
setActiveTab(newValue);
|
setActiveTab(newValue);
|
||||||
@@ -738,14 +787,24 @@ export default function Rules() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
|
<Alert severity="info">
|
||||||
|
{i18n("rules_warn_1")}
|
||||||
|
<br />
|
||||||
|
{i18n("rules_warn_2")}
|
||||||
|
</Alert>
|
||||||
|
|
||||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||||
<Tab label={i18n("edit_rules")} />
|
<Tab label={i18n("personal_rules")} />
|
||||||
<Tab label={i18n("subscribe_rules")} />
|
<Tab label={i18n("subscribe_rules")} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
<div hidden={activeTab !== 0}>{activeTab === 0 && <UserRules />}</div>
|
<div hidden={activeTab !== 0}>
|
||||||
<div hidden={activeTab !== 1}>{activeTab === 1 && <SubRules />}</div>
|
{activeTab === 0 && <UserRules subRules={subRules} />}
|
||||||
|
</div>
|
||||||
|
<div hidden={activeTab !== 1}>
|
||||||
|
{activeTab === 1 && <SubRules subRules={subRules} />}
|
||||||
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,20 +5,16 @@ import TextField from "@mui/material/TextField";
|
|||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import FormControl from "@mui/material/FormControl";
|
import FormControl from "@mui/material/FormControl";
|
||||||
import Select from "@mui/material/Select";
|
import Select from "@mui/material/Select";
|
||||||
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
|
import { useSetting } from "../../hooks/Setting";
|
||||||
import { limitNumber, debounce } from "../../libs/utils";
|
import { limitNumber } from "../../libs/utils";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import { UI_LANGS } from "../../config";
|
import { UI_LANGS } from "../../config";
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const setting = useSetting();
|
const { setting, updateSetting } = useSetting();
|
||||||
const updateSetting = useSettingUpdate();
|
|
||||||
|
|
||||||
const handleChange = useMemo(
|
const handleChange = (e) => {
|
||||||
() =>
|
|
||||||
debounce((e) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let { name, value } = e.target;
|
let { name, value } = e.target;
|
||||||
switch (name) {
|
switch (name) {
|
||||||
@@ -28,24 +24,26 @@ export default function Settings() {
|
|||||||
case "fetchInterval":
|
case "fetchInterval":
|
||||||
value = limitNumber(value, 0, 5000);
|
value = limitNumber(value, 0, 5000);
|
||||||
break;
|
break;
|
||||||
|
case "minLength":
|
||||||
|
value = limitNumber(value, 1, 100);
|
||||||
|
break;
|
||||||
|
case "maxLength":
|
||||||
|
value = limitNumber(value, 100, 10000);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
updateSetting({
|
updateSetting({
|
||||||
[name]: value,
|
[name]: value,
|
||||||
});
|
});
|
||||||
}, 500),
|
};
|
||||||
[updateSetting]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!setting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
uiLang,
|
uiLang,
|
||||||
googleUrl,
|
googleUrl,
|
||||||
fetchLimit,
|
fetchLimit,
|
||||||
fetchInterval,
|
fetchInterval,
|
||||||
|
minLength,
|
||||||
|
maxLength,
|
||||||
openaiUrl,
|
openaiUrl,
|
||||||
openaiKey,
|
openaiKey,
|
||||||
openaiModel,
|
openaiModel,
|
||||||
@@ -77,7 +75,7 @@ export default function Settings() {
|
|||||||
label={i18n("fetch_limit")}
|
label={i18n("fetch_limit")}
|
||||||
type="number"
|
type="number"
|
||||||
name="fetchLimit"
|
name="fetchLimit"
|
||||||
defaultValue={fetchLimit}
|
value={fetchLimit}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -86,7 +84,25 @@ export default function Settings() {
|
|||||||
label={i18n("fetch_interval")}
|
label={i18n("fetch_interval")}
|
||||||
type="number"
|
type="number"
|
||||||
name="fetchInterval"
|
name="fetchInterval"
|
||||||
defaultValue={fetchInterval}
|
value={fetchInterval}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -107,7 +123,7 @@ export default function Settings() {
|
|||||||
size="small"
|
size="small"
|
||||||
label={i18n("google_api")}
|
label={i18n("google_api")}
|
||||||
name="googleUrl"
|
name="googleUrl"
|
||||||
defaultValue={googleUrl}
|
value={googleUrl}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -115,7 +131,7 @@ export default function Settings() {
|
|||||||
size="small"
|
size="small"
|
||||||
label={i18n("openai_api")}
|
label={i18n("openai_api")}
|
||||||
name="openaiUrl"
|
name="openaiUrl"
|
||||||
defaultValue={openaiUrl}
|
value={openaiUrl}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -124,7 +140,7 @@ export default function Settings() {
|
|||||||
type="password"
|
type="password"
|
||||||
label={i18n("openai_key")}
|
label={i18n("openai_key")}
|
||||||
name="openaiKey"
|
name="openaiKey"
|
||||||
defaultValue={openaiKey}
|
value={openaiKey}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -132,7 +148,7 @@ export default function Settings() {
|
|||||||
size="small"
|
size="small"
|
||||||
label={i18n("openai_model")}
|
label={i18n("openai_model")}
|
||||||
name="openaiModel"
|
name="openaiModel"
|
||||||
defaultValue={openaiModel}
|
value={openaiModel}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -140,7 +156,7 @@ export default function Settings() {
|
|||||||
size="small"
|
size="small"
|
||||||
label={i18n("openai_prompt")}
|
label={i18n("openai_prompt")}
|
||||||
name="openaiPrompt"
|
name="openaiPrompt"
|
||||||
defaultValue={openaiPrompt}
|
value={openaiPrompt}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
multiline
|
multiline
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,32 +6,42 @@ import { useSync } from "../../hooks/Sync";
|
|||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import { URL_KISS_WORKER } from "../../config";
|
import { URL_KISS_WORKER } from "../../config";
|
||||||
import { debounce } from "../../libs/utils";
|
import { useState } from "react";
|
||||||
import { useMemo } from "react";
|
import { syncSettingAndRules } from "../../libs/sync";
|
||||||
import { syncAll } 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";
|
||||||
|
|
||||||
export default function SyncSetting() {
|
export default function SyncSetting() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const sync = useSync();
|
const { sync, updateSync } = useSync();
|
||||||
|
const alert = useAlert();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleChange = useMemo(
|
const handleChange = async (e) => {
|
||||||
() =>
|
|
||||||
debounce(async (e) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
await sync.update({
|
await updateSync({
|
||||||
[name]: value,
|
[name]: value,
|
||||||
});
|
});
|
||||||
await syncAll();
|
};
|
||||||
}, 1000),
|
|
||||||
[sync]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!sync.opt) {
|
const handleSyncTest = async (e) => {
|
||||||
return;
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await syncSettingAndRules();
|
||||||
|
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 (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
@@ -42,7 +52,7 @@ export default function SyncSetting() {
|
|||||||
size="small"
|
size="small"
|
||||||
label={i18n("data_sync_url")}
|
label={i18n("data_sync_url")}
|
||||||
name="syncUrl"
|
name="syncUrl"
|
||||||
defaultValue={syncUrl}
|
value={syncUrl}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
helperText={
|
helperText={
|
||||||
<Link href={URL_KISS_WORKER}>{i18n("about_sync_api")}</Link>
|
<Link href={URL_KISS_WORKER}>{i18n("about_sync_api")}</Link>
|
||||||
@@ -54,9 +64,28 @@ export default function SyncSetting() {
|
|||||||
type="password"
|
type="password"
|
||||||
label={i18n("data_sync_key")}
|
label={i18n("data_sync_key")}
|
||||||
name="syncKey"
|
name="syncKey"
|
||||||
defaultValue={syncKey}
|
value={syncKey}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={2}
|
||||||
|
useFlexGap
|
||||||
|
flexWrap="wrap"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
disabled={!syncUrl || !syncKey || loading}
|
||||||
|
onClick={handleSyncTest}
|
||||||
|
startIcon={<SyncIcon />}
|
||||||
|
>
|
||||||
|
{i18n("data_sync_test")}
|
||||||
|
</Button>
|
||||||
|
{loading && <CircularProgress size={16} />}
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ import Rules from "./Rules";
|
|||||||
import Setting from "./Setting";
|
import Setting from "./Setting";
|
||||||
import Layout from "./Layout";
|
import Layout from "./Layout";
|
||||||
import SyncSetting from "./SyncSetting";
|
import SyncSetting from "./SyncSetting";
|
||||||
import { StoragesProvider } from "../../hooks/Storage";
|
import { SettingProvider } from "../../hooks/Setting";
|
||||||
import ThemeProvider from "../../hooks/Theme";
|
import ThemeProvider from "../../hooks/Theme";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { isGm } from "../../libs/browser";
|
import { isGm } from "../../libs/client";
|
||||||
import { sleep } from "../../libs/utils";
|
import { sleep } from "../../libs/utils";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
import { syncAll } from "../../libs/sync";
|
import { trySyncSettingAndRules } from "../../libs/sync";
|
||||||
import { AlertProvider } from "../../hooks/Alert";
|
import { AlertProvider } from "../../hooks/Alert";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
|
||||||
export default function Options() {
|
export default function Options() {
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
@@ -24,6 +27,8 @@ export default function Options() {
|
|||||||
let i = 0;
|
let i = 0;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (window.APP_NAME === process.env.REACT_APP_NAME) {
|
if (window.APP_NAME === process.env.REACT_APP_NAME) {
|
||||||
|
// 同步数据
|
||||||
|
await trySyncSettingAndRules();
|
||||||
setReady(true);
|
setReady(true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -35,40 +40,65 @@ export default function Options() {
|
|||||||
|
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
|
||||||
// 同步数据
|
// 同步数据
|
||||||
syncAll();
|
await trySyncSettingAndRules();
|
||||||
|
setReady(true);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<center>
|
<center>
|
||||||
|
<Divider>
|
||||||
|
<Link
|
||||||
|
href={process.env.REACT_APP_HOMEPAGE}
|
||||||
|
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||||
|
</Divider>
|
||||||
<h2>
|
<h2>
|
||||||
Please confirm whether to install or enable{" "}
|
Please confirm whether to install or enable KISS Translator
|
||||||
<a href={process.env.REACT_APP_HOMEPAGE}>KISS Translator</a>{" "}
|
|
||||||
GreaseMonkey script?
|
GreaseMonkey script?
|
||||||
</h2>
|
</h2>
|
||||||
<h2>
|
<Stack spacing={2}>
|
||||||
<a href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>Click here</a>{" "}
|
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
||||||
to install, or <a href={process.env.REACT_APP_HOMEPAGE}>click here</a>{" "}
|
Install Userscript 1
|
||||||
for help.
|
</Link>
|
||||||
</h2>
|
<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>
|
</center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGm && !ready) {
|
if (!ready) {
|
||||||
return (
|
return (
|
||||||
<center>
|
<center>
|
||||||
|
<Divider>
|
||||||
|
<Link
|
||||||
|
href={process.env.REACT_APP_HOMEPAGE}
|
||||||
|
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||||
|
</Divider>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</center>
|
</center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StoragesProvider>
|
<SettingProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AlertProvider>
|
<AlertProvider>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
@@ -83,6 +113,6 @@ export default function Options() {
|
|||||||
</HashRouter>
|
</HashRouter>
|
||||||
</AlertProvider>
|
</AlertProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StoragesProvider>
|
</SettingProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import FormControlLabel from "@mui/material/FormControlLabel";
|
|||||||
import Switch from "@mui/material/Switch";
|
import Switch from "@mui/material/Switch";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import { sendTabMsg } from "../../libs/msg";
|
import { sendTabMsg } from "../../libs/msg";
|
||||||
import { browser, isExt } from "../../libs/browser";
|
import { browser } from "../../libs/browser";
|
||||||
|
import { isExt } from "../../libs/client";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
OPT_LANGS_TO,
|
OPT_LANGS_TO,
|
||||||
OPT_STYLE_ALL,
|
OPT_STYLE_ALL,
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
|
import { sendIframeMsg } from "../../libs/iframe";
|
||||||
|
|
||||||
export default function Popup({ setShowPopup, translator: tran }) {
|
export default function Popup({ setShowPopup, translator: tran }) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -40,6 +42,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
await sendTabMsg(MSG_TRANS_TOGGLE);
|
await sendTabMsg(MSG_TRANS_TOGGLE);
|
||||||
} else {
|
} else {
|
||||||
tran.toggle();
|
tran.toggle();
|
||||||
|
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[toggle trans]", err);
|
console.log("[toggle trans]", err);
|
||||||
@@ -55,6 +58,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||||
} else {
|
} else {
|
||||||
tran.updateRule({ [name]: value });
|
tran.updateRule({ [name]: value });
|
||||||
|
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[update rule]", err);
|
console.log("[update rule]", err);
|
||||||
@@ -101,7 +105,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
onChange={handleTransToggle}
|
onChange={handleTransToggle}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={i18n("translate")}
|
label={i18n("translate_alt")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -158,7 +162,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
size="small"
|
size="small"
|
||||||
value={textStyle}
|
value={textStyle}
|
||||||
name="textStyle"
|
name="textStyle"
|
||||||
label={i18n("text_style")}
|
label={i18n("text_style_alt")}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{OPT_STYLE_ALL.map((item) => (
|
{OPT_STYLE_ALL.map((item) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user