Compare commits

..

62 Commits

Author SHA1 Message Date
Gabe Yuan
620ac464eb v1.5.6 2023-08-27 17:59:47 +08:00
Gabe Yuan
62289f8ab8 catch detect lang err 2023-08-27 17:43:27 +08:00
Gabe Yuan
d84594da96 catch global error and display on top of page 2023-08-27 16:45:57 +08:00
Gabe Yuan
e1d74aae6a catch global error and display on top of page 2023-08-27 16:41:14 +08:00
Gabe Yuan
c4980d9eb7 fix rules 2023-08-26 22:12:48 +08:00
Gabe Yuan
882d83c6b7 update helper text 2023-08-26 15:08:21 +08:00
Gabe Yuan
c4a7fd81f8 v1.5.5 2023-08-26 14:47:15 +08:00
Gabe Yuan
0e55799109 fix sync test button 2023-08-26 14:42:50 +08:00
Gabe Yuan
a3cdcb2a1a add sync test button 2023-08-26 14:31:13 +08:00
Gabe Yuan
e0ccc298f9 add foxnews rule 2023-08-26 13:49:44 +08:00
Gabe Yuan
36b49bb577 modify fab opacity to 0.2 2023-08-26 13:45:24 +08:00
Gabe Yuan
2636c24e84 re translate when text changed 2023-08-26 13:10:13 +08:00
Gabe Yuan
6bcf294635 userscript in iframe 2023-08-26 12:11:21 +08:00
Gabe Yuan
c5fa6689a4 content script in iframe 2023-08-26 12:02:16 +08:00
Gabe Yuan
3bf0cb2485 usescript in iframe 2023-08-26 11:43:00 +08:00
Gabe Yuan
19c9335527 shadow root 2023-08-26 00:08:12 +08:00
Gabe Yuan
20da2e1b97 shadow root 2023-08-25 22:48:47 +08:00
Gabe Yuan
9eceb8641d shadow root 2023-08-25 22:48:11 +08:00
Gabe Yuan
86bc915d74 shadow root 2023-08-25 17:07:53 +08:00
Gabe Yuan
6b35525207 run script in iframe 2023-08-24 16:40:42 +08:00
Gabe Yuan
4633bf4fc6 run script in iframe 2023-08-24 16:39:35 +08:00
Gabe Yuan
2665f31d94 fix iframe bug 2023-08-24 16:21:01 +08:00
Gabe Yuan
6c4d3149eb fix shadow dom 2023-08-24 15:07:13 +08:00
Gabe Yuan
a2762e6ce6 fix shadow dom 2023-08-24 14:57:54 +08:00
Gabe Yuan
792a1bfcad Merge branch 'master' into dev 2023-08-24 10:10:26 +08:00
Gabe Yuan
a0eba9d60e update readme 2023-08-24 10:10:00 +08:00
Gabe Yuan
c2e0064253 support shadow dom 2023-08-23 18:01:47 +08:00
Gabe Yuan
f246efc84b support shadow dom 2023-08-23 17:53:46 +08:00
Gabe Yuan
4a3bf7e96c some minor modifications 2023-08-23 10:39:01 +08:00
Gabe Yuan
523b81090d min length & max length can be set 2023-08-22 21:45:23 +08:00
Gabe Yuan
d706c405d9 add shortcut text to pop page 2023-08-22 21:14:33 +08:00
Gabe Yuan
1191791447 v1.5.4 2023-08-22 17:52:12 +08:00
Gabe Yuan
5c510f2df2 add rules filter when add rule 2023-08-22 17:51:40 +08:00
Gabe Yuan
7c0aa23177 add rules filter when add rule 2023-08-22 17:46:57 +08:00
Gabe Yuan
4bc1c26653 add rules filter when add rule 2023-08-22 17:37:42 +08:00
Gabe Yuan
ca1e1148d6 sync subscribe rules when browser start or userscript run 2023-08-22 16:27:09 +08:00
Gabe Yuan
2224455a7f add text description for rules 2023-08-22 10:35:57 +08:00
Gabe Yuan
f463f3ce08 v1.5.3 2023-08-21 23:50:32 +08:00
Gabe Yuan
c0872db98c auto use unsafe fetch 2023-08-21 23:50:14 +08:00
Gabe Yuan
d3a5d91f01 auto use unsafe fetch 2023-08-21 23:46:42 +08:00
Gabe Yuan
3e9338be0e v1.5.2 2023-08-21 22:24:42 +08:00
Gabe Yuan
ef7f1ad638 fetch subrules use unsafe fetch 2023-08-21 21:35:53 +08:00
Gabe Yuan
1f10ebe404 fetch subrules use unsafe fetch 2023-08-21 21:31:20 +08:00
Gabe Yuan
f4a8251c61 add shortcut: Toggle Style 2023-08-21 16:06:21 +08:00
Gabe Yuan
f585a43480 v1.5.1 2023-08-21 14:52:57 +08:00
Gabe Yuan
3a11465c24 fix stack useFlexGap 2023-08-21 14:43:22 +08:00
Gabe Yuan
3c3ebdf96c add command shortcuts & menu command 2023-08-21 14:03:39 +08:00
Gabe Yuan
6b30f443e1 v1.5.0 2023-08-20 23:32:06 +08:00
Gabe Yuan
232e9a47a2 share rules 2023-08-20 23:30:08 +08:00
Gabe Yuan
7ec43a1d3f Subscribe Rules 2023-08-20 19:27:29 +08:00
Gabe Yuan
a8caa34bbe v1.4.6 2023-08-19 15:16:33 +08:00
Gabe Yuan
c2fd1fe9e0 fix storage bug 2023-08-19 13:48:03 +08:00
Gabe Yuan
2773a76af8 yarn install 2023-08-18 23:50:06 +08:00
Gabe Yuan
1dc7026e8f add rules generate script 2023-08-18 16:48:44 +08:00
Gabe Yuan
b36ede7393 fix userscript grant 2023-08-18 13:19:40 +08:00
Gabe Yuan
b18721a4e5 wildcard is supported 2023-08-18 13:16:17 +08:00
Gabe Yuan
01676bc682 fix fab at left default 2023-08-17 16:22:04 +08:00
Gabe Yuan
53c32f2bd8 v1.4.5 2023-08-17 16:01:55 +08:00
Gabe Yuan
0b9fe65833 inject builtin rules 2023-08-17 15:55:44 +08:00
Gabe Yuan
bd45947d68 add version to index page 2023-08-17 13:39:57 +08:00
Gabe Yuan
5d2e767e74 optimize debounce form & sync data 2023-08-17 13:27:22 +08:00
Gabe Yuan
30af4c11d0 update rules 2023-08-16 23:03:03 +08:00
41 changed files with 4257 additions and 2850 deletions

5
.babelrc Normal file
View File

@@ -0,0 +1,5 @@
{
"presets": [
"@babel/preset-env"
]
}

3
.env
View File

@@ -2,11 +2,12 @@ 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.4.4 REACT_APP_VERSION=1.5.6
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_OPTIONSPAGE=https://kiss-translator.rayjar.com/options
REACT_APP_OPTIONSPAGE2=https://fishjar.github.io/kiss-translator/options.html REACT_APP_OPTIONSPAGE2=https://fishjar.github.io/kiss-translator/options.html
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_LOGOURL=https://kiss-translator.rayjar.com/images/logo192.png
REACT_APP_RULESURL=https://kiss-translator.rayjar.com/kiss-translator-rules.json
REACT_APP_USERSCRIPT_DOWNLOADURL=https://kiss-translator.rayjar.com/kiss-translator.user.js 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_USERSCRIPT_DOWNLOADURL2=https://fishjar.github.io/kiss-translator/kiss-translator.user.js

View File

@@ -32,9 +32,9 @@ 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](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/) - [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)

View File

@@ -32,9 +32,9 @@
- [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)

View File

@@ -83,14 +83,12 @@ const userscriptWebpack = (config, env) => {
// @icon ${process.env.REACT_APP_LOGOURL} // @icon ${process.env.REACT_APP_LOGOURL}
// @downloadURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL} // @downloadURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL} // @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
// @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest
// @grant GM.xmlhttpRequest // @grant GM.registerMenuCommand
// @grant GM_setValue
// @grant GM.setValue // @grant GM.setValue
// @grant GM_getValue
// @grant GM.getValue // @grant GM.getValue
// @grant GM_deleteValue
// @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
@@ -98,6 +96,10 @@ const userscriptWebpack = (config, env) => {
// @connect api.openai.com // @connect api.openai.com
// @connect openai.azure.com // @connect openai.azure.com
// @connect workers.dev // @connect workers.dev
// @connect github.io
// @connect githubusercontent.com
// @connect kiss-translator.rayjar.com
// @connect ghproxy.com
// @run-at document-end // @run-at document-end
// ==/UserScript== // ==/UserScript==

View File

@@ -1,7 +1,7 @@
{ {
"name": "kiss-translator", "name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script", "description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.4.4", "version": "1.5.6",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"private": true, "private": true,
"dependencies": { "dependencies": {
@@ -25,7 +25,8 @@
"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": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript", "build:rules": "babel-node src/rules.js",
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript && yarn build:rules",
"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"
@@ -53,6 +54,10 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.22.10",
"@babel/node": "^7.22.10",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.22.10",
"react-app-rewired": "^2.2.1", "react-app-rewired": "^2.2.1",
"wrangler": "^3.4.0" "wrangler": "^3.4.0"
} }

View File

@@ -4,5 +4,11 @@
}, },
"app_description": { "app_description": {
"message": "A minimalist bilingual translation Extension & Greasemonkey Script" "message": "A minimalist bilingual translation Extension & Greasemonkey Script"
},
"toggle_translate": {
"message": "Toggle Translate"
},
"toggle_style": {
"message": "Toggle Style"
} }
} }

