Compare commits

..

40 Commits

Author SHA1 Message Date
Gabe Yuan
5c5a35d3bb v1.6.6 2023-09-03 21:47:38 +08:00
Gabe Yuan
2c24214f48 fix Violentmonkey --> .connect 2023-09-03 21:45:06 +08:00
Gabe Yuan
67d9e70b3c v1,6,5 2023-09-03 21:10:35 +08:00
Gabe Yuan
000a55f43b modify content.html 2023-09-03 21:08:45 +08:00
Gabe Yuan
4096a6976c add clear cache & api test button 2023-09-03 13:11:04 +08:00
Gabe Yuan
df4c4ebd50 fix i18n text 2023-09-03 00:26:57 +08:00
Gabe Yuan
b43bd4e0e2 add deepl @connect 2023-09-03 00:10:07 +08:00
Gabe Yuan
2660dbf866 update readme 2023-09-02 23:45:40 +08:00
Gabe Yuan
e0b7c60099 v1.6.4 2023-09-02 20:07:24 +08:00
Gabe Yuan
536b58bf67 fix fuzzy style hover bug 2023-09-02 19:55:26 +08:00
Gabe Yuan
6bb742f828 v1.6.3 2023-09-02 19:16:14 +08:00
Gabe Yuan
72742e5e12 fix build:script 2023-09-02 19:09:12 +08:00
Gabe Yuan
3667e0a509 update readme 2023-09-02 18:14:42 +08:00
Gabe Yuan
c2d7668ba7 v1.6.2 2023-09-02 17:26:37 +08:00
Gabe Yuan
aa830f5e20 update readme 2023-09-02 17:22:35 +08:00
Gabe Yuan
b593fa4146 add deepl api 2023-09-02 16:57:09 +08:00
Gabe Yuan
b00b906484 optimize inject script 2023-09-02 15:54:51 +08:00
Gabe Yuan
c1bd6a1be6 use random eventname 2023-09-02 14:14:27 +08:00
Gabe Yuan
36739f04b3 add more shortcut 2023-09-02 13:14:27 +08:00
Gabe Yuan
23eb92853e register a shortcut for userscript 2023-09-02 00:42:15 +08:00
Gabe Yuan
5ab2910dc7 v1.6.1 2023-09-01 22:46:41 +08:00
Gabe Yuan
40d07f6764 fix proxy link 2023-09-01 22:28:28 +08:00
Gabe Yuan
5c8e216169 overwrite subscribe rules 2023-09-01 22:27:25 +08:00
Gabe Yuan
5ba061deda merge yarn 2023-09-01 19:35:48 +08:00
Gabe Yuan
935c83185d fix style text 2023-09-01 17:07:21 +08:00
Gabe Yuan
6327391e65 hide color input when diy style 2023-09-01 17:02:47 +08:00
Gabe Yuan
3d656cf5b0 use debounce to sync setting 2023-09-01 16:39:57 +08:00
Gabe Yuan
d570a0f1a2 replace 'document.documentElement.clientWidth' to 'window.innerWidth' 2023-09-01 16:11:31 +08:00
Gabe Yuan
503a71302c support diy text styles 2023-09-01 15:57:05 +08:00
Gabe Yuan
3e36ceb5b9 num of newline characters 2023-09-01 11:21:06 +08:00
Gabe Yuan
cde7a1d49f add kiss-proxy link 2023-09-01 11:03:53 +08:00
Gabe Yuan
b14a38e4fb sync then reload setting 2023-09-01 10:15:57 +08:00
Gabe Yuan
732a526a8e v1.6.0 2023-08-31 21:24:40 +08:00
Gabe Yuan
2da5ffef44 update userscript download link 2023-08-31 21:21:36 +08:00
Gabe Yuan
2e6e52004f fix firefox bug 2023-08-31 20:57:51 +08:00
Gabe Yuan
4486ad353c dev.....! 2023-08-31 13:38:06 +08:00
Gabe Yuan
aa795e2731 dev...... 2023-08-31 00:18:57 +08:00
Gabe Yuan
c46fe7d1c6 dev... 2023-08-30 18:05:37 +08:00
Gabe Yuan
d7cee8cca6 catch caches is not defined 2023-08-29 21:33:27 +08:00
Gabe Yuan
11f790ace5 catch caches is not defined 2023-08-29 21:24:25 +08:00
52 changed files with 2320 additions and 1339 deletions

5
.env
View File

@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=1.5.8
REACT_APP_VERSION=1.6.6
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
@@ -21,3 +21,6 @@ REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt
REACT_APP_USERSCRIPT_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator.user.js
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator-ios-safari.user.js

View File

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

View File

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

View File

