Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2d7668ba7 | ||
|
|
aa830f5e20 | ||
|
|
b593fa4146 | ||
|
|
b00b906484 | ||
|
|
c1bd6a1be6 | ||
|
|
36739f04b3 | ||
|
|
23eb92853e |
2
.env
2
.env
@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
|
|||||||
|
|
||||||
REACT_APP_NAME=KISS Translator
|
REACT_APP_NAME=KISS Translator
|
||||||
REACT_APP_NAME_CN=简约翻译
|
REACT_APP_NAME_CN=简约翻译
|
||||||
REACT_APP_VERSION=1.6.1
|
REACT_APP_VERSION=1.6.2
|
||||||
|
|
||||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||||
|
|
||||||
|
|||||||
15
README.en.md
15
README.en.md
@@ -1,10 +1,10 @@
|
|||||||
## KISS Translator
|
# KISS Translator
|
||||||
|
|
||||||
A minimalist [bilingual translation Extension & Greasemonkey Script](https://github.com/fishjar/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)
|
[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.
|
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.
|
If you also like a little more simplicity, welcome to pick it up.
|
||||||
|
|
||||||
### Features
|
## Features
|
||||||
|
|
||||||
- Keep it simple, smart
|
- Keep it simple, smart
|
||||||
|
|
||||||
### Schedule
|
## Schedule
|
||||||
|
|
||||||
- [x] Provide trial installation package
|
- [x] Provide trial installation package
|
||||||
- [x] Adapt browser
|
- [x] Adapt browser
|
||||||
@@ -42,9 +42,10 @@ If you also like a little more simplicity, welcome to pick it up.
|
|||||||
- [x] Data Synchronization Function
|
- [x] Data Synchronization Function
|
||||||
- [x] Greasemonkey Script ([Setting Page 1](https://fishjar.github.io/kiss-translator/options.html)、[Setting Page 2](https://kiss-translator.rayjar.com/options))
|
- [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] [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)
|
- [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
|
```sh
|
||||||
git clone https://github.com/fishjar/kiss-translator.git
|
git clone https://github.com/fishjar/kiss-translator.git
|
||||||
@@ -53,10 +54,10 @@ yarn install
|
|||||||
yarn build
|
yarn build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Data Sync
|
## Data Sync
|
||||||
|
|
||||||
Goto: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
Goto: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||||
|
|
||||||
### Discussion
|
## Discussion
|
||||||
|
|
||||||
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
|
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -1,10 +1,10 @@
|
|||||||
## 简约翻译
|
# 简约翻译
|
||||||
|
|
||||||
一个简约的 [双语网页翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
一个简约的 [双语网页翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
||||||
|
|
||||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
[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) 一起使用,刚好形成很好补充。
|
本项目灵感来源于 [Immersive Translate](https://github.com/immersive-translate/immersive-translate),在试用了后,发现搭配本人早前开发的 [网页划词翻译扩展](https://github.com/fishjar/kiss-dictionary) 一起使用,刚好形成很好补充。
|
||||||
|
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
|
|
||||||
如果你也喜欢简约一点的,欢迎自取。
|
如果你也喜欢简约一点的,欢迎自取。
|
||||||
|
|
||||||
### 特点
|
## 特点
|
||||||
|
|
||||||
- 保持简约
|
- 保持简约
|
||||||
|
|
||||||
### 进度
|
## 进度
|
||||||
|
|
||||||
- [x] 提供试用安装包
|
- [x] 提供试用安装包
|
||||||
- [x] 适配浏览器
|
- [x] 适配浏览器
|
||||||
@@ -42,9 +42,10 @@
|
|||||||
- [x] 数据同步功能
|
- [x] 数据同步功能
|
||||||
- [x] 油猴脚本 ([设置页面 1](https://fishjar.github.io/kiss-translator/options.html)、[设置页面 2](https://kiss-translator.rayjar.com/options))
|
- [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] [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)
|
- [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
|
```sh
|
||||||
git clone https://github.com/fishjar/kiss-translator.git
|
git clone https://github.com/fishjar/kiss-translator.git
|
||||||
@@ -53,10 +54,10 @@ yarn install
|
|||||||
yarn build
|
yarn build
|
||||||
```
|
```
|
||||||
|
|
||||||
### 数据同步
|
## 数据同步
|
||||||
|
|
||||||
移步: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
移步: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||||
|
|
||||||
### 交流
|
## 交流
|
||||||
|
|
||||||
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
|
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kiss-translator",
|
"name": "kiss-translator",
|
||||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||||
"version": "1.6.1",
|
"version": "1.6.2",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"@emotion/styled": "^11.10.8",
|
"@emotion/styled": "^11.10.8",
|
||||||
"@mui/icons-material": "^5.11.11",
|
"@mui/icons-material": "^5.11.11",
|
||||||
"@mui/material": "^5.11.12",
|
"@mui/material": "^5.11.12",
|
||||||
|
"@violentmonkey/shortcut": "^1.3.0",
|
||||||
"query-string": "^8.1.0",
|
"query-string": "^8.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "1.6.1",
|
"version": "1.6.2",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
@@ -17,6 +17,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"commands": {
|
"commands": {
|
||||||
|
"_execute_browser_action": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+K"
|
||||||
|
}
|
||||||
|
},
|
||||||
"toggleTranslate": {
|
"toggleTranslate": {
|
||||||
"suggested_key": {
|
"suggested_key": {
|
||||||
"default": "Alt+Q"
|
"default": "Alt+Q"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "1.6.1",
|
"version": "1.6.2",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
@@ -18,6 +18,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"commands": {
|
"commands": {
|
||||||
|
"_execute_action": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+K"
|
||||||
|
}
|
||||||
|
},
|
||||||
"toggleTranslate": {
|
"toggleTranslate": {
|
||||||
"suggested_key": {
|
"suggested_key": {
|
||||||
"default": "Alt+Q"
|
"default": "Alt+Q"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { fetchPolyfill } from "../libs/fetch";
|
|||||||
import {
|
import {
|
||||||
OPT_TRANS_GOOGLE,
|
OPT_TRANS_GOOGLE,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_DEEPL,
|
||||||
OPT_TRANS_OPENAI,
|
OPT_TRANS_OPENAI,
|
||||||
URL_MICROSOFT_TRANS,
|
URL_MICROSOFT_TRANS,
|
||||||
OPT_LANGS_SPECIAL,
|
OPT_LANGS_SPECIAL,
|
||||||
@@ -95,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 翻译
|
* OpenAI 翻译
|
||||||
* @param {*} text
|
* @param {*} text
|
||||||
@@ -160,6 +191,10 @@ export const apiTranslate = async ({
|
|||||||
const res = await apiMicrosoftTranslate(translator, q, to, from);
|
const res = await apiMicrosoftTranslate(translator, q, to, from);
|
||||||
trText = res[0].translations[0].text;
|
trText = res[0].translations[0].text;
|
||||||
isSame = to === res[0].detectedLanguage.language;
|
isSame = to === res[0].detectedLanguage.language;
|
||||||
|
} else if (translator === OPT_TRANS_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) {
|
} else if (translator === OPT_TRANS_OPENAI) {
|
||||||
const res = await apiOpenaiTranslate(translator, q, to, from, setting);
|
const res = await apiOpenaiTranslate(translator, q, to, from, setting);
|
||||||
trText = res?.choices?.[0].message.content;
|
trText = res?.choices?.[0].message.content;
|
||||||
|
|||||||
@@ -260,6 +260,14 @@ export const I18N = {
|
|||||||
zh: `请检查url地址是否正确或稍后再试。`,
|
zh: `请检查url地址是否正确或稍后再试。`,
|
||||||
en: `Please check if the url address is correct or try again later.`,
|
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: {
|
openai_api: {
|
||||||
zh: `OpenAI 接口`,
|
zh: `OpenAI 接口`,
|
||||||
en: `OpenAI API`,
|
en: `OpenAI API`,
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ export const MSG_TRANS_GETRULE = "trans_getrule";
|
|||||||
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
||||||
export const MSG_TRANS_CURRULE = "trans_currule";
|
export const MSG_TRANS_CURRULE = "trans_currule";
|
||||||
|
|
||||||
export const EVENT_KISS = "kissEvent";
|
|
||||||
|
|
||||||
export const THEME_LIGHT = "light";
|
export const THEME_LIGHT = "light";
|
||||||
export const THEME_DARK = "dark";
|
export const THEME_DARK = "dark";
|
||||||
|
|
||||||
@@ -68,10 +66,12 @@ export const URL_MICROSOFT_TRANS =
|
|||||||
|
|
||||||
export const OPT_TRANS_GOOGLE = "Google";
|
export const OPT_TRANS_GOOGLE = "Google";
|
||||||
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
||||||
|
export const OPT_TRANS_DEEPL = "DeepL";
|
||||||
export const OPT_TRANS_OPENAI = "OpenAI";
|
export const OPT_TRANS_OPENAI = "OpenAI";
|
||||||
export const OPT_TRANS_ALL = [
|
export const OPT_TRANS_ALL = [
|
||||||
OPT_TRANS_GOOGLE,
|
OPT_TRANS_GOOGLE,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_DEEPL,
|
||||||
OPT_TRANS_OPENAI,
|
OPT_TRANS_OPENAI,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -121,6 +121,12 @@ export const OPT_LANGS_SPECIAL = {
|
|||||||
["zh-CN", "zh-Hans"],
|
["zh-CN", "zh-Hans"],
|
||||||
["zh-TW", "zh-Hant"],
|
["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_TRANS_OPENAI]: new Map(
|
||||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
),
|
),
|
||||||
@@ -202,6 +208,8 @@ export const DEFAULT_SETTING = {
|
|||||||
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
||||||
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
|
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
|
||||||
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口
|
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",
|
openaiUrl: "https://api.openai.com/v1/chat/completions",
|
||||||
openaiKey: "",
|
openaiKey: "",
|
||||||
openaiModel: "gpt-4",
|
openaiModel: "gpt-4",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
MSG_FETCH_CLEAR,
|
MSG_FETCH_CLEAR,
|
||||||
CACHE_NAME,
|
CACHE_NAME,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_DEEPL,
|
||||||
OPT_TRANS_OPENAI,
|
OPT_TRANS_OPENAI,
|
||||||
DEFAULT_FETCH_INTERVAL,
|
DEFAULT_FETCH_INTERVAL,
|
||||||
DEFAULT_FETCH_LIMIT,
|
DEFAULT_FETCH_LIMIT,
|
||||||
@@ -67,9 +68,11 @@ const newCacheReq = async (request) => {
|
|||||||
*/
|
*/
|
||||||
const fetchApi = async ({ input, init = {}, translator, token }) => {
|
const fetchApi = async ({ input, init = {}, translator, token }) => {
|
||||||
if (translator === OPT_TRANS_MICROSOFT) {
|
if (translator === OPT_TRANS_MICROSOFT) {
|
||||||
init.headers["Authorization"] = `Bearer ${token}`;
|
init.headers["Authorization"] = `Bearer ${token}`; // Microsoft
|
||||||
|
} else if (translator === OPT_TRANS_DEEPL) {
|
||||||
|
init.headers["Authorization"] = `DeepL-Auth-Key ${token}`; // DeepL
|
||||||
} else if (translator === OPT_TRANS_OPENAI) {
|
} 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
|
init.headers["api-key"] = token; // Azure OpenAI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
import { fetchGM } from "./fetch";
|
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接口信息
|
* 注入页面的脚本,请求并接受GM接口信息
|
||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
*/
|
*/
|
||||||
export const injectScript = (ping) => {
|
export const injectScript = (ping) => {
|
||||||
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
|
window.APP_INFO = {
|
||||||
const MSG_GM_setValue = "setValue";
|
name: process.env.REACT_APP_NAME,
|
||||||
const MSG_GM_getValue = "getValue";
|
version: process.env.REACT_APP_VERSION,
|
||||||
const MSG_GM_deleteValue = "deleteValue";
|
eventName: ping,
|
||||||
const MSG_GM_info = "info";
|
};
|
||||||
let GM_info;
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 适配GM脚本
|
||||||
|
*/
|
||||||
|
export const adaptScript = (ping) => {
|
||||||
const promiseGM = (action, args, timeout = 5000) =>
|
const promiseGM = (action, args, timeout = 5000) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const pong = btoa(Math.random()).slice(3, 11);
|
const pong = genEventName();
|
||||||
const handleEvent = (e) => {
|
const handleEvent = (e) => {
|
||||||
window.removeEventListener(pong, handleEvent);
|
window.removeEventListener(pong, handleEvent);
|
||||||
const { data, error } = e.detail;
|
const { data, error } = e.detail;
|
||||||
@@ -41,14 +52,13 @@ export const injectScript = (ping) => {
|
|||||||
setValue: (key, val) => promiseGM(MSG_GM_setValue, { key, val }),
|
setValue: (key, val) => promiseGM(MSG_GM_setValue, { key, val }),
|
||||||
getValue: (key) => promiseGM(MSG_GM_getValue, { key }),
|
getValue: (key) => promiseGM(MSG_GM_getValue, { key }),
|
||||||
deleteValue: (key) => promiseGM(MSG_GM_deleteValue, { key }),
|
deleteValue: (key) => promiseGM(MSG_GM_deleteValue, { key }),
|
||||||
getInfo: () => {
|
getInfo: async () => {
|
||||||
if (GM_info) {
|
if (!window.GM_info) {
|
||||||
return 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
|
* @param {*} param0
|
||||||
*/
|
*/
|
||||||
export const handlePing = async (e) => {
|
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;
|
const { action, args, pong } = e.detail;
|
||||||
let res;
|
let res;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
APP_LCNAME,
|
APP_LCNAME,
|
||||||
TRANS_MIN_LENGTH,
|
TRANS_MIN_LENGTH,
|
||||||
TRANS_MAX_LENGTH,
|
TRANS_MAX_LENGTH,
|
||||||
EVENT_KISS,
|
|
||||||
MSG_TRANS_CURRULE,
|
MSG_TRANS_CURRULE,
|
||||||
OPT_STYLE_DASHLINE,
|
OPT_STYLE_DASHLINE,
|
||||||
OPT_STYLE_FUZZY,
|
OPT_STYLE_FUZZY,
|
||||||
@@ -11,7 +10,7 @@ import {
|
|||||||
} from "../config";
|
} from "../config";
|
||||||
import Content from "../views/Content";
|
import Content from "../views/Content";
|
||||||
import { updateFetchPool, clearFetchPool } from "./fetch";
|
import { updateFetchPool, clearFetchPool } from "./fetch";
|
||||||
import { debounce } from "./utils";
|
import { debounce, genEventName } from "./utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 翻译类
|
* 翻译类
|
||||||
@@ -37,6 +36,7 @@ export class Translator {
|
|||||||
"script",
|
"script",
|
||||||
"iframe",
|
"iframe",
|
||||||
];
|
];
|
||||||
|
_eventName = genEventName();
|
||||||
|
|
||||||
// 显示
|
// 显示
|
||||||
_interseObserver = new IntersectionObserver(
|
_interseObserver = new IntersectionObserver(
|
||||||
@@ -105,6 +105,10 @@ export class Translator {
|
|||||||
return this._setting;
|
return this._setting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get eventName() {
|
||||||
|
return this._eventName;
|
||||||
|
}
|
||||||
|
|
||||||
get rule() {
|
get rule() {
|
||||||
// console.log("get rule", this._rule);
|
// console.log("get rule", this._rule);
|
||||||
return this._rule;
|
return this._rule;
|
||||||
@@ -115,8 +119,9 @@ export class Translator {
|
|||||||
this._rule = rule;
|
this._rule = rule;
|
||||||
|
|
||||||
// 广播消息
|
// 广播消息
|
||||||
|
const eventName = this._eventName;
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent(EVENT_KISS, {
|
new CustomEvent(eventName, {
|
||||||
detail: {
|
detail: {
|
||||||
action: MSG_TRANS_CURRULE,
|
action: MSG_TRANS_CURRULE,
|
||||||
args: rule,
|
args: rule,
|
||||||
|
|||||||
@@ -116,3 +116,9 @@ export const sha256 = async (text, salt) => {
|
|||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
.join("");
|
.join("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机事件名称
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const genEventName = () => btoa(Math.random()).slice(3, 11);
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import {
|
|||||||
} from "./libs/storage";
|
} from "./libs/storage";
|
||||||
import { Translator } from "./libs/translator";
|
import { Translator } from "./libs/translator";
|
||||||
import { trySyncAllSubRules } from "./libs/subRules";
|
import { trySyncAllSubRules } from "./libs/subRules";
|
||||||
import { isGm } from "./libs/client";
|
|
||||||
import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config";
|
import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config";
|
||||||
import { isIframe } from "./libs/iframe";
|
import { isIframe } from "./libs/iframe";
|
||||||
import { handlePing, injectScript } from "./libs/gm";
|
import { handlePing, injectScript } from "./libs/gm";
|
||||||
import { matchRule } from "./libs/rules";
|
import { matchRule } from "./libs/rules";
|
||||||
|
import { genEventName } from "./libs/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 入口函数
|
* 入口函数
|
||||||
@@ -28,9 +28,12 @@ const init = async () => {
|
|||||||
) {
|
) {
|
||||||
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
|
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
|
||||||
unsafeWindow.GM = GM;
|
unsafeWindow.GM = GM;
|
||||||
unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
|
unsafeWindow.APP_INFO = {
|
||||||
|
name: process.env.REACT_APP_NAME,
|
||||||
|
version: process.env.REACT_APP_VERSION,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
const ping = btoa(Math.random()).slice(3, 11);
|
const ping = genEventName();
|
||||||
window.addEventListener(ping, handlePing);
|
window.addEventListener(ping, handlePing);
|
||||||
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
|
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
@@ -88,28 +91,6 @@ const init = async () => {
|
|||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 注册菜单
|
|
||||||
if (isGm) {
|
|
||||||
try {
|
|
||||||
GM.registerMenuCommand(
|
|
||||||
"Toggle Translate",
|
|
||||||
(event) => {
|
|
||||||
translator.toggle();
|
|
||||||
},
|
|
||||||
"Q"
|
|
||||||
);
|
|
||||||
GM.registerMenuCommand(
|
|
||||||
"Toggle Style",
|
|
||||||
(event) => {
|
|
||||||
translator.toggleStyle();
|
|
||||||
},
|
|
||||||
"C"
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[registerMenuCommand]", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 同步订阅规则
|
// 同步订阅规则
|
||||||
trySyncAllSubRules(setting);
|
trySyncAllSubRules(setting);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { useEffect, useState, useMemo, useCallback } from "react";
|
|||||||
import { SettingProvider } from "../../hooks/Setting";
|
import { SettingProvider } from "../../hooks/Setting";
|
||||||
import Popup from "../Popup";
|
import Popup from "../Popup";
|
||||||
import { debounce } from "../../libs/utils";
|
import { debounce } from "../../libs/utils";
|
||||||
|
import * as shortcut from "@violentmonkey/shortcut";
|
||||||
|
import { isGm } from "../../libs/client";
|
||||||
|
|
||||||
export default function Action({ translator, fab }) {
|
export default function Action({ translator, fab }) {
|
||||||
const fabWidth = 40;
|
const fabWidth = 40;
|
||||||
@@ -44,6 +46,73 @@ export default function Action({ translator, fab }) {
|
|||||||
setMoved(true);
|
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(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("resize", handleWindowResize);
|
window.addEventListener("resize", handleWindowResize);
|
||||||
return () => {
|
return () => {
|
||||||
@@ -53,6 +122,7 @@ export default function Action({ translator, fab }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("click", handleWindowClick);
|
window.addEventListener("click", handleWindowClick);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("click", handleWindowClick);
|
window.removeEventListener("click", handleWindowClick);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
OPT_STYLE_HIGHLIGHT,
|
OPT_STYLE_HIGHLIGHT,
|
||||||
OPT_STYLE_DIY,
|
OPT_STYLE_DIY,
|
||||||
DEFAULT_COLOR,
|
DEFAULT_COLOR,
|
||||||
EVENT_KISS,
|
|
||||||
MSG_TRANS_CURRULE,
|
MSG_TRANS_CURRULE,
|
||||||
TRANS_NEWLINE_LENGTH,
|
TRANS_NEWLINE_LENGTH,
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
@@ -112,11 +111,11 @@ export default function Content({ q, translator }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener(EVENT_KISS, handleKissEvent);
|
window.addEventListener(translator.eventName, handleKissEvent);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener(EVENT_KISS, handleKissEvent);
|
window.removeEventListener(translator.eventName, handleKissEvent);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [translator.eventName]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export default function Settings() {
|
|||||||
minLength,
|
minLength,
|
||||||
maxLength,
|
maxLength,
|
||||||
openaiUrl,
|
openaiUrl,
|
||||||
|
deeplUrl = "",
|
||||||
|
deeplKey = "",
|
||||||
openaiKey,
|
openaiKey,
|
||||||
openaiModel,
|
openaiModel,
|
||||||
openaiPrompt,
|
openaiPrompt,
|
||||||
@@ -144,6 +146,22 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("openai_api")}
|
label={i18n("openai_api")}
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ import { AlertProvider } from "../../hooks/Alert";
|
|||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { adaptScript } from "../../libs/gm";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
|
||||||
export default function Options() {
|
export default function Options() {
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState("");
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -26,7 +28,22 @@ export default function Options() {
|
|||||||
// 等待GM注入
|
// 等待GM注入
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (;;) {
|
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();
|
await trySyncSettingAndRules();
|
||||||
setReady(true);
|
setReady(true);
|
||||||
@@ -34,7 +51,7 @@ export default function Options() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (++i > 8) {
|
if (++i > 8) {
|
||||||
setError(true);
|
setError("Time out.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +68,7 @@ export default function Options() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<center>
|
<center>
|
||||||
|
<Alert severity="error">{error}</Alert>
|
||||||
<Divider>
|
<Divider>
|
||||||
<Link
|
<Link
|
||||||
href={process.env.REACT_APP_HOMEPAGE}
|
href={process.env.REACT_APP_HOMEPAGE}
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@@ -1663,7 +1663,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.22.10, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7":
|
"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.22.10, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7":
|
||||||
version: 7.22.11
|
version: 7.22.11
|
||||||
resolution: "@babel/runtime@npm:7.22.11"
|
resolution: "@babel/runtime@npm:7.22.11"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3841,6 +3841,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@violentmonkey/shortcut@npm:^1.3.0":
|
||||||
|
version: 1.3.0
|
||||||
|
resolution: "@violentmonkey/shortcut@npm:1.3.0"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.20.1
|
||||||
|
checksum: 5c839c0193014ff29d000337c8cb03f950e5386379a5cc7c845b9e4c566126dc1cfc431b71bc591e30ce7b8a5e82c2fc38396d52b705352252eb0376a7d18fd3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@webassemblyjs/ast@npm:1.11.6, @webassemblyjs/ast@npm:^1.11.5":
|
"@webassemblyjs/ast@npm:1.11.6, @webassemblyjs/ast@npm:^1.11.5":
|
||||||
version: 1.11.6
|
version: 1.11.6
|
||||||
resolution: "@webassemblyjs/ast@npm:1.11.6"
|
resolution: "@webassemblyjs/ast@npm:1.11.6"
|
||||||
@@ -9516,6 +9525,7 @@ __metadata:
|
|||||||
"@emotion/styled": ^11.10.8
|
"@emotion/styled": ^11.10.8
|
||||||
"@mui/icons-material": ^5.11.11
|
"@mui/icons-material": ^5.11.11
|
||||||
"@mui/material": ^5.11.12
|
"@mui/material": ^5.11.12
|
||||||
|
"@violentmonkey/shortcut": ^1.3.0
|
||||||
query-string: ^8.1.0
|
query-string: ^8.1.0
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
react-app-rewired: ^2.2.1
|
react-app-rewired: ^2.2.1
|
||||||
|
|||||||
Reference in New Issue
Block a user