View File

@@ -4,5 +4,11 @@
}, },
"app_description": { "app_description": {
"message": "一个简约的双语网页翻译扩展 & 油猴脚本" "message": "一个简约的双语网页翻译扩展 & 油猴脚本"
},
"toggle_translate": {
"message": "切换翻译"
},
"toggle_style": {
"message": "切换样式"
} }
} }

View File

@@ -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>
Weve 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

View File

@@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.4.4", "version": "1.5.6",
"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,9 +12,24 @@
"content_scripts": [ "content_scripts": [
{ {
"js": ["content.js"], "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"], "permissions": ["<all_urls>", "storage"],
"icons": { "icons": {
"16": "images/logo16.png", "16": "images/logo16.png",

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.4.4", "version": "1.5.6",
"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,9 +13,24 @@
"content_scripts": [ "content_scripts": [
{ {
"js": ["content.js"], "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"], "permissions": ["storage"],
"host_permissions": ["<all_urls>"], "host_permissions": ["<all_urls>"],
"icons": { "icons": {

View File

@@ -8,9 +8,10 @@ import {
OPT_LANGS_SPECIAL, OPT_LANGS_SPECIAL,
PROMPT_PLACE_FROM, PROMPT_PLACE_FROM,
PROMPT_PLACE_TO, PROMPT_PLACE_TO,
KV_HEADER_KEY, KV_SALT_SYNC,
} from "../config"; } from "../config";
import { getSetting, detectLang } from "../libs"; import { getSetting, detectLang } from "../libs";
import { sha256 } from "../libs/utils";
/** /**
* 同步数据 * 同步数据
@@ -19,19 +20,16 @@ import { getSetting, detectLang } from "../libs";
* @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: {
{ "Content-type": "application/json",
headers: { Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
"Content-type": "application/json",
[KV_HEADER_KEY]: key,
},
method: "POST",
body: JSON.stringify(data),
}, },
{ useUnsafe: true } method: "POST",
); body: JSON.stringify(data),
isBg,
});
/** /**
* 谷歌翻译 * 谷歌翻译
@@ -52,15 +50,14 @@ const apiGoogleTranslate = async (translator, text, to, from) => {
}; };
const { googleUrl } = await getSetting(); const { googleUrl } = await getSetting();
const input = `${googleUrl}?${queryString.stringify(params)}`; const input = `${googleUrl}?${queryString.stringify(params)}`;
return fetchPolyfill( return fetchPolyfill(input, {
input, headers: {
{ "Content-type": "application/json",
headers: {
"Content-type": "application/json",
},
}, },
{ useCache: true, usePool: true, translator } useCache: true,
); usePool: true,
translator,
});
}; };
/** /**
@@ -77,17 +74,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: {
{ "Content-type": "application/json",
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify([{ Text: text }]),
}, },
{ useCache: true, usePool: true, translator } method: "POST",
); body: JSON.stringify([{ Text: text }]),
useCache: true,
usePool: true,
translator,
});
}; };
/** /**
@@ -103,31 +99,31 @@ const apiOpenaiTranslate = async (translator, text, to, from) => {
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: {
{ "Content-type": "application/json",
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,
}),
}, },
{ 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,
});
}; };
/** /**

View File

@@ -3,6 +3,10 @@ import {
MSG_FETCH, MSG_FETCH,
MSG_FETCH_LIMIT, MSG_FETCH_LIMIT,
MSG_FETCH_CLEAR, MSG_FETCH_CLEAR,
MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
CMD_TOGGLE_TRANSLATE,
CMD_TOGGLE_STYLE,
DEFAULT_SETTING, DEFAULT_SETTING,
DEFAULT_RULES, DEFAULT_RULES,
DEFAULT_SYNC, DEFAULT_SYNC,
@@ -10,36 +14,47 @@ import {
STOKEY_RULES, STOKEY_RULES,
STOKEY_SYNC, STOKEY_SYNC,
CACHE_NAME, CACHE_NAME,
STOKEY_RULESCACHE_PREFIX,
BUILTIN_RULES,
} from "./config"; } from "./config";
import storage from "./libs/storage"; import storage from "./libs/storage";
import { getSetting } from "./libs"; import { getSetting } from "./libs";
import { syncAll } from "./libs/sync"; import { trySyncAll } from "./libs/sync";
import { fetchData, fetchPool } from "./libs/fetch"; import { fetchData, fetchPool } from "./libs/fetch";
import { sendTabMsg } from "./libs/msg";
import { trySyncAllSubRules } from "./libs/rules";
/** /**
* 插件安装 * 插件安装
*/ */
browser.runtime.onInstalled.addListener(() => { browser.runtime.onInstalled.addListener(() => {
console.log("onInstalled"); console.log("KISS Translator onInstalled");
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING); storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES); storage.trySetObj(STOKEY_RULES, DEFAULT_RULES);
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC); storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
storage.trySetObj(
`${STOKEY_RULESCACHE_PREFIX}${process.env.REACT_APP_RULESURL}`,
BUILTIN_RULES
);
}); });
/** /**
* 浏览器启动 * 浏览器启动
*/ */
browser.runtime.onStartup.addListener(async () => { browser.runtime.onStartup.addListener(async () => {
console.log("onStartup"); console.log("browser onStartup");
// 同步数据 // 同步数据
await syncAll(); await trySyncAll(true);
// 清除缓存 // 清除缓存
const { clearCache } = await getSetting(); const setting = await getSetting();
if (clearCache) { if (setting.clearCache) {
caches.delete(CACHE_NAME); caches.delete(CACHE_NAME);
} }
// 同步订阅规则
trySyncAllSubRules(setting, true);
}); });
/** /**
@@ -49,8 +64,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 });
}) })
@@ -73,3 +88,19 @@ browser.runtime.onMessage.addListener(
return true; 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:
}
});

View File

@@ -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`,
@@ -104,6 +120,30 @@ export const I18N = {
zh: `添加`, zh: `添加`,
en: `Add`, en: `Add`,
}, },
inject_rules: {
zh: `注入订阅规则`,
en: `Inject Subscribe Rules`,
},
personal_rules: {
zh: `个人规则`,
en: `Personal Rules`,
},
subscribe_rules: {
zh: `订阅规则`,
en: `Subscribe Rules`,
},
subscribe_url: {
zh: `订阅地址`,
en: `Subscribe URL`,
},
rules_warn_1: {
zh: `1、“个人规则”一直生效选择“注入订阅规则”后“订阅规则”才会生效。`,
en: `1. The "Personal Rules" are always in effect. After selecting "Inject Subscription Rules", the "Subscription Rules" will take effect.`,
},
rules_warn_2: {
zh: `2、“订阅规则”的注入位置是倒数第二的位置因此除全局规则(*)外,“个人规则”优先级比“订阅规则”高,“个人规则”填写同样的网址会覆盖”订阅规则“的条目。`,
en: `2. The injection position of "Subscription Rules" is the penultimate position. Therefore, except for the global rules (*), the priority of "Personal Rules" is higher than that of "Subscription Rules". Filling in the same url in "Personal Rules" will overwrite "Subscription Rules" entry.`,
},
sync_warn: { 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.`,
@@ -149,12 +189,12 @@ export const I18N = {
en: `URL pattern`, en: `URL pattern`,
}, },
pattern_helper: { pattern_helper: {
zh: `多个URL支持英文逗号“,”分隔`, zh: `1、支持星号(*)通配符。2、多个URL支持英文逗号“,”分隔`,
en: `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, but different browsers support different writing methods. 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: `开启翻译`,
@@ -192,6 +232,10 @@ export const I18N = {
zh: `错误的文件类型`, zh: `错误的文件类型`,
en: `Wrong file type`, en: `Wrong file type`,
}, },
error_fetch_url: {
zh: `请检查url地址是否正确或稍后再试。`,
en: `Please check if the url address is correct or try again later.`,
},
openai_api: { openai_api: {
zh: `OpenAI 接口`, zh: `OpenAI 接口`,
en: `OpenAI API`, en: `OpenAI API`,
@@ -228,4 +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: {
zh: "抱歉,出错了!",
en: "Sorry, something went wrong!",
},
error_sync_setting: {
zh: "您的同步设置未填写,无法在线分享。",
en: "Your sync settings are missing and cannot be shared online.",
},
}; };

View File

@@ -1,5 +1,12 @@
import { DEFAULT_SELECTOR, RULES } from "./rules"; import {
DEFAULT_SELECTOR,
GLOBAL_KEY,
SHADOW_KEY,
DEFAULT_RULE,
BUILTIN_RULES,
} from "./rules";
export { I18N, UI_LANGS } from "./i18n"; export { I18N, UI_LANGS } from "./i18n";
export { GLOBAL_KEY, SHADOW_KEY, DEFAULT_RULE, BUILTIN_RULES };
const APP_NAME = process.env.REACT_APP_NAME.trim().split(/\s+/).join("-"); const APP_NAME = process.env.REACT_APP_NAME.trim().split(/\s+/).join("-");
@@ -10,8 +17,10 @@ export const STOKEY_SETTING = `${APP_NAME}_setting`;
export const STOKEY_RULES = `${APP_NAME}_rules`; export const STOKEY_RULES = `${APP_NAME}_rules`;
export const STOKEY_SYNC = `${APP_NAME}_sync`; export const STOKEY_SYNC = `${APP_NAME}_sync`;
export const STOKEY_FAB = `${APP_NAME}_fab`; export const STOKEY_FAB = `${APP_NAME}_fab`;
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
export const GLOBAL_KEY = "*"; export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
export const CMD_TOGGLE_STYLE = "toggleStyle";
export const CLIENT_WEB = "web"; export const CLIENT_WEB = "web";
export const CLIENT_CHROME = "chrome"; export const CLIENT_CHROME = "chrome";
@@ -20,9 +29,11 @@ export const CLIENT_FIREFOX = "firefox";
export const CLIENT_USERSCRIPT = "userscript"; export const CLIENT_USERSCRIPT = "userscript";
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX]; export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
export const KV_HEADER_KEY = "X-KISS-PSK";
export const KV_RULES_KEY = "KT_RULES"; export const KV_RULES_KEY = "KT_RULES";
export const KV_RULES_SHARE_KEY = "KT_RULES_SHARE";
export const KV_SETTING_KEY = "KT_SETTING"; export const KV_SETTING_KEY = "KT_SETTING";
export const KV_SALT_SYNC = "KISS-Translator-SYNC";
export const KV_SALT_SHARE = "KISS-Translator-SHARE";
export const CACHE_NAME = `${APP_NAME}_cache`; export const CACHE_NAME = `${APP_NAME}_cache`;
@@ -30,6 +41,7 @@ export const MSG_FETCH = "fetch";
export const MSG_FETCH_LIMIT = "fetch_limit"; export const MSG_FETCH_LIMIT = "fetch_limit";
export const MSG_FETCH_CLEAR = "fetch_clear"; export const MSG_FETCH_CLEAR = "fetch_clear";
export const MSG_TRANS_TOGGLE = "trans_toggle"; 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_GETRULE = "trans_getrule";
export const MSG_TRANS_PUTRULE = "trans_putrule"; export const MSG_TRANS_PUTRULE = "trans_putrule";
export const MSG_TRANS_CURRULE = "trans_currule"; export const MSG_TRANS_CURRULE = "trans_currule";
@@ -143,24 +155,30 @@ export const GLOBLA_RULE = {
bgColor: "", bgColor: "",
}; };
// 默认规则 // 订阅列表
export const DEFAULT_RULE = { export const DEFAULT_SUBRULES_LIST = [
pattern: "", {
selector: "", url: process.env.REACT_APP_RULESURL,
translator: GLOBAL_KEY, selected: true,
fromLang: GLOBAL_KEY, },
toLang: GLOBAL_KEY, {
textStyle: GLOBAL_KEY, url: "https://fishjar.github.io/kiss-translator/kiss-translator-rules.json",
transOpen: GLOBAL_KEY, },
bgColor: "", ];
};
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, // 是否注入订阅规则
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口 googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口
openaiUrl: "https://api.openai.com/v1/chat/completions", openaiUrl: "https://api.openai.com/v1/chat/completions",
openaiKey: "", openaiKey: "",
@@ -168,17 +186,7 @@ export const DEFAULT_SETTING = {
openaiPrompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`, openaiPrompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
}; };
export const DEFAULT_RULES = [ export const DEFAULT_RULES = [GLOBLA_RULE];
...RULES.map((item) => ({
...DEFAULT_RULE,
...item,
transOpen: "true",
})),
GLOBLA_RULE,
];
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
export const DEFAULT_SYNC = { export const DEFAULT_SYNC = {
syncUrl: "", // 数据同步接口 syncUrl: "", // 数据同步接口
@@ -187,4 +195,5 @@ export const DEFAULT_SYNC = {
settingSyncAt: 0, settingSyncAt: 0,
rulesUpdateAt: 0, rulesUpdateAt: 0,
rulesSyncAt: 0, rulesSyncAt: 0,
subRulesSyncAt: 0, // 订阅规则同步时间
}; };

View File

@@ -2,15 +2,38 @@ const els = `li, p, h1, h2, h3, h4, h5, h6, dd`;
export const DEFAULT_SELECTOR = `:is(${els})`; export const DEFAULT_SELECTOR = `:is(${els})`;
export const RULES = [ export const GLOBAL_KEY = "*";
export const SHADOW_KEY = ">>>";
export const DEFAULT_RULE = {
pattern: "",
selector: "",
translator: GLOBAL_KEY,
fromLang: GLOBAL_KEY,
toLang: GLOBAL_KEY,
textStyle: GLOBAL_KEY,
transOpen: GLOBAL_KEY,
bgColor: "",
};
const RULES = [
{
pattern: `www.google.com/search`,
selector: `h3, .IsZvec, .VwiC3b`,
},
{
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`, pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,
selector: DEFAULT_SELECTOR, selector: DEFAULT_SELECTOR,
}, },
{
pattern: `https://news.google.com/`,
selector: `h4`,
},
{ {
pattern: `themessenger.com`, pattern: `themessenger.com`,
selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`, selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
@@ -117,7 +140,7 @@ export const RULES = [
}, },
{ {
pattern: `https://github.com/`, pattern: `https://github.com/`,
selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop='description']`, selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop='description'], .markdown-title, bdi`,
}, },
{ {
pattern: `twitter.com`, pattern: `twitter.com`,
@@ -127,8 +150,10 @@ export const RULES = [
pattern: `youtube.com`, pattern: `youtube.com`,
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`, selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`,
}, },
{
pattern: `www.google.com/search`,
selector: `h3, .IsZvec, .VwiC3b`,
},
]; ];
export const BUILTIN_RULES = RULES.map((item) => ({
...DEFAULT_RULE,
...item,
transOpen: "true",
}));

View File

@@ -1,19 +1,22 @@
import { browser } from "./libs/browser"; import { browser } from "./libs/browser";
import { import {
MSG_TRANS_TOGGLE, MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
MSG_TRANS_GETRULE, MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE, MSG_TRANS_PUTRULE,
} from "./config"; } from "./config";
import { getSetting, getRules, matchRule } from "./libs"; import { getSetting, getRules, matchRule } from "./libs";
import { Translator } from "./libs/translator"; 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 setting = await getSetting();
const rules = await getRules(); const rules = await getRules();
const rule = matchRule(rules, document.location.href); const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting); const translator = new Translator(rule, setting);
// 监听消息 // 监听消息
@@ -22,6 +25,9 @@ import { Translator } from "./libs/translator";
case MSG_TRANS_TOGGLE: case MSG_TRANS_TOGGLE:
translator.toggle(); translator.toggle();
break; break;
case MSG_TRANS_TOGGLE_STYLE:
translator.toggleStyle();
break;
case MSG_TRANS_GETRULE: case MSG_TRANS_GETRULE:
break; break;
case MSG_TRANS_PUTRULE: case MSG_TRANS_PUTRULE:
@@ -32,4 +38,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);
}
})(); })();

60
src/hooks/Alert.js Normal file
View File

@@ -0,0 +1,60 @@
import { createContext, useContext, useState, forwardRef } from "react";
import Snackbar from "@mui/material/Snackbar";
import MuiAlert from "@mui/material/Alert";
const Alert = forwardRef(function Alert(props, ref) {
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
});
const AlertContext = createContext(null);
/**
* 左下角提示注入context后方便全局调用
* @param {*} param0
* @returns
*/
export function AlertProvider({ children }) {
const vertical = "top";
const horizontal = "center";
const [open, setOpen] = useState(false);
const [severity, setSeverity] = useState("info");
const [message, setMessage] = useState("");
const error = (msg) => showAlert(msg, "error");
const warning = (msg) => showAlert(msg, "warning");
const info = (msg) => showAlert(msg, "info");
const success = (msg) => showAlert(msg, "success");
const showAlert = (msg, type) => {
setOpen(true);
setMessage(msg);
setSeverity(type);
};
const handleClose = (_, reason) => {
if (reason === "clickaway") {
return;
}
setOpen(false);
};
return (
<AlertContext.Provider value={{ error, warning, info, success }}>
{children}
<Snackbar
open={open}
autoHideDuration={3000}
onClose={handleClose}
anchorOrigin={{ vertical, horizontal }}
>
<Alert onClose={handleClose} severity={severity} sx={{ width: "100%" }}>
{message}
</Alert>
</Snackbar>
</AlertContext.Provider>
);
}
export function useAlert() {
return useContext(AlertContext);
}

View File

@@ -1,16 +1,10 @@
import { import { STOKEY_RULES, DEFAULT_SUBRULES_LIST } from "../config";
STOKEY_RULES,
OPT_TRANS_ALL,
OPT_STYLE_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
GLOBAL_KEY,
} from "../config";
import storage from "../libs/storage"; import storage from "../libs/storage";
import { useStorages } from "./Storage"; import { useStorages } from "./Storage";
import { matchValue } from "../libs/utils"; 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";
/** /**
* 匹配规则增删改查 hook * 匹配规则增删改查 hook
@@ -25,11 +19,7 @@ export function useRules() {
const updateAt = sync.opt?.rulesUpdateAt ? Date.now() : 0; const updateAt = sync.opt?.rulesUpdateAt ? Date.now() : 0;
await storage.setObj(STOKEY_RULES, rules); await storage.setObj(STOKEY_RULES, rules);
await sync.update({ rulesUpdateAt: updateAt }); await sync.update({ rulesUpdateAt: updateAt });
try { trySyncRules();
await syncRules();
} catch (err) {
console.log("[sync rules]", err);
}
}; };
const add = async (rule) => { const add = async (rule) => {
@@ -65,43 +55,53 @@ export function useRules() {
const merge = async (newRules) => { const merge = async (newRules) => {
const rules = [...list]; const rules = [...list];
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]); newRules = checkRules(newRules);
const toLangs = OPT_LANGS_TO.map((item) => item[0]); newRules.forEach((newRule) => {
newRules const rule = rules.find((oldRule) => oldRule.pattern === newRule.pattern);
.filter(({ pattern }) => pattern && typeof pattern === "string") if (rule) {
.map( Object.assign(rule, newRule);
({ } else {
pattern, rules.unshift(newRule);
selector, }
translator, });
fromLang,
toLang,
textStyle,
transOpen,
bgColor,
}) => ({
pattern,
selector: typeof selector === "string" ? selector : "",
bgColor: typeof bgColor === "string" ? bgColor : "",
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
})
)
.forEach((newRule) => {
const rule = rules.find(
(oldRule) => oldRule.pattern === newRule.pattern
);
if (rule) {
Object.assign(rule, newRule);
} else {
rules.unshift(newRule);
}
});
await update(rules); await update(rules);
}; };
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 };
}

View File

@@ -2,6 +2,7 @@ import { STOKEY_SETTING } from "../config";
import storage from "../libs/storage"; import storage from "../libs/storage";
import { useStorages } from "./Storage"; import { useStorages } from "./Storage";
import { useSync } from "./Sync"; import { useSync } from "./Sync";
import { trySyncSetting } from "../libs/sync";
/** /**
* 设置hook * 设置hook
@@ -22,5 +23,6 @@ export function useSettingUpdate() {
const updateAt = sync.opt?.settingUpdateAt ? Date.now() : 0; const updateAt = sync.opt?.settingUpdateAt ? Date.now() : 0;
await storage.putObj(STOKEY_SETTING, obj); await storage.putObj(STOKEY_SETTING, obj);
await sync.update({ settingUpdateAt: updateAt }); await sync.update({ settingUpdateAt: updateAt });
trySyncSetting();
}; };
} }

View File

@@ -3,7 +3,6 @@ import { browser, isExt, isGm, isWeb } from "../libs/browser";
import { import {
STOKEY_SETTING, STOKEY_SETTING,
STOKEY_RULES, STOKEY_RULES,
STOKEY_MSAUTH,
STOKEY_SYNC, STOKEY_SYNC,
DEFAULT_SETTING, DEFAULT_SETTING,
DEFAULT_RULES, DEFAULT_RULES,
@@ -15,12 +14,13 @@ import storage from "../libs/storage";
* 默认配置 * 默认配置
*/ */
export const defaultStorage = { export const defaultStorage = {
[STOKEY_MSAUTH]: null,
[STOKEY_SETTING]: DEFAULT_SETTING, [STOKEY_SETTING]: DEFAULT_SETTING,
[STOKEY_RULES]: DEFAULT_RULES, [STOKEY_RULES]: DEFAULT_RULES,
[STOKEY_SYNC]: DEFAULT_SYNC, [STOKEY_SYNC]: DEFAULT_SYNC,
}; };
const activeKeys = Object.keys(defaultStorage);
const StoragesContext = createContext(null); const StoragesContext = createContext(null);
export function StoragesProvider({ children }) { export function StoragesProvider({ children }) {
@@ -38,7 +38,10 @@ export function StoragesProvider({ children }) {
} }
const newStorages = {}; const newStorages = {};
Object.entries(changes) Object.entries(changes)
.filter(([_, { oldValue, newValue }]) => oldValue !== newValue) .filter(
([key, { oldValue, newValue }]) =>
activeKeys.includes(key) && oldValue !== newValue
)
.forEach(([key, { newValue }]) => { .forEach(([key, { newValue }]) => {
newStorages[key] = JSON.parse(newValue); newStorages[key] = JSON.parse(newValue);
}); });
@@ -51,8 +54,7 @@ export function StoragesProvider({ children }) {
// 首次从storage同步配置到内存 // 首次从storage同步配置到内存
(async () => { (async () => {
const curStorages = {}; const curStorages = {};
const keys = Object.keys(defaultStorage); for (const key of activeKeys) {
for (const key of keys) {
const val = await storage.get(key); const val = await storage.get(key);
if (val) { if (val) {
curStorages[key] = JSON.parse(val); curStorages[key] = JSON.parse(val);

View File

@@ -1,17 +1,32 @@
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 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 { 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 }}>
<Stack spacing={2} direction="row" justifyContent="flex-end">
<Button
variant="text"
onClick={() => {
setLang((pre) => (pre === "zh" ? "en" : "zh"));
}}
>
{lang === "zh" ? "ENGLISH" : "中文"}
</Button>
</Stack>
<Divider>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Divider>
{loading ? ( {loading ? (
<center> <center>
<CircularProgress /> <CircularProgress />

View File

@@ -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,8 +73,13 @@ 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) {
return fetchGM(input, init); const connects = GM?.info?.script?.connects || [];
const url = new URL(input);
const isSafe = connects.find((item) => url.hostname.endsWith(item));
if (isSafe) {
return fetchGM(input, init);
}
} }
return fetch(input, init); return fetch(input, init);
}; };
@@ -98,14 +103,12 @@ 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); const cache = await caches.open(CACHE_NAME);
@@ -123,9 +126,9 @@ export const fetchData = async (
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) {
@@ -152,22 +155,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 sendMsg(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);
}; };
/** /**

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

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

View File

@@ -6,17 +6,11 @@ import {
STOKEY_FAB, STOKEY_FAB,
GLOBLA_RULE, GLOBLA_RULE,
GLOBAL_KEY, GLOBAL_KEY,
DEFAULT_SUBRULES_LIST,
} from "../config"; } from "../config";
import { browser } from "./browser"; import { browser } from "./browser";
import { isMatch } from "./utils";
/** import { loadSubRules } from "./rules";
* 获取节点列表并转为数组
* @param {*} selector
* @param {*} el
* @returns
*/
export const queryEls = (selector, el = document) =>
Array.from(el.querySelectorAll(selector));
/** /**
* 查询storage中的设置 * 查询storage中的设置
@@ -47,19 +41,35 @@ export const setFab = async (obj) => await storage.setObj(STOKEY_FAB, obj);
/** /**
* 根据href匹配规则 * 根据href匹配规则
* TODO: 支持通配符(*)匹配
* @param {*} rules * @param {*} rules
* @param {string} href * @param {string} href
* @returns * @returns
*/ */
export const matchRule = (rules, href) => { export const matchRule = async (
const rule = rules.find((rule) => rules,
rule.pattern.split(",").some((p) => href.includes(p.trim())) 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 loadSubRules(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 = const globalRule =
rules.find((rule) => rules.find((r) => r.pattern.split(",").some((p) => p.trim() === "*")) ||
rule.pattern.split(",").some((p) => p.trim() === "*") GLOBLA_RULE;
) || GLOBLA_RULE;
if (!rule) { if (!rule) {
return globalRule; return globalRule;
@@ -89,6 +99,10 @@ export const matchRule = (rules, href) => {
* @returns * @returns
*/ */
export const detectLang = async (q) => { export const detectLang = async (q) => {
const res = await browser?.i18n.detectLanguage(q); try {
return res?.languages?.[0]?.language; const res = await browser?.i18n.detectLanguage(q);
return res?.languages?.[0]?.language;
} catch (err) {
console.log("[detect lang]", err);
}
}; };

View File

@@ -1,3 +1,11 @@
/**
* 任务池
* @param {*} fn
* @param {*} preFn
* @param {*} _interval
* @param {*} _limit
* @returns
*/
export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => { export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
const pool = []; const pool = [];
const maxRetry = 2; // 最大重试次数 const maxRetry = 2; // 最大重试次数
@@ -6,11 +14,6 @@ export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
let interval = _interval; // 间隔时间 let interval = _interval; // 间隔时间
let timer = null; let timer = null;
/**
* 任务池
* @param {*} item
* @param {*} preArgs
*/
const handleTask = async (item, preArgs) => { const handleTask = async (item, preArgs) => {
curCount++; curCount++;
const { args, resolve, reject, retry } = item; const { args, resolve, reject, retry } = item;

145
src/libs/rules.js Normal file
View File

@@ -0,0 +1,145 @@
import storage from "./storage";
import { fetchPolyfill } from "./fetch";
import { matchValue, type } from "./utils";
import {
STOKEY_RULESCACHE_PREFIX,
GLOBAL_KEY,
OPT_TRANS_ALL,
OPT_STYLE_ALL,
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]);
/**
* 检查过滤rules
* @param {*} rules
* @returns
*/
export const checkRules = (rules) => {
if (type(rules) === "string") {
rules = JSON.parse(rules);
}
if (type(rules) !== "array") {
throw new Error("data error");
}
const patternSet = new Set();
rules = rules
.filter((rule) => type(rule) === "object")
.filter(({ pattern }) => {
if (type(pattern) !== "string" || patternSet.has(pattern.trim())) {
return false;
}
patternSet.add(pattern.trim());
return true;
})
.map(
({
pattern,
selector,
translator,
fromLang,
toLang,
textStyle,
transOpen,
bgColor,
}) => ({
pattern: pattern.trim(),
selector: type(selector) === "string" ? selector : "",
bgColor: type(bgColor) === "string" ? bgColor : "",
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
})
);
return rules;
};
/**
* 订阅规则的本地缓存
*/
export const rulesCache = {
fetch: async (url, isBg = false) => {
const res = await fetchPolyfill(url, { isBg });
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}`);
},
};
/**
* 同步订阅规则
* @param {*} url
* @returns
*/
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;
}
return await syncSubRules(url);
};

View File

@@ -3,72 +3,134 @@ import {
DEFAULT_SYNC, DEFAULT_SYNC,
KV_SETTING_KEY, KV_SETTING_KEY,
KV_RULES_KEY, KV_RULES_KEY,
KV_RULES_SHARE_KEY,
STOKEY_SETTING, STOKEY_SETTING,
STOKEY_RULES, STOKEY_RULES,
KV_SALT_SHARE,
} from "../config"; } from "../config";
import storage from "../libs/storage"; import storage from "../libs/storage";
import { getSetting, getRules } from "."; import { getSetting, getRules } from ".";
import { apiSyncData } from "../apis"; import { apiSyncData } from "../apis";
import { sha256 } from "./utils";
const loadOpt = 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 () => { /**
const { syncUrl, syncKey, settingUpdateAt } = await loadOpt(); * 同步设置
* @returns
*/
export const syncSetting = async (isBg = false) => {
const { syncUrl, syncKey, settingUpdateAt } = await syncOpt.load();
if (!syncUrl || !syncKey) { if (!syncUrl || !syncKey) {
return; return;
} }
const setting = await getSetting(); const setting = await getSetting();
const res = await apiSyncData(syncUrl, syncKey, { const res = await apiSyncData(
key: KV_SETTING_KEY, syncUrl,
value: setting, syncKey,
updateAt: settingUpdateAt, {
}); key: KV_SETTING_KEY,
value: setting,
updateAt: settingUpdateAt,
},
isBg
);
if (res && res.updateAt > settingUpdateAt) { if (res && res.updateAt > settingUpdateAt) {
await storage.putObj(STOKEY_SYNC, { await syncOpt.update({
settingUpdateAt: res.updateAt, settingUpdateAt: res.updateAt,
settingSyncAt: res.updateAt, settingSyncAt: res.updateAt,
}); });
await storage.setObj(STOKEY_SETTING, res.value); await storage.setObj(STOKEY_SETTING, res.value);
} else { } else {
await storage.putObj(STOKEY_SYNC, { await syncOpt.update({ settingSyncAt: res.updateAt });
settingSyncAt: res.updateAt,
});
} }
}; };
export const syncRules = async () => { export const trySyncSetting = async (isBg = false) => {
const { syncUrl, syncKey, rulesUpdateAt } = await loadOpt(); try {
await syncSetting(isBg);
} catch (err) {
console.log("[sync setting]", err);
}
};
/**
* 同步规则
* @returns
*/
export const syncRules = async (isBg = false) => {
const { syncUrl, syncKey, rulesUpdateAt } = await syncOpt.load();
if (!syncUrl || !syncKey) { if (!syncUrl || !syncKey) {
return; return;
} }
const rules = await getRules(); const rules = await getRules();
const res = await apiSyncData(syncUrl, syncKey, { const res = await apiSyncData(
key: KV_RULES_KEY, syncUrl,
value: rules, syncKey,
updateAt: rulesUpdateAt, {
}); key: KV_RULES_KEY,
value: rules,
updateAt: rulesUpdateAt,
},
isBg
);
if (res && res.updateAt > rulesUpdateAt) { if (res && res.updateAt > rulesUpdateAt) {
await storage.putObj(STOKEY_SYNC, { await syncOpt.update({
rulesUpdateAt: res.updateAt, rulesUpdateAt: res.updateAt,
rulesSyncAt: res.updateAt, rulesSyncAt: res.updateAt,
}); });
await storage.setObj(STOKEY_RULES, res.value); await storage.setObj(STOKEY_RULES, res.value);
} else { } else {
await storage.putObj(STOKEY_SYNC, { await syncOpt.update({ rulesSyncAt: res.updateAt });
rulesSyncAt: res.updateAt,
});
} }
}; };
export const syncAll = async () => { export const trySyncRules = async (isBg = false) => {
try { try {
await syncSetting(); await syncRules(isBg);
await syncRules();
} catch (err) { } catch (err) {
console.log("[sync all]", err); console.log("[sync user rules]", err);
} }
}; };
/**
* 同步分享规则
* @param {*} param0
* @returns
*/
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
await apiSyncData(syncUrl, syncKey, {
key: KV_RULES_SHARE_KEY,
value: rules,
updateAt: Date.now(),
});
const psk = await sha256(syncKey, KV_SALT_SHARE);
const shareUrl = `${syncUrl}?psk=${psk}`;
return shareUrl;
};
/**
* 同步个人设置和规则
* @returns
*/
export const syncAll = async (isBg = false) => {
await syncSetting(isBg);
await syncRules(isBg);
};
export const trySyncAll = async (isBg = false) => {
await trySyncSetting(isBg);
await trySyncRules(isBg);
};

View File

@@ -5,18 +5,41 @@ import {
TRANS_MAX_LENGTH, TRANS_MAX_LENGTH,
EVENT_KISS, EVENT_KISS,
MSG_TRANS_CURRULE, MSG_TRANS_CURRULE,
OPT_STYLE_DASHLINE,
OPT_STYLE_FUZZY,
SHADOW_KEY,
} from "../config"; } from "../config";
import { StoragesProvider } from "../hooks/Storage";
import { queryEls } from ".";
import Content from "../views/Content"; import Content from "../views/Content";
import { fetchUpdate, fetchClear } from "./fetch"; import { fetchUpdate, fetchClear } from "./fetch";
import { debounce } from "./utils";
/** /**
* 翻译类 * 翻译类
*/ */
export class Translator { export class Translator {
_rule = {}; _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( _interseObserver = new IntersectionObserver(
(intersections) => { (intersections) => {
intersections.forEach((intersection) => { intersections.forEach((intersection) => {
@@ -31,22 +54,46 @@ 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
_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); fetchUpdate(fetchInterval, fetchLimit);
this._overrideAttachShadow();
this._minLength = minLength ?? TRANS_MIN_LENGTH;
this._maxLength = maxLength ?? TRANS_MAX_LENGTH;
this.rule = rule; this.rule = rule;
if (rule.transOpen === "true") { if (rule.transOpen === "true") {
this._register(); this._register();
@@ -87,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 = () => { _register = () => {
// 监听节点变化 // 搜索节点
this._mutaObserver.observe(document, { this._queryNodes();
childList: true,
subtree: true, this._rootNodes.forEach((node) => {
// 监听节点变化;
this._mutaObserver.observe(node, {
childList: true,
subtree: true,
// characterData: true,
});
}); });
// 监听节点显示 this._tranNodes.forEach((_, node) => {
queryEls(this.rule.selector).forEach((el) => { // 监听节点显示
this._interseObserver.observe(el); this._interseObserver.observe(node);
}); });
}; };
@@ -105,49 +224,67 @@ 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(); fetchClear();
}; };
_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) {
return; 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(); 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; 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;";
el.parentElement.style.cssText += if (el.parentElement) {
"-webkit-line-clamp: unset; max-height: none; height: auto;"; el.parentElement.style.cssText +=
"-webkit-line-clamp: unset; max-height: none; height: auto;";
}
const root = createRoot(span); const root = createRoot(traEl);
root.render( root.render(<Content q={q} translator={this} />);
<StoragesProvider>
<Content q={q} translator={this} />
</StoragesProvider>
);
}; };
} }

View File

@@ -51,3 +51,63 @@ export const debounce = (func, delay = 200) => {
}, delay); }, delay);
}; };
}; };
/**
* 字符串通配符(*)匹配
* @param {*} s
* @param {*} p
* @returns
*/
export const isMatch = (s, p) => {
if (s.length === 0 || p.length === 0) {
return false;
}
p = `*${p}*`;
let [sIndex, pIndex] = [0, 0];
let [sRecord, pRecord] = [-1, -1];
while (sIndex < s.length && pRecord < p.length) {
if (p[pIndex] === "*") {
pIndex++;
[sRecord, pRecord] = [sIndex, pIndex];
} else if (s[sIndex] === p[pIndex]) {
sIndex++;
pIndex++;
} else if (sRecord + 1 < s.length) {
sRecord++;
[sIndex, pIndex] = [sRecord, pRecord];
} else {
return false;
}
}
if (p.length === pIndex) {
return true;
}
return p.slice(pIndex).replaceAll("*", "") === "";
};
/**
* 类型检查
* @param {*} o
* @returns
*/
export const type = (o) => {
const s = Object.prototype.toString.call(o);
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
/**
* sha256
* @param {*} text
* @returns
*/
export const sha256 = async (text, salt) => {
const data = new TextEncoder().encode(text + salt);
const digest = await crypto.subtle.digest({ name: "SHA-256" }, data);
return [...new Uint8Array(digest)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};

17
src/rules.js Normal file
View File

@@ -0,0 +1,17 @@
import fs from "fs";
import path from "path";
import { BUILTIN_RULES } from "./config/rules";
(() => {
try {
const data = JSON.stringify(BUILTIN_RULES, null, " ");
const file = path.resolve(
__dirname,
"../build/web/kiss-translator-rules.json"
);
fs.writeFileSync(file, data);
console.info(`Built-in rules generated: ${file}`);
} catch (err) {
console.error(err);
}
})();

View File

@@ -5,11 +5,15 @@ import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react"; import { CacheProvider } from "@emotion/react";
import { getSetting, getRules, matchRule, getFab } from "./libs"; import { getSetting, getRules, matchRule, getFab } from "./libs";
import { Translator } from "./libs/translator"; 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";
/** /**
* 入口函数 * 入口函数
*/ */
(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) ||
@@ -21,23 +25,36 @@ import { Translator } from "./libs/translator";
return; return;
} }
// skip iframe
if (window.self !== window.top) {
return;
}
// 翻译页面 // 翻译页面
const href = isIframe ? document.referrer : document.location.href;
const setting = await getSetting(); const setting = await getSetting();
const rules = await getRules(); const rules = await getRules();
const rule = matchRule(rules, document.location.href); 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 getFab();
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);
@@ -54,4 +71,40 @@ import { Translator } from "./libs/translator";
</CacheProvider> </CacheProvider>
</React.StrictMode> </React.StrictMode>
); );
// 注册菜单
if (isGm) {
try {
GM.registerMenuCommand(
"Toggle Translate",
(event) => {
translator.toggle();
},
"Q"
);
GM.registerMenuCommand(
"Toggle Style",
(event) => {
translator.toggleStyle();
},
"C"
);
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}
// 同步订阅规则
trySyncAllSubRules(setting);
};
(async () => {
try {
await init();
} catch (err) {
const $err = document.createElement("div");
$err.innerText = `KISS-Translator: ${err.message}`;
$err.style.cssText = "background:red; color:#fff; z-index:10000;";
document.body.prepend($err);
}
})(); })();

View File

@@ -30,6 +30,8 @@ const getEdgePosition = (
edge = "top"; edge = "top";
top = 0; top = 0;
} }
left = limitNumber(left, 0, windowWidth - width);
top = limitNumber(top, 0, windowHeight - height);
return { x: left, y: top, edge, hide: false }; return { x: left, y: top, edge, hide: false };
}; };
@@ -161,7 +163,7 @@ export default function Draggable({
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]);

View File

@@ -76,7 +76,7 @@ export default function Action({ translator, fab }) {
windowSize, windowSize,
width: fabWidth, width: fabWidth,
height: fabWidth, height: fabWidth,
left: fab.x ?? windowSize.w - fabWidth, left: fab.x ?? 0,
top: fab.y ?? windowSize.h / 2, top: fab.y ?? windowSize.h / 2,
}; };
@@ -112,7 +112,9 @@ export default function Action({ translator, fab }) {
} }
> >
<Paper> <Paper>
<Popup setShowPopup={setShowPopup} translator={translator} /> {showPopup && (
<Popup setShowPopup={setShowPopup} translator={translator} />
)}
</Paper> </Paper>
</Draggable> </Draggable>
<Draggable <Draggable

View File

@@ -6,7 +6,6 @@ import Box from "@mui/material/Box";
import Navigator from "./Navigator"; import Navigator from "./Navigator";
import Header from "./Header"; import Header from "./Header";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { syncAll } from "../../libs/sync";
export default function Layout() { export default function Layout() {
const navWidth = 256; const navWidth = 256;
@@ -21,7 +20,6 @@ export default function Layout() {
useEffect(() => { useEffect(() => {
setOpen(false); setOpen(false);
syncAll();
}, [location]); }, [location]);
return ( return (

View File

@@ -2,6 +2,8 @@ import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack"; 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 Alert from "@mui/material/Alert";
import { import {
GLOBAL_KEY, GLOBAL_KEY,
DEFAULT_RULE, DEFAULT_RULE,
@@ -10,7 +12,7 @@ import {
OPT_TRANS_ALL, OPT_TRANS_ALL,
OPT_STYLE_ALL, OPT_STYLE_ALL,
} from "../../config"; } from "../../config";
import { useState, useRef } 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";
@@ -22,8 +24,24 @@ 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 FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Radio from "@mui/material/Radio";
import RadioGroup from "@mui/material/RadioGroup";
import DeleteIcon from "@mui/icons-material/Delete";
import IconButton from "@mui/material/IconButton";
import ShareIcon from "@mui/icons-material/Share";
import SyncIcon from "@mui/icons-material/Sync";
import { useSubrules } from "../../hooks/Rules";
import { rulesCache, loadSubRules, syncSubRules } from "../../libs/rules";
import { useAlert } from "../../hooks/Alert";
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 initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
const editMode = !!rule; const editMode = !!rule;
@@ -57,10 +75,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) => {
@@ -122,6 +151,7 @@ function RuleFields({ rule, rules, setShow }) {
disabled={rule?.pattern === "*" || disabled} disabled={rule?.pattern === "*" || disabled}
onChange={handleChange} onChange={handleChange}
onFocus={handleFocus} onFocus={handleFocus}
multiline
/> />
<TextField <TextField
size="small" size="small"
@@ -134,8 +164,6 @@ function RuleFields({ rule, rules, setShow }) {
onChange={handleChange} onChange={handleChange}
onFocus={handleFocus} onFocus={handleFocus}
multiline multiline
minRows={2}
maxRows={10}
/> />
<Box> <Box>
@@ -246,61 +274,91 @@ function RuleFields({ rule, rules, setShow }) {
</Grid> </Grid>
</Box> </Box>
{editMode ? ( {rules &&
// 编辑 (editMode ? (
<Stack direction="row" spacing={2}> // 编辑
{disabled ? ( <Stack direction="row" spacing={2}>
<> {disabled ? (
<Button <>
size="small" <Button
variant="contained" size="small"
onClick={(e) => { variant="contained"
e.preventDefault(); onClick={(e) => {
setDisabled(false); e.preventDefault();
}} setDisabled(false);
> }}
{i18n("edit")} >
</Button> {i18n("edit")}
{rule?.pattern !== "*" && ( </Button>
{rule?.pattern !== "*" && (
<Button
size="small"
variant="outlined"
onClick={(e) => {
e.preventDefault();
rules.del(rule.pattern);
}}
>
{i18n("delete")}
</Button>
)}
</>
) : (
<>
<Button size="small" variant="contained" type="submit">
{i18n("save")}
</Button>
<Button <Button
size="small" size="small"
variant="outlined" variant="outlined"
onClick={(e) => { onClick={handleCancel}
e.preventDefault();
rules.del(rule.pattern);
}}
> >
{i18n("delete")} {i18n("cancel")}
</Button> </Button>
)} </>
</> )}
) : ( </Stack>
<> ) : (
<Button size="small" variant="contained" type="submit"> // 添加
{i18n("save")} <Stack direction="row" spacing={2}>
</Button> <Button size="small" variant="contained" type="submit">
<Button size="small" variant="outlined" onClick={handleCancel}> {i18n("save")}
{i18n("cancel")} </Button>
</Button> <Button size="small" variant="outlined" onClick={handleCancel}>
</> {i18n("cancel")}
)} </Button>
</Stack> </Stack>
) : ( ))}
// 添加
<Stack direction="row" spacing={2}>
<Button size="small" variant="contained" type="submit">
{i18n("save")}
</Button>
<Button size="small" variant="outlined" onClick={handleCancel}>
{i18n("cancel")}
</Button>
</Stack>
)}
</Stack> </Stack>
</form> </form>
); );
} }
function RuleAccordion({ rule, rules }) {
const [expanded, setExpanded] = useState(false);
const handleChange = (e) => {
setExpanded((pre) => !pre);
};
return (
<Accordion expanded={expanded} onChange={handleChange}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography
style={{
opacity: rules ? 1 : 0.5,
}}
>
{rule.pattern}
</Typography>
</AccordionSummary>
<AccordionDetails>
{expanded && <RuleFields rule={rule} rules={rules} />}
</AccordionDetails>
</Accordion>
);
}
function DownloadButton({ data, text, fileName }) { function DownloadButton({ data, text, fileName }) {
const handleClick = (e) => { const handleClick = (e) => {
e.preventDefault(); e.preventDefault();
@@ -351,10 +409,61 @@ function UploadButton({ onChange, text }) {
); );
} }
export default function Rules() { function ShareButton({ rules, injectRules, selectedSub }) {
const alert = useAlert();
const i18n = useI18n();
const handleClick = async () => {
try {
const { syncUrl, syncKey } = await syncOpt.load();
if (!syncUrl || !syncKey) {
alert.warning(i18n("error_sync_setting"));
return;
}
const shareRules = [...rules.list];
if (injectRules) {
const subRules = await loadSubRules(selectedSub?.url);
shareRules.splice(-1, 0, ...subRules);
}
const url = await syncShareRules({
rules: shareRules,
syncUrl,
syncKey,
});
window.open(url, "_blank");
} catch (err) {
alert.warning(i18n("error_got_some_wrong"));
console.log("[share rules]", err);
}
};
return (
<Button
size="small"
variant="outlined"
onClick={handleClick}
startIcon={<ShareIcon />}
>
{"分享"}
</Button>
);
}
function UserRules() {
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 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;
const handleImport = (e) => { const handleImport = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
@@ -378,43 +487,337 @@ export default function Rules() {
reader.readAsText(file); reader.readAsText(file);
}; };
const handleInject = () => {
updateSetting({
injectRules: !injectRules,
});
};
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" alignItems="center" spacing={2} useFlexGap flexWrap="wrap">
<Button
size="small"
variant="contained"
disabled={showAdd}
onClick={(e) => {
e.preventDefault();
setShowAdd(true);
}}
>
{i18n("add")}
</Button>
<UploadButton text={i18n("import")} onChange={handleImport} />
<DownloadButton
data={JSON.stringify([...rules.list].reverse(), null, "\t")}
text={i18n("export")}
/>
<ShareButton
rules={rules}
injectRules={injectRules}
selectedSub={selectedSub}
/>
<FormControlLabel
control={
<Switch
size="small"
checked={injectRules}
onChange={handleInject}
/>
}
label={i18n("inject_rules")}
/>
</Stack>
{showAdd && (
<RuleFields
rules={rules}
setShow={setShowAdd}
setKeyword={setKeyword}
/>
)}
<Box>
{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>
);
}
function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
const [loading, setLoading] = useState(false);
const handleDel = async () => {
try {
await subrules.del(url);
await rulesCache.del(url);
} catch (err) {
console.log("[del subrules]", err);
}
};
const handleSync = async () => {
try {
setLoading(true);
const rules = await syncSubRules(url);
if (rules.length > 0 && url === selectedUrl) {
setRules(rules);
}
} catch (err) {
console.log("[sync sub rules]", err);
} finally {
setLoading(false);
}
};
return (
<Stack direction="row" alignItems="center" spacing={2}>
<FormControlLabel value={url} control={<Radio />} label={url} />
{loading ? (
<CircularProgress size={16} />
) : (
<IconButton size="small" onClick={handleSync}>
<SyncIcon fontSize="small" />
</IconButton>
)}
{index !== 0 && selectedUrl !== url && (
<IconButton size="small" onClick={handleDel}>
<DeleteIcon fontSize="small" />
</IconButton>
)}
</Stack>
);
}
function SubRulesEdit({ subrules }) {
const i18n = useI18n();
const [inputText, setInputText] = useState("");
const [inputError, setInputError] = useState("");
const [showInput, setShowInput] = useState(false);
const [loading, setLoading] = useState(false);
const handleCancel = (e) => {
e.preventDefault();
setShowInput(false);
setInputText("");
setInputError("");
};
const handleSave = async (e) => {
e.preventDefault();
const url = inputText.trim();
if (!url) {
setInputError(i18n("error_cant_be_blank"));
return;
}
if (subrules.list.find((item) => item.url === url)) {
setInputError(i18n("error_duplicate_values"));
return;
}
try {
setLoading(true);
const rules = await syncSubRules(url);
if (rules.length === 0) {
throw new Error("empty rules");
}
await subrules.add(url);
setShowInput(false);
setInputText("");
} catch (err) {
console.log("[fetch rules]", err);
setInputError(i18n("error_fetch_url"));
} finally {
setLoading(false);
}
};
const handleInput = (e) => {
e.preventDefault();
setInputText(e.target.value);
};
const handleFocus = (e) => {
e.preventDefault();
setInputError("");
};
return (
<>
<Stack direction="row" alignItems="center" spacing={2}>
<Button
size="small"
variant="contained"
disabled={showInput}
onClick={(e) => {
e.preventDefault();
setShowInput(true);
}}
>
{i18n("add")}
</Button>
</Stack>
{showInput && (
<>
<TextField
size="small"
value={inputText}
error={!!inputError}
helperText={inputError}
onChange={handleInput}
onFocus={handleFocus}
label={i18n("subscribe_url")}
/>
<Stack direction="row" alignItems="center" spacing={2}>
<Button
size="small"
variant="contained"
onClick={handleSave}
disabled={loading}
>
{i18n("save")}
</Button>
<Button size="small" variant="outlined" onClick={handleCancel}>
{i18n("cancel")}
</Button>
</Stack>
</>
)}
</>
);
}
function SubRules() {
const [loading, setLoading] = useState(false);
const [rules, setRules] = useState([]);
const subrules = useSubrules();
const selectedSub = subrules.list.find((item) => item.selected);
const handleSelect = (e) => {
const url = e.target.value;
subrules.select(url);
};
useEffect(() => {
(async () => {
if (selectedSub?.url) {
try {
setLoading(true);
const rules = await loadSubRules(selectedSub?.url);
setRules(rules);
} catch (err) {
console.log("[load rules]", err);
} finally {
setLoading(false);
}
}
})();
}, [selectedSub?.url]);
return (
<Stack spacing={3}>
<SubRulesEdit subrules={subrules} />
<RadioGroup value={selectedSub?.url} onChange={handleSelect}>
{subrules.list.map((item, index) => (
<SubRulesItem
key={item.url}
url={item.url}
index={index}
selectedUrl={selectedSub?.url}
subrules={subrules}
setRules={setRules}
/>
))}
</RadioGroup>
<Box>
{loading ? (
<center>
<CircularProgress />
</center>
) : (
rules.map((rule) => <RuleAccordion key={rule.pattern} rule={rule} />)
)}
</Box>
</Stack>
);
}
export default function Rules() {
const i18n = useI18n();
const [activeTab, setActiveTab] = useState(0);
const handleTabChange = (e, newValue) => {
setActiveTab(newValue);
};
return ( return (
<Box> <Box>
<Stack spacing={3}> <Stack spacing={3}>
<Stack direction="row" spacing={2}> <Alert severity="info">
<Button {i18n("rules_warn_1")}
size="small" <br />
variant="contained" {i18n("rules_warn_2")}
disabled={showAdd} </Alert>
onClick={(e) => {
e.preventDefault();
setShowAdd(true);
}}
>
{i18n("add")}
</Button>
<UploadButton text={i18n("import")} onChange={handleImport} /> <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<DownloadButton <Tabs value={activeTab} onChange={handleTabChange}>
data={JSON.stringify([...rules.list].reverse(), null, "\t")} <Tab label={i18n("personal_rules")} />
text={i18n("export")} <Tab label={i18n("subscribe_rules")} />
/> </Tabs>
</Stack>
{showAdd && <RuleFields rules={rules} setShow={setShowAdd} />}
<Box>
{rules.list.map((rule) => (
<Accordion key={rule.pattern}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{rule.pattern}</Typography>
</AccordionSummary>
<AccordionDetails>
<RuleFields rule={rule} rules={rules} />
</AccordionDetails>
</Accordion>
))}
</Box> </Box>
<div hidden={activeTab !== 0}>{activeTab === 0 && <UserRules />}</div>
<div hidden={activeTab !== 1}>{activeTab === 1 && <SubRules />}</div>
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -6,15 +6,43 @@ 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, useSettingUpdate } from "../../hooks/Setting";
import { limitNumber } from "../../libs/utils"; import { limitNumber, debounce } 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 = useSetting();
const updateSetting = useSettingUpdate(); const updateSetting = useSettingUpdate();
const handleChange = useMemo(
() =>
debounce((e) => {
e.preventDefault();
let { name, value } = e.target;
switch (name) {
case "fetchLimit":
value = limitNumber(value, 1, 100);
break;
case "fetchInterval":
value = limitNumber(value, 0, 5000);
break;
case "minLength":
value = limitNumber(value, 1, 100);
break;
case "maxLength":
value = limitNumber(value, 100, 10000);
break;
default:
}
updateSetting({
[name]: value,
});
}, 500),
[updateSetting]
);
if (!setting) { if (!setting) {
return; return;
} }
@@ -24,6 +52,8 @@ export default function Settings() {
googleUrl, googleUrl,
fetchLimit, fetchLimit,
fetchInterval, fetchInterval,
minLength,
maxLength,
openaiUrl, openaiUrl,
openaiKey, openaiKey,
openaiModel, openaiModel,
@@ -37,13 +67,10 @@ export default function Settings() {
<FormControl size="small"> <FormControl size="small">
<InputLabel>{i18n("ui_lang")}</InputLabel> <InputLabel>{i18n("ui_lang")}</InputLabel>
<Select <Select
name="uiLang"
value={uiLang} value={uiLang}
label={i18n("ui_lang")} label={i18n("ui_lang")}
onChange={(e) => { onChange={handleChange}
updateSetting({
uiLang: e.target.value,
});
}}
> >
{UI_LANGS.map(([lang, name]) => ( {UI_LANGS.map(([lang, name]) => (
<MenuItem key={lang} value={lang}> <MenuItem key={lang} value={lang}>
@@ -57,36 +84,45 @@ export default function Settings() {
size="small" size="small"
label={i18n("fetch_limit")} label={i18n("fetch_limit")}
type="number" type="number"
name="fetchLimit"
defaultValue={fetchLimit} defaultValue={fetchLimit}
onChange={(e) => { onChange={handleChange}
updateSetting({
fetchLimit: limitNumber(e.target.value, 1, 100),
});
}}
/> />
<TextField <TextField
size="small" size="small"
label={i18n("fetch_interval")} label={i18n("fetch_interval")}
type="number" type="number"
name="fetchInterval"
defaultValue={fetchInterval} defaultValue={fetchInterval}
onChange={(e) => { onChange={handleChange}
updateSetting({ />
fetchInterval: limitNumber(e.target.value, 0, 5000),
}); <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"> <FormControl size="small">
<InputLabel>{i18n("clear_cache")}</InputLabel> <InputLabel>{i18n("clear_cache")}</InputLabel>
<Select <Select
name="clearCache"
value={clearCache} value={clearCache}
label={i18n("clear_cache")} label={i18n("clear_cache")}
onChange={(e) => { onChange={handleChange}
updateSetting({
clearCache: e.target.value,
});
}}
> >
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem> <MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem> <MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
@@ -96,60 +132,43 @@ export default function Settings() {
<TextField <TextField
size="small" size="small"
label={i18n("google_api")} label={i18n("google_api")}
name="googleUrl"
defaultValue={googleUrl} defaultValue={googleUrl}
onChange={(e) => { onChange={handleChange}
updateSetting({
googleUrl: e.target.value,
});
}}
/> />
<TextField <TextField
size="small" size="small"
label={i18n("openai_api")} label={i18n("openai_api")}
name="openaiUrl"
defaultValue={openaiUrl} defaultValue={openaiUrl}
onChange={(e) => { onChange={handleChange}
updateSetting({
openaiUrl: e.target.value,
});
}}
/> />
<TextField <TextField
size="small" size="small"
type="password" type="password"
label={i18n("openai_key")} label={i18n("openai_key")}
name="openaiKey"
defaultValue={openaiKey} defaultValue={openaiKey}
onChange={(e) => { onChange={handleChange}
updateSetting({
openaiKey: e.target.value,
});
}}
/> />
<TextField <TextField
size="small" size="small"
label={i18n("openai_model")} label={i18n("openai_model")}
name="openaiModel"
defaultValue={openaiModel} defaultValue={openaiModel}
onChange={(e) => { onChange={handleChange}
updateSetting({
openaiModel: e.target.value,
});
}}
/> />
<TextField <TextField
size="small" size="small"
label={i18n("openai_prompt")} label={i18n("openai_prompt")}
name="openaiPrompt"
defaultValue={openaiPrompt} defaultValue={openaiPrompt}
onChange={(e) => { onChange={handleChange}
updateSetting({
openaiPrompt: e.target.value,
});
}}
multiline multiline
minRows={2}
maxRows={10}
/> />
</Stack> </Stack>
</Box> </Box>

View File

@@ -3,23 +3,55 @@ import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import { useSync } from "../../hooks/Sync"; import { useSync } from "../../hooks/Sync";
import { syncAll } from "../../libs/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 { 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() { export default function SyncSetting() {
const i18n = useI18n(); const i18n = useI18n();
const sync = useSync(); const sync = useSync();
const alert = useAlert();
const [loading, setLoading] = useState(false);
const handleChange = useMemo(
() =>
debounce(async (e) => {
e.preventDefault();
const { name, value } = e.target;
await sync.update({
[name]: value,
});
// 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) { if (!sync.opt) {
return; return;
} }
const { syncUrl, syncKey } = sync.opt; const { syncUrl, syncKey } = sync.opt;
const handleSyncBlur = () => {
syncAll();
};
return ( return (
<Box> <Box>
@@ -29,13 +61,9 @@ export default function SyncSetting() {
<TextField <TextField
size="small" size="small"
label={i18n("data_sync_url")} label={i18n("data_sync_url")}
name="syncUrl"
defaultValue={syncUrl} defaultValue={syncUrl}
onChange={(e) => { onChange={handleChange}
sync.update({
syncUrl: e.target.value,
});
}}
onBlur={handleSyncBlur}
helperText={ helperText={
<Link href={URL_KISS_WORKER}>{i18n("about_sync_api")}</Link> <Link href={URL_KISS_WORKER}>{i18n("about_sync_api")}</Link>
} }
@@ -45,14 +73,23 @@ export default function SyncSetting() {
size="small" size="small"
type="password" type="password"
label={i18n("data_sync_key")} label={i18n("data_sync_key")}
name="syncKey"
defaultValue={syncKey} defaultValue={syncKey}
onChange={(e) => { onChange={handleChange}
sync.update({
syncKey: e.target.value,
});
}}
onBlur={handleSyncBlur}
/> />
<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>
); );

View File

@@ -10,31 +10,35 @@ import { useEffect, useState } from "react";
import { isGm } from "../../libs/browser"; import { isGm } from "../../libs/browser";
import { sleep } from "../../libs/utils"; import { sleep } from "../../libs/utils";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import { trySyncAll } from "../../libs/sync";
import { AlertProvider } from "../../hooks/Alert";
export default function Options() { export default function Options() {
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
useEffect(() => { useEffect(() => {
if (!isGm) {
return;
}
(async () => { (async () => {
let i = 0; if (isGm) {
for (;;) { // 等待GM注入
if (window.APP_NAME === process.env.REACT_APP_NAME) { let i = 0;
setReady(true); for (;;) {
break; if (window.APP_NAME === process.env.REACT_APP_NAME) {
} setReady(true);
break;
}
if (++i > 8) { if (++i > 8) {
setError(true); setError(true);
break; break;
} }
await sleep(1000); await sleep(1000);
}
} }
// 同步数据
trySyncAll();
})(); })();
}, []); }, []);
@@ -66,16 +70,18 @@ export default function Options() {
return ( return (
<StoragesProvider> <StoragesProvider>
<ThemeProvider> <ThemeProvider>
<HashRouter> <AlertProvider>
<Routes> <HashRouter>
<Route path="/" element={<Layout />}> <Routes>
<Route index element={<Setting />} /> <Route path="/" element={<Layout />}>
<Route path="rules" element={<Rules />} /> <Route index element={<Setting />} />
<Route path="sync" element={<SyncSetting />} /> <Route path="rules" element={<Rules />} />
<Route path="about" element={<About />} /> <Route path="sync" element={<SyncSetting />} />
</Route> <Route path="about" element={<About />} />
</Routes> </Route>
</HashRouter> </Routes>
</HashRouter>
</AlertProvider>
</ThemeProvider> </ThemeProvider>
</StoragesProvider> </StoragesProvider>
); );

View File

@@ -18,6 +18,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 +41,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 +57,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 +104,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
onChange={handleTransToggle} onChange={handleTransToggle}
/> />
} }
label={i18n("translate")} label={i18n("translate_alt")}
/> />
<TextField <TextField
@@ -158,7 +161,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) => (

4651
yarn.lock

File diff suppressed because it is too large Load Diff