Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13e7c1b754 | ||
|
|
d314d5515f | ||
|
|
09b19e3ca0 | ||
|
|
687bd11fd1 | ||
|
|
56cb1cd30d | ||
|
|
7a3df25521 | ||
|
|
ea8919ba07 | ||
|
|
3dece4fcdb | ||
|
|
df950a1bd2 | ||
|
|
74b9ee31fa | ||
|
|
64cd55fe58 | ||
|
|
e80ede14fb | ||
|
|
45ba9d3320 | ||
|
|
47c7048538 | ||
|
|
f9bfa8101f | ||
|
|
620ac464eb | ||
|
|
62289f8ab8 | ||
|
|
d84594da96 | ||
|
|
e1d74aae6a | ||
|
|
c4980d9eb7 | ||
|
|
882d83c6b7 | ||
|
|
c4a7fd81f8 | ||
|
|
0e55799109 | ||
|
|
a3cdcb2a1a | ||
|
|
e0ccc298f9 | ||
|
|
36b49bb577 | ||
|
|
2636c24e84 | ||
|
|
6bcf294635 | ||
|
|
c5fa6689a4 | ||
|
|
3bf0cb2485 | ||
|
|
19c9335527 | ||
|
|
20da2e1b97 | ||
|
|
9eceb8641d | ||
|
|
86bc915d74 | ||
|
|
6b35525207 | ||
|
|
4633bf4fc6 | ||
|
|
2665f31d94 | ||
|
|
6c4d3149eb | ||
|
|
a2762e6ce6 | ||
|
|
792a1bfcad | ||
|
|
a0eba9d60e | ||
|
|
c2e0064253 | ||
|
|
f246efc84b | ||
|
|
4a3bf7e96c | ||
|
|
523b81090d | ||
|
|
d706c405d9 | ||
|
|
1191791447 | ||
|
|
5c510f2df2 | ||
|
|
7c0aa23177 | ||
|
|
4bc1c26653 | ||
|
|
ca1e1148d6 | ||
|
|
2224455a7f | ||
|
|
f463f3ce08 | ||
|
|
c0872db98c | ||
|
|
d3a5d91f01 | ||
|
|
3e9338be0e | ||
|
|
ef7f1ad638 | ||
|
|
1f10ebe404 | ||
|
|
f4a8251c61 | ||
|
|
f585a43480 | ||
|
|
3a11465c24 | ||
|
|
3c3ebdf96c |
23
.env
23
.env
@@ -2,11 +2,22 @@ GENERATE_SOURCEMAP=false
|
||||
|
||||
REACT_APP_NAME=KISS Translator
|
||||
REACT_APP_NAME_CN=简约翻译
|
||||
REACT_APP_VERSION=1.5.0
|
||||
REACT_APP_VERSION=1.5.8
|
||||
|
||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||
REACT_APP_OPTIONSPAGE=https://kiss-translator.rayjar.com/options
|
||||
REACT_APP_OPTIONSPAGE2=https://fishjar.github.io/kiss-translator/options.html
|
||||
|
||||
REACT_APP_OPTIONSPAGE=https://fishjar.github.io/kiss-translator/options.html
|
||||
REACT_APP_OPTIONSPAGE2=https://kiss-translator.rayjar.com/options
|
||||
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
|
||||
REACT_APP_LOGOURL=https://kiss-translator.rayjar.com/images/logo192.png
|
||||
REACT_APP_USERSCRIPT_DOWNLOADURL=https://kiss-translator.rayjar.com/kiss-translator.user.js
|
||||
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
|
||||
|
||||
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
|
||||
REACT_APP_LOGOURL2=https://kiss-translator.rayjar.com/images/logo192.png
|
||||
|
||||
REACT_APP_RULESURL=https://fishjar.github.io/kiss-translator/kiss-translator-rules.json
|
||||
REACT_APP_RULESURL2=https://kiss-translator.rayjar.com/kiss-translator-rules.json
|
||||
|
||||
REACT_APP_VERSIONFILE=https://fishjar.github.io/kiss-translator/version.txt
|
||||
REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt
|
||||
|
||||
REACT_APP_USERSCRIPT_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
|
||||
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator.user.js
|
||||
|
||||
@@ -32,9 +32,9 @@ If you also like a little more simplicity, welcome to pick it up.
|
||||
- [x] Microsoft
|
||||
- [x] OpenAI
|
||||
- [ ] DeepL
|
||||
- [ ] Upload to app Store
|
||||
- [x] Upload to app Store
|
||||
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
|
||||
- [ ] Edge
|
||||
- [x] [Edge](https://microsoftedge.microsoft.com/addons/detail/kiss-translator/jemckldkclkinpjighnoilpbldbdmmlh)
|
||||
- [x] [Firefox](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
|
||||
- [ ] Safari
|
||||
- [x] [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||
@@ -42,7 +42,7 @@ If you also like a little more simplicity, welcome to pick it up.
|
||||
- [x] Data Synchronization Function
|
||||
- [x] Greasemonkey Script ([link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js))
|
||||
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox)
|
||||
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (need test)
|
||||
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari)
|
||||
|
||||
### Guide
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
- [x] Microsoft
|
||||
- [x] OpenAI
|
||||
- [ ] DeepL
|
||||
- [ ] 上架应用市场
|
||||
- [x] 上架应用市场
|
||||
- [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/)
|
||||
- [ ] Safari
|
||||
- [x] [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||
@@ -42,7 +42,7 @@
|
||||
- [x] 数据同步功能
|
||||
- [x] 油猴脚本([链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js))
|
||||
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox)
|
||||
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (待测)
|
||||
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari)
|
||||
|
||||
### 指引
|
||||
|
||||
|
||||
@@ -84,10 +84,12 @@ const userscriptWebpack = (config, env) => {
|
||||
// @downloadURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||
// @grant GM.xmlHttpRequest
|
||||
// @grant GM.registerMenuCommand
|
||||
// @grant GM.setValue
|
||||
// @grant GM.getValue
|
||||
// @grant GM.deleteValue
|
||||
// @grant unsafeWindow
|
||||
// @grant GM.info
|
||||
// @inject-into content
|
||||
// @connect translate.googleapis.com
|
||||
// @connect api-edge.cognitive.microsofttranslator.com
|
||||
// @connect edge.microsoft.com
|
||||
@@ -97,6 +99,7 @@ const userscriptWebpack = (config, env) => {
|
||||
// @connect github.io
|
||||
// @connect githubusercontent.com
|
||||
// @connect kiss-translator.rayjar.com
|
||||
// @connect ghproxy.com
|
||||
// @run-at document-end
|
||||
// ==/UserScript==
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kiss-translator",
|
||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.8",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@@ -37,8 +37,7 @@
|
||||
"react-app/jest"
|
||||
],
|
||||
"globals": {
|
||||
"GM": true,
|
||||
"unsafeWindow": true
|
||||
"GM": true
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
@@ -4,5 +4,11 @@
|
||||
},
|
||||
"app_description": {
|
||||
"message": "A minimalist bilingual translation Extension & Greasemonkey Script"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "Toggle Translate"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "Toggle Style"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,11 @@
|
||||
},
|
||||
"app_description": {
|
||||
"message": "一个简约的双语网页翻译扩展 & 油猴脚本"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "切换翻译"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "切换样式"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,63 @@
|
||||
max-height: 1.2em;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// (() => {
|
||||
// var shadow = document.querySelector("#shadow1");
|
||||
// var root = shadow.attachShadow({ mode: "open" });
|
||||
// var newLine = document.createElement("p");
|
||||
// newLine.innerText = "new line";
|
||||
// root.appendChild(newLine);
|
||||
// })();
|
||||
|
||||
// setTimeout(function () {
|
||||
// var shadow = document.querySelector("#shadow2");
|
||||
// var root = shadow.attachShadow({ mode: "open" });
|
||||
// }, 1000);
|
||||
|
||||
// setTimeout(() => {
|
||||
// var newLine = document.createElement("p");
|
||||
// newLine.innerText = "new line";
|
||||
// var shadow = document.querySelector("#shadow2");
|
||||
// shadow.shadowRoot.appendChild(newLine);
|
||||
// }, 2000);
|
||||
|
||||
// setTimeout(() => {
|
||||
// var newLine = document.createElement("div");
|
||||
// newLine.innerHTML = "<p>second line</p><p>third line</p>";
|
||||
// var shadow = document.querySelector("#shadow2");
|
||||
// shadow.shadowRoot.appendChild(newLine);
|
||||
// }, 3000);
|
||||
|
||||
// setTimeout(function () {
|
||||
// var el = document.querySelector("h2");
|
||||
// el.innerText = "hello world";
|
||||
|
||||
// var title = document.querySelector("#addtitle");
|
||||
// title.innerHTML =
|
||||
// "<div><p>second title</p><ul><li>second title</li><li><p>second title</p></li></ul></div>";
|
||||
// }, 1000);
|
||||
|
||||
setTimeout(function () {
|
||||
var el = document.querySelector("h2>p>span");
|
||||
el.innerText = "hello world";
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<h2>
|
||||
<p><span>React is a JavaScript library for building user interfaces.</span></p>
|
||||
</h2>
|
||||
<div id="addtitle"></div>
|
||||
<h2>Shadow 1</h2>
|
||||
<div id="shadow1"></div>
|
||||
<h2>Shadow 2</h2>
|
||||
<div id="shadow2"></div>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
@@ -53,7 +104,16 @@
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<h2>
|
||||
React Server Components (or RSC) is a new application architecture
|
||||
designed by the React team.
|
||||
</h2>
|
||||
<iframe
|
||||
id="iframe1"
|
||||
width="800px"
|
||||
height="600px"
|
||||
src="http://localhost:3000/index.html"
|
||||
></iframe>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
@@ -86,7 +146,10 @@
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<h2>
|
||||
We’ve first shared our research on RSC in an introductory talk and an
|
||||
RFC.
|
||||
</h2>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
@@ -119,7 +182,17 @@
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<h2>
|
||||
To recap them, we are introducing a new kind of component—Server
|
||||
Components—that run ahead of time and are excluded from your JavaScript
|
||||
bundle.
|
||||
</h2>
|
||||
<iframe
|
||||
id="iframe2"
|
||||
width="800px"
|
||||
height="600px"
|
||||
src="https://react.dev/"
|
||||
></iframe>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
@@ -153,175 +226,42 @@
|
||||
<br />
|
||||
<br />
|
||||
<div class="cont cont1">
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Declarative: React makes it painless to create interactive UIs.
|
||||
Design simple views for each state in your application, and React
|
||||
will efficiently update and render just the right components when
|
||||
your data changes. Declarative views make your code more
|
||||
predictable, simpler to understand, and easier to debug.
|
||||
</li>
|
||||
<li>
|
||||
Component-Based: Build encapsulated components that manage their own
|
||||
state, then compose them to make complex UIs. Since component logic
|
||||
is written in JavaScript instead of templates, you can easily pass
|
||||
rich data through your app and keep the state out of the DOM.
|
||||
</li>
|
||||
<li>
|
||||
React 使创建交互式 UI
|
||||
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
|
||||
能高效更新并渲染合适的组件。
|
||||
</li>
|
||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<div class="cont cont2">
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Declarative: React makes it painless to create interactive UIs.
|
||||
Design simple views for each state in your application, and React
|
||||
will efficiently update and render just the right components when
|
||||
your data changes. Declarative views make your code more
|
||||
predictable, simpler to understand, and easier to debug.
|
||||
</li>
|
||||
<li>
|
||||
Component-Based: Build encapsulated components that manage their own
|
||||
state, then compose them to make complex UIs. Since component logic
|
||||
is written in JavaScript instead of templates, you can easily pass
|
||||
rich data through your app and keep the state out of the DOM.
|
||||
</li>
|
||||
<li>
|
||||
React 使创建交互式 UI
|
||||
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
|
||||
能高效更新并渲染合适的组件。
|
||||
</li>
|
||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<div class="cont cont3">
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Declarative: React makes it painless to create interactive UIs.
|
||||
Design simple views for each state in your application, and React
|
||||
will efficiently update and render just the right components when
|
||||
your data changes. Declarative views make your code more
|
||||
predictable, simpler to understand, and easier to debug.
|
||||
</li>
|
||||
<li>
|
||||
Component-Based: Build encapsulated components that manage their own
|
||||
state, then compose them to make complex UIs. Since component logic
|
||||
is written in JavaScript instead of templates, you can easily pass
|
||||
rich data through your app and keep the state out of the DOM.
|
||||
</li>
|
||||
<li>
|
||||
React 使创建交互式 UI
|
||||
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
|
||||
能高效更新并渲染合适的组件。
|
||||
</li>
|
||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<div class="cont cont4">
|
||||
<h2>
|
||||
React is a <code>JavaScript</code> <a href="#">library</a> for
|
||||
building user interfaces.
|
||||
Server Components can run during the build, letting you read from the
|
||||
filesystem or fetch static content.
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Declarative: React makes it painless to create interactive UIs.
|
||||
Design simple views for each state in your application, and React
|
||||
will efficiently update and render just the right components when
|
||||
your data changes. Declarative views make your code more
|
||||
predictable, simpler to understand, and easier to debug.
|
||||
</li>
|
||||
<li>
|
||||
Component-Based: Build encapsulated components that manage their own
|
||||
state, then compose them to make complex UIs. Since component logic
|
||||
is written in JavaScript instead of templates, you can easily pass
|
||||
rich data through your app and keep the state out of the DOM.
|
||||
</li>
|
||||
<li>
|
||||
React 使创建交互式 UI
|
||||
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
|
||||
能高效更新并渲染合适的组件。
|
||||
They can also run on the server, letting you access your data layer
|
||||
without having to build an API. You can pass data by props from
|
||||
Server Components to the interactive Client Components in the
|
||||
browser.
|
||||
</li>
|
||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<div class="cont cont5">
|
||||
<h2>React is a JavaScript library for building user interfaces.</h2>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<div class="cont cont2">
|
||||
<h2>
|
||||
Since our last update, we have merged the React Server Components RFC
|
||||
to ratify the proposal.
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Declarative: React makes it painless to create interactive UIs.
|
||||
Design simple views for each state in your application, and React
|
||||
will efficiently update and render just the right components when
|
||||
your data changes. Declarative views make your code more
|
||||
predictable, simpler to understand, and easier to debug.
|
||||
</li>
|
||||
<li>
|
||||
Component-Based: Build encapsulated components that manage their own
|
||||
state, then compose them to make complex UIs. Since component logic
|
||||
is written in JavaScript instead of templates, you can easily pass
|
||||
rich data through your app and keep the state out of the DOM.
|
||||
RSC combines the simple “request/response” mental model of
|
||||
server-centric Multi-Page Apps with the seamless interactivity of
|
||||
client-centric Single-Page Apps, giving you the best of both worlds.
|
||||
</li>
|
||||
<li>
|
||||
React 使创建交互式 UI
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.8",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -12,9 +12,24 @@
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": ["content.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
"matches": ["<all_urls>"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"toggleTranslate": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+Q"
|
||||
},
|
||||
"description": "__MSG_toggle_translate__"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
}
|
||||
},
|
||||
"permissions": ["<all_urls>", "storage"],
|
||||
"icons": {
|
||||
"16": "images/logo16.png",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.8",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -13,9 +13,24 @@
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": ["content.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
"matches": ["<all_urls>"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"toggleTranslate": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+Q"
|
||||
},
|
||||
"description": "__MSG_toggle_translate__"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
}
|
||||
},
|
||||
"permissions": ["storage"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"icons": {
|
||||
|
||||
@@ -20,19 +20,16 @@ import { sha256 } from "../libs/utils";
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
export const apiSyncData = async (url, key, data) =>
|
||||
fetchPolyfill(
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
export const apiSyncData = async (url, key, data, isBg = false) =>
|
||||
fetchPolyfill(url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
||||
},
|
||||
{ useUnsafe: true }
|
||||
);
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
isBg,
|
||||
});
|
||||
|
||||
/**
|
||||
* 谷歌翻译
|
||||
@@ -53,15 +50,14 @@ const apiGoogleTranslate = async (translator, text, to, from) => {
|
||||
};
|
||||
const { googleUrl } = await getSetting();
|
||||
const input = `${googleUrl}?${queryString.stringify(params)}`;
|
||||
return fetchPolyfill(
|
||||
input,
|
||||
{
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
return fetchPolyfill(input, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
{ useCache: true, usePool: true, translator }
|
||||
);
|
||||
useCache: true,
|
||||
usePool: true,
|
||||
translator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -78,17 +74,16 @@ const apiMicrosoftTranslate = (translator, text, to, from) => {
|
||||
"api-version": "3.0",
|
||||
};
|
||||
const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`;
|
||||
return fetchPolyfill(
|
||||
input,
|
||||
{
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify([{ Text: text }]),
|
||||
return fetchPolyfill(input, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
{ useCache: true, usePool: true, translator }
|
||||
);
|
||||
method: "POST",
|
||||
body: JSON.stringify([{ Text: text }]),
|
||||
useCache: true,
|
||||
usePool: true,
|
||||
translator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -104,31 +99,31 @@ const apiOpenaiTranslate = async (translator, text, to, from) => {
|
||||
let prompt = openaiPrompt
|
||||
.replaceAll(PROMPT_PLACE_FROM, from)
|
||||
.replaceAll(PROMPT_PLACE_TO, to);
|
||||
return fetchPolyfill(
|
||||
openaiUrl,
|
||||
{
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
model: openaiModel,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: prompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: text,
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 256,
|
||||
}),
|
||||
return fetchPolyfill(openaiUrl, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
{ useCache: true, usePool: true, translator, token: openaiKey }
|
||||
);
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
model: openaiModel,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: prompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: text,
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 256,
|
||||
}),
|
||||
useCache: true,
|
||||
usePool: true,
|
||||
translator,
|
||||
token: openaiKey,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,10 @@ import {
|
||||
MSG_FETCH,
|
||||
MSG_FETCH_LIMIT,
|
||||
MSG_FETCH_CLEAR,
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
CMD_TOGGLE_TRANSLATE,
|
||||
CMD_TOGGLE_STYLE,
|
||||
DEFAULT_SETTING,
|
||||
DEFAULT_RULES,
|
||||
DEFAULT_SYNC,
|
||||
@@ -10,11 +14,15 @@ import {
|
||||
STOKEY_RULES,
|
||||
STOKEY_SYNC,
|
||||
CACHE_NAME,
|
||||
STOKEY_RULESCACHE_PREFIX,
|
||||
BUILTIN_RULES,
|
||||
} from "./config";
|
||||
import storage from "./libs/storage";
|
||||
import { getSetting } from "./libs";
|
||||
import { syncAll } from "./libs/sync";
|
||||
import { trySyncAll } from "./libs/sync";
|
||||
import { fetchData, fetchPool } from "./libs/fetch";
|
||||
import { sendTabMsg } from "./libs/msg";
|
||||
import { trySyncAllSubRules } from "./libs/rules";
|
||||
|
||||
/**
|
||||
* 插件安装
|
||||
@@ -24,7 +32,10 @@ browser.runtime.onInstalled.addListener(() => {
|
||||
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
|
||||
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES);
|
||||
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
|
||||
// todo:缓存内置rules
|
||||
storage.trySetObj(
|
||||
`${STOKEY_RULESCACHE_PREFIX}${process.env.REACT_APP_RULESURL}`,
|
||||
BUILTIN_RULES
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -34,13 +45,16 @@ browser.runtime.onStartup.addListener(async () => {
|
||||
console.log("browser onStartup");
|
||||
|
||||
// 同步数据
|
||||
await syncAll();
|
||||
await trySyncAll(true);
|
||||
|
||||
// 清除缓存
|
||||
const { clearCache } = await getSetting();
|
||||
if (clearCache) {
|
||||
const setting = await getSetting();
|
||||
if (setting.clearCache) {
|
||||
caches.delete(CACHE_NAME);
|
||||
}
|
||||
|
||||
// 同步订阅规则
|
||||
trySyncAllSubRules(setting, true);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -50,8 +64,8 @@ browser.runtime.onMessage.addListener(
|
||||
({ action, args }, sender, sendResponse) => {
|
||||
switch (action) {
|
||||
case MSG_FETCH:
|
||||
const { input, init, opts } = args;
|
||||
fetchData(input, init, opts)
|
||||
const { input, opts } = args;
|
||||
fetchData(input, opts)
|
||||
.then((data) => {
|
||||
sendResponse({ data });
|
||||
})
|
||||
@@ -74,3 +88,19 @@ browser.runtime.onMessage.addListener(
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 监听快捷键
|
||||
*/
|
||||
browser.commands.onCommand.addListener((command) => {
|
||||
// console.log(`Command: ${command}`);
|
||||
switch (command) {
|
||||
case CMD_TOGGLE_TRANSLATE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
break;
|
||||
case CMD_TOGGLE_STYLE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,6 +12,10 @@ export const I18N = {
|
||||
zh: `翻译`,
|
||||
en: `Translate`,
|
||||
},
|
||||
translate_alt: {
|
||||
zh: `翻译 (Alt+Q)`,
|
||||
en: `Translate (Alt+Q)`,
|
||||
},
|
||||
basic_setting: {
|
||||
zh: `基本设置`,
|
||||
en: `Basic Setting`,
|
||||
@@ -41,12 +45,20 @@ export const I18N = {
|
||||
en: `Interface Language`,
|
||||
},
|
||||
fetch_limit: {
|
||||
zh: `最大请求数量`,
|
||||
en: `Maximum Number Of Request`,
|
||||
zh: `最大请求数量 (1-100)`,
|
||||
en: `Maximum Number Of Request (1-100)`,
|
||||
},
|
||||
fetch_interval: {
|
||||
zh: `请求间隔时间(ms)`,
|
||||
en: `Request Interval(ms)`,
|
||||
zh: `请求间隔时间 (0-5000ms)`,
|
||||
en: `Request Interval (0-5000ms)`,
|
||||
},
|
||||
min_translate_length: {
|
||||
zh: `最小翻译长度 (1-100)`,
|
||||
en: `Min Translate Length (1-100)`,
|
||||
},
|
||||
max_translate_length: {
|
||||
zh: `最大翻译长度 (100-10000)`,
|
||||
en: `Max Translate Length (100-10000)`,
|
||||
},
|
||||
translate_service: {
|
||||
zh: `翻译服务`,
|
||||
@@ -64,6 +76,10 @@ export const I18N = {
|
||||
zh: `文字样式`,
|
||||
en: `Text Style`,
|
||||
},
|
||||
text_style_alt: {
|
||||
zh: `文字样式 (Alt+C)`,
|
||||
en: `Text Style (Alt+C)`,
|
||||
},
|
||||
bg_color: {
|
||||
zh: `样式颜色`,
|
||||
en: `Style Color`,
|
||||
@@ -108,9 +124,9 @@ export const I18N = {
|
||||
zh: `注入订阅规则`,
|
||||
en: `Inject Subscribe Rules`,
|
||||
},
|
||||
edit_rules: {
|
||||
zh: `编辑规则`,
|
||||
en: `Edit Rules`,
|
||||
personal_rules: {
|
||||
zh: `个人规则`,
|
||||
en: `Personal Rules`,
|
||||
},
|
||||
subscribe_rules: {
|
||||
zh: `订阅规则`,
|
||||
@@ -120,6 +136,14 @@ export const I18N = {
|
||||
zh: `订阅地址`,
|
||||
en: `Subscribe URL`,
|
||||
},
|
||||
rules_warn_1: {
|
||||
zh: `1、“个人规则”一直生效,选择“注入订阅规则”后,“订阅规则”才会生效。`,
|
||||
en: `1. The "Personal Rules" are always in effect. After selecting "Inject Subscription Rules", the "Subscription Rules" will take effect.`,
|
||||
},
|
||||
rules_warn_2: {
|
||||
zh: `2、“订阅规则”的注入位置是倒数第二的位置,因此除全局规则(*)外,“个人规则”优先级比“订阅规则”高,“个人规则”填写同样的网址会覆盖”订阅规则“的条目。`,
|
||||
en: `2. The injection position of "Subscription Rules" is the penultimate position. Therefore, except for the global rules (*), the priority of "Personal Rules" is higher than that of "Subscription Rules". Filling in the same url in "Personal Rules" will overwrite "Subscription Rules" entry.`,
|
||||
},
|
||||
sync_warn: {
|
||||
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
|
||||
en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`,
|
||||
@@ -169,8 +193,8 @@ export const I18N = {
|
||||
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`,
|
||||
},
|
||||
selector_helper: {
|
||||
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。`,
|
||||
en: `1. Follow CSS selector rules. 2. Leave blank to adopt the global setting.`,
|
||||
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`,
|
||||
en: `1. Follow 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: {
|
||||
zh: `开启翻译`,
|
||||
@@ -248,12 +272,24 @@ export const I18N = {
|
||||
zh: `数据同步密钥`,
|
||||
en: `Data Sync Key`,
|
||||
},
|
||||
data_sync_test: {
|
||||
zh: `数据同步测试`,
|
||||
en: `Data Sync Test`,
|
||||
},
|
||||
data_sync_success: {
|
||||
zh: `数据同步成功!`,
|
||||
en: `Data Sync Success`,
|
||||
},
|
||||
data_sync_error: {
|
||||
zh: `数据同步失败!`,
|
||||
en: `Data Sync Error`,
|
||||
},
|
||||
error_got_some_wrong: {
|
||||
zh: "抱歉,出错了!",
|
||||
en: "Sorry, something went wrong!",
|
||||
zh: `抱歉,出错了!`,
|
||||
en: `Sorry, something went wrong!`,
|
||||
},
|
||||
error_sync_setting: {
|
||||
zh: "您的同步设置未填写,无法在线分享。",
|
||||
en: "Your sync settings are missing and cannot be shared online.",
|
||||
zh: `您的同步设置未填写,无法在线分享。`,
|
||||
en: `Your sync settings are missing and cannot be shared online.`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {
|
||||
DEFAULT_SELECTOR,
|
||||
GLOBAL_KEY,
|
||||
SHADOW_KEY,
|
||||
DEFAULT_RULE,
|
||||
BUILTIN_RULES,
|
||||
} from "./rules";
|
||||
export { I18N, UI_LANGS } from "./i18n";
|
||||
export { GLOBAL_KEY, DEFAULT_RULE, BUILTIN_RULES };
|
||||
export { GLOBAL_KEY, SHADOW_KEY, DEFAULT_RULE, BUILTIN_RULES };
|
||||
|
||||
const APP_NAME = process.env.REACT_APP_NAME.trim().split(/\s+/).join("-");
|
||||
|
||||
@@ -18,6 +19,9 @@ export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
||||
export const STOKEY_FAB = `${APP_NAME}_fab`;
|
||||
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
|
||||
|
||||
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
|
||||
export const CMD_TOGGLE_STYLE = "toggleStyle";
|
||||
|
||||
export const CLIENT_WEB = "web";
|
||||
export const CLIENT_CHROME = "chrome";
|
||||
export const CLIENT_EDGE = "edge";
|
||||
@@ -37,6 +41,7 @@ export const MSG_FETCH = "fetch";
|
||||
export const MSG_FETCH_LIMIT = "fetch_limit";
|
||||
export const MSG_FETCH_CLEAR = "fetch_clear";
|
||||
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
||||
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
|
||||
export const MSG_TRANS_GETRULE = "trans_getrule";
|
||||
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
||||
export const MSG_TRANS_CURRULE = "trans_currule";
|
||||
@@ -153,7 +158,7 @@ export const GLOBLA_RULE = {
|
||||
// 订阅列表
|
||||
export const DEFAULT_SUBRULES_LIST = [
|
||||
{
|
||||
url: "https://kiss-translator.rayjar.com/kiss-translator-rules.json",
|
||||
url: process.env.REACT_APP_RULESURL,
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
@@ -161,11 +166,16 @@ export const DEFAULT_SUBRULES_LIST = [
|
||||
},
|
||||
];
|
||||
|
||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
||||
|
||||
export const DEFAULT_SETTING = {
|
||||
darkMode: false, // 深色模式
|
||||
uiLang: "en", // 界面语言
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
||||
minLength: TRANS_MIN_LENGTH,
|
||||
maxLength: TRANS_MAX_LENGTH,
|
||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||
injectRules: true, // 是否注入订阅规则
|
||||
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
||||
@@ -178,9 +188,6 @@ export const DEFAULT_SETTING = {
|
||||
|
||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||
|
||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
||||
|
||||
export const DEFAULT_SYNC = {
|
||||
syncUrl: "", // 数据同步接口
|
||||
syncKey: "", // 数据同步密钥
|
||||
@@ -188,4 +195,5 @@ export const DEFAULT_SYNC = {
|
||||
settingSyncAt: 0,
|
||||
rulesUpdateAt: 0,
|
||||
rulesSyncAt: 0,
|
||||
subRulesSyncAt: 0, // 订阅规则同步时间
|
||||
};
|
||||
|
||||
@@ -4,6 +4,8 @@ export const DEFAULT_SELECTOR = `:is(${els})`;
|
||||
|
||||
export const GLOBAL_KEY = "*";
|
||||
|
||||
export const SHADOW_KEY = ">>>";
|
||||
|
||||
export const DEFAULT_RULE = {
|
||||
pattern: "",
|
||||
selector: "",
|
||||
@@ -21,9 +23,13 @@ const RULES = [
|
||||
selector: `h3, .IsZvec, .VwiC3b`,
|
||||
},
|
||||
{
|
||||
pattern: `https://news.google.com/`,
|
||||
pattern: `news.google.com`,
|
||||
selector: `h4`,
|
||||
},
|
||||
{
|
||||
pattern: `www.foxnews.com`,
|
||||
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
|
||||
},
|
||||
{
|
||||
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,
|
||||
selector: DEFAULT_SELECTOR,
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { browser } from "./libs/browser";
|
||||
import {
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
} from "./config";
|
||||
import { getSetting, getRules, matchRule } from "./libs";
|
||||
import { Translator } from "./libs/translator";
|
||||
import { isIframe } from "./libs/iframe";
|
||||
|
||||
/**
|
||||
* 入口函数
|
||||
*/
|
||||
(async () => {
|
||||
const init = async () => {
|
||||
const href = isIframe ? document.referrer : document.location.href;
|
||||
const setting = await getSetting();
|
||||
const rules = await getRules();
|
||||
const rule = await matchRule(rules, document.location.href, setting);
|
||||
const rule = await matchRule(rules, href, setting);
|
||||
const translator = new Translator(rule, setting);
|
||||
|
||||
// 监听消息
|
||||
@@ -22,6 +25,9 @@ import { Translator } from "./libs/translator";
|
||||
case MSG_TRANS_TOGGLE:
|
||||
translator.toggle();
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
translator.toggleStyle();
|
||||
break;
|
||||
case MSG_TRANS_GETRULE:
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
@@ -32,4 +38,15 @@ import { Translator } from "./libs/translator";
|
||||
}
|
||||
return { data: translator.rule };
|
||||
});
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await init();
|
||||
} catch (err) {
|
||||
const $err = document.createElement("div");
|
||||
$err.innerText = `KISS-Translator: ${err.message}`;
|
||||
$err.style.cssText = "background:red; color:#fff; z-index:10000;";
|
||||
document.body.prepend($err);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { STOKEY_RULES, DEFAULT_SUBRULES_LIST } from "../config";
|
||||
import storage from "../libs/storage";
|
||||
import { useStorages } from "./Storage";
|
||||
import { syncRules } from "../libs/sync";
|
||||
import { trySyncRules } from "../libs/sync";
|
||||
import { useSync } from "./Sync";
|
||||
import { useSetting, useSettingUpdate } from "./Setting";
|
||||
import { checkRules } from "../libs/rules";
|
||||
@@ -19,7 +19,7 @@ export function useRules() {
|
||||
const updateAt = sync.opt?.rulesUpdateAt ? Date.now() : 0;
|
||||
await storage.setObj(STOKEY_RULES, rules);
|
||||
await sync.update({ rulesUpdateAt: updateAt });
|
||||
syncRules();
|
||||
trySyncRules();
|
||||
};
|
||||
|
||||
const add = async (rule) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { STOKEY_SETTING } from "../config";
|
||||
import storage from "../libs/storage";
|
||||
import { useStorages } from "./Storage";
|
||||
import { useSync } from "./Sync";
|
||||
import { syncSetting } from "../libs/sync";
|
||||
import { trySyncSetting } from "../libs/sync";
|
||||
|
||||
/**
|
||||
* 设置hook
|
||||
@@ -23,6 +23,6 @@ export function useSettingUpdate() {
|
||||
const updateAt = sync.opt?.settingUpdateAt ? Date.now() : 0;
|
||||
await storage.putObj(STOKEY_SETTING, obj);
|
||||
await sync.update({ settingUpdateAt: updateAt });
|
||||
syncSetting();
|
||||
trySyncSetting();
|
||||
};
|
||||
}
|
||||
|
||||
39
src/index.js
39
src/index.js
@@ -1,19 +1,52 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Button from "@mui/material/Button";
|
||||
import Link from "@mui/material/Link";
|
||||
import { useFetch } from "./hooks/Fetch";
|
||||
import { I18N, URL_RAW_PREFIX } from "./config";
|
||||
|
||||
function App() {
|
||||
const [lang, setLang] = useState("zh");
|
||||
const [data, loading, error] = useFetch(
|
||||
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.["zh"]}`
|
||||
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}`
|
||||
);
|
||||
return (
|
||||
<Paper sx={{ padding: 2, margin: 2 }}>
|
||||
<Divider>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Divider>
|
||||
<Stack spacing={2} direction="row" justifyContent="flex-end">
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setLang((pre) => (pre === "zh" ? "en" : "zh"));
|
||||
}}
|
||||
>
|
||||
{lang === "zh" ? "ENGLISH" : "中文"}
|
||||
</Button>
|
||||
</Stack>
|
||||
<Divider>
|
||||
<Link
|
||||
href={process.env.REACT_APP_HOMEPAGE}
|
||||
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||
</Divider>
|
||||
<Stack spacing={2} direction="row" useFlexGap flexWrap="wrap">
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
||||
Install Userscript 1
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
|
||||
Install Userscript 2
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
|
||||
Open Options Page 1
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
|
||||
Open Options Page 2
|
||||
</Link>
|
||||
</Stack>
|
||||
|
||||
{loading ? (
|
||||
<center>
|
||||
<CircularProgress />
|
||||
|
||||
@@ -19,7 +19,7 @@ import { msAuth } from "./auth";
|
||||
* @param {*} init
|
||||
* @returns
|
||||
*/
|
||||
const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
||||
export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
GM.xmlHttpRequest({
|
||||
method,
|
||||
@@ -65,7 +65,7 @@ const newCacheReq = async (request) => {
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const fetchApi = async ({ input, init, useUnsafe, translator, token }) => {
|
||||
const fetchApi = async ({ input, init = {}, translator, token }) => {
|
||||
if (translator === OPT_TRANS_MICROSOFT) {
|
||||
init.headers["Authorization"] = `Bearer ${token}`;
|
||||
} else if (translator === OPT_TRANS_OPENAI) {
|
||||
@@ -73,8 +73,23 @@ const fetchApi = async ({ input, init, useUnsafe, translator, token }) => {
|
||||
init.headers["api-key"] = token; // Azure OpenAI
|
||||
}
|
||||
|
||||
if (isGm && !useUnsafe) {
|
||||
return fetchGM(input, init);
|
||||
if (isGm) {
|
||||
let info;
|
||||
if (window.KISS_GM) {
|
||||
info = await window.KISS_GM.getInfo();
|
||||
} else {
|
||||
info = GM.info;
|
||||
}
|
||||
const connects = info?.script?.connects || [];
|
||||
const url = new URL(input);
|
||||
const isSafe = connects.find((item) => url.hostname.endsWith(item));
|
||||
if (isSafe) {
|
||||
if (window.KISS_GM) {
|
||||
return window.KISS_GM.fetch(input, init);
|
||||
} else {
|
||||
return fetchGM(input, init);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fetch(input, init);
|
||||
};
|
||||
@@ -98,14 +113,12 @@ export const fetchPool = taskPool(
|
||||
/**
|
||||
* 请求数据统一接口
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @param {*} opts
|
||||
* @returns
|
||||
*/
|
||||
export const fetchData = async (
|
||||
input,
|
||||
init,
|
||||
{ useCache, usePool, translator, useUnsafe, token } = {}
|
||||
{ useCache, usePool, translator, token, ...init } = {}
|
||||
) => {
|
||||
const cacheReq = await newCacheReq(new Request(input, init));
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
@@ -123,9 +136,9 @@ export const fetchData = async (
|
||||
if (!res) {
|
||||
// 发送请求
|
||||
if (usePool) {
|
||||
res = await fetchPool.push({ input, init, useUnsafe, translator, token });
|
||||
res = await fetchPool.push({ input, init, translator, token });
|
||||
} else {
|
||||
res = await fetchApi({ input, init, useUnsafe, translator, token });
|
||||
res = await fetchApi({ input, init, translator, token });
|
||||
}
|
||||
|
||||
if (!res?.ok) {
|
||||
@@ -152,22 +165,21 @@ export const fetchData = async (
|
||||
/**
|
||||
* fetch 兼容性封装
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @param {*} opts
|
||||
* @returns
|
||||
*/
|
||||
export const fetchPolyfill = async (input, init, opts) => {
|
||||
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
|
||||
// 插件
|
||||
if (isExt) {
|
||||
const res = await sendMsg(MSG_FETCH, { input, init, opts });
|
||||
if (isExt && !isBg) {
|
||||
const res = await sendMsg(MSG_FETCH, { input, opts });
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// 油猴/网页
|
||||
return await fetchData(input, init, opts);
|
||||
// 油猴/网页/BackgroundPage
|
||||
return await fetchData(input, opts);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
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 }, "*");
|
||||
});
|
||||
};
|
||||
@@ -10,16 +10,7 @@ import {
|
||||
} from "../config";
|
||||
import { browser } from "./browser";
|
||||
import { isMatch } from "./utils";
|
||||
import { tryLoadRules } from "./rules";
|
||||
|
||||
/**
|
||||
* 获取节点列表并转为数组
|
||||
* @param {*} selector
|
||||
* @param {*} el
|
||||
* @returns
|
||||
*/
|
||||
export const queryEls = (selector, el = document) =>
|
||||
Array.from(el.querySelectorAll(selector));
|
||||
import { loadSubRules } from "./rules";
|
||||
|
||||
/**
|
||||
* 查询storage中的设置
|
||||
@@ -57,13 +48,14 @@ export const setFab = async (obj) => await storage.setObj(STOKEY_FAB, obj);
|
||||
export const matchRule = async (
|
||||
rules,
|
||||
href,
|
||||
{ injectRules, subrulesList = DEFAULT_SUBRULES_LIST }
|
||||
{ injectRules = true, subrulesList = DEFAULT_SUBRULES_LIST }
|
||||
) => {
|
||||
rules = [...rules];
|
||||
if (injectRules) {
|
||||
try {
|
||||
const selectedSub = subrulesList.find((item) => item.selected);
|
||||
if (selectedSub?.url) {
|
||||
const subRules = await tryLoadRules(selectedSub.url);
|
||||
const subRules = await loadSubRules(selectedSub.url);
|
||||
rules.splice(-1, 0, ...subRules);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -71,13 +63,13 @@ export const matchRule = async (
|
||||
}
|
||||
}
|
||||
|
||||
const rule = rules.find((rule) =>
|
||||
rule.pattern.split(",").some((p) => isMatch(href, p.trim()))
|
||||
const rule = rules.find((r) =>
|
||||
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
|
||||
);
|
||||
|
||||
const globalRule =
|
||||
rules.find((rule) =>
|
||||
rule.pattern.split(",").some((p) => p.trim() === "*")
|
||||
) || GLOBLA_RULE;
|
||||
rules.find((r) => r.pattern.split(",").some((p) => p.trim() === "*")) ||
|
||||
GLOBLA_RULE;
|
||||
|
||||
if (!rule) {
|
||||
return globalRule;
|
||||
@@ -107,6 +99,10 @@ export const matchRule = async (
|
||||
* @returns
|
||||
*/
|
||||
export const detectLang = async (q) => {
|
||||
const res = await browser?.i18n.detectLanguage(q);
|
||||
return res?.languages?.[0]?.language;
|
||||
try {
|
||||
const res = await browser?.i18n?.detectLanguage(q);
|
||||
return res?.languages?.[0]?.language;
|
||||
} catch (err) {
|
||||
console.log("[detect lang]", err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
} from "../config";
|
||||
import { syncOpt } from "./sync";
|
||||
|
||||
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
|
||||
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
|
||||
@@ -62,11 +63,11 @@ export const checkRules = (rules) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 本地rules缓存
|
||||
* 订阅规则的本地缓存
|
||||
*/
|
||||
export const rulesCache = {
|
||||
fetch: async (url) => {
|
||||
const res = await fetchPolyfill(url);
|
||||
fetch: async (url, isBg = false) => {
|
||||
const res = await fetchPolyfill(url, { isBg });
|
||||
const rules = checkRules(res).filter(
|
||||
(rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== ""
|
||||
);
|
||||
@@ -84,16 +85,61 @@ export const rulesCache = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 从缓存或远程加载订阅的rules
|
||||
* 同步订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const tryLoadRules = async (url) => {
|
||||
let rules = await rulesCache.get(url);
|
||||
export const syncSubRules = async (url, isBg = false) => {
|
||||
const rules = await rulesCache.fetch(url, isBg);
|
||||
if (rules.length > 0) {
|
||||
await rulesCache.set(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 syncOpt.load();
|
||||
const now = Date.now();
|
||||
const interval = 24 * 60 * 60 * 1000; // 间隔一天
|
||||
if (now - subRulesSyncAt > interval) {
|
||||
await syncAllSubRules(subrulesList, isBg);
|
||||
await syncOpt.update({ subRulesSyncAt: now });
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[try sync all subrules]", err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从缓存或远程加载订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const loadSubRules = async (url) => {
|
||||
const rules = await rulesCache.get(url);
|
||||
if (rules?.length) {
|
||||
return rules;
|
||||
}
|
||||
rules = await rulesCache.fetch(url);
|
||||
await rulesCache.set(url, rules);
|
||||
return rules;
|
||||
return await syncSubRules(url);
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@ async function set(key, val) {
|
||||
if (isExt) {
|
||||
await browser.storage.local.set({ [key]: val });
|
||||
} else if (isGm) {
|
||||
const oldValue = await GM.getValue(key);
|
||||
await GM.setValue(key, val);
|
||||
const oldValue = await (window.KISS_GM || GM).getValue(key);
|
||||
await (window.KISS_GM || GM).setValue(key, val);
|
||||
window.dispatchEvent(
|
||||
new StorageEvent("storage", {
|
||||
key,
|
||||
@@ -31,7 +31,7 @@ async function get(key) {
|
||||
const val = await browser.storage.local.get([key]);
|
||||
return val[key];
|
||||
} else if (isGm) {
|
||||
const val = await GM.getValue(key);
|
||||
const val = await (window.KISS_GM || GM).getValue(key);
|
||||
return val;
|
||||
}
|
||||
return window.localStorage.getItem(key);
|
||||
@@ -41,8 +41,8 @@ async function del(key) {
|
||||
if (isExt) {
|
||||
await browser.storage.local.remove([key]);
|
||||
} else if (isGm) {
|
||||
const oldValue = await GM.getValue(key);
|
||||
await GM.deleteValue(key);
|
||||
const oldValue = await (window.KISS_GM || GM).getValue(key);
|
||||
await (window.KISS_GM || GM).deleteValue(key);
|
||||
window.dispatchEvent(
|
||||
new StorageEvent("storage", {
|
||||
key,
|
||||
|
||||
137
src/libs/sync.js
137
src/libs/sync.js
@@ -13,69 +13,103 @@ import { getSetting, getRules } from ".";
|
||||
import { apiSyncData } from "../apis";
|
||||
import { sha256 } from "./utils";
|
||||
|
||||
export const loadSyncOpt = async () =>
|
||||
(await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC;
|
||||
/**
|
||||
* 同步相关数据
|
||||
*/
|
||||
export const syncOpt = {
|
||||
load: async () => (await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC,
|
||||
update: async (obj) => {
|
||||
await storage.putObj(STOKEY_SYNC, obj);
|
||||
},
|
||||
};
|
||||
|
||||
export const syncSetting = async () => {
|
||||
try {
|
||||
const { syncUrl, syncKey, settingUpdateAt } = await loadSyncOpt();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 同步设置
|
||||
* @returns
|
||||
*/
|
||||
export const syncSetting = async (isBg = false) => {
|
||||
const { syncUrl, syncKey, settingUpdateAt } = await syncOpt.load();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setting = await getSetting();
|
||||
const res = await apiSyncData(syncUrl, syncKey, {
|
||||
const setting = await getSetting();
|
||||
const res = await apiSyncData(
|
||||
syncUrl,
|
||||
syncKey,
|
||||
{
|
||||
key: KV_SETTING_KEY,
|
||||
value: setting,
|
||||
updateAt: settingUpdateAt,
|
||||
});
|
||||
},
|
||||
isBg
|
||||
);
|
||||
|
||||
if (res && res.updateAt > settingUpdateAt) {
|
||||
await storage.putObj(STOKEY_SYNC, {
|
||||
settingUpdateAt: res.updateAt,
|
||||
settingSyncAt: res.updateAt,
|
||||
});
|
||||
await storage.setObj(STOKEY_SETTING, res.value);
|
||||
} else {
|
||||
await storage.putObj(STOKEY_SYNC, {
|
||||
settingSyncAt: res.updateAt,
|
||||
});
|
||||
}
|
||||
if (res && res.updateAt > settingUpdateAt) {
|
||||
await syncOpt.update({
|
||||
settingUpdateAt: res.updateAt,
|
||||
settingSyncAt: res.updateAt,
|
||||
});
|
||||
await storage.setObj(STOKEY_SETTING, res.value);
|
||||
} else {
|
||||
await syncOpt.update({ settingSyncAt: res.updateAt });
|
||||
}
|
||||
};
|
||||
|
||||
export const trySyncSetting = async (isBg = false) => {
|
||||
try {
|
||||
await syncSetting(isBg);
|
||||
} catch (err) {
|
||||
console.log("[sync setting]", err);
|
||||
}
|
||||
};
|
||||
|
||||
export const syncRules = async () => {
|
||||
try {
|
||||
const { syncUrl, syncKey, rulesUpdateAt } = await loadSyncOpt();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 同步规则
|
||||
* @returns
|
||||
*/
|
||||
export const syncRules = async (isBg = false) => {
|
||||
const { syncUrl, syncKey, rulesUpdateAt } = await syncOpt.load();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rules = await getRules();
|
||||
const res = await apiSyncData(syncUrl, syncKey, {
|
||||
const rules = await getRules();
|
||||
const res = await apiSyncData(
|
||||
syncUrl,
|
||||
syncKey,
|
||||
{
|
||||
key: KV_RULES_KEY,
|
||||
value: rules,
|
||||
updateAt: rulesUpdateAt,
|
||||
});
|
||||
},
|
||||
isBg
|
||||
);
|
||||
|
||||
if (res && res.updateAt > rulesUpdateAt) {
|
||||
await storage.putObj(STOKEY_SYNC, {
|
||||
rulesUpdateAt: res.updateAt,
|
||||
rulesSyncAt: res.updateAt,
|
||||
});
|
||||
await storage.setObj(STOKEY_RULES, res.value);
|
||||
} else {
|
||||
await storage.putObj(STOKEY_SYNC, {
|
||||
rulesSyncAt: res.updateAt,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[sync rules]", err);
|
||||
if (res && res.updateAt > rulesUpdateAt) {
|
||||
await syncOpt.update({
|
||||
rulesUpdateAt: res.updateAt,
|
||||
rulesSyncAt: res.updateAt,
|
||||
});
|
||||
await storage.setObj(STOKEY_RULES, res.value);
|
||||
} else {
|
||||
await syncOpt.update({ rulesSyncAt: res.updateAt });
|
||||
}
|
||||
};
|
||||
|
||||
export const trySyncRules = async (isBg = false) => {
|
||||
try {
|
||||
await syncRules(isBg);
|
||||
} catch (err) {
|
||||
console.log("[sync user rules]", err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步分享规则
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
||||
await apiSyncData(syncUrl, syncKey, {
|
||||
key: KV_RULES_SHARE_KEY,
|
||||
@@ -87,7 +121,16 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
||||
return shareUrl;
|
||||
};
|
||||
|
||||
export const syncAll = async () => {
|
||||
await syncSetting();
|
||||
await syncRules();
|
||||
/**
|
||||
* 同步个人设置和规则
|
||||
* @returns
|
||||
*/
|
||||
export const syncAll = async (isBg = false) => {
|
||||
await syncSetting(isBg);
|
||||
await syncRules(isBg);
|
||||
};
|
||||
|
||||
export const trySyncAll = async (isBg = false) => {
|
||||
await trySyncSetting(isBg);
|
||||
await trySyncRules(isBg);
|
||||
};
|
||||
|
||||
@@ -5,17 +5,41 @@ import {
|
||||
TRANS_MAX_LENGTH,
|
||||
EVENT_KISS,
|
||||
MSG_TRANS_CURRULE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_FUZZY,
|
||||
SHADOW_KEY,
|
||||
} from "../config";
|
||||
import { queryEls } from ".";
|
||||
import Content from "../views/Content";
|
||||
import { fetchUpdate, fetchClear } from "./fetch";
|
||||
import { debounce } from "./utils";
|
||||
|
||||
/**
|
||||
* 翻译类
|
||||
*/
|
||||
export class Translator {
|
||||
_rule = {};
|
||||
_minLength = 0;
|
||||
_maxLength = 0;
|
||||
_skipNodeNames = [
|
||||
APP_LCNAME,
|
||||
"style",
|
||||
"svg",
|
||||
"img",
|
||||
"audio",
|
||||
"video",
|
||||
"textarea",
|
||||
"input",
|
||||
"button",
|
||||
"select",
|
||||
"option",
|
||||
"head",
|
||||
"script",
|
||||
"iframe",
|
||||
];
|
||||
_rootNodes = new Set();
|
||||
_tranNodes = new Map();
|
||||
|
||||
// 显示
|
||||
_interseObserver = new IntersectionObserver(
|
||||
(intersections) => {
|
||||
intersections.forEach((intersection) => {
|
||||
@@ -30,22 +54,46 @@ export class Translator {
|
||||
}
|
||||
);
|
||||
|
||||
// 变化
|
||||
_mutaObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
try {
|
||||
queryEls(this.rule.selector, node).forEach((el) => {
|
||||
this._interseObserver.observe(el);
|
||||
});
|
||||
} catch (err) {
|
||||
//
|
||||
if (
|
||||
!this._skipNodeNames.includes(mutation.target.localName) &&
|
||||
mutation.addedNodes.length > 0
|
||||
) {
|
||||
const nodes = Array.from(mutation.addedNodes).filter((node) => {
|
||||
if (
|
||||
this._skipNodeNames.includes(node.localName) ||
|
||||
node.id === APP_LCNAME
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (nodes.length > 0) {
|
||||
// const rootNode = mutation.target.getRootNode();
|
||||
// todo
|
||||
this._reTranslate();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
constructor(rule, { fetchInterval, fetchLimit }) {
|
||||
// 插入 shadowroot
|
||||
_overrideAttachShadow = () => {
|
||||
const _this = this;
|
||||
const _attachShadow = HTMLElement.prototype.attachShadow;
|
||||
HTMLElement.prototype.attachShadow = function () {
|
||||
_this._reTranslate();
|
||||
return _attachShadow.apply(this, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
constructor(rule, { fetchInterval, fetchLimit, minLength, maxLength }) {
|
||||
fetchUpdate(fetchInterval, fetchLimit);
|
||||
this._overrideAttachShadow();
|
||||
this._minLength = minLength ?? TRANS_MIN_LENGTH;
|
||||
this._maxLength = maxLength ?? TRANS_MAX_LENGTH;
|
||||
this.rule = rule;
|
||||
if (rule.transOpen === "true") {
|
||||
this._register();
|
||||
@@ -86,16 +134,88 @@ export class Translator {
|
||||
}
|
||||
};
|
||||
|
||||
toggleStyle = () => {
|
||||
const textStyle =
|
||||
this.rule.textStyle === OPT_STYLE_FUZZY
|
||||
? OPT_STYLE_DASHLINE
|
||||
: OPT_STYLE_FUZZY;
|
||||
this.rule = { ...this.rule, textStyle };
|
||||
};
|
||||
|
||||
_querySelectorAll = (selector, node) => {
|
||||
try {
|
||||
return Array.from(node.querySelectorAll(selector));
|
||||
} catch (err) {
|
||||
console.log(`[querySelectorAll err]: ${selector}`);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
_queryFilter = (selector, rootNode) => {
|
||||
return this._querySelectorAll(selector, rootNode).filter(
|
||||
(node) => this._queryFilter(selector, node).length === 0
|
||||
);
|
||||
};
|
||||
|
||||
_queryNodes = (rootNode = document) => {
|
||||
// const childRoots = Array.from(rootNode.querySelectorAll("*"))
|
||||
// .map((item) => item.shadowRoot)
|
||||
// .filter(Boolean);
|
||||
// const childNodes = childRoots.map((item) => this._queryNodes(item));
|
||||
// const nodes = Array.from(rootNode.querySelectorAll(this.rule.selector));
|
||||
// return nodes.concat(childNodes).flat();
|
||||
|
||||
this._rootNodes.add(rootNode);
|
||||
this._rule.selector
|
||||
.split(";")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((selector) => {
|
||||
if (selector.includes(SHADOW_KEY)) {
|
||||
const [outSelector, inSelector] = selector
|
||||
.split(SHADOW_KEY)
|
||||
.map((item) => item.trim());
|
||||
if (outSelector && inSelector) {
|
||||
const outNodes = this._querySelectorAll(outSelector, rootNode);
|
||||
outNodes.forEach((outNode) => {
|
||||
if (outNode.shadowRoot) {
|
||||
this._rootNodes.add(outNode.shadowRoot);
|
||||
this._queryFilter(inSelector, outNode.shadowRoot).forEach(
|
||||
(item) => {
|
||||
if (!this._tranNodes.has(item)) {
|
||||
this._tranNodes.set(item, "");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this._queryFilter(selector, rootNode).forEach((item) => {
|
||||
if (!this._tranNodes.has(item)) {
|
||||
this._tranNodes.set(item, "");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
_register = () => {
|
||||
// 监听节点变化
|
||||
this._mutaObserver.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
// 搜索节点
|
||||
this._queryNodes();
|
||||
|
||||
this._rootNodes.forEach((node) => {
|
||||
// 监听节点变化;
|
||||
this._mutaObserver.observe(node, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
// characterData: true,
|
||||
});
|
||||
});
|
||||
|
||||
// 监听节点显示
|
||||
queryEls(this.rule.selector).forEach((el) => {
|
||||
this._interseObserver.observe(el);
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
// 监听节点显示
|
||||
this._interseObserver.observe(node);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -104,45 +224,67 @@ export class Translator {
|
||||
this._mutaObserver.disconnect();
|
||||
|
||||
// 解除节点显示监听
|
||||
queryEls(this.rule.selector).forEach((el) =>
|
||||
this._interseObserver.unobserve(el)
|
||||
);
|
||||
this._interseObserver.disconnect();
|
||||
|
||||
// 移除已插入元素
|
||||
queryEls(APP_LCNAME).forEach((el) => el.remove());
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
node.querySelector(APP_LCNAME)?.remove();
|
||||
});
|
||||
|
||||
// 清空节点集合
|
||||
this._rootNodes.clear();
|
||||
this._tranNodes.clear();
|
||||
|
||||
// 清空任务池
|
||||
fetchClear();
|
||||
};
|
||||
|
||||
_render = (el) => {
|
||||
// 含子元素
|
||||
if (el.querySelector(this.rule.selector)) {
|
||||
return;
|
||||
_reTranslate = debounce(() => {
|
||||
if (this._rule.transOpen === "true") {
|
||||
this._register();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
_render = (el) => {
|
||||
let traEl = el.querySelector(APP_LCNAME);
|
||||
|
||||
// 已翻译
|
||||
if (el.querySelector(APP_LCNAME)) {
|
||||
return;
|
||||
if (traEl) {
|
||||
const preText = this._tranNodes.get(el);
|
||||
const curText = el.innerText.trim();
|
||||
// const traText = traEl.innerText.trim();
|
||||
|
||||
// todo
|
||||
// 1. traText when loading
|
||||
// 2. replace startsWith
|
||||
if (curText.startsWith(preText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
traEl.remove();
|
||||
}
|
||||
|
||||
// 太长或太短
|
||||
const q = el.innerText.trim();
|
||||
if (!q || q.length < TRANS_MIN_LENGTH || q.length > TRANS_MAX_LENGTH) {
|
||||
this._tranNodes.set(el, q);
|
||||
|
||||
// 太长或太短
|
||||
if (!q || q.length < this._minLength || q.length > this._maxLength) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("---> ", q);
|
||||
|
||||
const span = document.createElement(APP_LCNAME);
|
||||
span.style.visibility = "visible";
|
||||
el.appendChild(span);
|
||||
traEl = document.createElement(APP_LCNAME);
|
||||
traEl.style.visibility = "visible";
|
||||
el.appendChild(traEl);
|
||||
el.style.cssText +=
|
||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||
el.parentElement.style.cssText +=
|
||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||
if (el.parentElement) {
|
||||
el.parentElement.style.cssText +=
|
||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||
}
|
||||
|
||||
const root = createRoot(span);
|
||||
const root = createRoot(traEl);
|
||||
root.render(<Content q={q} translator={this} />);
|
||||
};
|
||||
}
|
||||
|
||||
11
src/rules.js
11
src/rules.js
@@ -3,6 +3,7 @@ import path from "path";
|
||||
import { BUILTIN_RULES } from "./config/rules";
|
||||
|
||||
(() => {
|
||||
// rules
|
||||
try {
|
||||
const data = JSON.stringify(BUILTIN_RULES, null, " ");
|
||||
const file = path.resolve(
|
||||
@@ -14,4 +15,14 @@ import { BUILTIN_RULES } from "./config/rules";
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
// version
|
||||
try {
|
||||
var pjson = require("../package.json");
|
||||
const file = path.resolve(__dirname, "../build/web/version.txt");
|
||||
fs.writeFileSync(file, pjson.version);
|
||||
console.info(`Version file generated: ${file}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -5,39 +5,65 @@ import createCache from "@emotion/cache";
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import { getSetting, getRules, matchRule, getFab } from "./libs";
|
||||
import { Translator } from "./libs/translator";
|
||||
import { trySyncAllSubRules } from "./libs/rules";
|
||||
import { isGm } from "./libs/browser";
|
||||
import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config";
|
||||
import { isIframe } from "./libs/iframe";
|
||||
import { handlePing, injectScript } from "./libs/gm";
|
||||
|
||||
/**
|
||||
* 入口函数
|
||||
*/
|
||||
(async () => {
|
||||
const init = async () => {
|
||||
// 设置页面
|
||||
if (
|
||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
|
||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE) ||
|
||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE2)
|
||||
) {
|
||||
unsafeWindow.GM = GM;
|
||||
unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
|
||||
return;
|
||||
}
|
||||
|
||||
// skip iframe
|
||||
if (window.self !== window.top) {
|
||||
// unsafeWindow.GM = GM;
|
||||
// unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
|
||||
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}")`;
|
||||
if (document.head) {
|
||||
document.head.append(script);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 翻译页面
|
||||
const href = isIframe ? document.referrer : document.location.href;
|
||||
const setting = await getSetting();
|
||||
const rules = await getRules();
|
||||
const rule = await matchRule(rules, document.location.href, setting);
|
||||
const rule = await matchRule(rules, href, setting);
|
||||
const translator = new Translator(rule, setting);
|
||||
|
||||
if (isIframe) {
|
||||
// iframe
|
||||
window.addEventListener("message", (e) => {
|
||||
const action = e?.data?.action;
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
translator.toggle();
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
translator.updateRule(e.data.args || {});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 浮球按钮
|
||||
const fab = await getFab();
|
||||
const $action = document.createElement("div");
|
||||
$action.setAttribute("id", "kiss-translator");
|
||||
document.body.parentElement.appendChild($action);
|
||||
const shadowContainer = $action.attachShadow({ mode: "open" });
|
||||
const shadowContainer = $action.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
const shadowRootElement = document.createElement("div");
|
||||
shadowContainer.appendChild(emotionRoot);
|
||||
@@ -54,4 +80,40 @@ import { Translator } from "./libs/translator";
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// 注册菜单
|
||||
if (isGm) {
|
||||
try {
|
||||
GM.registerMenuCommand(
|
||||
"Toggle Translate",
|
||||
(event) => {
|
||||
translator.toggle();
|
||||
},
|
||||
"Q"
|
||||
);
|
||||
GM.registerMenuCommand(
|
||||
"Toggle Style",
|
||||
(event) => {
|
||||
translator.toggleStyle();
|
||||
},
|
||||
"C"
|
||||
);
|
||||
} catch (err) {
|
||||
console.log("[registerMenuCommand]", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 同步订阅规则
|
||||
trySyncAllSubRules(setting);
|
||||
};
|
||||
|
||||
(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);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -163,7 +163,7 @@ export default function Draggable({
|
||||
|
||||
const opacity = useMemo(() => {
|
||||
if (snapEdge) {
|
||||
return position.hide ? 0.1 : 1;
|
||||
return position.hide ? 0.2 : 1;
|
||||
}
|
||||
return origin ? 0.8 : 1;
|
||||
}, [origin, snapEdge, position.hide]);
|
||||
|
||||
@@ -112,7 +112,9 @@ export default function Action({ translator, fab }) {
|
||||
}
|
||||
>
|
||||
<Paper>
|
||||
<Popup setShowPopup={setShowPopup} translator={translator} />
|
||||
{showPopup && (
|
||||
<Popup setShowPopup={setShowPopup} translator={translator} />
|
||||
)}
|
||||
</Paper>
|
||||
</Draggable>
|
||||
<Draggable
|
||||
|
||||
@@ -47,31 +47,28 @@ export default function Content({ q, translator }) {
|
||||
|
||||
const style = useMemo(() => {
|
||||
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) {
|
||||
case OPT_STYLE_LINE: // 下划线
|
||||
return {
|
||||
opacity: hover ? 1 : 0.6,
|
||||
textDecoration: `underline 2px ${lineColor}`,
|
||||
textUnderlineOffset: "0.3em",
|
||||
};
|
||||
return underlineStyle("solid");
|
||||
case OPT_STYLE_DOTLINE: // 点状线
|
||||
return {
|
||||
opacity: hover ? 1 : 0.6,
|
||||
textDecoration: `dotted underline 2px ${lineColor}`,
|
||||
textUnderlineOffset: "0.3em",
|
||||
};
|
||||
return underlineStyle("dotted");
|
||||
case OPT_STYLE_DASHLINE: // 虚线
|
||||
return {
|
||||
opacity: hover ? 1 : 0.6,
|
||||
textDecoration: `dashed underline 2px ${lineColor}`,
|
||||
textUnderlineOffset: "0.3em",
|
||||
};
|
||||
return underlineStyle("dashed");
|
||||
case OPT_STYLE_WAVYLINE: // 波浪线
|
||||
return {
|
||||
opacity: hover ? 1 : 0.6,
|
||||
textDecoration: `wavy underline 2px ${lineColor}`,
|
||||
textUnderlineOffset: "0.3em",
|
||||
};
|
||||
return underlineStyle("wavy");
|
||||
case OPT_STYLE_FUZZY: // 模糊
|
||||
return {
|
||||
filter: hover ? "none" : "blur(5px)",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useDarkModeSwitch } from "../../hooks/ColorMode";
|
||||
import { useDarkMode } from "../../hooks/ColorMode";
|
||||
import LightModeIcon from "@mui/icons-material/LightMode";
|
||||
import DarkModeIcon from "@mui/icons-material/DarkMode";
|
||||
import Link from "@mui/material/Link";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
|
||||
function Header(props) {
|
||||
@@ -35,9 +36,13 @@ function Header(props) {
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>{`${i18n("app_name")} v${
|
||||
process.env.REACT_APP_VERSION
|
||||
}`}</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Link
|
||||
underline="none"
|
||||
color="inherit"
|
||||
href={process.env.REACT_APP_HOMEPAGE}
|
||||
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
|
||||
</Box>
|
||||
<IconButton onClick={switchColorMode} color="inherit">
|
||||
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
|
||||
</IconButton>
|
||||
|
||||
@@ -3,6 +3,7 @@ import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Button from "@mui/material/Button";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import {
|
||||
GLOBAL_KEY,
|
||||
DEFAULT_RULE,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
} from "../../config";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useState, useRef, useEffect, useMemo } from "react";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
@@ -35,11 +36,12 @@ import IconButton from "@mui/material/IconButton";
|
||||
import ShareIcon from "@mui/icons-material/Share";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import { useSubrules } from "../../hooks/Rules";
|
||||
import { rulesCache, tryLoadRules } from "../../libs/rules";
|
||||
import { rulesCache, loadSubRules, syncSubRules } from "../../libs/rules";
|
||||
import { useAlert } from "../../hooks/Alert";
|
||||
import { loadSyncOpt, syncShareRules } from "../../libs/sync";
|
||||
import { syncOpt, syncShareRules } from "../../libs/sync";
|
||||
import { debounce } from "../../libs/utils";
|
||||
|
||||
function RuleFields({ rule, rules, setShow }) {
|
||||
function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
|
||||
const editMode = !!rule;
|
||||
|
||||
@@ -73,10 +75,21 @@ function RuleFields({ rule, rules, setShow }) {
|
||||
setErrors((pre) => ({ ...pre, [name]: "" }));
|
||||
};
|
||||
|
||||
const handlePatternChange = useMemo(
|
||||
() =>
|
||||
debounce(async (patterns) => {
|
||||
setKeyword(patterns.trim());
|
||||
}, 500),
|
||||
[setKeyword]
|
||||
);
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
const { name, value } = e.target;
|
||||
setFormValues((pre) => ({ ...pre, [name]: value }));
|
||||
if (name === "pattern" && !editMode) {
|
||||
handlePatternChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = (e) => {
|
||||
@@ -401,7 +414,7 @@ function ShareButton({ rules, injectRules, selectedSub }) {
|
||||
const i18n = useI18n();
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
const { syncUrl, syncKey } = await loadSyncOpt();
|
||||
const { syncUrl, syncKey } = await syncOpt.load();
|
||||
if (!syncUrl || !syncKey) {
|
||||
alert.warning(i18n("error_sync_setting"));
|
||||
return;
|
||||
@@ -409,7 +422,7 @@ function ShareButton({ rules, injectRules, selectedSub }) {
|
||||
|
||||
const shareRules = [...rules.list];
|
||||
if (injectRules) {
|
||||
const subRules = await tryLoadRules(selectedSub?.url);
|
||||
const subRules = await loadSubRules(selectedSub?.url);
|
||||
shareRules.splice(-1, 0, ...subRules);
|
||||
}
|
||||
|
||||
@@ -445,6 +458,9 @@ function UserRules() {
|
||||
const setting = useSetting();
|
||||
const updateSetting = useSettingUpdate();
|
||||
const subrules = useSubrules();
|
||||
const [subRules, setSubRules] = useState([]);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
|
||||
const selectedSub = subrules.list.find((item) => item.selected);
|
||||
|
||||
const injectRules = !!setting?.injectRules;
|
||||
@@ -477,9 +493,28 @@ function UserRules() {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (selectedSub?.url) {
|
||||
try {
|
||||
const rules = await loadSubRules(selectedSub?.url);
|
||||
setSubRules(rules);
|
||||
} catch (err) {
|
||||
console.log("[load rules]", err);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [selectedSub?.url]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showAdd) {
|
||||
setKeyword("");
|
||||
}
|
||||
}, [showAdd]);
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Stack direction="row" alignItems="center" spacing={2} useFlexGap flexWrap="wrap">
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
@@ -516,13 +551,37 @@ function UserRules() {
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{showAdd && <RuleFields rules={rules} setShow={setShowAdd} />}
|
||||
{showAdd && (
|
||||
<RuleFields
|
||||
rules={rules}
|
||||
setShow={setShowAdd}
|
||||
setKeyword={setKeyword}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
{rules.list.map((rule) => (
|
||||
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} />
|
||||
))}
|
||||
{rules.list
|
||||
.filter(
|
||||
(rule) =>
|
||||
rule.pattern.includes(keyword) || keyword.includes(rule.pattern)
|
||||
)
|
||||
.map((rule) => (
|
||||
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} />
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{injectRules && (
|
||||
<Box>
|
||||
{subRules
|
||||
.filter(
|
||||
(rule) =>
|
||||
rule.pattern.includes(keyword) || keyword.includes(rule.pattern)
|
||||
)
|
||||
.map((rule) => (
|
||||
<RuleAccordion key={rule.pattern} rule={rule} />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -542,13 +601,12 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const rules = await rulesCache.fetch(url);
|
||||
await rulesCache.set(url, rules);
|
||||
if (url === selectedUrl) {
|
||||
const rules = await syncSubRules(url);
|
||||
if (rules.length > 0 && url === selectedUrl) {
|
||||
setRules(rules);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[sync rules]", err);
|
||||
console.log("[sync sub rules]", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -580,6 +638,7 @@ function SubRulesEdit({ subrules }) {
|
||||
const [inputText, setInputText] = useState("");
|
||||
const [inputError, setInputError] = useState("");
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleCancel = (e) => {
|
||||
e.preventDefault();
|
||||
@@ -603,17 +662,19 @@ function SubRulesEdit({ subrules }) {
|
||||
}
|
||||
|
||||
try {
|
||||
const rules = await rulesCache.fetch(url);
|
||||
setLoading(true);
|
||||
const rules = await syncSubRules(url);
|
||||
if (rules.length === 0) {
|
||||
throw new Error("empty rules");
|
||||
}
|
||||
await rulesCache.set(url, rules);
|
||||
await subrules.add(url);
|
||||
setShowInput(false);
|
||||
setInputText("");
|
||||
} catch (err) {
|
||||
console.log("[fetch rules]", err);
|
||||
setInputError(i18n("error_fetch_url"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -656,7 +717,12 @@ function SubRulesEdit({ subrules }) {
|
||||
/>
|
||||
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<Button size="small" variant="contained" onClick={handleSave}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
>
|
||||
{i18n("save")}
|
||||
</Button>
|
||||
<Button size="small" variant="outlined" onClick={handleCancel}>
|
||||
@@ -686,7 +752,7 @@ function SubRules() {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const rules = await tryLoadRules(selectedSub?.url);
|
||||
const rules = await loadSubRules(selectedSub?.url);
|
||||
setRules(rules);
|
||||
} catch (err) {
|
||||
console.log("[load rules]", err);
|
||||
@@ -738,9 +804,15 @@ export default function Rules() {
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Alert severity="info">
|
||||
{i18n("rules_warn_1")}
|
||||
<br />
|
||||
{i18n("rules_warn_2")}
|
||||
</Alert>
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<Tab label={i18n("edit_rules")} />
|
||||
<Tab label={i18n("personal_rules")} />
|
||||
<Tab label={i18n("subscribe_rules")} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
@@ -28,6 +28,12 @@ export default function Settings() {
|
||||
case "fetchInterval":
|
||||
value = limitNumber(value, 0, 5000);
|
||||
break;
|
||||
case "minLength":
|
||||
value = limitNumber(value, 1, 100);
|
||||
break;
|
||||
case "maxLength":
|
||||
value = limitNumber(value, 100, 10000);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
updateSetting({
|
||||
@@ -46,6 +52,8 @@ export default function Settings() {
|
||||
googleUrl,
|
||||
fetchLimit,
|
||||
fetchInterval,
|
||||
minLength,
|
||||
maxLength,
|
||||
openaiUrl,
|
||||
openaiKey,
|
||||
openaiModel,
|
||||
@@ -90,6 +98,24 @@ export default function Settings() {
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("min_translate_length")}
|
||||
type="number"
|
||||
name="minLength"
|
||||
defaultValue={minLength}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("max_translate_length")}
|
||||
type="number"
|
||||
name="maxLength"
|
||||
defaultValue={maxLength}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("clear_cache")}</InputLabel>
|
||||
<Select
|
||||
|
||||
@@ -7,12 +7,18 @@ import Alert from "@mui/material/Alert";
|
||||
import Link from "@mui/material/Link";
|
||||
import { URL_KISS_WORKER } from "../../config";
|
||||
import { debounce } from "../../libs/utils";
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
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() {
|
||||
const i18n = useI18n();
|
||||
const sync = useSync();
|
||||
const alert = useAlert();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleChange = useMemo(
|
||||
() =>
|
||||
@@ -22,11 +28,25 @@ export default function SyncSetting() {
|
||||
await sync.update({
|
||||
[name]: value,
|
||||
});
|
||||
await syncAll();
|
||||
}, 1000),
|
||||
// trySyncAll();
|
||||
}, 500),
|
||||
[sync]
|
||||
);
|
||||
|
||||
const handleSyncTest = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setLoading(true);
|
||||
await syncAll();
|
||||
alert.success(i18n("data_sync_success"));
|
||||
} catch (err) {
|
||||
console.log("[sync all]", err);
|
||||
alert.error(i18n("data_sync_error"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!sync.opt) {
|
||||
return;
|
||||
}
|
||||
@@ -57,6 +77,19 @@ export default function SyncSetting() {
|
||||
defaultValue={syncKey}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<Stack direction="row" alignItems="center" spacing={2} useFlexGap flexWrap="wrap">
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
disabled={!syncUrl || !syncKey || loading}
|
||||
onClick={handleSyncTest}
|
||||
startIcon={<SyncIcon />}
|
||||
>
|
||||
{i18n("data_sync_test")}
|
||||
</Button>
|
||||
{loading && <CircularProgress size={16} />}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -10,8 +10,11 @@ import { useEffect, useState } from "react";
|
||||
import { isGm } from "../../libs/browser";
|
||||
import { sleep } from "../../libs/utils";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { syncAll } from "../../libs/sync";
|
||||
import { trySyncAll } from "../../libs/sync";
|
||||
import { AlertProvider } from "../../hooks/Alert";
|
||||
import Link from "@mui/material/Link";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Stack from "@mui/material/Stack";
|
||||
|
||||
export default function Options() {
|
||||
const [error, setError] = useState(false);
|
||||
@@ -38,23 +41,36 @@ export default function Options() {
|
||||
}
|
||||
|
||||
// 同步数据
|
||||
syncAll();
|
||||
trySyncAll();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<center>
|
||||
<Divider>
|
||||
<Link
|
||||
href={process.env.REACT_APP_HOMEPAGE}
|
||||
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||
</Divider>
|
||||
<h2>
|
||||
Please confirm whether to install or enable{" "}
|
||||
<a href={process.env.REACT_APP_HOMEPAGE}>KISS Translator</a>{" "}
|
||||
Please confirm whether to install or enable KISS Translator
|
||||
GreaseMonkey script?
|
||||
</h2>
|
||||
<h2>
|
||||
<a href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>Click here</a>{" "}
|
||||
to install, or <a href={process.env.REACT_APP_HOMEPAGE}>click here</a>{" "}
|
||||
for help.
|
||||
</h2>
|
||||
<Stack spacing={2}>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
||||
Install Userscript 1
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
|
||||
Install Userscript 2
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
|
||||
Open Options Page 1
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
|
||||
Open Options Page 2
|
||||
</Link>
|
||||
</Stack>
|
||||
</center>
|
||||
);
|
||||
}
|
||||
@@ -62,6 +78,11 @@ export default function Options() {
|
||||
if (isGm && !ready) {
|
||||
return (
|
||||
<center>
|
||||
<Divider>
|
||||
<Link
|
||||
href={process.env.REACT_APP_HOMEPAGE}
|
||||
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||
</Divider>
|
||||
<CircularProgress />
|
||||
</center>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
OPT_LANGS_TO,
|
||||
OPT_STYLE_ALL,
|
||||
} from "../../config";
|
||||
import { sendIframeMsg } from "../../libs/iframe";
|
||||
|
||||
export default function Popup({ setShowPopup, translator: tran }) {
|
||||
const i18n = useI18n();
|
||||
@@ -40,6 +41,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
await sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
} else {
|
||||
tran.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[toggle trans]", err);
|
||||
@@ -55,6 +57,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||
} else {
|
||||
tran.updateRule({ [name]: value });
|
||||
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[update rule]", err);
|
||||
@@ -101,7 +104,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
onChange={handleTransToggle}
|
||||
/>
|
||||
}
|
||||
label={i18n("translate")}
|
||||
label={i18n("translate_alt")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
@@ -158,7 +161,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
size="small"
|
||||
value={textStyle}
|
||||
name="textStyle"
|
||||
label={i18n("text_style")}
|
||||
label={i18n("text_style_alt")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
|
||||
Reference in New Issue
Block a user