Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f463f3ce08 | ||
|
|
c0872db98c | ||
|
|
d3a5d91f01 | ||
|
|
3e9338be0e | ||
|
|
ef7f1ad638 | ||
|
|
1f10ebe404 | ||
|
|
f4a8251c61 | ||
|
|
f585a43480 | ||
|
|
3a11465c24 | ||
|
|
3c3ebdf96c | ||
|
|
6b30f443e1 | ||
|
|
232e9a47a2 | ||
|
|
7ec43a1d3f | ||
|
|
a8caa34bbe | ||
|
|
c2fd1fe9e0 | ||
|
|
2773a76af8 | ||
|
|
1dc7026e8f | ||
|
|
b36ede7393 | ||
|
|
b18721a4e5 | ||
|
|
01676bc682 |
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.4.5
|
REACT_APP_VERSION=1.5.3
|
||||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||||
REACT_APP_OPTIONSPAGE=https://kiss-translator.rayjar.com/options
|
REACT_APP_OPTIONSPAGE=https://kiss-translator.rayjar.com/options
|
||||||
REACT_APP_OPTIONSPAGE2=https://fishjar.github.io/kiss-translator/options.html
|
REACT_APP_OPTIONSPAGE2=https://fishjar.github.io/kiss-translator/options.html
|
||||||
|
|||||||
@@ -83,14 +83,12 @@ const userscriptWebpack = (config, env) => {
|
|||||||
// @icon ${process.env.REACT_APP_LOGOURL}
|
// @icon ${process.env.REACT_APP_LOGOURL}
|
||||||
// @downloadURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
// @downloadURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||||
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||||
// @grant GM_xmlhttpRequest
|
// @grant GM.xmlHttpRequest
|
||||||
// @grant GM.xmlhttpRequest
|
// @grant GM.registerMenuCommand
|
||||||
// @grant GM_setValue
|
|
||||||
// @grant GM.setValue
|
// @grant GM.setValue
|
||||||
// @grant GM_getValue
|
|
||||||
// @grant GM.getValue
|
// @grant GM.getValue
|
||||||
// @grant GM_deleteValue
|
|
||||||
// @grant GM.deleteValue
|
// @grant GM.deleteValue
|
||||||
|
// @grant GM.info
|
||||||
// @grant unsafeWindow
|
// @grant unsafeWindow
|
||||||
// @connect translate.googleapis.com
|
// @connect translate.googleapis.com
|
||||||
// @connect api-edge.cognitive.microsofttranslator.com
|
// @connect api-edge.cognitive.microsofttranslator.com
|
||||||
@@ -98,6 +96,10 @@ const userscriptWebpack = (config, env) => {
|
|||||||
// @connect api.openai.com
|
// @connect api.openai.com
|
||||||
// @connect openai.azure.com
|
// @connect openai.azure.com
|
||||||
// @connect workers.dev
|
// @connect workers.dev
|
||||||
|
// @connect github.io
|
||||||
|
// @connect githubusercontent.com
|
||||||
|
// @connect kiss-translator.rayjar.com
|
||||||
|
// @connect ghproxy.com
|
||||||
// @run-at document-end
|
// @run-at document-end
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kiss-translator",
|
"name": "kiss-translator",
|
||||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||||
"version": "1.4.5",
|
"version": "1.5.3",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -25,7 +25,8 @@
|
|||||||
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json",
|
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json",
|
||||||
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
|
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
|
||||||
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/kiss-translator.user.js build/userscript/kiss-translator.user.js",
|
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/kiss-translator.user.js build/userscript/kiss-translator.user.js",
|
||||||
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript",
|
"build:rules": "babel-node src/rules.js",
|
||||||
|
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript && yarn build:rules",
|
||||||
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
|
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
|
||||||
"test": "react-app-rewired test",
|
"test": "react-app-rewired test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
@@ -53,6 +54,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.22.10",
|
||||||
|
"@babel/node": "^7.22.10",
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
|
"@babel/preset-env": "^7.22.10",
|
||||||
"react-app-rewired": "^2.2.1",
|
"react-app-rewired": "^2.2.1",
|
||||||
"wrangler": "^3.4.0"
|
"wrangler": "^3.4.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,11 @@
|
|||||||
},
|
},
|
||||||
"app_description": {
|
"app_description": {
|
||||||
"message": "A minimalist bilingual translation Extension & Greasemonkey Script"
|
"message": "A minimalist bilingual translation Extension & Greasemonkey Script"
|
||||||
|
},
|
||||||
|
"toggle_translate": {
|
||||||
|
"message": "Toggle Translate"
|
||||||
|
},
|
||||||
|
"toggle_style": {
|
||||||
|
"message": "Toggle Style"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,11 @@
|
|||||||
},
|
},
|
||||||
"app_description": {
|
"app_description": {
|
||||||
"message": "一个简约的双语网页翻译扩展 & 油猴脚本"
|
"message": "一个简约的双语网页翻译扩展 & 油猴脚本"
|
||||||
|
},
|
||||||
|
"toggle_translate": {
|
||||||
|
"message": "切换翻译"
|
||||||
|
},
|
||||||
|
"toggle_style": {
|
||||||
|
"message": "切换样式"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "1.4.5",
|
"version": "1.5.3",
|
||||||
"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",
|
||||||
@@ -15,6 +15,20 @@
|
|||||||
"matches": ["<all_urls>"]
|
"matches": ["<all_urls>"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"commands": {
|
||||||
|
"toggleTranslate": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+Q"
|
||||||
|
},
|
||||||
|
"description": "__MSG_toggle_translate__"
|
||||||
|
},
|
||||||
|
"toggleStyle": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+C"
|
||||||
|
},
|
||||||
|
"description": "__MSG_toggle_style__"
|
||||||
|
}
|
||||||
|
},
|
||||||
"permissions": ["<all_urls>", "storage"],
|
"permissions": ["<all_urls>", "storage"],
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "images/logo16.png",
|
"16": "images/logo16.png",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "1.4.5",
|
"version": "1.5.3",
|
||||||
"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",
|
||||||
@@ -16,6 +16,20 @@
|
|||||||
"matches": ["<all_urls>"]
|
"matches": ["<all_urls>"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"commands": {
|
||||||
|
"toggleTranslate": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+Q"
|
||||||
|
},
|
||||||
|
"description": "__MSG_toggle_translate__"
|
||||||
|
},
|
||||||
|
"toggleStyle": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+C"
|
||||||
|
},
|
||||||
|
"description": "__MSG_toggle_style__"
|
||||||
|
}
|
||||||
|
},
|
||||||
"permissions": ["storage"],
|
"permissions": ["storage"],
|
||||||
"host_permissions": ["<all_urls>"],
|
"host_permissions": ["<all_urls>"],
|
||||||
"icons": {
|
"icons": {
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import {
|
|||||||
OPT_LANGS_SPECIAL,
|
OPT_LANGS_SPECIAL,
|
||||||
PROMPT_PLACE_FROM,
|
PROMPT_PLACE_FROM,
|
||||||
PROMPT_PLACE_TO,
|
PROMPT_PLACE_TO,
|
||||||
KV_HEADER_KEY,
|
KV_SALT_SYNC,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { getSetting, detectLang } from "../libs";
|
import { getSetting, detectLang } from "../libs";
|
||||||
|
import { sha256 } from "../libs/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步数据
|
* 同步数据
|
||||||
@@ -20,18 +21,14 @@ import { getSetting, detectLang } from "../libs";
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiSyncData = async (url, key, data) =>
|
export const apiSyncData = async (url, key, data) =>
|
||||||
fetchPolyfill(
|
fetchPolyfill(url, {
|
||||||
url,
|
headers: {
|
||||||
{
|
"Content-type": "application/json",
|
||||||
headers: {
|
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
||||||
"Content-type": "application/json",
|
|
||||||
[KV_HEADER_KEY]: key,
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
},
|
},
|
||||||
{ useUnsafe: true }
|
method: "POST",
|
||||||
);
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 谷歌翻译
|
* 谷歌翻译
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
MSG_FETCH,
|
MSG_FETCH,
|
||||||
MSG_FETCH_LIMIT,
|
MSG_FETCH_LIMIT,
|
||||||
MSG_FETCH_CLEAR,
|
MSG_FETCH_CLEAR,
|
||||||
|
MSG_TRANS_TOGGLE,
|
||||||
|
MSG_TRANS_TOGGLE_STYLE,
|
||||||
|
CMD_TOGGLE_TRANSLATE,
|
||||||
|
CMD_TOGGLE_STYLE,
|
||||||
DEFAULT_SETTING,
|
DEFAULT_SETTING,
|
||||||
DEFAULT_RULES,
|
DEFAULT_RULES,
|
||||||
DEFAULT_SYNC,
|
DEFAULT_SYNC,
|
||||||
@@ -15,6 +19,7 @@ import storage from "./libs/storage";
|
|||||||
import { getSetting } from "./libs";
|
import { getSetting } from "./libs";
|
||||||
import { syncAll } from "./libs/sync";
|
import { syncAll } from "./libs/sync";
|
||||||
import { fetchData, fetchPool } from "./libs/fetch";
|
import { fetchData, fetchPool } from "./libs/fetch";
|
||||||
|
import { sendTabMsg } from "./libs/msg";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件安装
|
* 插件安装
|
||||||
@@ -24,6 +29,7 @@ browser.runtime.onInstalled.addListener(() => {
|
|||||||
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
|
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
|
||||||
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES);
|
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES);
|
||||||
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
|
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
|
||||||
|
// todo:缓存内置rules
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,3 +79,19 @@ browser.runtime.onMessage.addListener(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听快捷键
|
||||||
|
*/
|
||||||
|
browser.commands.onCommand.addListener((command) => {
|
||||||
|
// console.log(`Command: ${command}`);
|
||||||
|
switch (command) {
|
||||||
|
case CMD_TOGGLE_TRANSLATE:
|
||||||
|
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||||
|
break;
|
||||||
|
case CMD_TOGGLE_STYLE:
|
||||||
|
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -105,8 +105,20 @@ export const I18N = {
|
|||||||
en: `Add`,
|
en: `Add`,
|
||||||
},
|
},
|
||||||
inject_rules: {
|
inject_rules: {
|
||||||
zh: `注入内置规则`,
|
zh: `注入订阅规则`,
|
||||||
en: `Inject Built-in Rules`,
|
en: `Inject Subscribe Rules`,
|
||||||
|
},
|
||||||
|
edit_rules: {
|
||||||
|
zh: `编辑规则`,
|
||||||
|
en: `Edit Rules`,
|
||||||
|
},
|
||||||
|
subscribe_rules: {
|
||||||
|
zh: `订阅规则`,
|
||||||
|
en: `Subscribe Rules`,
|
||||||
|
},
|
||||||
|
subscribe_url: {
|
||||||
|
zh: `订阅地址`,
|
||||||
|
en: `Subscribe URL`,
|
||||||
},
|
},
|
||||||
sync_warn: {
|
sync_warn: {
|
||||||
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
|
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
|
||||||
@@ -153,8 +165,8 @@ export const I18N = {
|
|||||||
en: `URL pattern`,
|
en: `URL pattern`,
|
||||||
},
|
},
|
||||||
pattern_helper: {
|
pattern_helper: {
|
||||||
zh: `多个URL支持英文逗号“,”分隔`,
|
zh: `1、支持星号(*)通配符。2、多个URL支持英文逗号“,”分隔。`,
|
||||||
en: `Multiple URLs can be separated by English commas ","`,
|
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`,
|
||||||
},
|
},
|
||||||
selector_helper: {
|
selector_helper: {
|
||||||
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。`,
|
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。`,
|
||||||
@@ -196,6 +208,10 @@ export const I18N = {
|
|||||||
zh: `错误的文件类型`,
|
zh: `错误的文件类型`,
|
||||||
en: `Wrong file type`,
|
en: `Wrong file type`,
|
||||||
},
|
},
|
||||||
|
error_fetch_url: {
|
||||||
|
zh: `请检查url地址是否正确或稍后再试。`,
|
||||||
|
en: `Please check if the url address is correct or try again later.`,
|
||||||
|
},
|
||||||
openai_api: {
|
openai_api: {
|
||||||
zh: `OpenAI 接口`,
|
zh: `OpenAI 接口`,
|
||||||
en: `OpenAI API`,
|
en: `OpenAI API`,
|
||||||
@@ -232,4 +248,12 @@ export const I18N = {
|
|||||||
zh: `数据同步密钥`,
|
zh: `数据同步密钥`,
|
||||||
en: `Data Sync Key`,
|
en: `Data Sync Key`,
|
||||||
},
|
},
|
||||||
|
error_got_some_wrong: {
|
||||||
|
zh: "抱歉,出错了!",
|
||||||
|
en: "Sorry, something went wrong!",
|
||||||
|
},
|
||||||
|
error_sync_setting: {
|
||||||
|
zh: "您的同步设置未填写,无法在线分享。",
|
||||||
|
en: "Your sync settings are missing and cannot be shared online.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { DEFAULT_SELECTOR, RULES } from "./rules";
|
import {
|
||||||
|
DEFAULT_SELECTOR,
|
||||||
|
GLOBAL_KEY,
|
||||||
|
DEFAULT_RULE,
|
||||||
|
BUILTIN_RULES,
|
||||||
|
} from "./rules";
|
||||||
export { I18N, UI_LANGS } from "./i18n";
|
export { I18N, UI_LANGS } from "./i18n";
|
||||||
|
export { GLOBAL_KEY, DEFAULT_RULE, BUILTIN_RULES };
|
||||||
|
|
||||||
const APP_NAME = process.env.REACT_APP_NAME.trim().split(/\s+/).join("-");
|
const APP_NAME = process.env.REACT_APP_NAME.trim().split(/\s+/).join("-");
|
||||||
|
|
||||||
@@ -10,8 +16,10 @@ export const STOKEY_SETTING = `${APP_NAME}_setting`;
|
|||||||
export const STOKEY_RULES = `${APP_NAME}_rules`;
|
export const STOKEY_RULES = `${APP_NAME}_rules`;
|
||||||
export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
||||||
export const STOKEY_FAB = `${APP_NAME}_fab`;
|
export const STOKEY_FAB = `${APP_NAME}_fab`;
|
||||||
|
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
|
||||||
|
|
||||||
export const GLOBAL_KEY = "*";
|
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
|
||||||
|
export const CMD_TOGGLE_STYLE = "toggleStyle";
|
||||||
|
|
||||||
export const CLIENT_WEB = "web";
|
export const CLIENT_WEB = "web";
|
||||||
export const CLIENT_CHROME = "chrome";
|
export const CLIENT_CHROME = "chrome";
|
||||||
@@ -20,9 +28,11 @@ export const CLIENT_FIREFOX = "firefox";
|
|||||||
export const CLIENT_USERSCRIPT = "userscript";
|
export const CLIENT_USERSCRIPT = "userscript";
|
||||||
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
|
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
|
||||||
|
|
||||||
export const KV_HEADER_KEY = "X-KISS-PSK";
|
|
||||||
export const KV_RULES_KEY = "KT_RULES";
|
export const KV_RULES_KEY = "KT_RULES";
|
||||||
|
export const KV_RULES_SHARE_KEY = "KT_RULES_SHARE";
|
||||||
export const KV_SETTING_KEY = "KT_SETTING";
|
export const KV_SETTING_KEY = "KT_SETTING";
|
||||||
|
export const KV_SALT_SYNC = "KISS-Translator-SYNC";
|
||||||
|
export const KV_SALT_SHARE = "KISS-Translator-SHARE";
|
||||||
|
|
||||||
export const CACHE_NAME = `${APP_NAME}_cache`;
|
export const CACHE_NAME = `${APP_NAME}_cache`;
|
||||||
|
|
||||||
@@ -30,6 +40,7 @@ export const MSG_FETCH = "fetch";
|
|||||||
export const MSG_FETCH_LIMIT = "fetch_limit";
|
export const MSG_FETCH_LIMIT = "fetch_limit";
|
||||||
export const MSG_FETCH_CLEAR = "fetch_clear";
|
export const MSG_FETCH_CLEAR = "fetch_clear";
|
||||||
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
||||||
|
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
|
||||||
export const MSG_TRANS_GETRULE = "trans_getrule";
|
export const MSG_TRANS_GETRULE = "trans_getrule";
|
||||||
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
||||||
export const MSG_TRANS_CURRULE = "trans_currule";
|
export const MSG_TRANS_CURRULE = "trans_currule";
|
||||||
@@ -143,17 +154,16 @@ export const GLOBLA_RULE = {
|
|||||||
bgColor: "",
|
bgColor: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 默认规则
|
// 订阅列表
|
||||||
export const DEFAULT_RULE = {
|
export const DEFAULT_SUBRULES_LIST = [
|
||||||
pattern: "",
|
{
|
||||||
selector: "",
|
url: "https://kiss-translator.rayjar.com/kiss-translator-rules.json",
|
||||||
translator: GLOBAL_KEY,
|
selected: true,
|
||||||
fromLang: GLOBAL_KEY,
|
},
|
||||||
toLang: GLOBAL_KEY,
|
{
|
||||||
textStyle: GLOBAL_KEY,
|
url: "https://fishjar.github.io/kiss-translator/kiss-translator-rules.json",
|
||||||
transOpen: GLOBAL_KEY,
|
},
|
||||||
bgColor: "",
|
];
|
||||||
};
|
|
||||||
|
|
||||||
export const DEFAULT_SETTING = {
|
export const DEFAULT_SETTING = {
|
||||||
darkMode: false, // 深色模式
|
darkMode: false, // 深色模式
|
||||||
@@ -161,7 +171,8 @@ export const DEFAULT_SETTING = {
|
|||||||
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
||||||
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
||||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||||
injectRules: true, // 是否注入内置规则
|
injectRules: true, // 是否注入订阅规则
|
||||||
|
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
||||||
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口
|
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口
|
||||||
openaiUrl: "https://api.openai.com/v1/chat/completions",
|
openaiUrl: "https://api.openai.com/v1/chat/completions",
|
||||||
openaiKey: "",
|
openaiKey: "",
|
||||||
@@ -169,20 +180,7 @@ export const DEFAULT_SETTING = {
|
|||||||
openaiPrompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
|
openaiPrompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_RULES = [
|
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||||
{
|
|
||||||
...DEFAULT_RULE,
|
|
||||||
...RULES[0],
|
|
||||||
transOpen: "true",
|
|
||||||
},
|
|
||||||
GLOBLA_RULE,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const BUILTIN_RULES = RULES.map((item) => ({
|
|
||||||
...DEFAULT_RULE,
|
|
||||||
...item,
|
|
||||||
transOpen: "true",
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
||||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
||||||
|
|||||||
@@ -2,7 +2,20 @@ const els = `li, p, h1, h2, h3, h4, h5, h6, dd`;
|
|||||||
|
|
||||||
export const DEFAULT_SELECTOR = `:is(${els})`;
|
export const DEFAULT_SELECTOR = `:is(${els})`;
|
||||||
|
|
||||||
export const RULES = [
|
export const GLOBAL_KEY = "*";
|
||||||
|
|
||||||
|
export const DEFAULT_RULE = {
|
||||||
|
pattern: "",
|
||||||
|
selector: "",
|
||||||
|
translator: GLOBAL_KEY,
|
||||||
|
fromLang: GLOBAL_KEY,
|
||||||
|
toLang: GLOBAL_KEY,
|
||||||
|
textStyle: GLOBAL_KEY,
|
||||||
|
transOpen: GLOBAL_KEY,
|
||||||
|
bgColor: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const RULES = [
|
||||||
{
|
{
|
||||||
pattern: `www.google.com/search`,
|
pattern: `www.google.com/search`,
|
||||||
selector: `h3, .IsZvec, .VwiC3b`,
|
selector: `h3, .IsZvec, .VwiC3b`,
|
||||||
@@ -132,3 +145,9 @@ export const RULES = [
|
|||||||
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`,
|
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const BUILTIN_RULES = RULES.map((item) => ({
|
||||||
|
...DEFAULT_RULE,
|
||||||
|
...item,
|
||||||
|
transOpen: "true",
|
||||||
|
}));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { browser } from "./libs/browser";
|
import { browser } from "./libs/browser";
|
||||||
import {
|
import {
|
||||||
MSG_TRANS_TOGGLE,
|
MSG_TRANS_TOGGLE,
|
||||||
|
MSG_TRANS_TOGGLE_STYLE,
|
||||||
MSG_TRANS_GETRULE,
|
MSG_TRANS_GETRULE,
|
||||||
MSG_TRANS_PUTRULE,
|
MSG_TRANS_PUTRULE,
|
||||||
} from "./config";
|
} from "./config";
|
||||||
@@ -13,7 +14,7 @@ import { Translator } from "./libs/translator";
|
|||||||
(async () => {
|
(async () => {
|
||||||
const setting = await getSetting();
|
const setting = await getSetting();
|
||||||
const rules = await getRules();
|
const rules = await getRules();
|
||||||
const rule = matchRule(rules, document.location.href, setting);
|
const rule = await matchRule(rules, document.location.href, setting);
|
||||||
const translator = new Translator(rule, setting);
|
const translator = new Translator(rule, setting);
|
||||||
|
|
||||||
// 监听消息
|
// 监听消息
|
||||||
@@ -22,6 +23,9 @@ import { Translator } from "./libs/translator";
|
|||||||
case MSG_TRANS_TOGGLE:
|
case MSG_TRANS_TOGGLE:
|
||||||
translator.toggle();
|
translator.toggle();
|
||||||
break;
|
break;
|
||||||
|
case MSG_TRANS_TOGGLE_STYLE:
|
||||||
|
translator.toggleStyle();
|
||||||
|
break;
|
||||||
case MSG_TRANS_GETRULE:
|
case MSG_TRANS_GETRULE:
|
||||||
break;
|
break;
|
||||||
case MSG_TRANS_PUTRULE:
|
case MSG_TRANS_PUTRULE:
|
||||||
|
|||||||
60
src/hooks/Alert.js
Normal file
60
src/hooks/Alert.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { createContext, useContext, useState, forwardRef } from "react";
|
||||||
|
import Snackbar from "@mui/material/Snackbar";
|
||||||
|
import MuiAlert from "@mui/material/Alert";
|
||||||
|
|
||||||
|
const Alert = forwardRef(function Alert(props, ref) {
|
||||||
|
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertContext = createContext(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 左下角提示,注入context后,方便全局调用
|
||||||
|
* @param {*} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function AlertProvider({ children }) {
|
||||||
|
const vertical = "top";
|
||||||
|
const horizontal = "center";
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [severity, setSeverity] = useState("info");
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
|
||||||
|
const error = (msg) => showAlert(msg, "error");
|
||||||
|
const warning = (msg) => showAlert(msg, "warning");
|
||||||
|
const info = (msg) => showAlert(msg, "info");
|
||||||
|
const success = (msg) => showAlert(msg, "success");
|
||||||
|
|
||||||
|
const showAlert = (msg, type) => {
|
||||||
|
setOpen(true);
|
||||||
|
setMessage(msg);
|
||||||
|
setSeverity(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (_, reason) => {
|
||||||
|
if (reason === "clickaway") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertContext.Provider value={{ error, warning, info, success }}>
|
||||||
|
{children}
|
||||||
|
<Snackbar
|
||||||
|
open={open}
|
||||||
|
autoHideDuration={3000}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorOrigin={{ vertical, horizontal }}
|
||||||
|
>
|
||||||
|
<Alert onClose={handleClose} severity={severity} sx={{ width: "100%" }}>
|
||||||
|
{message}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</AlertContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAlert() {
|
||||||
|
return useContext(AlertContext);
|
||||||
|
}
|
||||||
@@ -1,16 +1,10 @@
|
|||||||
import {
|
import { STOKEY_RULES, DEFAULT_SUBRULES_LIST } from "../config";
|
||||||
STOKEY_RULES,
|
|
||||||
OPT_TRANS_ALL,
|
|
||||||
OPT_STYLE_ALL,
|
|
||||||
OPT_LANGS_FROM,
|
|
||||||
OPT_LANGS_TO,
|
|
||||||
GLOBAL_KEY,
|
|
||||||
} from "../config";
|
|
||||||
import storage from "../libs/storage";
|
import storage from "../libs/storage";
|
||||||
import { useStorages } from "./Storage";
|
import { useStorages } from "./Storage";
|
||||||
import { matchValue } from "../libs/utils";
|
|
||||||
import { syncRules } from "../libs/sync";
|
import { syncRules } from "../libs/sync";
|
||||||
import { useSync } from "./Sync";
|
import { useSync } from "./Sync";
|
||||||
|
import { useSetting, useSettingUpdate } from "./Setting";
|
||||||
|
import { checkRules } from "../libs/rules";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 匹配规则增删改查 hook
|
* 匹配规则增删改查 hook
|
||||||
@@ -61,43 +55,53 @@ export function useRules() {
|
|||||||
|
|
||||||
const merge = async (newRules) => {
|
const merge = async (newRules) => {
|
||||||
const rules = [...list];
|
const rules = [...list];
|
||||||
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
|
newRules = checkRules(newRules);
|
||||||
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
|
newRules.forEach((newRule) => {
|
||||||
newRules
|
const rule = rules.find((oldRule) => oldRule.pattern === newRule.pattern);
|
||||||
.filter(({ pattern }) => pattern && typeof pattern === "string")
|
if (rule) {
|
||||||
.map(
|
Object.assign(rule, newRule);
|
||||||
({
|
} else {
|
||||||
pattern,
|
rules.unshift(newRule);
|
||||||
selector,
|
}
|
||||||
translator,
|
});
|
||||||
fromLang,
|
|
||||||
toLang,
|
|
||||||
textStyle,
|
|
||||||
transOpen,
|
|
||||||
bgColor,
|
|
||||||
}) => ({
|
|
||||||
pattern,
|
|
||||||
selector: typeof selector === "string" ? selector : "",
|
|
||||||
bgColor: typeof bgColor === "string" ? bgColor : "",
|
|
||||||
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
|
|
||||||
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
|
|
||||||
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
|
|
||||||
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
|
|
||||||
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.forEach((newRule) => {
|
|
||||||
const rule = rules.find(
|
|
||||||
(oldRule) => oldRule.pattern === newRule.pattern
|
|
||||||
);
|
|
||||||
if (rule) {
|
|
||||||
Object.assign(rule, newRule);
|
|
||||||
} else {
|
|
||||||
rules.unshift(newRule);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await update(rules);
|
await update(rules);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { list, add, del, put, merge };
|
return { list, add, del, put, merge };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅规则
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useSubrules() {
|
||||||
|
const setting = useSetting();
|
||||||
|
const updateSetting = useSettingUpdate();
|
||||||
|
const list = setting?.subrulesList || DEFAULT_SUBRULES_LIST;
|
||||||
|
|
||||||
|
const select = async (url) => {
|
||||||
|
const subrulesList = [...list];
|
||||||
|
subrulesList.forEach((item) => {
|
||||||
|
if (item.url === url) {
|
||||||
|
item.selected = true;
|
||||||
|
} else {
|
||||||
|
item.selected = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await updateSetting({ subrulesList });
|
||||||
|
};
|
||||||
|
|
||||||
|
const add = async (url) => {
|
||||||
|
const subrulesList = [...list];
|
||||||
|
subrulesList.push({ url });
|
||||||
|
await updateSetting({ subrulesList });
|
||||||
|
};
|
||||||
|
|
||||||
|
const del = async (url) => {
|
||||||
|
let subrulesList = [...list];
|
||||||
|
subrulesList = subrulesList.filter((item) => item.url !== url);
|
||||||
|
await updateSetting({ subrulesList });
|
||||||
|
};
|
||||||
|
|
||||||
|
return { list, select, add, del };
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { browser, isExt, isGm, isWeb } from "../libs/browser";
|
|||||||
import {
|
import {
|
||||||
STOKEY_SETTING,
|
STOKEY_SETTING,
|
||||||
STOKEY_RULES,
|
STOKEY_RULES,
|
||||||
STOKEY_MSAUTH,
|
|
||||||
STOKEY_SYNC,
|
STOKEY_SYNC,
|
||||||
DEFAULT_SETTING,
|
DEFAULT_SETTING,
|
||||||
DEFAULT_RULES,
|
DEFAULT_RULES,
|
||||||
@@ -15,12 +14,13 @@ import storage from "../libs/storage";
|
|||||||
* 默认配置
|
* 默认配置
|
||||||
*/
|
*/
|
||||||
export const defaultStorage = {
|
export const defaultStorage = {
|
||||||
[STOKEY_MSAUTH]: null,
|
|
||||||
[STOKEY_SETTING]: DEFAULT_SETTING,
|
[STOKEY_SETTING]: DEFAULT_SETTING,
|
||||||
[STOKEY_RULES]: DEFAULT_RULES,
|
[STOKEY_RULES]: DEFAULT_RULES,
|
||||||
[STOKEY_SYNC]: DEFAULT_SYNC,
|
[STOKEY_SYNC]: DEFAULT_SYNC,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const activeKeys = Object.keys(defaultStorage);
|
||||||
|
|
||||||
const StoragesContext = createContext(null);
|
const StoragesContext = createContext(null);
|
||||||
|
|
||||||
export function StoragesProvider({ children }) {
|
export function StoragesProvider({ children }) {
|
||||||
@@ -38,7 +38,10 @@ export function StoragesProvider({ children }) {
|
|||||||
}
|
}
|
||||||
const newStorages = {};
|
const newStorages = {};
|
||||||
Object.entries(changes)
|
Object.entries(changes)
|
||||||
.filter(([_, { oldValue, newValue }]) => oldValue !== newValue)
|
.filter(
|
||||||
|
([key, { oldValue, newValue }]) =>
|
||||||
|
activeKeys.includes(key) && oldValue !== newValue
|
||||||
|
)
|
||||||
.forEach(([key, { newValue }]) => {
|
.forEach(([key, { newValue }]) => {
|
||||||
newStorages[key] = JSON.parse(newValue);
|
newStorages[key] = JSON.parse(newValue);
|
||||||
});
|
});
|
||||||
@@ -51,8 +54,7 @@ export function StoragesProvider({ children }) {
|
|||||||
// 首次从storage同步配置到内存
|
// 首次从storage同步配置到内存
|
||||||
(async () => {
|
(async () => {
|
||||||
const curStorages = {};
|
const curStorages = {};
|
||||||
const keys = Object.keys(defaultStorage);
|
for (const key of activeKeys) {
|
||||||
for (const key of keys) {
|
|
||||||
const val = await storage.get(key);
|
const val = await storage.get(key);
|
||||||
if (val) {
|
if (val) {
|
||||||
curStorages[key] = JSON.parse(val);
|
curStorages[key] = JSON.parse(val);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const newCacheReq = async (request) => {
|
|||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const fetchApi = async ({ input, init, useUnsafe, translator, token }) => {
|
const fetchApi = async ({ input, init, translator, token }) => {
|
||||||
if (translator === OPT_TRANS_MICROSOFT) {
|
if (translator === OPT_TRANS_MICROSOFT) {
|
||||||
init.headers["Authorization"] = `Bearer ${token}`;
|
init.headers["Authorization"] = `Bearer ${token}`;
|
||||||
} else if (translator === OPT_TRANS_OPENAI) {
|
} else if (translator === OPT_TRANS_OPENAI) {
|
||||||
@@ -73,8 +73,13 @@ const fetchApi = async ({ input, init, useUnsafe, translator, token }) => {
|
|||||||
init.headers["api-key"] = token; // Azure OpenAI
|
init.headers["api-key"] = token; // Azure OpenAI
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGm && !useUnsafe) {
|
if (isGm) {
|
||||||
return fetchGM(input, init);
|
const connects = GM?.info?.script?.connects || [];
|
||||||
|
const url = new URL(input);
|
||||||
|
const isSafe = connects.find((item) => url.hostname.endsWith(item));
|
||||||
|
if (isSafe) {
|
||||||
|
return fetchGM(input, init);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return fetch(input, init);
|
return fetch(input, init);
|
||||||
};
|
};
|
||||||
@@ -105,7 +110,7 @@ export const fetchPool = taskPool(
|
|||||||
export const fetchData = async (
|
export const fetchData = async (
|
||||||
input,
|
input,
|
||||||
init,
|
init,
|
||||||
{ useCache, usePool, translator, useUnsafe, token } = {}
|
{ useCache, usePool, translator, token } = {}
|
||||||
) => {
|
) => {
|
||||||
const cacheReq = await newCacheReq(new Request(input, init));
|
const cacheReq = await newCacheReq(new Request(input, init));
|
||||||
const cache = await caches.open(CACHE_NAME);
|
const cache = await caches.open(CACHE_NAME);
|
||||||
@@ -123,9 +128,9 @@ export const fetchData = async (
|
|||||||
if (!res) {
|
if (!res) {
|
||||||
// 发送请求
|
// 发送请求
|
||||||
if (usePool) {
|
if (usePool) {
|
||||||
res = await fetchPool.push({ input, init, useUnsafe, translator, token });
|
res = await fetchPool.push({ input, init, translator, token });
|
||||||
} else {
|
} else {
|
||||||
res = await fetchApi({ input, init, useUnsafe, translator, token });
|
res = await fetchApi({ input, init, translator, token });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res?.ok) {
|
if (!res?.ok) {
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import {
|
|||||||
STOKEY_FAB,
|
STOKEY_FAB,
|
||||||
GLOBLA_RULE,
|
GLOBLA_RULE,
|
||||||
GLOBAL_KEY,
|
GLOBAL_KEY,
|
||||||
BUILTIN_RULES,
|
DEFAULT_SUBRULES_LIST,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { browser } from "./browser";
|
import { browser } from "./browser";
|
||||||
|
import { isMatch } from "./utils";
|
||||||
|
import { tryLoadRules } from "./rules";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取节点列表并转为数组
|
* 获取节点列表并转为数组
|
||||||
@@ -48,18 +50,29 @@ export const setFab = async (obj) => await storage.setObj(STOKEY_FAB, obj);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据href匹配规则
|
* 根据href匹配规则
|
||||||
* TODO: 支持通配符(*)匹配
|
|
||||||
* @param {*} rules
|
* @param {*} rules
|
||||||
* @param {string} href
|
* @param {string} href
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const matchRule = (rules, href, { injectRules }) => {
|
export const matchRule = async (
|
||||||
|
rules,
|
||||||
|
href,
|
||||||
|
{ injectRules, subrulesList = DEFAULT_SUBRULES_LIST }
|
||||||
|
) => {
|
||||||
if (injectRules) {
|
if (injectRules) {
|
||||||
rules.splice(-1, 0, ...BUILTIN_RULES);
|
try {
|
||||||
|
const selectedSub = subrulesList.find((item) => item.selected);
|
||||||
|
if (selectedSub?.url) {
|
||||||
|
const subRules = await tryLoadRules(selectedSub.url);
|
||||||
|
rules.splice(-1, 0, ...subRules);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[load injectRules]", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rule = rules.find((rule) =>
|
const rule = rules.find((rule) =>
|
||||||
rule.pattern.split(",").some((p) => href.includes(p.trim()))
|
rule.pattern.split(",").some((p) => isMatch(href, p.trim()))
|
||||||
);
|
);
|
||||||
const globalRule =
|
const globalRule =
|
||||||
rules.find((rule) =>
|
rules.find((rule) =>
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* 任务池
|
||||||
|
* @param {*} fn
|
||||||
|
* @param {*} preFn
|
||||||
|
* @param {*} _interval
|
||||||
|
* @param {*} _limit
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
||||||
const pool = [];
|
const pool = [];
|
||||||
const maxRetry = 2; // 最大重试次数
|
const maxRetry = 2; // 最大重试次数
|
||||||
@@ -6,11 +14,6 @@ export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
|||||||
let interval = _interval; // 间隔时间
|
let interval = _interval; // 间隔时间
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务池
|
|
||||||
* @param {*} item
|
|
||||||
* @param {*} preArgs
|
|
||||||
*/
|
|
||||||
const handleTask = async (item, preArgs) => {
|
const handleTask = async (item, preArgs) => {
|
||||||
curCount++;
|
curCount++;
|
||||||
const { args, resolve, reject, retry } = item;
|
const { args, resolve, reject, retry } = item;
|
||||||
|
|||||||
99
src/libs/rules.js
Normal file
99
src/libs/rules.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import storage from "./storage";
|
||||||
|
import { fetchPolyfill } from "./fetch";
|
||||||
|
import { matchValue, type } from "./utils";
|
||||||
|
import {
|
||||||
|
STOKEY_RULESCACHE_PREFIX,
|
||||||
|
GLOBAL_KEY,
|
||||||
|
OPT_TRANS_ALL,
|
||||||
|
OPT_STYLE_ALL,
|
||||||
|
OPT_LANGS_FROM,
|
||||||
|
OPT_LANGS_TO,
|
||||||
|
} from "../config";
|
||||||
|
|
||||||
|
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
|
||||||
|
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查过滤rules
|
||||||
|
* @param {*} rules
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const checkRules = (rules) => {
|
||||||
|
if (type(rules) === "string") {
|
||||||
|
rules = JSON.parse(rules);
|
||||||
|
}
|
||||||
|
if (type(rules) !== "array") {
|
||||||
|
throw new Error("data error");
|
||||||
|
}
|
||||||
|
|
||||||
|
const patternSet = new Set();
|
||||||
|
rules = rules
|
||||||
|
.filter((rule) => type(rule) === "object")
|
||||||
|
.filter(({ pattern }) => {
|
||||||
|
if (type(pattern) !== "string" || patternSet.has(pattern.trim())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
patternSet.add(pattern.trim());
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(
|
||||||
|
({
|
||||||
|
pattern,
|
||||||
|
selector,
|
||||||
|
translator,
|
||||||
|
fromLang,
|
||||||
|
toLang,
|
||||||
|
textStyle,
|
||||||
|
transOpen,
|
||||||
|
bgColor,
|
||||||
|
}) => ({
|
||||||
|
pattern: pattern.trim(),
|
||||||
|
selector: type(selector) === "string" ? selector : "",
|
||||||
|
bgColor: type(bgColor) === "string" ? bgColor : "",
|
||||||
|
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
|
||||||
|
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
|
||||||
|
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
|
||||||
|
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
|
||||||
|
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地rules缓存
|
||||||
|
*/
|
||||||
|
export const rulesCache = {
|
||||||
|
fetch: async (url) => {
|
||||||
|
const res = await fetchPolyfill(url);
|
||||||
|
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}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从缓存或远程加载订阅的rules
|
||||||
|
* @param {*} url
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const tryLoadRules = async (url) => {
|
||||||
|
let rules = await rulesCache.get(url);
|
||||||
|
if (rules?.length) {
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
rules = await rulesCache.fetch(url);
|
||||||
|
await rulesCache.set(url, rules);
|
||||||
|
return rules;
|
||||||
|
};
|
||||||
@@ -3,18 +3,22 @@ import {
|
|||||||
DEFAULT_SYNC,
|
DEFAULT_SYNC,
|
||||||
KV_SETTING_KEY,
|
KV_SETTING_KEY,
|
||||||
KV_RULES_KEY,
|
KV_RULES_KEY,
|
||||||
|
KV_RULES_SHARE_KEY,
|
||||||
STOKEY_SETTING,
|
STOKEY_SETTING,
|
||||||
STOKEY_RULES,
|
STOKEY_RULES,
|
||||||
|
KV_SALT_SHARE,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import storage from "../libs/storage";
|
import storage from "../libs/storage";
|
||||||
import { getSetting, getRules } from ".";
|
import { getSetting, getRules } from ".";
|
||||||
import { apiSyncData } from "../apis";
|
import { apiSyncData } from "../apis";
|
||||||
|
import { sha256 } from "./utils";
|
||||||
|
|
||||||
const loadOpt = async () => (await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC;
|
export const loadSyncOpt = async () =>
|
||||||
|
(await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC;
|
||||||
|
|
||||||
export const syncSetting = async () => {
|
export const syncSetting = async () => {
|
||||||
try {
|
try {
|
||||||
const { syncUrl, syncKey, settingUpdateAt } = await loadOpt();
|
const { syncUrl, syncKey, settingUpdateAt } = await loadSyncOpt();
|
||||||
if (!syncUrl || !syncKey) {
|
if (!syncUrl || !syncKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -44,7 +48,7 @@ export const syncSetting = async () => {
|
|||||||
|
|
||||||
export const syncRules = async () => {
|
export const syncRules = async () => {
|
||||||
try {
|
try {
|
||||||
const { syncUrl, syncKey, rulesUpdateAt } = await loadOpt();
|
const { syncUrl, syncKey, rulesUpdateAt } = await loadSyncOpt();
|
||||||
if (!syncUrl || !syncKey) {
|
if (!syncUrl || !syncKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -68,10 +72,21 @@ export const syncRules = async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[sync rules]", err);
|
console.log("[sync user rules]", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
||||||
|
await apiSyncData(syncUrl, syncKey, {
|
||||||
|
key: KV_RULES_SHARE_KEY,
|
||||||
|
value: rules,
|
||||||
|
updateAt: Date.now(),
|
||||||
|
});
|
||||||
|
const psk = await sha256(syncKey, KV_SALT_SHARE);
|
||||||
|
const shareUrl = `${syncUrl}?psk=${psk}`;
|
||||||
|
return shareUrl;
|
||||||
|
};
|
||||||
|
|
||||||
export const syncAll = async () => {
|
export const syncAll = async () => {
|
||||||
await syncSetting();
|
await syncSetting();
|
||||||
await syncRules();
|
await syncRules();
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import {
|
|||||||
TRANS_MAX_LENGTH,
|
TRANS_MAX_LENGTH,
|
||||||
EVENT_KISS,
|
EVENT_KISS,
|
||||||
MSG_TRANS_CURRULE,
|
MSG_TRANS_CURRULE,
|
||||||
|
OPT_STYLE_DASHLINE,
|
||||||
|
OPT_STYLE_FUZZY,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { StoragesProvider } from "../hooks/Storage";
|
|
||||||
import { queryEls } from ".";
|
import { queryEls } from ".";
|
||||||
import Content from "../views/Content";
|
import Content from "../views/Content";
|
||||||
import { fetchUpdate, fetchClear } from "./fetch";
|
import { fetchUpdate, fetchClear } from "./fetch";
|
||||||
@@ -87,6 +88,14 @@ export class Translator {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleStyle = () => {
|
||||||
|
const textStyle =
|
||||||
|
this.rule.textStyle === OPT_STYLE_FUZZY
|
||||||
|
? OPT_STYLE_DASHLINE
|
||||||
|
: OPT_STYLE_FUZZY;
|
||||||
|
this.rule = { ...this.rule, textStyle };
|
||||||
|
};
|
||||||
|
|
||||||
_register = () => {
|
_register = () => {
|
||||||
// 监听节点变化
|
// 监听节点变化
|
||||||
this._mutaObserver.observe(document, {
|
this._mutaObserver.observe(document, {
|
||||||
@@ -144,10 +153,6 @@ export class Translator {
|
|||||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||||
|
|
||||||
const root = createRoot(span);
|
const root = createRoot(span);
|
||||||
root.render(
|
root.render(<Content q={q} translator={this} />);
|
||||||
<StoragesProvider>
|
|
||||||
<Content q={q} translator={this} />
|
|
||||||
</StoragesProvider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,3 +51,63 @@ export const debounce = (func, delay = 200) => {
|
|||||||
}, delay);
|
}, delay);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字符串通配符(*)匹配
|
||||||
|
* @param {*} s
|
||||||
|
* @param {*} p
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const isMatch = (s, p) => {
|
||||||
|
if (s.length === 0 || p.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
p = `*${p}*`;
|
||||||
|
|
||||||
|
let [sIndex, pIndex] = [0, 0];
|
||||||
|
let [sRecord, pRecord] = [-1, -1];
|
||||||
|
while (sIndex < s.length && pRecord < p.length) {
|
||||||
|
if (p[pIndex] === "*") {
|
||||||
|
pIndex++;
|
||||||
|
[sRecord, pRecord] = [sIndex, pIndex];
|
||||||
|
} else if (s[sIndex] === p[pIndex]) {
|
||||||
|
sIndex++;
|
||||||
|
pIndex++;
|
||||||
|
} else if (sRecord + 1 < s.length) {
|
||||||
|
sRecord++;
|
||||||
|
[sIndex, pIndex] = [sRecord, pRecord];
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.length === pIndex) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.slice(pIndex).replaceAll("*", "") === "";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型检查
|
||||||
|
* @param {*} o
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const type = (o) => {
|
||||||
|
const s = Object.prototype.toString.call(o);
|
||||||
|
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sha256
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const sha256 = async (text, salt) => {
|
||||||
|
const data = new TextEncoder().encode(text + salt);
|
||||||
|
const digest = await crypto.subtle.digest({ name: "SHA-256" }, data);
|
||||||
|
return [...new Uint8Array(digest)]
|
||||||
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
|
.join("");
|
||||||
|
};
|
||||||
|
|||||||
17
src/rules.js
Normal file
17
src/rules.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { BUILTIN_RULES } from "./config/rules";
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
const data = JSON.stringify(BUILTIN_RULES, null, " ");
|
||||||
|
const file = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../build/web/kiss-translator-rules.json"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(file, data);
|
||||||
|
console.info(`Built-in rules generated: ${file}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -29,7 +29,7 @@ import { Translator } from "./libs/translator";
|
|||||||
// 翻译页面
|
// 翻译页面
|
||||||
const setting = await getSetting();
|
const setting = await getSetting();
|
||||||
const rules = await getRules();
|
const rules = await getRules();
|
||||||
const rule = matchRule(rules, document.location.href, setting);
|
const rule = await matchRule(rules, document.location.href, setting);
|
||||||
const translator = new Translator(rule, setting);
|
const translator = new Translator(rule, setting);
|
||||||
|
|
||||||
// 浮球按钮
|
// 浮球按钮
|
||||||
@@ -54,4 +54,20 @@ import { Translator } from "./libs/translator";
|
|||||||
</CacheProvider>
|
</CacheProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 注册菜单
|
||||||
|
GM.registerMenuCommand(
|
||||||
|
"Toggle Translate",
|
||||||
|
(event) => {
|
||||||
|
translator.toggle();
|
||||||
|
},
|
||||||
|
"Q"
|
||||||
|
);
|
||||||
|
GM.registerMenuCommand(
|
||||||
|
"Toggle Style",
|
||||||
|
(event) => {
|
||||||
|
translator.toggleStyle();
|
||||||
|
},
|
||||||
|
"C"
|
||||||
|
);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export default function Action({ translator, fab }) {
|
|||||||
windowSize,
|
windowSize,
|
||||||
width: fabWidth,
|
width: fabWidth,
|
||||||
height: fabWidth,
|
height: fabWidth,
|
||||||
left: fab.x ?? windowSize.w - fabWidth,
|
left: fab.x ?? 0,
|
||||||
top: fab.y ?? windowSize.h / 2,
|
top: fab.y ?? windowSize.h / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,7 +112,9 @@ export default function Action({ translator, fab }) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Paper>
|
<Paper>
|
||||||
<Popup setShowPopup={setShowPopup} translator={translator} />
|
{showPopup && (
|
||||||
|
<Popup setShowPopup={setShowPopup} translator={translator} />
|
||||||
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
<Draggable
|
<Draggable
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Box from "@mui/material/Box";
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
import {
|
import {
|
||||||
GLOBAL_KEY,
|
GLOBAL_KEY,
|
||||||
DEFAULT_RULE,
|
DEFAULT_RULE,
|
||||||
@@ -9,9 +10,8 @@ import {
|
|||||||
OPT_LANGS_TO,
|
OPT_LANGS_TO,
|
||||||
OPT_TRANS_ALL,
|
OPT_TRANS_ALL,
|
||||||
OPT_STYLE_ALL,
|
OPT_STYLE_ALL,
|
||||||
BUILTIN_RULES,
|
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Accordion from "@mui/material/Accordion";
|
import Accordion from "@mui/material/Accordion";
|
||||||
@@ -26,6 +26,18 @@ import FileUploadIcon from "@mui/icons-material/FileUpload";
|
|||||||
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
|
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
|
||||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
import Switch from "@mui/material/Switch";
|
import Switch from "@mui/material/Switch";
|
||||||
|
import Tabs from "@mui/material/Tabs";
|
||||||
|
import Tab from "@mui/material/Tab";
|
||||||
|
import Radio from "@mui/material/Radio";
|
||||||
|
import RadioGroup from "@mui/material/RadioGroup";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import ShareIcon from "@mui/icons-material/Share";
|
||||||
|
import SyncIcon from "@mui/icons-material/Sync";
|
||||||
|
import { useSubrules } from "../../hooks/Rules";
|
||||||
|
import { rulesCache, tryLoadRules } from "../../libs/rules";
|
||||||
|
import { useAlert } from "../../hooks/Alert";
|
||||||
|
import { loadSyncOpt, syncShareRules } from "../../libs/sync";
|
||||||
|
|
||||||
function RuleFields({ rule, rules, setShow }) {
|
function RuleFields({ rule, rules, setShow }) {
|
||||||
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
|
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
|
||||||
@@ -384,12 +396,57 @@ function UploadButton({ onChange, text }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Rules() {
|
function ShareButton({ rules, injectRules, selectedSub }) {
|
||||||
|
const alert = useAlert();
|
||||||
|
const i18n = useI18n();
|
||||||
|
const handleClick = async () => {
|
||||||
|
try {
|
||||||
|
const { syncUrl, syncKey } = await loadSyncOpt();
|
||||||
|
if (!syncUrl || !syncKey) {
|
||||||
|
alert.warning(i18n("error_sync_setting"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareRules = [...rules.list];
|
||||||
|
if (injectRules) {
|
||||||
|
const subRules = await tryLoadRules(selectedSub?.url);
|
||||||
|
shareRules.splice(-1, 0, ...subRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await syncShareRules({
|
||||||
|
rules: shareRules,
|
||||||
|
syncUrl,
|
||||||
|
syncKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.open(url, "_blank");
|
||||||
|
} catch (err) {
|
||||||
|
alert.warning(i18n("error_got_some_wrong"));
|
||||||
|
console.log("[share rules]", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleClick}
|
||||||
|
startIcon={<ShareIcon />}
|
||||||
|
>
|
||||||
|
{"分享"}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserRules() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const rules = useRules();
|
const rules = useRules();
|
||||||
const [showAdd, setShowAdd] = useState(false);
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
const setting = useSetting();
|
const setting = useSetting();
|
||||||
const updateSetting = useSettingUpdate();
|
const updateSetting = useSettingUpdate();
|
||||||
|
const subrules = useSubrules();
|
||||||
|
const selectedSub = subrules.list.find((item) => item.selected);
|
||||||
|
|
||||||
const injectRules = !!setting?.injectRules;
|
const injectRules = !!setting?.injectRules;
|
||||||
|
|
||||||
const handleImport = (e) => {
|
const handleImport = (e) => {
|
||||||
@@ -420,55 +477,275 @@ export default function Rules() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Stack direction="row" spacing={2} useFlexGap flexWrap="wrap">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
disabled={showAdd}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowAdd(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n("add")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<UploadButton text={i18n("import")} onChange={handleImport} />
|
||||||
|
<DownloadButton
|
||||||
|
data={JSON.stringify([...rules.list].reverse(), null, "\t")}
|
||||||
|
text={i18n("export")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ShareButton
|
||||||
|
rules={rules}
|
||||||
|
injectRules={injectRules}
|
||||||
|
selectedSub={selectedSub}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={injectRules}
|
||||||
|
onChange={handleInject}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n("inject_rules")}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{showAdd && <RuleFields rules={rules} setShow={setShowAdd} />}
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
{rules.list.map((rule) => (
|
||||||
|
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleDel = async () => {
|
||||||
|
try {
|
||||||
|
await subrules.del(url);
|
||||||
|
await rulesCache.del(url);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[del subrules]", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSync = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const rules = await rulesCache.fetch(url);
|
||||||
|
await rulesCache.set(url, rules);
|
||||||
|
if (url === selectedUrl) {
|
||||||
|
setRules(rules);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[sync sub rules]", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="row" alignItems="center" spacing={2}>
|
||||||
|
<FormControlLabel value={url} control={<Radio />} label={url} />
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<CircularProgress size={16} />
|
||||||
|
) : (
|
||||||
|
<IconButton size="small" onClick={handleSync}>
|
||||||
|
<SyncIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{index !== 0 && selectedUrl !== url && (
|
||||||
|
<IconButton size="small" onClick={handleDel}>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubRulesEdit({ subrules }) {
|
||||||
|
const i18n = useI18n();
|
||||||
|
const [inputText, setInputText] = useState("");
|
||||||
|
const [inputError, setInputError] = useState("");
|
||||||
|
const [showInput, setShowInput] = useState(false);
|
||||||
|
|
||||||
|
const handleCancel = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowInput(false);
|
||||||
|
setInputText("");
|
||||||
|
setInputError("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const url = inputText.trim();
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
setInputError(i18n("error_cant_be_blank"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subrules.list.find((item) => item.url === url)) {
|
||||||
|
setInputError(i18n("error_duplicate_values"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rules = await rulesCache.fetch(url);
|
||||||
|
if (rules.length === 0) {
|
||||||
|
throw new Error("empty rules");
|
||||||
|
}
|
||||||
|
await rulesCache.set(url, rules);
|
||||||
|
await subrules.add(url);
|
||||||
|
setShowInput(false);
|
||||||
|
setInputText("");
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[fetch rules]", err);
|
||||||
|
setInputError(i18n("error_fetch_url"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInput = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setInputText(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setInputError("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={2}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
disabled={showInput}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowInput(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n("add")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{showInput && (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={inputText}
|
||||||
|
error={!!inputError}
|
||||||
|
helperText={inputError}
|
||||||
|
onChange={handleInput}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
label={i18n("subscribe_url")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack direction="row" alignItems="center" spacing={2}>
|
||||||
|
<Button size="small" variant="contained" onClick={handleSave}>
|
||||||
|
{i18n("save")}
|
||||||
|
</Button>
|
||||||
|
<Button size="small" variant="outlined" onClick={handleCancel}>
|
||||||
|
{i18n("cancel")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubRules() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [rules, setRules] = useState([]);
|
||||||
|
const subrules = useSubrules();
|
||||||
|
const selectedSub = subrules.list.find((item) => item.selected);
|
||||||
|
|
||||||
|
const handleSelect = (e) => {
|
||||||
|
const url = e.target.value;
|
||||||
|
subrules.select(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (selectedSub?.url) {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const rules = await tryLoadRules(selectedSub?.url);
|
||||||
|
setRules(rules);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[load rules]", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [selectedSub?.url]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<SubRulesEdit subrules={subrules} />
|
||||||
|
|
||||||
|
<RadioGroup value={selectedSub?.url} onChange={handleSelect}>
|
||||||
|
{subrules.list.map((item, index) => (
|
||||||
|
<SubRulesItem
|
||||||
|
key={item.url}
|
||||||
|
url={item.url}
|
||||||
|
index={index}
|
||||||
|
selectedUrl={selectedSub?.url}
|
||||||
|
subrules={subrules}
|
||||||
|
setRules={setRules}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
{loading ? (
|
||||||
|
<center>
|
||||||
|
<CircularProgress />
|
||||||
|
</center>
|
||||||
|
) : (
|
||||||
|
rules.map((rule) => <RuleAccordion key={rule.pattern} rule={rule} />)
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Rules() {
|
||||||
|
const i18n = useI18n();
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
|
||||||
|
const handleTabChange = (e, newValue) => {
|
||||||
|
setActiveTab(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Stack direction="row" spacing={2}>
|
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||||
<Button
|
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||||
size="small"
|
<Tab label={i18n("edit_rules")} />
|
||||||
variant="contained"
|
<Tab label={i18n("subscribe_rules")} />
|
||||||
disabled={showAdd}
|
</Tabs>
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setShowAdd(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{i18n("add")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<UploadButton text={i18n("import")} onChange={handleImport} />
|
|
||||||
<DownloadButton
|
|
||||||
data={JSON.stringify([...rules.list].reverse(), null, "\t")}
|
|
||||||
text={i18n("export")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
checked={injectRules}
|
|
||||||
onChange={handleInject}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={i18n("inject_rules")}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{showAdd && <RuleFields rules={rules} setShow={setShowAdd} />}
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
{rules.list.map((rule) => (
|
|
||||||
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} />
|
|
||||||
))}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
<div hidden={activeTab !== 0}>{activeTab === 0 && <UserRules />}</div>
|
||||||
{injectRules && (
|
<div hidden={activeTab !== 1}>{activeTab === 1 && <SubRules />}</div>
|
||||||
<Box>
|
|
||||||
{BUILTIN_RULES.map((rule) => (
|
|
||||||
<RuleAccordion key={rule.pattern} rule={rule} />
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Link from "@mui/material/Link";
|
|||||||
import { URL_KISS_WORKER } from "../../config";
|
import { URL_KISS_WORKER } from "../../config";
|
||||||
import { debounce } from "../../libs/utils";
|
import { debounce } from "../../libs/utils";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { syncAll } from "../../libs/sync";
|
||||||
|
|
||||||
export default function SyncSetting() {
|
export default function SyncSetting() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -15,13 +16,14 @@ export default function SyncSetting() {
|
|||||||
|
|
||||||
const handleChange = useMemo(
|
const handleChange = useMemo(
|
||||||
() =>
|
() =>
|
||||||
debounce((e) => {
|
debounce(async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
sync.update({
|
await sync.update({
|
||||||
[name]: value,
|
[name]: value,
|
||||||
});
|
});
|
||||||
}, 500),
|
await syncAll();
|
||||||
|
}, 1000),
|
||||||
[sync]
|
[sync]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { isGm } from "../../libs/browser";
|
|||||||
import { sleep } from "../../libs/utils";
|
import { sleep } from "../../libs/utils";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
import { syncAll } from "../../libs/sync";
|
import { syncAll } from "../../libs/sync";
|
||||||
|
import { AlertProvider } from "../../hooks/Alert";
|
||||||
|
|
||||||
export default function Options() {
|
export default function Options() {
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
@@ -69,16 +70,18 @@ export default function Options() {
|
|||||||
return (
|
return (
|
||||||
<StoragesProvider>
|
<StoragesProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<HashRouter>
|
<AlertProvider>
|
||||||
<Routes>
|
<HashRouter>
|
||||||
<Route path="/" element={<Layout />}>
|
<Routes>
|
||||||
<Route index element={<Setting />} />
|
<Route path="/" element={<Layout />}>
|
||||||
<Route path="rules" element={<Rules />} />
|
<Route index element={<Setting />} />
|
||||||
<Route path="sync" element={<SyncSetting />} />
|
<Route path="rules" element={<Rules />} />
|
||||||
<Route path="about" element={<About />} />
|
<Route path="sync" element={<SyncSetting />} />
|
||||||
</Route>
|
<Route path="about" element={<About />} />
|
||||||
</Routes>
|
</Route>
|
||||||
</HashRouter>
|
</Routes>
|
||||||
|
</HashRouter>
|
||||||
|
</AlertProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StoragesProvider>
|
</StoragesProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user