@@ -89,10 +89,12 @@ const userscriptWebpack = (config, env) => {
// @grant GM.getValue
// @grant GM.deleteValue
// @grant GM.info
// @inject-into content
// @grant unsafeWindow
// @connect translate.googleapis.com
// @connect api-edge.cognitive.microsofttranslator.com
// @connect edge.microsoft.com
// @connect api-free.deepl.com
// @connect api.deepl.com
// @connect api.openai.com
// @connect openai.azure.com
// @connect workers.dev
@@ -100,6 +102,7 @@ const userscriptWebpack = (config, env) => {
// @connect githubusercontent.com
// @connect kiss-translator.rayjar.com
// @connect ghproxy.com
// @connect localhost:3000
// @run-at document-end
// ==/UserScript==

View File

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

View File

@@ -64,8 +64,25 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<div id="content">
<p>You need to enable JavaScript to run <span>this app.</span></p>
The <span>embargo</span> has just lifted to confirm that AmpereOne is
coming to Google Cloud with the C3A instances.
<br />
But these upcoming instances for now are only in private preview form.
<br />
<br />
Needless to say I also haven't had any AmpereOne access to check out the
performance and power efficiency of these new Arm server processors from
Ampere Computing.
<br />
</div>
<h2>
<p><span>React is a JavaScript library for building user interfaces.</span></p>
<p>
<span
>React is a JavaScript library for building user interfaces.</span
>
</p>
</h2>
<div id="addtitle"></div>
<h2>Shadow 1</h2>

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.5.8",
"version": "1.6.6",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -17,6 +17,11 @@
}
],
"commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Alt+K"
}
},
"toggleTranslate": {
"suggested_key": {
"default": "Alt+Q"

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.5.8",
"version": "1.6.6",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -18,6 +18,11 @@
}
],
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Alt+K"
}
},
"toggleTranslate": {
"suggested_key": {
"default": "Alt+Q"

View File

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

View File

@@ -7,35 +7,19 @@ import {
MSG_TRANS_TOGGLE_STYLE,
CMD_TOGGLE_TRANSLATE,
CMD_TOGGLE_STYLE,
DEFAULT_SETTING,
DEFAULT_RULES,
DEFAULT_SYNC,
STOKEY_SETTING,
STOKEY_RULES,
STOKEY_SYNC,
CACHE_NAME,
STOKEY_RULESCACHE_PREFIX,
BUILTIN_RULES,
} from "./config";
import storage from "./libs/storage";
import { getSetting } from "./libs";
import { trySyncAll } from "./libs/sync";
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
import { trySyncSettingAndRules } from "./libs/sync";
import { fetchData, fetchPool } from "./libs/fetch";
import { sendTabMsg } from "./libs/msg";
import { trySyncAllSubRules } from "./libs/rules";
import { trySyncAllSubRules } from "./libs/subRules";
import { tryClearCaches } from "./libs";
/**
* 插件安装
*/
browser.runtime.onInstalled.addListener(() => {
console.log("KISS Translator onInstalled");
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES);
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
storage.trySetObj(
`${STOKEY_RULESCACHE_PREFIX}${process.env.REACT_APP_RULESURL}`,
BUILTIN_RULES
);
tryInitDefaultData();
});
/**
@@ -45,12 +29,12 @@ browser.runtime.onStartup.addListener(async () => {
console.log("browser onStartup");
// 同步数据
await trySyncAll(true);
await trySyncSettingAndRules(true);
// 清除缓存
const setting = await getSetting();
const setting = await getSettingWithDefault();
if (setting.clearCache) {
caches.delete(CACHE_NAME);
tryClearCaches();
}
// 同步订阅规则

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

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

View File

@@ -60,6 +60,10 @@ export const I18N = {
zh: `最大翻译长度 (100-10000)`,
en: `Max Translate Length (100-10000)`,
},
num_of_newline_characters: {
zh: `换行字符数 (1-1000)`,
en: `Number of Newline Characters (1-1000)`,
},
translate_service: {
zh: `翻译服务`,
en: `Translate Service`,
@@ -84,6 +88,10 @@ export const I18N = {
zh: `样式颜色`,
en: `Style Color`,
},
remain_unchanged: {
zh: `保留不变`,
en: `Remain Unchanged`,
},
google_api: {
zh: `谷歌翻译接口`,
en: `Google Translate API`,
@@ -132,6 +140,10 @@ export const I18N = {
zh: `订阅规则`,
en: `Subscribe Rules`,
},
overwrite_subscribe_rules: {
zh: `覆写订阅规则`,
en: `Overwrite Subscribe Rules`,
},
subscribe_url: {
zh: `订阅地址`,
en: `Subscribe URL`,
@@ -152,6 +164,10 @@ export const I18N = {
zh: `查看关于数据同步接口部署`,
en: `View About Data Synchronization Interface Deployment`,
},
about_api_proxy: {
zh: `查看自建一个翻译接口代理`,
en: `Check out the self-built translation interface proxy`,
},
style_none: {
zh: ``,
en: `None`,
@@ -180,6 +196,14 @@ export const I18N = {
zh: `高亮`,
en: `Highlight`,
},
diy_style: {
zh: `自定义样式`,
en: `Custom Style`,
},
diy_style_helper: {
zh: `遵循“styled-components”的语法`,
en: `Follow the syntax of "styled-components"`,
},
setting: {
zh: `设置`,
en: `Setting`,
@@ -193,8 +217,8 @@ export const I18N = {
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`,
},
selector_helper: {
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`,
en: `1. Follow the CSS selector rules. 2. Leave blank to adopt the global setting. 3. Separate multiple CSS selectors with ";". 4. The "shadow root" selector and the internal selector are separated by ">>>".`,
zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`,
en: `1. Follow CSS selector syntax. 2. Leave blank to adopt the global setting. 3. Separate multiple CSS selectors with ";". 4. The "shadow root" selector and the internal selector are separated by ">>>".`,
},
translate_switch: {
zh: `开启翻译`,
@@ -236,6 +260,14 @@ export const I18N = {
zh: `请检查url地址是否正确或稍后再试。`,
en: `Please check if the url address is correct or try again later.`,
},
deepl_api: {
zh: `DeepL 接口`,
en: `DeepL API`,
},
deepl_key: {
zh: `DeepL 密钥`,
en: `DeepL Key`,
},
openai_api: {
zh: `OpenAI 接口`,
en: `OpenAI API`,
@@ -252,9 +284,9 @@ export const I18N = {
zh: `OpenAI 提示词`,
en: `OpenAI Prompt`,
},
clear_cache: {
zh: `是否清除缓存`,
en: `Whether clear cache`,
if_clear_cache: {
zh: `是否清除缓存 (仅用于扩展)`,
en: `Whether clear cache (only for extension)`,
},
clear_cache_never: {
zh: `不清除缓存`,
@@ -276,13 +308,13 @@ export const I18N = {
zh: `数据同步测试`,
en: `Data Sync Test`,
},
data_sync_success: {
zh: `数据同步成功!`,
en: `Data Sync Success`,
sync_success: {
zh: `同步成功!`,
en: `Sync Success`,
},
data_sync_error: {
zh: `数据同步失败!`,
en: `Data Sync Error`,
sync_failed: {
zh: `同步失败!`,
en: `Sync Error`,
},
error_got_some_wrong: {
zh: `抱歉,出错了!`,
@@ -292,4 +324,32 @@ export const I18N = {
zh: `您的同步设置未填写,无法在线分享。`,
en: `Your sync settings are missing and cannot be shared online.`,
},
click_test: {
zh: `点击测试`,
en: `Click Test`,
},
test_success: {
zh: `测试成功`,
en: `Test success`,
},
test_failed: {
zh: `测试失败`,
en: `Test failed`,
},
clear_all_cache_now: {
zh: `立即清除全部缓存`,
en: `Clear all cache now`,
},
clear_cache: {
zh: `清除缓存`,
en: `Clear Cache`,
},
clear_success: {
zh: `清除成功`,
en: `Clear success`,
},
clear_failed: {
zh: `清除失败`,
en: `Clear failed`,
},
};

View File

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

View File

@@ -3,6 +3,7 @@ const els = `li, p, h1, h2, h3, h4, h5, h6, dd`;
export const DEFAULT_SELECTOR = `:is(${els})`;
export const GLOBAL_KEY = "*";
export const REMAIN_KEY = "-";
export const SHADOW_KEY = ">>>";
@@ -15,6 +16,30 @@ export const DEFAULT_RULE = {
textStyle: GLOBAL_KEY,
transOpen: GLOBAL_KEY,
bgColor: "",
textDiyStyle: "",
};
const DEFAULT_DIY_STYLE = `color: #666;
background: linear-gradient(
45deg,
LightGreen 20%,
LightPink 20% 40%,
LightSalmon 40% 60%,
LightSeaGreen 60% 80%,
LightSkyBlue 80%
);
&:hover {
color: #333;
};`;
export const DEFAULT_OW_RULE = {
translator: REMAIN_KEY,
fromLang: REMAIN_KEY,
toLang: REMAIN_KEY,
textStyle: REMAIN_KEY,
transOpen: REMAIN_KEY,
bgColor: "",
textDiyStyle: DEFAULT_DIY_STYLE,
};
const RULES = [

View File

@@ -5,17 +5,18 @@ import {
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
} from "./config";
import { getSetting, getRules, matchRule } from "./libs";
import { getSettingWithDefault, getRulesWithDefault } from "./libs/storage";
import { Translator } from "./libs/translator";
import { isIframe } from "./libs/iframe";
import { matchRule } from "./libs/rules";
/**
* 入口函数
*/
const init = async () => {
const href = isIframe ? document.referrer : document.location.href;
const setting = await getSetting();
const rules = await getRules();
const setting = await getSettingWithDefault();
const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -39,6 +39,12 @@ function App() {
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install Userscript 2
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install Userscript Safari 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install Userscript Safari 2
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
Open Options Page 1
</Link>

View File

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

View File

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

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

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

View File

@@ -1,5 +1,5 @@
import { isExt, isGm } from "./browser";
import { sendMsg } from "./msg";
import { isExt, isGm } from "./client";
import { sendBgMsg } from "./msg";
import { taskPool } from "./pool";
import {
MSG_FETCH,
@@ -7,6 +7,7 @@ import {
MSG_FETCH_CLEAR,
CACHE_NAME,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI,
DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT,
@@ -67,9 +68,11 @@ const newCacheReq = async (request) => {
*/
const fetchApi = async ({ input, init = {}, translator, token }) => {
if (translator === OPT_TRANS_MICROSOFT) {
init.headers["Authorization"] = `Bearer ${token}`;
init.headers["Authorization"] = `Bearer ${token}`; // Microsoft
} else if (translator === OPT_TRANS_DEEPL) {
init.headers["Authorization"] = `DeepL-Auth-Key ${token}`; // DeepL
} else if (translator === OPT_TRANS_OPENAI) {
init.headers["Authorization"] = `Bearer ${token}`; // // OpenAI
init.headers["Authorization"] = `Bearer ${token}`; // OpenAI
init.headers["api-key"] = token; // Azure OpenAI
}
@@ -80,7 +83,9 @@ const fetchApi = async ({ input, init = {}, translator, token }) => {
} else {
info = GM.info;
}
const connects = info?.script?.connects || [];
// Tampermonkey --> .connects
// Violentmonkey --> .connect
const connects = info?.script?.connects || info?.script?.connect || [];
const url = new URL(input);
const isSafe = connects.find((item) => url.hostname.endsWith(item));
if (isSafe) {
@@ -121,15 +126,15 @@ export const fetchData = async (
{ useCache, usePool, translator, token, ...init } = {}
) => {
const cacheReq = await newCacheReq(new Request(input, init));
const cache = await caches.open(CACHE_NAME);
let res;
// 查询缓存
if (useCache) {
try {
const cache = await caches.open(CACHE_NAME);
res = await cache.match(cacheReq);
} catch (err) {
console.log("[cache match]", err);
console.log("[cache match]", err.message);
}
}
@@ -148,9 +153,10 @@ export const fetchData = async (
// 插入缓存
if (useCache) {
try {
const cache = await caches.open(CACHE_NAME);
await cache.put(cacheReq, res.clone());
} catch (err) {
console.log("[cache put]", err);
console.log("[cache put]", err.message);
}
}
}
@@ -171,7 +177,7 @@ export const fetchData = async (
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
// 插件
if (isExt && !isBg) {
const res = await sendMsg(MSG_FETCH, { input, opts });
const res = await sendBgMsg(MSG_FETCH, { input, opts });
if (res.error) {
throw new Error(res.error);
}
@@ -187,9 +193,9 @@ export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
* @param {*} interval
* @param {*} limit
*/
export const fetchUpdate = async (interval, limit) => {
export const updateFetchPool = async (interval, limit) => {
if (isExt) {
const res = await sendMsg(MSG_FETCH_LIMIT, { interval, limit });
const res = await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit });
if (res.error) {
throw new Error(res.error);
}
@@ -201,9 +207,9 @@ export const fetchUpdate = async (interval, limit) => {
/**
* 清空任务池
*/
export const fetchClear = async () => {
export const clearFetchPool = async () => {
if (isExt) {
const res = await sendMsg(MSG_FETCH_CLEAR);
const res = await sendBgMsg(MSG_FETCH_CLEAR);
if (res.error) {
throw new Error(res.error);
}

View File

@@ -1,20 +1,31 @@
import { fetchGM } from "./fetch";
import { genEventName } from "./utils";
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
const MSG_GM_setValue = "setValue";
const MSG_GM_getValue = "getValue";
const MSG_GM_deleteValue = "deleteValue";
const MSG_GM_info = "info";
/**
* 注入页面的脚本请求并接受GM接口信息
* @param {*} param0
*/
export const injectScript = (ping) => {
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
const MSG_GM_setValue = "setValue";
const MSG_GM_getValue = "getValue";
const MSG_GM_deleteValue = "deleteValue";
const MSG_GM_info = "info";
let GM_info;
window.APP_INFO = {
name: process.env.REACT_APP_NAME,
version: process.env.REACT_APP_VERSION,
eventName: ping,
};
};
/**
* 适配GM脚本
*/
export const adaptScript = (ping) => {
const promiseGM = (action, args, timeout = 5000) =>
new Promise((resolve, reject) => {
const pong = btoa(Math.random()).slice(3, 11);
const pong = genEventName();
const handleEvent = (e) => {
window.removeEventListener(pong, handleEvent);
const { data, error } = e.detail;
@@ -41,14 +52,13 @@ export const injectScript = (ping) => {
setValue: (key, val) => promiseGM(MSG_GM_setValue, { key, val }),
getValue: (key) => promiseGM(MSG_GM_getValue, { key }),
deleteValue: (key) => promiseGM(MSG_GM_deleteValue, { key }),
getInfo: () => {
if (GM_info) {
return GM_info;
getInfo: async () => {
if (!window.GM_info) {
window.GM_info = await promiseGM(MSG_GM_info);
}
return promiseGM(MSG_GM_info);
return window.GM_info;
},
};
window.APP_NAME = process.env.REACT_APP_NAME;
};
/**
@@ -56,11 +66,6 @@ export const injectScript = (ping) => {
* @param {*} param0
*/
export const handlePing = async (e) => {
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
const MSG_GM_setValue = "setValue";
const MSG_GM_getValue = "getValue";
const MSG_GM_deleteValue = "deleteValue";
const MSG_GM_info = "info";
const { action, args, pong } = e.detail;
let res;
try {

View File

@@ -1,96 +1,15 @@
import storage from "./storage";
import {
DEFAULT_SETTING,
STOKEY_SETTING,
STOKEY_RULES,
STOKEY_FAB,
GLOBLA_RULE,
GLOBAL_KEY,
DEFAULT_SUBRULES_LIST,
} from "../config";
import { CACHE_NAME } from "../config";
import { browser } from "./browser";
import { isMatch } from "./utils";
import { loadSubRules } from "./rules";
/**
* 查询storage中的设置
* @returns
* 清除缓存数据
*/
export const getSetting = async () => ({
...DEFAULT_SETTING,
...((await storage.getObj(STOKEY_SETTING)) || {}),
});
/**
* 查询规则列表
* @returns
*/
export const getRules = async () => (await storage.getObj(STOKEY_RULES)) || [];
/**
* 查询fab位置信息
* @returns
*/
export const getFab = async () => (await storage.getObj(STOKEY_FAB)) || {};
/**
* 设置fab位置信息
* @returns
*/
export const setFab = async (obj) => await storage.setObj(STOKEY_FAB, obj);
/**
* 根据href匹配规则
* @param {*} rules
* @param {string} href
* @returns
*/
export const matchRule = async (
rules,
href,
{ injectRules = 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);
}
export const tryClearCaches = async () => {
try {
caches.delete(CACHE_NAME);
} catch (err) {
console.log("[clean caches]", err.message);
}
const rule = rules.find((r) =>
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
);
const globalRule =
rules.find((r) => r.pattern.split(",").some((p) => p.trim() === "*")) ||
GLOBLA_RULE;
if (!rule) {
return globalRule;
}
rule.selector =
rule?.selector?.trim() ||
globalRule?.selector?.trim() ||
GLOBLA_RULE.selector;
rule.bgColor = rule?.bgColor?.trim() || globalRule?.bgColor?.trim();
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach(
(key) => {
if (rule[key] === GLOBAL_KEY) {
rule[key] = globalRule[key];
}
}
);
return rule;
};
/**
@@ -98,11 +17,11 @@ export const matchRule = async (
* @param {*} q
* @returns
*/
export const detectLang = async (q) => {
export const tryDetectLang = async (q) => {
try {
const res = await browser?.i18n?.detectLanguage(q);
return res?.languages?.[0]?.language;
} catch (err) {
console.log("[detect lang]", err);
console.log("[detect lang]", err.message);
}
};

View File

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

View File

@@ -1,18 +1,94 @@
import storage from "./storage";
import { fetchPolyfill } from "./fetch";
import { matchValue, type } from "./utils";
import { matchValue, type, isMatch } from "./utils";
import {
STOKEY_RULESCACHE_PREFIX,
GLOBAL_KEY,
REMAIN_KEY,
OPT_TRANS_ALL,
OPT_STYLE_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
GLOBLA_RULE,
DEFAULT_SUBRULES_LIST,
DEFAULT_OW_RULE,
} from "../config";
import { syncOpt } from "./sync";
import { loadOrFetchSubRules } from "./subRules";
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
/**
* 根据href匹配规则
* @param {*} rules
* @param {string} href
* @returns
*/
export const matchRule = async (
rules,
href,
{
injectRules = true,
subrulesList = DEFAULT_SUBRULES_LIST,
owSubrule = DEFAULT_OW_RULE,
}
) => {
rules = [...rules];
if (injectRules) {
try {
const selectedSub = subrulesList.find((item) => item.selected);
if (selectedSub?.url) {
const mixRule = {};
Object.entries(owSubrule)
.filter(([key, val]) => {
if (
owSubrule.textStyle === REMAIN_KEY &&
(key === "bgColor" || key === "textDiyStyle")
) {
return false;
}
return val !== REMAIN_KEY;
})
.forEach(([key, val]) => {
mixRule[key] = val;
});
const subRules = (await loadOrFetchSubRules(selectedSub.url)).map(
(item) => ({ ...item, ...mixRule })
);
rules.splice(-1, 0, ...subRules);
}
} catch (err) {
console.log("[load injectRules]", err);
}
}
const rule = rules.find((r) =>
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
);
const globalRule =
rules.find((r) =>
r.pattern.split(",").some((p) => p.trim() === GLOBAL_KEY)
) || GLOBLA_RULE;
if (!rule) {
return globalRule;
}
rule.selector =
rule?.selector?.trim() ||
globalRule?.selector?.trim() ||
GLOBLA_RULE.selector;
rule.bgColor = rule?.bgColor?.trim() || globalRule?.bgColor?.trim();
rule.textDiyStyle =
rule?.textDiyStyle?.trim() || globalRule?.textDiyStyle?.trim();
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach(
(key) => {
if (rule[key] === GLOBAL_KEY) {
rule[key] = globalRule[key];
}
}
);
return rule;
};
/**
* 检查过滤rules
@@ -27,6 +103,8 @@ export const checkRules = (rules) => {
throw new Error("data error");
}
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
const patternSet = new Set();
rules = rules
.filter((rule) => type(rule) === "object")
@@ -47,10 +125,12 @@ export const checkRules = (rules) => {
textStyle,
transOpen,
bgColor,
textDiyStyle,
}) => ({
pattern: pattern.trim(),
selector: type(selector) === "string" ? selector : "",
bgColor: type(bgColor) === "string" ? bgColor : "",
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
@@ -61,85 +141,3 @@ export const checkRules = (rules) => {
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

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

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

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

View File

@@ -1,39 +1,31 @@
import {
STOKEY_SYNC,
DEFAULT_SYNC,
KV_SETTING_KEY,
KV_RULES_KEY,
KV_RULES_SHARE_KEY,
STOKEY_SETTING,
STOKEY_RULES,
KV_SALT_SHARE,
} from "../config";
import storage from "../libs/storage";
import { getSetting, getRules } from ".";
import {
getSyncWithDefault,
updateSync,
getSettingWithDefault,
getRulesWithDefault,
setSetting,
setRules,
} from "./storage";
import { apiSyncData } from "../apis";
import { sha256 } from "./utils";
/**
* 同步相关数据
*/
export const syncOpt = {
load: async () => (await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC,
update: async (obj) => {
await storage.putObj(STOKEY_SYNC, obj);
},
};
/**
* 同步设置
* @returns
*/
export const syncSetting = async (isBg = false) => {
const { syncUrl, syncKey, settingUpdateAt } = await syncOpt.load();
const syncSetting = async (isBg = false) => {
const { syncUrl, syncKey, settingUpdateAt } = await getSyncWithDefault();
if (!syncUrl || !syncKey) {
return;
}
const setting = await getSetting();
const setting = await getSettingWithDefault();
const res = await apiSyncData(
syncUrl,
syncKey,
@@ -46,19 +38,20 @@ export const syncSetting = async (isBg = false) => {
);
if (res && res.updateAt > settingUpdateAt) {
await syncOpt.update({
await updateSync({
settingUpdateAt: res.updateAt,
settingSyncAt: res.updateAt,
});
await storage.setObj(STOKEY_SETTING, res.value);
await setSetting(res.value);
return res.value;
} else {
await syncOpt.update({ settingSyncAt: res.updateAt });
await updateSync({ settingSyncAt: res.updateAt });
}
};
export const trySyncSetting = async (isBg = false) => {
try {
await syncSetting(isBg);
return await syncSetting(isBg);
} catch (err) {
console.log("[sync setting]", err);
}
@@ -68,13 +61,13 @@ export const trySyncSetting = async (isBg = false) => {
* 同步规则
* @returns
*/
export const syncRules = async (isBg = false) => {
const { syncUrl, syncKey, rulesUpdateAt } = await syncOpt.load();
const syncRules = async (isBg = false) => {
const { syncUrl, syncKey, rulesUpdateAt } = await getSyncWithDefault();
if (!syncUrl || !syncKey) {
return;
}
const rules = await getRules();
const rules = await getRulesWithDefault();
const res = await apiSyncData(
syncUrl,
syncKey,
@@ -87,19 +80,20 @@ export const syncRules = async (isBg = false) => {
);
if (res && res.updateAt > rulesUpdateAt) {
await syncOpt.update({
await updateSync({
rulesUpdateAt: res.updateAt,
rulesSyncAt: res.updateAt,
});
await storage.setObj(STOKEY_RULES, res.value);
await setRules(res.value);
return res.value;
} else {
await syncOpt.update({ rulesSyncAt: res.updateAt });
await updateSync({ rulesSyncAt: res.updateAt });
}
};
export const trySyncRules = async (isBg = false) => {
try {
await syncRules(isBg);
return await syncRules(isBg);
} catch (err) {
console.log("[sync user rules]", err);
}
@@ -125,12 +119,10 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
* 同步个人设置和规则
* @returns
*/
export const syncAll = async (isBg = false) => {
await syncSetting(isBg);
await syncRules(isBg);
export const syncSettingAndRules = async (isBg = false) => {
return [await syncSetting(isBg), await syncRules(isBg)];
};
export const trySyncAll = async (isBg = false) => {
await trySyncSetting(isBg);
await trySyncRules(isBg);
export const trySyncSettingAndRules = async (isBg = false) => {
return [await trySyncSetting(isBg), await trySyncRules(isBg)];
};

View File

@@ -3,23 +3,23 @@ import {
APP_LCNAME,
TRANS_MIN_LENGTH,
TRANS_MAX_LENGTH,
EVENT_KISS,
MSG_TRANS_CURRULE,
OPT_STYLE_DASHLINE,
OPT_STYLE_FUZZY,
SHADOW_KEY,
} from "../config";
import Content from "../views/Content";
import { fetchUpdate, fetchClear } from "./fetch";
import { debounce } from "./utils";
import { updateFetchPool, clearFetchPool } from "./fetch";
import { debounce, genEventName } from "./utils";
/**
* 翻译类
*/
export class Translator {
_rule = {};
_minLength = 0;
_maxLength = 0;
_setting = {};
_rootNodes = new Set();
_tranNodes = new Map();
_skipNodeNames = [
APP_LCNAME,
"style",
@@ -36,8 +36,7 @@ export class Translator {
"script",
"iframe",
];
_rootNodes = new Set();
_tranNodes = new Map();
_eventName = genEventName();
// 显示
_interseObserver = new IntersectionObserver(
@@ -89,17 +88,27 @@ export class Translator {
};
};
constructor(rule, { fetchInterval, fetchLimit, minLength, maxLength }) {
fetchUpdate(fetchInterval, fetchLimit);
constructor(rule, setting) {
const { fetchInterval, fetchLimit } = setting;
updateFetchPool(fetchInterval, fetchLimit);
this._overrideAttachShadow();
this._minLength = minLength ?? TRANS_MIN_LENGTH;
this._maxLength = maxLength ?? TRANS_MAX_LENGTH;
this.rule = rule;
this._setting = setting;
this._rule = rule;
if (rule.transOpen === "true") {
this._register();
}
}
get setting() {
return this._setting;
}
get eventName() {
return this._eventName;
}
get rule() {
// console.log("get rule", this._rule);
return this._rule;
@@ -110,8 +119,9 @@ export class Translator {
this._rule = rule;
// 广播消息
const eventName = this._eventName;
window.dispatchEvent(
new CustomEvent(EVENT_KISS, {
new CustomEvent(eventName, {
detail: {
action: MSG_TRANS_CURRULE,
args: rule,
@@ -236,7 +246,7 @@ export class Translator {
this._tranNodes.clear();
// 清空任务池
fetchClear();
clearFetchPool();
};
_reTranslate = debounce(() => {
@@ -268,7 +278,11 @@ export class Translator {
this._tranNodes.set(el, q);
// 太长或太短
if (!q || q.length < this._minLength || q.length > this._maxLength) {
if (
!q ||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH)
) {
return;
}

View File

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

View File

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

View File

@@ -3,13 +3,18 @@ import ReactDOM from "react-dom/client";
import Action from "./views/Action";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { getSetting, getRules, matchRule, getFab } from "./libs";
import {
getSettingWithDefault,
getRulesWithDefault,
getFabWithDefault,
} from "./libs/storage";
import { Translator } from "./libs/translator";
import { trySyncAllSubRules } from "./libs/rules";
import { isGm } from "./libs/browser";
import { trySyncAllSubRules } from "./libs/subRules";
import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config";
import { isIframe } from "./libs/iframe";
import { handlePing, injectScript } from "./libs/gm";
import { matchRule } from "./libs/rules";
import { genEventName } from "./libs/utils";
/**
* 入口函数
@@ -21,23 +26,28 @@ const init = async () => {
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE) ||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE2)
) {
// unsafeWindow.GM = GM;
// unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
const ping = btoa(Math.random()).slice(3, 11);
window.addEventListener(ping, handlePing);
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
const script = document.createElement("script");
script.textContent = `(${injectScript})("${ping}")`;
if (document.head) {
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
unsafeWindow.GM = GM;
unsafeWindow.APP_INFO = {
name: process.env.REACT_APP_NAME,
version: process.env.REACT_APP_VERSION,
};
} else {
const ping = genEventName();
window.addEventListener(ping, handlePing);
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
const script = document.createElement("script");
script.textContent = `(${injectScript})("${ping}")`;
document.head.append(script);
}
return;
}
// 翻译页面
const href = isIframe ? document.referrer : document.location.href;
const setting = await getSetting();
const rules = await getRules();
const setting = await getSettingWithDefault();
const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);
@@ -59,7 +69,7 @@ const init = async () => {
}
// 浮球按钮
const fab = await getFab();
const fab = await getFabWithDefault();
const $action = document.createElement("div");
$action.setAttribute("id", "kiss-translator");
document.body.parentElement.appendChild($action);
@@ -81,28 +91,6 @@ const init = async () => {
</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);
};

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu";
import Toolbar from "@mui/material/Toolbar";
import Box from "@mui/material/Box";
import { useDarkModeSwitch } from "../../hooks/ColorMode";
import { useDarkMode } from "../../hooks/ColorMode";
import LightModeIcon from "@mui/icons-material/LightMode";
import DarkModeIcon from "@mui/icons-material/DarkMode";
@@ -14,8 +13,7 @@ import { useI18n } from "../../hooks/I18n";
function Header(props) {
const i18n = useI18n();
const { onDrawerToggle } = props;
const switchColorMode = useDarkModeSwitch();
const darkMode = useDarkMode();
const { darkMode, toggleDarkMode } = useDarkMode();
return (
<AppBar
@@ -43,7 +41,7 @@ function Header(props) {
href={process.env.REACT_APP_HOMEPAGE}
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
</Box>
<IconButton onClick={switchColorMode} color="inherit">
<IconButton onClick={toggleDarkMode} color="inherit">
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
</IconButton>
</Toolbar>

View File

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

View File

@@ -11,6 +11,8 @@ import {
OPT_LANGS_TO,
OPT_TRANS_ALL,
OPT_STYLE_ALL,
OPT_STYLE_DIY,
OPT_STYLE_USE_COLOR,
} from "../../config";
import { useState, useRef, useEffect, useMemo } from "react";
import { useI18n } from "../../hooks/I18n";
@@ -24,7 +26,7 @@ import MenuItem from "@mui/material/MenuItem";
import Grid from "@mui/material/Grid";
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import FileUploadIcon from "@mui/icons-material/FileUpload";
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
import { useSetting } from "../../hooks/Setting";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Tabs from "@mui/material/Tabs";
@@ -35,14 +37,20 @@ import DeleteIcon from "@mui/icons-material/Delete";
import IconButton from "@mui/material/IconButton";
import ShareIcon from "@mui/icons-material/Share";
import SyncIcon from "@mui/icons-material/Sync";
import { useSubrules } from "../../hooks/Rules";
import { rulesCache, loadSubRules, syncSubRules } from "../../libs/rules";
import { useSubRules } from "../../hooks/SubRules";
import { syncSubRules } from "../../libs/subRules";
import { loadOrFetchSubRules } from "../../libs/subRules";
import { useAlert } from "../../hooks/Alert";
import { syncOpt, syncShareRules } from "../../libs/sync";
import { syncShareRules } from "../../libs/sync";
import { debounce } from "../../libs/utils";
import { delSubRules, getSyncWithDefault } from "../../libs/storage";
import OwSubRule from "./OwSubRule";
function RuleFields({ rule, rules, setShow, setKeyword }) {
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
const initFormValues = rule || {
...DEFAULT_RULE,
transOpen: "true",
};
const editMode = !!rule;
const i18n = useI18n();
@@ -58,6 +66,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
textStyle,
transOpen,
bgColor,
textDiyStyle,
} = formValues;
const hasSamePattern = (str) => {
@@ -132,7 +141,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
}
};
const globalItem = rule?.pattern !== "*" && (
const GlobalItem = rule?.pattern !== "*" && (
<MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}>
{GLOBAL_KEY}
</MenuItem>
@@ -179,7 +188,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled}
onChange={handleChange}
>
{globalItem}
{GlobalItem}
<MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem>
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
</TextField>
@@ -195,7 +204,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled}
onChange={handleChange}
>
{globalItem}
{GlobalItem}
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
@@ -214,7 +223,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled}
onChange={handleChange}
>
{globalItem}
{GlobalItem}
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
@@ -233,7 +242,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled}
onChange={handleChange}
>
{globalItem}
{GlobalItem}
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
@@ -252,7 +261,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled}
onChange={handleChange}
>
{globalItem}
{GlobalItem}
{OPT_STYLE_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
@@ -260,20 +269,35 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
size="small"
fullWidth
name="bgColor"
value={bgColor}
label={i18n("bg_color")}
disabled={disabled}
onChange={handleChange}
/>
</Grid>
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
size="small"
fullWidth
name="bgColor"
value={bgColor}
label={i18n("bg_color")}
disabled={disabled}
onChange={handleChange}
/>
</Grid>
)}
</Grid>
</Box>
{textStyle === OPT_STYLE_DIY && (
<TextField
size="small"
label={i18n("diy_style")}
helperText={i18n("diy_style_helper")}
name="textDiyStyle"
value={textDiyStyle}
disabled={disabled}
onChange={handleChange}
multiline
/>
)}
{rules &&
(editMode ? (
// 编辑
@@ -409,12 +433,12 @@ function UploadButton({ onChange, text }) {
);
}
function ShareButton({ rules, injectRules, selectedSub }) {
function ShareButton({ rules, injectRules, selectedUrl }) {
const alert = useAlert();
const i18n = useI18n();
const handleClick = async () => {
try {
const { syncUrl, syncKey } = await syncOpt.load();
const { syncUrl, syncKey } = await getSyncWithDefault();
if (!syncUrl || !syncKey) {
alert.warning(i18n("error_sync_setting"));
return;
@@ -422,7 +446,7 @@ function ShareButton({ rules, injectRules, selectedSub }) {
const shareRules = [...rules.list];
if (injectRules) {
const subRules = await loadSubRules(selectedSub?.url);
const subRules = await loadOrFetchSubRules(selectedUrl);
shareRules.splice(-1, 0, ...subRules);
}
@@ -451,19 +475,15 @@ function ShareButton({ rules, injectRules, selectedSub }) {
);
}
function UserRules() {
function UserRules({ subRules }) {
const i18n = useI18n();
const rules = useRules();
const [showAdd, setShowAdd] = useState(false);
const setting = useSetting();
const updateSetting = useSettingUpdate();
const subrules = useSubrules();
const [subRules, setSubRules] = useState([]);
const { setting, updateSetting } = useSetting();
const [keyword, setKeyword] = useState("");
const selectedSub = subrules.list.find((item) => item.selected);
const injectRules = !!setting?.injectRules;
const { selectedUrl, selectedRules } = subRules;
const handleImport = (e) => {
const file = e.target.files[0];
@@ -493,19 +513,6 @@ function UserRules() {
});
};
useEffect(() => {
(async () => {
if (selectedSub?.url) {
try {
const rules = await loadSubRules(selectedSub?.url);
setSubRules(rules);
} catch (err) {
console.log("[load rules]", err);
}
}
})();
}, [selectedSub?.url]);
useEffect(() => {
if (!showAdd) {
setKeyword("");
@@ -514,7 +521,13 @@ function UserRules() {
return (
<Stack spacing={3}>
<Stack direction="row" alignItems="center" spacing={2} useFlexGap flexWrap="wrap">
<Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
<Button
size="small"
variant="contained"
@@ -536,7 +549,7 @@ function UserRules() {
<ShareButton
rules={rules}
injectRules={injectRules}
selectedSub={selectedSub}
selectedUrl={selectedUrl}
/>
<FormControlLabel
@@ -572,7 +585,7 @@ function UserRules() {
{injectRules && (
<Box>
{subRules
{selectedRules
.filter(
(rule) =>
rule.pattern.includes(keyword) || keyword.includes(rule.pattern)
@@ -586,13 +599,13 @@ function UserRules() {
);
}
function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
const [loading, setLoading] = useState(false);
const handleDel = async () => {
try {
await subrules.del(url);
await rulesCache.del(url);
await delSub(url);
await delSubRules(url);
} catch (err) {
console.log("[del subrules]", err);
}
@@ -603,7 +616,7 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
setLoading(true);
const rules = await syncSubRules(url);
if (rules.length > 0 && url === selectedUrl) {
setRules(rules);
setSelectedRules(rules);
}
} catch (err) {
console.log("[sync sub rules]", err);
@@ -633,7 +646,7 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
);
}
function SubRulesEdit({ subrules }) {
function SubRulesEdit({ subList, addSub }) {
const i18n = useI18n();
const [inputText, setInputText] = useState("");
const [inputError, setInputError] = useState("");
@@ -656,7 +669,7 @@ function SubRulesEdit({ subrules }) {
return;
}
if (subrules.list.find((item) => item.url === url)) {
if (subList.find((item) => item.url === url)) {
setInputError(i18n("error_duplicate_values"));
return;
}
@@ -667,7 +680,7 @@ function SubRulesEdit({ subrules }) {
if (rules.length === 0) {
throw new Error("empty rules");
}
await subrules.add(url);
await addSub(url);
setShowInput(false);
setInputText("");
} catch (err) {
@@ -735,47 +748,36 @@ function SubRulesEdit({ subrules }) {
);
}
function SubRules() {
const [loading, setLoading] = useState(false);
const [rules, setRules] = useState([]);
const subrules = useSubrules();
const selectedSub = subrules.list.find((item) => item.selected);
function SubRules({ subRules }) {
const {
subList,
selectSub,
addSub,
delSub,
selectedUrl,
selectedRules,
setSelectedRules,
loading,
} = subRules;
const handleSelect = (e) => {
const url = e.target.value;
subrules.select(url);
selectSub(url);
};
useEffect(() => {
(async () => {
if (selectedSub?.url) {
try {
setLoading(true);
const rules = await loadSubRules(selectedSub?.url);
setRules(rules);
} catch (err) {
console.log("[load rules]", err);
} finally {
setLoading(false);
}
}
})();
}, [selectedSub?.url]);
return (
<Stack spacing={3}>
<SubRulesEdit subrules={subrules} />
<SubRulesEdit subList={subList} addSub={addSub} />
<RadioGroup value={selectedSub?.url} onChange={handleSelect}>
{subrules.list.map((item, index) => (
<RadioGroup value={selectedUrl} onChange={handleSelect}>
{subList.map((item, index) => (
<SubRulesItem
key={item.url}
url={item.url}
index={index}
selectedUrl={selectedSub?.url}
subrules={subrules}
setRules={setRules}
selectedUrl={selectedUrl}
delSub={delSub}
setSelectedRules={setSelectedRules}
/>
))}
</RadioGroup>
@@ -786,7 +788,9 @@ function SubRules() {
<CircularProgress />
</center>
) : (
rules.map((rule) => <RuleAccordion key={rule.pattern} rule={rule} />)
selectedRules.map((rule) => (
<RuleAccordion key={rule.pattern} rule={rule} />
))
)}
</Box>
</Stack>
@@ -796,6 +800,7 @@ function SubRules() {
export default function Rules() {
const i18n = useI18n();
const [activeTab, setActiveTab] = useState(0);
const subRules = useSubRules();
const handleTabChange = (e, newValue) => {
setActiveTab(newValue);
@@ -814,10 +819,16 @@ export default function Rules() {
<Tabs value={activeTab} onChange={handleTabChange}>
<Tab label={i18n("personal_rules")} />
<Tab label={i18n("subscribe_rules")} />
<Tab label={i18n("overwrite_subscribe_rules")} />
</Tabs>
</Box>
<div hidden={activeTab !== 0}>{activeTab === 0 && <UserRules />}</div>
<div hidden={activeTab !== 1}>{activeTab === 1 && <SubRules />}</div>
<div hidden={activeTab !== 0}>
{activeTab === 0 && <UserRules subRules={subRules} />}
</div>
<div hidden={activeTab !== 1}>
{activeTab === 1 && <SubRules subRules={subRules} />}
</div>
<div hidden={activeTab !== 2}>{activeTab === 2 && <OwSubRule />}</div>
</Stack>
</Box>
);

View File

@@ -5,47 +5,80 @@ import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
import { limitNumber, debounce } from "../../libs/utils";
import Link from "@mui/material/Link";
import FormHelperText from "@mui/material/FormHelperText";
import { useSetting } from "../../hooks/Setting";
import { limitNumber } from "../../libs/utils";
import { useI18n } from "../../hooks/I18n";
import { UI_LANGS } from "../../config";
import { useMemo } from "react";
import { apiTranslate } from "../../apis";
import { useAlert } from "../../hooks/Alert";
import {
UI_LANGS,
URL_KISS_PROXY,
TRANS_NEWLINE_LENGTH,
CACHE_NAME,
OPT_TRANS_GOOGLE,
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI,
} from "../../config";
export default function Settings() {
const i18n = useI18n();
const setting = useSetting();
const updateSetting = useSettingUpdate();
const { setting, updateSetting } = useSetting();
const alert = useAlert();
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]
);
const handleChange = (e) => {
e.preventDefault();
let { name, value } = e.target;
switch (name) {
case "fetchLimit":
value = limitNumber(value, 1, 100);
break;
case "fetchInterval":
value = limitNumber(value, 0, 5000);
break;
case "minLength":
value = limitNumber(value, 1, 100);
break;
case "maxLength":
value = limitNumber(value, 100, 10000);
break;
case "newlineLength":
value = limitNumber(value, 1, 1000);
break;
default:
}
updateSetting({
[name]: value,
});
};
if (!setting) {
return;
}
const handleClearCache = () => {
try {
caches.delete(CACHE_NAME);
alert.success(i18n("clear_success"));
} catch (err) {
console.log("[clear cache]", err);
}
};
const handleApiTest = async (translator) => {
try {
const [text] = await apiTranslate({
translator,
q: "hello world",
fromLang: "en",
toLang: "zh-CN",
setting,
});
if (!text) {
throw new Error("empty reault");
}
alert.success(i18n("test_success"));
} catch (err) {
alert.error(`${i18n("test_failed")}: ${err.message}`);
}
};
const {
uiLang,
@@ -55,10 +88,13 @@ export default function Settings() {
minLength,
maxLength,
openaiUrl,
deeplUrl = "",
deeplKey = "",
openaiKey,
openaiModel,
openaiPrompt,
clearCache,
newlineLength = TRANS_NEWLINE_LENGTH,
} = setting;
return (
@@ -85,7 +121,7 @@ export default function Settings() {
label={i18n("fetch_limit")}
type="number"
name="fetchLimit"
defaultValue={fetchLimit}
value={fetchLimit}
onChange={handleChange}
/>
@@ -94,7 +130,7 @@ export default function Settings() {
label={i18n("fetch_interval")}
type="number"
name="fetchInterval"
defaultValue={fetchInterval}
value={fetchInterval}
onChange={handleChange}
/>
@@ -103,7 +139,7 @@ export default function Settings() {
label={i18n("min_translate_length")}
type="number"
name="minLength"
defaultValue={minLength}
value={minLength}
onChange={handleChange}
/>
@@ -112,45 +148,126 @@ export default function Settings() {
label={i18n("max_translate_length")}
type="number"
name="maxLength"
defaultValue={maxLength}
value={maxLength}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("num_of_newline_characters")}
type="number"
name="newlineLength"
value={newlineLength}
onChange={handleChange}
/>
<FormControl size="small">
<InputLabel>{i18n("clear_cache")}</InputLabel>
<InputLabel>{i18n("if_clear_cache")}</InputLabel>
<Select
name="clearCache"
value={clearCache}
label={i18n("clear_cache")}
label={i18n("if_clear_cache")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
</Select>
<FormHelperText>
<Link component="button" onClick={handleClearCache}>
{i18n("clear_all_cache_now")}
</Link>
</FormHelperText>
</FormControl>
<TextField
size="small"
label={i18n("google_api")}
label={
<>
{i18n("google_api")}
{googleUrl && (
<Link
sx={{ marginLeft: "1em" }}
component="button"
onClick={() => {
handleApiTest(OPT_TRANS_GOOGLE);
}}
>
{i18n("click_test")}
</Link>
)}
</>
}
name="googleUrl"
defaultValue={googleUrl}
value={googleUrl}
onChange={handleChange}
helperText={
<Link href={URL_KISS_PROXY}>{i18n("about_api_proxy")}</Link>
}
/>
<TextField
size="small"
label={
<>
{i18n("deepl_api")}
{deeplUrl && (
<Link
sx={{ marginLeft: "1em" }}
component="button"
onClick={() => {
handleApiTest(OPT_TRANS_DEEPL);
}}
>
{i18n("click_test")}
</Link>
)}
</>
}
name="deeplUrl"
value={deeplUrl}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("openai_api")}
name="openaiUrl"
defaultValue={openaiUrl}
label={i18n("deepl_key")}
name="deeplKey"
value={deeplKey}
onChange={handleChange}
/>
<TextField
size="small"
label={
<>
{i18n("openai_api")}
{openaiUrl && openaiPrompt && (
<Link
sx={{ marginLeft: "1em" }}
component="button"
onClick={() => {
handleApiTest(OPT_TRANS_OPENAI);
}}
>
{i18n("click_test")}
</Link>
)}
</>
}
name="openaiUrl"
value={openaiUrl}
onChange={handleChange}
helperText={
<Link href={URL_KISS_PROXY}>{i18n("about_api_proxy")}</Link>
}
/>
<TextField
size="small"
type="password"
label={i18n("openai_key")}
name="openaiKey"
defaultValue={openaiKey}
value={openaiKey}
onChange={handleChange}
/>
@@ -158,7 +275,7 @@ export default function Settings() {
size="small"
label={i18n("openai_model")}
name="openaiModel"
defaultValue={openaiModel}
value={openaiModel}
onChange={handleChange}
/>
@@ -166,7 +283,7 @@ export default function Settings() {
size="small"
label={i18n("openai_prompt")}
name="openaiPrompt"
defaultValue={openaiPrompt}
value={openaiPrompt}
onChange={handleChange}
multiline
/>

View File

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

View File

@@ -4,20 +4,22 @@ import Rules from "./Rules";
import Setting from "./Setting";
import Layout from "./Layout";
import SyncSetting from "./SyncSetting";
import { StoragesProvider } from "../../hooks/Storage";
import { SettingProvider } from "../../hooks/Setting";
import ThemeProvider from "../../hooks/Theme";
import { useEffect, useState } from "react";
import { isGm } from "../../libs/browser";
import { isGm } from "../../libs/client";
import { sleep } from "../../libs/utils";
import CircularProgress from "@mui/material/CircularProgress";
import { trySyncAll } from "../../libs/sync";
import { trySyncSettingAndRules } from "../../libs/sync";
import { AlertProvider } from "../../hooks/Alert";
import Link from "@mui/material/Link";
import Divider from "@mui/material/Divider";
import Stack from "@mui/material/Stack";
import { adaptScript } from "../../libs/gm";
import Alert from "@mui/material/Alert";
export default function Options() {
const [error, setError] = useState(false);
const [error, setError] = useState("");
const [ready, setReady] = useState(false);
useEffect(() => {
@@ -26,28 +28,47 @@ export default function Options() {
// 等待GM注入
let i = 0;
for (;;) {
if (window.APP_NAME === process.env.REACT_APP_NAME) {
if (window?.APP_INFO?.name === process.env.REACT_APP_NAME) {
const { version, eventName } = window.APP_INFO;
// 检查版本是否一致
if (version !== process.env.REACT_APP_VERSION) {
setError(
`The version of the script(v${version}) and this page(v${process.env.REACT_APP_VERSION}) are inconsistent.`
);
break;
}
if (eventName) {
// 注入GM接口
adaptScript(eventName);
}
// 同步数据
await trySyncSettingAndRules();
setReady(true);
break;
}
if (++i > 8) {
setError(true);
setError("Time out.");
break;
}
await sleep(1000);
}
} else {
// 同步数据
await trySyncSettingAndRules();
setReady(true);
}
// 同步数据
trySyncAll();
})();
}, []);
if (error) {
return (
<center>
<Alert severity="error">{error}</Alert>
<Divider>
<Link
href={process.env.REACT_APP_HOMEPAGE}
@@ -64,6 +85,12 @@ export default function Options() {
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install Userscript 2
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install Userscript Safari 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install Userscript Safari 2
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
Open Options Page 1
</Link>
@@ -75,7 +102,7 @@ export default function Options() {
);
}
if (isGm && !ready) {
if (!ready) {
return (
<center>
<Divider>
@@ -89,7 +116,7 @@ export default function Options() {
}
return (
<StoragesProvider>
<SettingProvider>
<ThemeProvider>
<AlertProvider>
<HashRouter>
@@ -104,6 +131,6 @@ export default function Options() {
</HashRouter>
</AlertProvider>
</ThemeProvider>
</StoragesProvider>
</SettingProvider>
);
}

View File

@@ -6,7 +6,8 @@ import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Button from "@mui/material/Button";
import { sendTabMsg } from "../../libs/msg";
import { browser, isExt } from "../../libs/browser";
import { browser } from "../../libs/browser";
import { isExt } from "../../libs/client";
import { useI18n } from "../../hooks/I18n";
import TextField from "@mui/material/TextField";
import {
@@ -17,6 +18,8 @@ import {
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_STYLE_ALL,
OPT_STYLE_USE_COLOR,
CACHE_NAME,
} from "../../config";
import { sendIframeMsg } from "../../libs/iframe";
@@ -64,6 +67,14 @@ export default function Popup({ setShowPopup, translator: tran }) {
}
};
const handleClearCache = () => {
try {
caches.delete(CACHE_NAME);
} catch (err) {
console.log("[clear cache]", err);
}
};
useEffect(() => {
if (!isExt) {
return;
@@ -97,15 +108,27 @@ export default function Popup({ setShowPopup, translator: tran }) {
return (
<Box minWidth={300} sx={{ p: 2 }}>
<Stack spacing={2}>
<FormControlLabel
control={
<Switch
checked={transOpen === "true"}
onChange={handleTransToggle}
/>
}
label={i18n("translate_alt")}
/>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<FormControlLabel
control={
<Switch
checked={transOpen === "true"}
onChange={handleTransToggle}
/>
}
label={i18n("translate_alt")}
/>
{!isExt && (
<Button variant="text" onClick={handleClearCache}>
{i18n("clear_cache")}
</Button>
)}
</Stack>
<TextField
select
@@ -171,13 +194,15 @@ export default function Popup({ setShowPopup, translator: tran }) {
))}
</TextField>
<TextField
size="small"
name="bgColor"
value={bgColor}
label={i18n("bg_color")}
onChange={handleChange}
/>
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
<TextField
size="small"
name="bgColor"
value={bgColor}
label={i18n("bg_color")}
onChange={handleChange}
/>
)}
<Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")}

979
yarn.lock

File diff suppressed because it is too large Load Diff