Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0b7c60099 | ||
|
|
536b58bf67 | ||
|
|
6bb742f828 | ||
|
|
72742e5e12 | ||
|
|
3667e0a509 | ||
|
|
c2d7668ba7 | ||
|
|
aa830f5e20 | ||
|
|
b593fa4146 | ||
|
|
b00b906484 | ||
|
|
c1bd6a1be6 | ||
|
|
36739f04b3 | ||
|
|
23eb92853e | ||
|
|
5ab2910dc7 | ||
|
|
40d07f6764 | ||
|
|
5c8e216169 | ||
|
|
5ba061deda | ||
|
|
935c83185d | ||
|
|
6327391e65 | ||
|
|
3d656cf5b0 | ||
|
|
d570a0f1a2 | ||
|
|
503a71302c | ||
|
|
3e36ceb5b9 | ||
|
|
cde7a1d49f | ||
|
|
b14a38e4fb | ||
|
|
732a526a8e | ||
|
|
2da5ffef44 | ||
|
|
2e6e52004f | ||
|
|
4486ad353c | ||
|
|
aa795e2731 | ||
|
|
c46fe7d1c6 | ||
|
|
d7cee8cca6 | ||
|
|
11f790ace5 |
5
.env
5
.env
@@ -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.4
|
||||
|
||||
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
|
||||
|
||||
31
README.en.md
31
README.en.md
@@ -1,10 +1,10 @@
|
||||
## KISS Translator
|
||||
# KISS Translator
|
||||
|
||||
A minimalist [bilingual translation Extension & Greasemonkey Script](https://github.com/fishjar/kiss-translator).
|
||||
|
||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||
|
||||
### Inspiration
|
||||
## Inspiration
|
||||
|
||||
The inspiration for this project comes from [Immersive Translate](https://github.com/immersive-translate/immersive-translate). After trying it out, I found that it can be used together with the [Webpage Word Translation Extension](https://github.com/fishjar/kiss-dictionary) developed by me earlier, which just forms a very good supplement.
|
||||
|
||||
@@ -14,11 +14,11 @@ It just so happens that I am obsessed with translation tools. Based on the conce
|
||||
|
||||
If you also like a little more simplicity, welcome to pick it up.
|
||||
|
||||
### Features
|
||||
## Features
|
||||
|
||||
- Keep it simple, smart
|
||||
|
||||
### Schedule
|
||||
## Schedule
|
||||
|
||||
- [x] Provide trial installation package
|
||||
- [x] Adapt browser
|
||||
@@ -31,20 +31,21 @@ If you also like a little more simplicity, welcome to pick it up.
|
||||
- [x] Google
|
||||
- [x] Microsoft
|
||||
- [x] OpenAI
|
||||
- [ ] DeepL
|
||||
- [x] 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 +54,10 @@ yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Data Sync
|
||||
## Data Sync
|
||||
|
||||
Goto: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||
|
||||
### Discussion
|
||||
## Discussion
|
||||
|
||||
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||
|
||||
31
README.md
31
README.md
@@ -1,10 +1,10 @@
|
||||
## 简约翻译
|
||||
# 简约翻译
|
||||
|
||||
一个简约的 [双语网页翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
||||
|
||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||
|
||||
### 缘由
|
||||
## 缘由
|
||||
|
||||
本项目灵感来源于 [Immersive Translate](https://github.com/immersive-translate/immersive-translate),在试用了后,发现搭配本人早前开发的 [网页划词翻译扩展](https://github.com/fishjar/kiss-dictionary) 一起使用,刚好形成很好补充。
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
|
||||
如果你也喜欢简约一点的,欢迎自取。
|
||||
|
||||
### 特点
|
||||
## 特点
|
||||
|
||||
- 保持简约
|
||||
|
||||
### 进度
|
||||
## 进度
|
||||
|
||||
- [x] 提供试用安装包
|
||||
- [x] 适配浏览器
|
||||
@@ -31,20 +31,21 @@
|
||||
- [x] Google
|
||||
- [x] Microsoft
|
||||
- [x] OpenAI
|
||||
- [ ] DeepL
|
||||
- [x] 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 +54,10 @@ yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 数据同步
|
||||
## 数据同步
|
||||
|
||||
移步: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||
|
||||
### 交流
|
||||
## 交流
|
||||
|
||||
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||
|
||||
@@ -89,7 +89,7 @@ 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
|
||||
|
||||
12
package.json
12
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kiss-translator",
|
||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||
"version": "1.5.8",
|
||||
"version": "1.6.4",
|
||||
"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": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.5.8",
|
||||
"version": "1.6.4",
|
||||
"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"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.5.8",
|
||||
"version": "1.6.4",
|
||||
"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"
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
4
src/config/app.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const APP_NAME = process.env.REACT_APP_NAME.trim()
|
||||
.split(/\s+/)
|
||||
.join("-");
|
||||
export const APP_LCNAME = APP_NAME.toLowerCase();
|
||||
@@ -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`,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { useSetting, useSettingUpdate } from "./Setting";
|
||||
import { useCallback } from "react";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
/**
|
||||
* 深色模式hook
|
||||
* @returns
|
||||
*/
|
||||
export function useDarkMode() {
|
||||
const setting = useSetting();
|
||||
return !!setting?.darkMode;
|
||||
}
|
||||
const {
|
||||
setting: { darkMode },
|
||||
updateSetting,
|
||||
} = useSetting();
|
||||
|
||||
/**
|
||||
* 切换深色模式
|
||||
* @returns
|
||||
*/
|
||||
export function useDarkModeSwitch() {
|
||||
const darkMode = useDarkMode();
|
||||
const updateSetting = useSettingUpdate();
|
||||
return async () => {
|
||||
const toggleDarkMode = useCallback(async () => {
|
||||
await updateSetting({ darkMode: !darkMode });
|
||||
};
|
||||
}, [darkMode, updateSetting]);
|
||||
|
||||
return { darkMode, toggleDarkMode };
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import { useFetch } from "./Fetch";
|
||||
* @returns
|
||||
*/
|
||||
export const useI18n = () => {
|
||||
const { uiLang } = useSetting() ?? {};
|
||||
const {
|
||||
setting: { uiLang },
|
||||
} = useSetting();
|
||||
return (key, defaultText = "") => I18N?.[key]?.[uiLang] ?? defaultText;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
99
src/hooks/SubRules.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { DEFAULT_SUBRULES_LIST, DEFAULT_OW_RULE } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { loadOrFetchSubRules } from "../libs/subRules";
|
||||
import { delSubRules } from "../libs/storage";
|
||||
|
||||
/**
|
||||
* 订阅规则
|
||||
* @returns
|
||||
*/
|
||||
export function useSubRules() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRules, setSelectedRules] = useState([]);
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const list = setting?.subrulesList || DEFAULT_SUBRULES_LIST;
|
||||
|
||||
const selectedSub = useMemo(() => list.find((item) => item.selected), [list]);
|
||||
const selectedUrl = selectedSub.url;
|
||||
|
||||
const selectSub = useCallback(
|
||||
async (url) => {
|
||||
const subrulesList = [...list];
|
||||
subrulesList.forEach((item) => {
|
||||
if (item.url === url) {
|
||||
item.selected = true;
|
||||
} else {
|
||||
item.selected = false;
|
||||
}
|
||||
});
|
||||
await updateSetting({ subrulesList });
|
||||
},
|
||||
[list, updateSetting]
|
||||
);
|
||||
|
||||
const addSub = useCallback(
|
||||
async (url) => {
|
||||
const subrulesList = [...list];
|
||||
subrulesList.push({ url, selected: false });
|
||||
await updateSetting({ subrulesList });
|
||||
},
|
||||
[list, updateSetting]
|
||||
);
|
||||
|
||||
const delSub = useCallback(
|
||||
async (url) => {
|
||||
let subrulesList = [...list];
|
||||
subrulesList = subrulesList.filter((item) => item.url !== url);
|
||||
await updateSetting({ subrulesList });
|
||||
await delSubRules(url);
|
||||
},
|
||||
[list, updateSetting]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (selectedUrl) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const rules = await loadOrFetchSubRules(selectedUrl);
|
||||
setSelectedRules(rules);
|
||||
} catch (err) {
|
||||
console.log("[loadOrFetchSubRules]", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [selectedUrl]);
|
||||
|
||||
return {
|
||||
subList: list,
|
||||
selectSub,
|
||||
addSub,
|
||||
delSub,
|
||||
selectedSub,
|
||||
selectedUrl,
|
||||
selectedRules,
|
||||
setSelectedRules,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆写订阅规则
|
||||
* @returns
|
||||
*/
|
||||
export function useOwSubRule() {
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const { owSubrule = DEFAULT_OW_RULE } = setting;
|
||||
|
||||
const updateOwSubrule = useCallback(
|
||||
async (obj) => {
|
||||
await updateSetting({ owSubrule: { ...owSubrule, ...obj } });
|
||||
},
|
||||
[owSubrule, updateSetting]
|
||||
);
|
||||
|
||||
return { owSubrule, updateOwSubrule };
|
||||
}
|
||||
@@ -1,20 +1,11 @@
|
||||
import { useCallback } from "react";
|
||||
import { STOKEY_SYNC } from "../config";
|
||||
import storage from "../libs/storage";
|
||||
import { useStorages } from "./Storage";
|
||||
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
|
||||
/**
|
||||
* sync hook
|
||||
* @returns
|
||||
*/
|
||||
export function useSync() {
|
||||
const storages = useStorages();
|
||||
const opt = storages?.[STOKEY_SYNC];
|
||||
const update = useCallback(async (obj) => {
|
||||
await storage.putObj(STOKEY_SYNC, obj);
|
||||
}, []);
|
||||
return {
|
||||
opt,
|
||||
update,
|
||||
};
|
||||
const { data, update } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
|
||||
return { sync: data, updateSync: update };
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import { THEME_DARK, THEME_LIGHT } from "../config";
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function MuiThemeProvider({ children, options }) {
|
||||
const darkMode = useDarkMode();
|
||||
export default function Theme({ children, options }) {
|
||||
const { darkMode } = useDarkMode();
|
||||
const theme = useMemo(() => {
|
||||
return createTheme({
|
||||
palette: {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { detectLang } from "../libs";
|
||||
import { tryDetectLang } from "../libs";
|
||||
import { apiTranslate } from "../apis";
|
||||
|
||||
/**
|
||||
* 翻译hook
|
||||
* @param {*} q
|
||||
* @param {*} rule
|
||||
* @param {*} setting
|
||||
* @returns
|
||||
*/
|
||||
export function useTranslate(q, rule) {
|
||||
export function useTranslate(q, rule, setting) {
|
||||
const [text, setText] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sameLang, setSamelang] = useState(false);
|
||||
@@ -21,8 +22,8 @@ export function useTranslate(q, rule) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const deLang = await detectLang(q);
|
||||
if (toLang.includes(deLang)) {
|
||||
const deLang = await tryDetectLang(q);
|
||||
if (deLang && toLang.includes(deLang)) {
|
||||
setSamelang(true);
|
||||
} else {
|
||||
const [trText, isSame] = await apiTranslate({
|
||||
@@ -30,6 +31,7 @@ export function useTranslate(q, rule) {
|
||||
q,
|
||||
fromLang,
|
||||
toLang,
|
||||
setting,
|
||||
});
|
||||
setText(trText);
|
||||
setSamelang(isSame);
|
||||
@@ -40,7 +42,7 @@ export function useTranslate(q, rule) {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [q, translator, fromLang, toLang]);
|
||||
}, [q, translator, fromLang, toLang, setting]);
|
||||
|
||||
return { text, sameLang, loading };
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import storage from "./storage";
|
||||
import { STOKEY_MSAUTH, URL_MICROSOFT_AUTH } from "../config";
|
||||
import { getMsauth, setMsauth } from "./storage";
|
||||
import { URL_MICROSOFT_AUTH } from "../config";
|
||||
import { fetchData } from "./fetch";
|
||||
|
||||
const parseMSToken = (token) => {
|
||||
@@ -26,9 +26,9 @@ const _msAuth = () => {
|
||||
}
|
||||
|
||||
// 查询storage缓存
|
||||
const res = (await storage.getObj(STOKEY_MSAUTH)) || {};
|
||||
token = res.token;
|
||||
exp = res.exp;
|
||||
const res = await getMsauth();
|
||||
token = res?.token;
|
||||
exp = res?.exp;
|
||||
if (token && exp * 1000 > now + 1000) {
|
||||
return [token, exp];
|
||||
}
|
||||
@@ -36,7 +36,7 @@ const _msAuth = () => {
|
||||
// 缓存没有或失效,查询接口
|
||||
token = await fetchData(URL_MICROSOFT_AUTH);
|
||||
exp = parseMSToken(token);
|
||||
await storage.setObj(STOKEY_MSAUTH, { token, exp });
|
||||
await setMsauth({ token, exp });
|
||||
return [token, exp];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
|
||||
// import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
|
||||
|
||||
/**
|
||||
* 浏览器兼容插件,另可用于判断是插件模式还是网页模式,方便开发
|
||||
@@ -13,7 +13,3 @@ function _browser() {
|
||||
}
|
||||
|
||||
export const browser = _browser();
|
||||
export const client = process.env.REACT_APP_CLIENT;
|
||||
export const isExt = CLIENT_EXTS.includes(client);
|
||||
export const isGm = client === CLIENT_USERSCRIPT;
|
||||
export const isWeb = client === CLIENT_WEB;
|
||||
|
||||
6
src/libs/client.js
Normal file
6
src/libs/client.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
|
||||
|
||||
export const client = process.env.REACT_APP_CLIENT;
|
||||
export const isExt = CLIENT_EXTS.includes(client);
|
||||
export const isGm = client === CLIENT_USERSCRIPT;
|
||||
export const isWeb = client === CLIENT_WEB;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isExt, isGm } from "./browser";
|
||||
import { sendMsg } from "./msg";
|
||||
import { isExt, isGm } from "./client";
|
||||
import { sendBgMsg } from "./msg";
|
||||
import { taskPool } from "./pool";
|
||||
import {
|
||||
MSG_FETCH,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MSG_FETCH_CLEAR,
|
||||
CACHE_NAME,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
DEFAULT_FETCH_INTERVAL,
|
||||
DEFAULT_FETCH_LIMIT,
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -121,15 +124,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 +151,10 @@ export const fetchData = async (
|
||||
// 插入缓存
|
||||
if (useCache) {
|
||||
try {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
await cache.put(cacheReq, res.clone());
|
||||
} catch (err) {
|
||||
console.log("[cache put]", err);
|
||||
console.log("[cache put]", err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,7 +175,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 +191,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 +205,9 @@ export const fetchUpdate = async (interval, limit) => {
|
||||
/**
|
||||
* 清空任务池
|
||||
*/
|
||||
export const fetchClear = async () => {
|
||||
export const clearFetchPool = async () => {
|
||||
if (isExt) {
|
||||
const res = await sendMsg(MSG_FETCH_CLEAR);
|
||||
const res = await sendBgMsg(MSG_FETCH_CLEAR);
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,8 +6,8 @@ import { browser } from "./browser";
|
||||
* @param {*} args
|
||||
* @returns
|
||||
*/
|
||||
export const sendMsg = (action, args) =>
|
||||
browser?.runtime?.sendMessage({ action, args });
|
||||
export const sendBgMsg = (action, args) =>
|
||||
browser.runtime.sendMessage({ action, args });
|
||||
|
||||
/**
|
||||
* 发送消息给当前页面
|
||||
@@ -16,6 +16,6 @@ export const sendMsg = (action, args) =>
|
||||
* @returns
|
||||
*/
|
||||
export const sendTabMsg = async (action, args) => {
|
||||
const tabs = await browser?.tabs.query({ active: true, currentWindow: true });
|
||||
return await browser?.tabs.sendMessage(tabs[0].id, { action, args });
|
||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
return browser.tabs.sendMessage(tabs[0].id, { action, args });
|
||||
};
|
||||
|
||||
@@ -1,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);
|
||||
};
|
||||
|
||||
@@ -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
72
src/libs/subRules.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { GLOBAL_KEY } from "../config";
|
||||
import {
|
||||
getSyncWithDefault,
|
||||
updateSync,
|
||||
setSubRules,
|
||||
getSubRules,
|
||||
} from "./storage";
|
||||
import { apiFetchRules } from "../apis";
|
||||
import { checkRules } from "./rules";
|
||||
|
||||
/**
|
||||
* 同步订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const syncSubRules = async (url, isBg = false) => {
|
||||
const res = await apiFetchRules(url, isBg);
|
||||
const rules = checkRules(res).filter(
|
||||
(rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== ""
|
||||
);
|
||||
if (rules.length > 0) {
|
||||
await setSubRules(url, rules);
|
||||
}
|
||||
return rules;
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步所有订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const syncAllSubRules = async (subrulesList, isBg = false) => {
|
||||
for (let subrules of subrulesList) {
|
||||
try {
|
||||
await syncSubRules(subrules.url, isBg);
|
||||
} catch (err) {
|
||||
console.log(`[sync subrule error]: ${subrules.url}`, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据时间同步所有订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
|
||||
try {
|
||||
const { subRulesSyncAt } = await getSyncWithDefault();
|
||||
const now = Date.now();
|
||||
const interval = 24 * 60 * 60 * 1000; // 间隔一天
|
||||
if (now - subRulesSyncAt > interval) {
|
||||
await syncAllSubRules(subrulesList, isBg);
|
||||
await updateSync({ subRulesSyncAt: now });
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[try sync all subrules]", err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从缓存或远程加载订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const loadOrFetchSubRules = async (url) => {
|
||||
const rules = await getSubRules(url);
|
||||
if (rules?.length) {
|
||||
return rules;
|
||||
}
|
||||
return syncSubRules(url);
|
||||
};
|
||||
@@ -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)];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
175
src/views/Options/OwSubRule.js
Normal file
175
src/views/Options/OwSubRule.js
Normal file
@@ -0,0 +1,175 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_STYLE_DIY,
|
||||
OPT_STYLE_USE_COLOR,
|
||||
} from "../../config";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { useOwSubRule } from "../../hooks/SubRules";
|
||||
|
||||
export default function OwSubRule() {
|
||||
const i18n = useI18n();
|
||||
const { owSubrule, updateOwSubrule } = useOwSubRule();
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
const { name, value } = e.target;
|
||||
updateOwSubrule({ [name]: value });
|
||||
};
|
||||
|
||||
const {
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
} = owSubrule;
|
||||
|
||||
const RemainItem = (
|
||||
<MenuItem key={REMAIN_KEY} value={REMAIN_KEY}>
|
||||
{i18n("remain_unchanged")}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const GlobalItem = (
|
||||
<MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}>
|
||||
{GLOBAL_KEY}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={12}>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transOpen"
|
||||
value={transOpen}
|
||||
label={i18n("translate_switch")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
<MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem>
|
||||
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="translator"
|
||||
value={translator}
|
||||
label={i18n("translate_service")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_TRANS_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="fromLang"
|
||||
value={fromLang}
|
||||
label={i18n("from_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="toLang"
|
||||
value={toLang}
|
||||
label={i18n("to_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="textStyle"
|
||||
value={textStyle}
|
||||
label={i18n("text_style")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{RemainItem}
|
||||
{GlobalItem}
|
||||
{OPT_STYLE_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
name="bgColor"
|
||||
value={bgColor}
|
||||
label={i18n("bg_color")}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{textStyle === OPT_STYLE_DIY && (
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("diy_style")}
|
||||
helperText={i18n("diy_style_helper")}
|
||||
name="textDiyStyle"
|
||||
value={textDiyStyle}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -5,47 +5,41 @@ import TextField from "@mui/material/TextField";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import Select from "@mui/material/Select";
|
||||
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
|
||||
import { limitNumber, debounce } from "../../libs/utils";
|
||||
import Link from "@mui/material/Link";
|
||||
import { useSetting } from "../../hooks/Setting";
|
||||
import { limitNumber } from "../../libs/utils";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import { UI_LANGS } from "../../config";
|
||||
import { useMemo } from "react";
|
||||
import { UI_LANGS, URL_KISS_PROXY, TRANS_NEWLINE_LENGTH } from "../../config";
|
||||
|
||||
export default function Settings() {
|
||||
const i18n = useI18n();
|
||||
const setting = useSetting();
|
||||
const updateSetting = useSettingUpdate();
|
||||
const { setting, updateSetting } = useSetting();
|
||||
|
||||
const handleChange = useMemo(
|
||||
() =>
|
||||
debounce((e) => {
|
||||
e.preventDefault();
|
||||
let { name, value } = e.target;
|
||||
switch (name) {
|
||||
case "fetchLimit":
|
||||
value = limitNumber(value, 1, 100);
|
||||
break;
|
||||
case "fetchInterval":
|
||||
value = limitNumber(value, 0, 5000);
|
||||
break;
|
||||
case "minLength":
|
||||
value = limitNumber(value, 1, 100);
|
||||
break;
|
||||
case "maxLength":
|
||||
value = limitNumber(value, 100, 10000);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
updateSetting({
|
||||
[name]: value,
|
||||
});
|
||||
}, 500),
|
||||
[updateSetting]
|
||||
);
|
||||
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
uiLang,
|
||||
@@ -55,10 +49,13 @@ export default function Settings() {
|
||||
minLength,
|
||||
maxLength,
|
||||
openaiUrl,
|
||||
deeplUrl = "",
|
||||
deeplKey = "",
|
||||
openaiKey,
|
||||
openaiModel,
|
||||
openaiPrompt,
|
||||
clearCache,
|
||||
newlineLength = TRANS_NEWLINE_LENGTH,
|
||||
} = setting;
|
||||
|
||||
return (
|
||||
@@ -85,7 +82,7 @@ export default function Settings() {
|
||||
label={i18n("fetch_limit")}
|
||||
type="number"
|
||||
name="fetchLimit"
|
||||
defaultValue={fetchLimit}
|
||||
value={fetchLimit}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -94,7 +91,7 @@ export default function Settings() {
|
||||
label={i18n("fetch_interval")}
|
||||
type="number"
|
||||
name="fetchInterval"
|
||||
defaultValue={fetchInterval}
|
||||
value={fetchInterval}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -103,7 +100,7 @@ export default function Settings() {
|
||||
label={i18n("min_translate_length")}
|
||||
type="number"
|
||||
name="minLength"
|
||||
defaultValue={minLength}
|
||||
value={minLength}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -112,7 +109,16 @@ 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}
|
||||
/>
|
||||
|
||||
@@ -133,7 +139,26 @@ export default function Settings() {
|
||||
size="small"
|
||||
label={i18n("google_api")}
|
||||
name="googleUrl"
|
||||
defaultValue={googleUrl}
|
||||
value={googleUrl}
|
||||
onChange={handleChange}
|
||||
helperText={
|
||||
<Link href={URL_KISS_PROXY}>{i18n("about_api_proxy")}</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("deepl_api")}
|
||||
name="deeplUrl"
|
||||
value={deeplUrl}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("deepl_key")}
|
||||
name="deeplKey"
|
||||
value={deeplKey}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -141,8 +166,11 @@ export default function Settings() {
|
||||
size="small"
|
||||
label={i18n("openai_api")}
|
||||
name="openaiUrl"
|
||||
defaultValue={openaiUrl}
|
||||
value={openaiUrl}
|
||||
onChange={handleChange}
|
||||
helperText={
|
||||
<Link href={URL_KISS_PROXY}>{i18n("about_api_proxy")}</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
@@ -150,7 +178,7 @@ export default function Settings() {
|
||||
type="password"
|
||||
label={i18n("openai_key")}
|
||||
name="openaiKey"
|
||||
defaultValue={openaiKey}
|
||||
value={openaiKey}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -158,7 +186,7 @@ export default function Settings() {
|
||||
size="small"
|
||||
label={i18n("openai_model")}
|
||||
name="openaiModel"
|
||||
defaultValue={openaiModel}
|
||||
value={openaiModel}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -166,7 +194,7 @@ export default function Settings() {
|
||||
size="small"
|
||||
label={i18n("openai_prompt")}
|
||||
name="openaiPrompt"
|
||||
defaultValue={openaiPrompt}
|
||||
value={openaiPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
/>
|
||||
|
||||
@@ -6,38 +6,35 @@ 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();
|
||||
await syncSettingAndRules();
|
||||
await reloadSetting();
|
||||
alert.success(i18n("data_sync_success"));
|
||||
} catch (err) {
|
||||
console.log("[sync all]", err);
|
||||
@@ -47,11 +44,7 @@ export default function SyncSetting() {
|
||||
}
|
||||
};
|
||||
|
||||
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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,7 @@ import {
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_STYLE_USE_COLOR,
|
||||
} from "../../config";
|
||||
import { sendIframeMsg } from "../../libs/iframe";
|
||||
|
||||
@@ -171,13 +173,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")}
|
||||
|
||||
Reference in New Issue
Block a user