Compare commits

...

29 Commits

Author SHA1 Message Date
Gabe Yuan
732a526a8e v1.6.0 2023-08-31 21:24:40 +08:00
Gabe Yuan
2da5ffef44 update userscript download link 2023-08-31 21:21:36 +08:00
Gabe Yuan
2e6e52004f fix firefox bug 2023-08-31 20:57:51 +08:00
Gabe Yuan
4486ad353c dev.....! 2023-08-31 13:38:06 +08:00
Gabe Yuan
aa795e2731 dev...... 2023-08-31 00:18:57 +08:00
Gabe Yuan
c46fe7d1c6 dev... 2023-08-30 18:05:37 +08:00
Gabe Yuan
d7cee8cca6 catch caches is not defined 2023-08-29 21:33:27 +08:00
Gabe Yuan
11f790ace5 catch caches is not defined 2023-08-29 21:24:25 +08:00
Gabe Yuan
13e7c1b754 fix safari webkit 2023-08-29 17:34:37 +08:00
Gabe Yuan
d314d5515f v1.5.8 2023-08-29 16:52:48 +08:00
Gabe Yuan
09b19e3ca0 fix webkit style in safari 2023-08-29 16:48:38 +08:00
Gabe Yuan
687bd11fd1 fix some text 2023-08-29 13:14:12 +08:00
Gabe Yuan
56cb1cd30d fix links 2023-08-29 11:53:02 +08:00
Gabe Yuan
7a3df25521 generate version file to web 2023-08-29 10:41:20 +08:00
Gabe Yuan
ea8919ba07 update readme 2023-08-29 10:30:18 +08:00
Gabe Yuan
3dece4fcdb add version tag to loading page 2023-08-29 01:35:09 +08:00
Gabe Yuan
df950a1bd2 use createElement script 2023-08-29 01:17:22 +08:00
Gabe Yuan
74b9ee31fa eslint-disable-line 2023-08-29 00:52:37 +08:00
Gabe Yuan
64cd55fe58 set no-eval 2023-08-29 00:42:11 +08:00
Gabe Yuan
e80ede14fb v1.5.7 2023-08-29 00:09:09 +08:00
Gabe Yuan
45ba9d3320 use inject-into replace unsafeWindow 2023-08-29 00:06:50 +08:00
Gabe Yuan
47c7048538 injectscript... 2023-08-28 17:59:51 +08:00
Gabe Yuan
f9bfa8101f fix detectLanguage 2023-08-28 11:14:03 +08:00
Gabe Yuan
620ac464eb v1.5.6 2023-08-27 17:59:47 +08:00
Gabe Yuan
62289f8ab8 catch detect lang err 2023-08-27 17:43:27 +08:00
Gabe Yuan
d84594da96 catch global error and display on top of page 2023-08-27 16:45:57 +08:00
Gabe Yuan
e1d74aae6a catch global error and display on top of page 2023-08-27 16:41:14 +08:00
Gabe Yuan
c4980d9eb7 fix rules 2023-08-26 22:12:48 +08:00
Gabe Yuan
882d83c6b7 update helper text 2023-08-26 15:08:21 +08:00
49 changed files with 1061 additions and 825 deletions

27
.env
View File

@@ -2,12 +2,25 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=1.5.5
REACT_APP_VERSION=1.6.0
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
REACT_APP_OPTIONSPAGE=https://kiss-translator.rayjar.com/options
REACT_APP_OPTIONSPAGE2=https://fishjar.github.io/kiss-translator/options.html
REACT_APP_OPTIONSPAGE=https://fishjar.github.io/kiss-translator/options.html
REACT_APP_OPTIONSPAGE2=https://kiss-translator.rayjar.com/options
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
REACT_APP_LOGOURL=https://kiss-translator.rayjar.com/images/logo192.png
REACT_APP_RULESURL=https://kiss-translator.rayjar.com/kiss-translator-rules.json
REACT_APP_USERSCRIPT_DOWNLOADURL=https://kiss-translator.rayjar.com/kiss-translator.user.js
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
REACT_APP_LOGOURL2=https://kiss-translator.rayjar.com/images/logo192.png
REACT_APP_RULESURL=https://fishjar.github.io/kiss-translator/kiss-translator-rules.json
REACT_APP_RULESURL2=https://kiss-translator.rayjar.com/kiss-translator-rules.json
REACT_APP_VERSIONFILE=https://fishjar.github.io/kiss-translator/version.txt
REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt
REACT_APP_USERSCRIPT_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator.user.js
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator-ios-safari.user.js

View File

@@ -33,16 +33,16 @@ If you also like a little more simplicity, welcome to pick it up.
- [x] OpenAI
- [ ] DeepL
- [x] Upload to app Store
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
- [x] [Edge](https://microsoftedge.microsoft.com/addons/detail/kiss-translator/jemckldkclkinpjighnoilpbldbdmmlh)
- [x] [Firefox](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [x] Chrome [Install Link](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
- [x] Edge [Install Link](https://microsoftedge.microsoft.com/addons/detail/kiss-translator/jemckldkclkinpjighnoilpbldbdmmlh)
- [x] Firefox [Install Link](https://addons.mozilla.org/en-US/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] Greasy Fork [Install Link](https://greasyfork.org/en/scripts/472840-kiss-translator)
- [x] Open source
- [x] Data Synchronization Function
- [x] Greasemonkey Script ([link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js))
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox)
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (need test)
- [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] [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

View File

@@ -33,16 +33,16 @@
- [x] OpenAI
- [ ] DeepL
- [x] 上架应用市场
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] [Edge](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] [Firefox](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Edge [安装地址](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] Greasy Fork [安装地址](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] 开放源代码
- [x] 数据同步功能
- [x] 油猴脚本([链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js))
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox)
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (待测)
- [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] [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)
### 指引

View File

@@ -1,7 +1,7 @@
{
"name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.5.5",
"version": "1.6.0",
"author": "Gabe<yugang2002@gmail.com>",
"private": true,
"dependencies": {
@@ -25,8 +25,9 @@
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json",
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/kiss-translator.user.js build/userscript/kiss-translator.user.js",
"build:userscript-ios": "file1=build/userscript/kiss-translator.user.js file2=build/userscript/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
"build:rules": "babel-node src/rules.js",
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript && yarn build:rules",
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript && yarn build:userscript-ios && yarn build:rules",
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
"test": "react-app-rewired test",
"eject": "react-scripts eject"

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.5.5",
"version": "1.6.0",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.5.5",
"version": "1.6.0",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -10,7 +10,7 @@ import {
PROMPT_PLACE_TO,
KV_SALT_SYNC,
} from "../config";
import { getSetting, detectLang } from "../libs";
import { tryDetectLang } from "../libs";
import { sha256 } from "../libs/utils";
/**
@@ -31,6 +31,15 @@ export const apiSyncData = async (url, key, data, isBg = false) =>
isBg,
});
/**
* 下载订阅规则
* @param {*} url
* @param {*} isBg
* @returns
*/
export const apiFetchRules = (url, isBg = false) =>
fetchPolyfill(url, { isBg });
/**
* 谷歌翻译
* @param {*} text
@@ -38,7 +47,8 @@ export const apiSyncData = async (url, key, data, isBg = false) =>
* @param {*} from
* @returns
*/
const apiGoogleTranslate = async (translator, text, to, from) => {
const apiGoogleTranslate = async (translator, text, to, from, setting) => {
const { googleUrl } = setting;
const params = {
client: "gtx",
dt: "t",
@@ -48,7 +58,6 @@ const apiGoogleTranslate = async (translator, text, to, from) => {
tl: to,
q: text,
};
const { googleUrl } = await getSetting();
const input = `${googleUrl}?${queryString.stringify(params)}`;
return fetchPolyfill(input, {
headers: {
@@ -93,9 +102,8 @@ const apiMicrosoftTranslate = (translator, text, to, from) => {
* @param {*} from
* @returns
*/
const apiOpenaiTranslate = async (translator, text, to, from) => {
const { openaiUrl, openaiKey, openaiModel, openaiPrompt } =
await getSetting();
const apiOpenaiTranslate = async (translator, text, to, from, setting) => {
const { openaiUrl, openaiKey, openaiModel, openaiPrompt } = setting;
let prompt = openaiPrompt
.replaceAll(PROMPT_PLACE_FROM, from)
.replaceAll(PROMPT_PLACE_TO, to);
@@ -131,7 +139,13 @@ const apiOpenaiTranslate = async (translator, text, to, from) => {
* @param {*} param0
* @returns
*/
export const apiTranslate = async ({ translator, q, fromLang, toLang }) => {
export const apiTranslate = async ({
translator,
q,
fromLang,
toLang,
setting,
}) => {
let trText = "";
let isSame = false;
@@ -139,7 +153,7 @@ export const apiTranslate = async ({ translator, q, fromLang, toLang }) => {
let to = OPT_LANGS_SPECIAL?.[translator]?.get(toLang) ?? toLang;
if (translator === OPT_TRANS_GOOGLE) {
const res = await apiGoogleTranslate(translator, q, to, from);
const res = await apiGoogleTranslate(translator, q, to, from, setting);
trText = res.sentences.map((item) => item.trans).join(" ");
isSame = to === res.src;
} else if (translator === OPT_TRANS_MICROSOFT) {
@@ -147,9 +161,11 @@ export const apiTranslate = async ({ translator, q, fromLang, toLang }) => {
trText = res[0].translations[0].text;
isSame = to === res[0].detectedLanguage.language;
} else if (translator === OPT_TRANS_OPENAI) {
const res = await apiOpenaiTranslate(translator, q, to, from);
const res = await apiOpenaiTranslate(translator, q, to, from, setting);
trText = res?.choices?.[0].message.content;
isSame = (await detectLang(q)) === (await detectLang(trText));
const sLang = await tryDetectLang(q);
const tLang = await tryDetectLang(trText);
isSame = q === trText || (sLang && tLang && sLang === tLang);
}
return [trText, isSame];

View File

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

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

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

View File

@@ -193,8 +193,8 @@ export const I18N = {
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`,
},
selector_helper: {
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。`,
en: `1. Follow CSS selector rules. 2. Leave blank to adopt the global setting.`,
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`,
en: `1. Follow the CSS selector rules. 2. Leave blank to adopt the global setting. 3. Separate multiple CSS selectors with ";". 4. The "shadow root" selector and the internal selector are separated by ">>>".`,
},
translate_switch: {
zh: `开启翻译`,
@@ -285,11 +285,11 @@ export const I18N = {
en: `Data Sync Error`,
},
error_got_some_wrong: {
zh: "抱歉,出错了!",
en: "Sorry, something went wrong!",
zh: `抱歉,出错了!`,
en: `Sorry, something went wrong!`,
},
error_sync_setting: {
zh: "您的同步设置未填写,无法在线分享。",
en: "Your sync settings are missing and cannot be shared online.",
zh: `您的同步设置未填写,无法在线分享。`,
en: `Your sync settings are missing and cannot be shared online.`,
},
};

View File

@@ -5,12 +5,9 @@ import {
DEFAULT_RULE,
BUILTIN_RULES,
} from "./rules";
import { APP_NAME, APP_LCNAME } from "./app";
export { I18N, UI_LANGS } from "./i18n";
export { GLOBAL_KEY, SHADOW_KEY, DEFAULT_RULE, BUILTIN_RULES };
const APP_NAME = process.env.REACT_APP_NAME.trim().split(/\s+/).join("-");
export const APP_LCNAME = APP_NAME.toLowerCase();
export { GLOBAL_KEY, SHADOW_KEY, DEFAULT_RULE, BUILTIN_RULES, APP_LCNAME };
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
export const STOKEY_SETTING = `${APP_NAME}_setting`;
@@ -162,7 +159,8 @@ export const DEFAULT_SUBRULES_LIST = [
selected: true,
},
{
url: "https://fishjar.github.io/kiss-translator/kiss-translator-rules.json",
url: process.env.REACT_APP_RULESURL2,
selected: false,
},
];

View File

@@ -28,7 +28,7 @@ const RULES = [
},
{
pattern: `www.foxnews.com`,
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content :is(li, p, h1, h2, h3, h4, h5, h6, dd); [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
},
{
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,

View File

@@ -5,17 +5,18 @@ import {
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
} from "./config";
import { getSetting, getRules, matchRule } from "./libs";
import { getSettingWithDefault, getRulesWithDefault } from "./libs/storage";
import { Translator } from "./libs/translator";
import { isIframe } from "./libs/iframe";
import { matchRule } from "./libs/rules";
/**
* 入口函数
*/
(async () => {
const init = async () => {
const href = isIframe ? document.referrer : document.location.href;
const setting = await getSetting();
const rules = await getRules();
const setting = await getSettingWithDefault();
const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);
@@ -38,4 +39,15 @@ import { isIframe } from "./libs/iframe";
}
return { data: translator.rule };
});
};
(async () => {
try {
await init();
} catch (err) {
const $err = document.createElement("div");
$err.innerText = `KISS-Translator: ${err.message}`;
$err.style.cssText = "background:red; color:#fff; z-index:10000;";
document.body.prepend($err);
}
})();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import ReactMarkdown from "react-markdown";
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Button from "@mui/material/Button";
import Link from "@mui/material/Link";
import { useFetch } from "./hooks/Fetch";
import { I18N, URL_RAW_PREFIX } from "./config";
@@ -26,7 +27,32 @@ function App() {
{lang === "zh" ? "ENGLISH" : "中文"}
</Button>
</Stack>
<Divider>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Divider>
<Divider>
<Link
href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider>
<Stack spacing={2} direction="row" useFlexGap flexWrap="wrap">
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
Install Userscript 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install Userscript 2
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install Userscript Safari 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install Userscript Safari 2
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
Open Options Page 1
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
Open Options Page 2
</Link>
</Stack>
{loading ? (
<center>
<CircularProgress />

View File

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

View File

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

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

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

View File

@@ -1,5 +1,5 @@
import { isExt, isGm } from "./browser";
import { sendMsg } from "./msg";
import { isExt, isGm } from "./client";
import { sendBgMsg } from "./msg";
import { taskPool } from "./pool";
import {
MSG_FETCH,
@@ -19,7 +19,7 @@ import { msAuth } from "./auth";
* @param {*} init
* @returns
*/
const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method,
@@ -74,11 +74,21 @@ const fetchApi = async ({ input, init = {}, translator, token }) => {
}
if (isGm) {
const connects = GM?.info?.script?.connects || [];
let info;
if (window.KISS_GM) {
info = await window.KISS_GM.getInfo();
} else {
info = GM.info;
}
const connects = info?.script?.connects || [];
const url = new URL(input);
const isSafe = connects.find((item) => url.hostname.endsWith(item));
if (isSafe) {
return fetchGM(input, init);
if (window.KISS_GM) {
return window.KISS_GM.fetch(input, init);
} else {
return fetchGM(input, init);
}
}
}
return fetch(input, init);
@@ -111,15 +121,15 @@ export const fetchData = async (
{ useCache, usePool, translator, token, ...init } = {}
) => {
const cacheReq = await newCacheReq(new Request(input, init));
const cache = await caches.open(CACHE_NAME);
let res;
// 查询缓存
if (useCache) {
try {
const cache = await caches.open(CACHE_NAME);
res = await cache.match(cacheReq);
} catch (err) {
console.log("[cache match]", err);
console.log("[cache match]", err.message);
}
}
@@ -138,9 +148,10 @@ export const fetchData = async (
// 插入缓存
if (useCache) {
try {
const cache = await caches.open(CACHE_NAME);
await cache.put(cacheReq, res.clone());
} catch (err) {
console.log("[cache put]", err);
console.log("[cache put]", err.message);
}
}
}
@@ -161,7 +172,7 @@ export const fetchData = async (
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
// 插件
if (isExt && !isBg) {
const res = await sendMsg(MSG_FETCH, { input, opts });
const res = await sendBgMsg(MSG_FETCH, { input, opts });
if (res.error) {
throw new Error(res.error);
}
@@ -177,9 +188,9 @@ export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
* @param {*} interval
* @param {*} limit
*/
export const fetchUpdate = async (interval, limit) => {
export const updateFetchPool = async (interval, limit) => {
if (isExt) {
const res = await sendMsg(MSG_FETCH_LIMIT, { interval, limit });
const res = await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit });
if (res.error) {
throw new Error(res.error);
}
@@ -191,9 +202,9 @@ export const fetchUpdate = async (interval, limit) => {
/**
* 清空任务池
*/
export const fetchClear = async () => {
export const clearFetchPool = async () => {
if (isExt) {
const res = await sendMsg(MSG_FETCH_CLEAR);
const res = await sendBgMsg(MSG_FETCH_CLEAR);
if (res.error) {
throw new Error(res.error);
}

97
src/libs/gm.js Normal file
View File

@@ -0,0 +1,97 @@
import { fetchGM } from "./fetch";
/**
* 注入页面的脚本请求并接受GM接口信息
* @param {*} param0
*/
export const injectScript = (ping) => {
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
const MSG_GM_setValue = "setValue";
const MSG_GM_getValue = "getValue";
const MSG_GM_deleteValue = "deleteValue";
const MSG_GM_info = "info";
let GM_info;
const promiseGM = (action, args, timeout = 5000) =>
new Promise((resolve, reject) => {
const pong = btoa(Math.random()).slice(3, 11);
const handleEvent = (e) => {
window.removeEventListener(pong, handleEvent);
const { data, error } = e.detail;
if (error) {
reject(new Error(error));
} else {
resolve(data);
}
};
window.addEventListener(pong, handleEvent);
window.dispatchEvent(
new CustomEvent(ping, { detail: { action, args, pong } })
);
setTimeout(() => {
window.removeEventListener(pong, handleEvent);
reject(new Error("timeout"));
}, timeout);
});
window.KISS_GM = {
fetch: (input, init) => promiseGM(MSG_GM_xmlHttpRequest, { input, init }),
setValue: (key, val) => promiseGM(MSG_GM_setValue, { key, val }),
getValue: (key) => promiseGM(MSG_GM_getValue, { key }),
deleteValue: (key) => promiseGM(MSG_GM_deleteValue, { key }),
getInfo: () => {
if (GM_info) {
return GM_info;
}
return promiseGM(MSG_GM_info);
},
};
window.APP_NAME = process.env.REACT_APP_NAME;
};
/**
* 监听并回应页面对GM接口的请求
* @param {*} param0
*/
export const handlePing = async (e) => {
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
const MSG_GM_setValue = "setValue";
const MSG_GM_getValue = "getValue";
const MSG_GM_deleteValue = "deleteValue";
const MSG_GM_info = "info";
const { action, args, pong } = e.detail;
let res;
try {
switch (action) {
case MSG_GM_xmlHttpRequest:
const { input, init } = args;
res = await fetchGM(input, init);
break;
case MSG_GM_setValue:
const { key, val } = args;
await GM.setValue(key, val);
res = val;
break;
case MSG_GM_getValue:
res = await GM.getValue(args.key);
break;
case MSG_GM_deleteValue:
await GM.deleteValue(args.key);
res = "ok";
break;
case MSG_GM_info:
res = GM.info;
break;
default:
throw new Error(`message action is unavailable: ${action}`);
}
window.dispatchEvent(new CustomEvent(pong, { detail: { data: res } }));
} catch (err) {
window.dispatchEvent(
new CustomEvent(pong, { detail: { error: err.message } })
);
}
};

View File

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

View File

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

View File

@@ -1,18 +1,68 @@
import storage from "./storage";
import { fetchPolyfill } from "./fetch";
import { matchValue, type } from "./utils";
import { matchValue, type, isMatch } from "./utils";
import {
STOKEY_RULESCACHE_PREFIX,
GLOBAL_KEY,
OPT_TRANS_ALL,
OPT_STYLE_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
GLOBLA_RULE,
DEFAULT_SUBRULES_LIST,
} from "../config";
import { syncOpt } from "./sync";
import { loadOrFetchSubRules } from "./subRules";
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
/**
* 根据href匹配规则
* @param {*} rules
* @param {string} href
* @returns
*/
export const matchRule = async (
rules,
href,
{ injectRules = true, subrulesList = DEFAULT_SUBRULES_LIST }
) => {
rules = [...rules];
if (injectRules) {
try {
const selectedSub = subrulesList.find((item) => item.selected);
if (selectedSub?.url) {
const subRules = await loadOrFetchSubRules(selectedSub.url);
rules.splice(-1, 0, ...subRules);
}
} catch (err) {
console.log("[load injectRules]", err);
}
}
const rule = rules.find((r) =>
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
);
const globalRule =
rules.find((r) => r.pattern.split(",").some((p) => p.trim() === "*")) ||
GLOBLA_RULE;
if (!rule) {
return globalRule;
}
rule.selector =
rule?.selector?.trim() ||
globalRule?.selector?.trim() ||
GLOBLA_RULE.selector;
rule.bgColor = rule?.bgColor?.trim() || globalRule?.bgColor?.trim();
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach(
(key) => {
if (rule[key] === GLOBAL_KEY) {
rule[key] = globalRule[key];
}
}
);
return rule;
};
/**
* 检查过滤rules
@@ -27,6 +77,8 @@ export const checkRules = (rules) => {
throw new Error("data error");
}
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
const patternSet = new Set();
rules = rules
.filter((rule) => type(rule) === "object")
@@ -61,85 +113,3 @@ export const checkRules = (rules) => {
return rules;
};
/**
* 订阅规则的本地缓存
*/
export const rulesCache = {
fetch: async (url, isBg = false) => {
const res = await fetchPolyfill(url, { isBg });
const rules = checkRules(res).filter(
(rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== ""
);
return rules;
},
set: async (url, rules) => {
await storage.setObj(`${STOKEY_RULESCACHE_PREFIX}${url}`, rules);
},
get: async (url) => {
return await storage.getObj(`${STOKEY_RULESCACHE_PREFIX}${url}`);
},
del: async (url) => {
await storage.del(`${STOKEY_RULESCACHE_PREFIX}${url}`);
},
};
/**
* 同步订阅规则
* @param {*} url
* @returns
*/
export const syncSubRules = async (url, isBg = false) => {
const rules = await rulesCache.fetch(url, isBg);
if (rules.length > 0) {
await rulesCache.set(url, rules);
}
return rules;
};
/**
* 同步所有订阅规则
* @param {*} url
* @returns
*/
export const syncAllSubRules = async (subrulesList, isBg = false) => {
for (let subrules of subrulesList) {
try {
await syncSubRules(subrules.url, isBg);
} catch (err) {
console.log(`[sync subrule error]: ${subrules.url}`, err);
}
}
};
/**
* 根据时间同步所有订阅规则
* @param {*} url
* @returns
*/
export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
try {
const { subRulesSyncAt } = await syncOpt.load();
const now = Date.now();
const interval = 24 * 60 * 60 * 1000; // 间隔一天
if (now - subRulesSyncAt > interval) {
await syncAllSubRules(subrulesList, isBg);
await syncOpt.update({ subRulesSyncAt: now });
}
} catch (err) {
console.log("[try sync all subrules]", err);
}
};
/**
* 从缓存或远程加载订阅规则
* @param {*} url
* @returns
*/
export const loadSubRules = async (url) => {
const rules = await rulesCache.get(url);
if (rules?.length) {
return rules;
}
return await syncSubRules(url);
};

View File

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

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

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

View File

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

View File

@@ -10,7 +10,7 @@ import {
SHADOW_KEY,
} from "../config";
import Content from "../views/Content";
import { fetchUpdate, fetchClear } from "./fetch";
import { updateFetchPool, clearFetchPool } from "./fetch";
import { debounce } from "./utils";
/**
@@ -18,8 +18,9 @@ import { debounce } from "./utils";
*/
export class Translator {
_rule = {};
_minLength = 0;
_maxLength = 0;
_setting = {};
_rootNodes = new Set();
_tranNodes = new Map();
_skipNodeNames = [
APP_LCNAME,
"style",
@@ -36,8 +37,6 @@ export class Translator {
"script",
"iframe",
];
_rootNodes = new Set();
_tranNodes = new Map();
// 显示
_interseObserver = new IntersectionObserver(
@@ -89,17 +88,23 @@ export class Translator {
};
};
constructor(rule, { fetchInterval, fetchLimit, minLength, maxLength }) {
fetchUpdate(fetchInterval, fetchLimit);
constructor(rule, setting) {
const { fetchInterval, fetchLimit } = setting;
updateFetchPool(fetchInterval, fetchLimit);
this._overrideAttachShadow();
this._minLength = minLength ?? TRANS_MIN_LENGTH;
this._maxLength = maxLength ?? TRANS_MAX_LENGTH;
this.rule = rule;
this._setting = setting;
this._rule = rule;
if (rule.transOpen === "true") {
this._register();
}
}
get setting() {
return this._setting;
}
get rule() {
// console.log("get rule", this._rule);
return this._rule;
@@ -236,7 +241,7 @@ export class Translator {
this._tranNodes.clear();
// 清空任务池
fetchClear();
clearFetchPool();
};
_reTranslate = debounce(() => {
@@ -268,7 +273,11 @@ export class Translator {
this._tranNodes.set(el, q);
// 太长或太短
if (!q || q.length < this._minLength || q.length > this._maxLength) {
if (
!q ||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH)
) {
return;
}

View File

@@ -34,7 +34,12 @@ export const matchValue = (arr, val) => {
* @returns
*/
export const sleep = (delay) =>
new Promise((resolve) => setTimeout(resolve, delay));
new Promise((resolve) => {
const timer = setTimeout(() => {
clearTimeout(timer);
resolve();
}, delay);
});
/**
* 防抖函数

View File

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

View File

@@ -3,6 +3,7 @@ import path from "path";
import { BUILTIN_RULES } from "./config/rules";
(() => {
// rules
try {
const data = JSON.stringify(BUILTIN_RULES, null, " ");
const file = path.resolve(
@@ -14,4 +15,14 @@ import { BUILTIN_RULES } from "./config/rules";
} catch (err) {
console.error(err);
}
// version
try {
var pjson = require("../package.json");
const file = path.resolve(__dirname, "../build/web/version.txt");
fs.writeFileSync(file, pjson.version);
console.info(`Version file generated: ${file}`);
} catch (err) {
console.error(err);
}
})();

View File

@@ -3,32 +3,48 @@ import ReactDOM from "react-dom/client";
import Action from "./views/Action";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { getSetting, getRules, matchRule, getFab } from "./libs";
import {
getSettingWithDefault,
getRulesWithDefault,
getFabWithDefault,
} from "./libs/storage";
import { Translator } from "./libs/translator";
import { trySyncAllSubRules } from "./libs/rules";
import { isGm } from "./libs/browser";
import { trySyncAllSubRules } from "./libs/subRules";
import { isGm } from "./libs/client";
import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config";
import { isIframe } from "./libs/iframe";
import { handlePing, injectScript } from "./libs/gm";
import { matchRule } from "./libs/rules";
/**
* 入口函数
*/
(async () => {
const init = async () => {
// 设置页面
if (
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE) ||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE2)
) {
unsafeWindow.GM = GM;
unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
unsafeWindow.GM = GM;
unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
} else {
const ping = btoa(Math.random()).slice(3, 11);
window.addEventListener(ping, handlePing);
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
const script = document.createElement("script");
script.textContent = `(${injectScript})("${ping}")`;
document.head.append(script);
}
return;
}
// 翻译页面
const href = isIframe ? document.referrer : document.location.href;
const setting = await getSetting();
const rules = await getRules();
const setting = await getSettingWithDefault();
const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);
@@ -50,7 +66,7 @@ import { isIframe } from "./libs/iframe";
}
// 浮球按钮
const fab = await getFab();
const fab = await getFabWithDefault();
const $action = document.createElement("div");
$action.setAttribute("id", "kiss-translator");
document.body.parentElement.appendChild($action);
@@ -74,22 +90,37 @@ import { isIframe } from "./libs/iframe";
// 注册菜单
if (isGm) {
GM.registerMenuCommand(
"Toggle Translate",
(event) => {
translator.toggle();
},
"Q"
);
GM.registerMenuCommand(
"Toggle Style",
(event) => {
translator.toggleStyle();
},
"C"
);
try {
GM.registerMenuCommand(
"Toggle Translate",
(event) => {
translator.toggle();
},
"Q"
);
GM.registerMenuCommand(
"Toggle Style",
(event) => {
translator.toggleStyle();
},
"C"
);
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}
// 同步订阅规则
trySyncAllSubRules(setting);
};
(async () => {
try {
await init();
} catch (err) {
const $err = document.createElement("div");
$err.innerText = `KISS-Translator: ${err.message}`;
$err.style.cssText = "background:red; color:#fff; z-index:10000;";
document.body.prepend($err);
}
})();

View File

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

View File

@@ -8,7 +8,7 @@ import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import Stack from "@mui/material/Stack";
import { useEffect, useState, useMemo, useCallback } from "react";
import { StoragesProvider } from "../../hooks/Storage";
import { SettingProvider } from "../../hooks/Setting";
import Popup from "../Popup";
import { debounce } from "../../libs/utils";
@@ -81,7 +81,7 @@ export default function Action({ translator, fab }) {
};
return (
<StoragesProvider>
<SettingProvider>
<ThemeProvider>
<Draggable
key="pop"
@@ -139,6 +139,6 @@ export default function Action({ translator, fab }) {
}
/>
</ThemeProvider>
</StoragesProvider>
</SettingProvider>
);
}

View File

@@ -16,7 +16,7 @@ import { useTranslate } from "../../hooks/Translate";
export default function Content({ q, translator }) {
const [rule, setRule] = useState(translator.rule);
const [hover, setHover] = useState(false);
const { text, sameLang, loading } = useTranslate(q, rule);
const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
const { textStyle, bgColor } = rule;
const handleMouseEnter = () => {
@@ -47,31 +47,28 @@ export default function Content({ q, translator }) {
const style = useMemo(() => {
const lineColor = bgColor || "";
const underlineStyle = (st) => ({
opacity: hover ? 1 : 0.6,
textDecorationLine: "underline",
textDecorationColor: lineColor,
textDecorationStyle: st,
textDecorationThickness: "2px",
textUnderlineOffset: "0.3em",
WebkittextDecorationLine: "underline",
WebkittextDecorationColor: lineColor,
WebkittextDecorationStyle: st,
WebkittextDecorationThickness: "2px",
WebkittextTextUnderlineOffset: "0.3em",
});
switch (textStyle) {
case OPT_STYLE_LINE: // 下划线
return {
opacity: hover ? 1 : 0.6,
textDecoration: `underline 2px ${lineColor}`,
textUnderlineOffset: "0.3em",
};
return underlineStyle("solid");
case OPT_STYLE_DOTLINE: // 点状线
return {
opacity: hover ? 1 : 0.6,
textDecoration: `dotted underline 2px ${lineColor}`,
textUnderlineOffset: "0.3em",
};
return underlineStyle("dotted");
case OPT_STYLE_DASHLINE: // 虚线
return {
opacity: hover ? 1 : 0.6,
textDecoration: `dashed underline 2px ${lineColor}`,
textUnderlineOffset: "0.3em",
};
return underlineStyle("dashed");
case OPT_STYLE_WAVYLINE: // 波浪线
return {
opacity: hover ? 1 : 0.6,
textDecoration: `wavy underline 2px ${lineColor}`,
textUnderlineOffset: "0.3em",
};
return underlineStyle("wavy");
case OPT_STYLE_FUZZY: // 模糊
return {
filter: hover ? "none" : "blur(5px)",

View File

@@ -4,17 +4,16 @@ import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu";
import Toolbar from "@mui/material/Toolbar";
import Box from "@mui/material/Box";
import { useDarkModeSwitch } from "../../hooks/ColorMode";
import { useDarkMode } from "../../hooks/ColorMode";
import LightModeIcon from "@mui/icons-material/LightMode";
import DarkModeIcon from "@mui/icons-material/DarkMode";
import Link from "@mui/material/Link";
import { useI18n } from "../../hooks/I18n";
function Header(props) {
const i18n = useI18n();
const { onDrawerToggle } = props;
const switchColorMode = useDarkModeSwitch();
const darkMode = useDarkMode();
const { darkMode, toggleDarkMode } = useDarkMode();
return (
<AppBar
@@ -35,10 +34,14 @@ function Header(props) {
<MenuIcon />
</IconButton>
</Box>
<Box sx={{ flexGrow: 1 }}>{`${i18n("app_name")} v${
process.env.REACT_APP_VERSION
}`}</Box>
<IconButton onClick={switchColorMode} color="inherit">
<Box sx={{ flexGrow: 1 }}>
<Link
underline="none"
color="inherit"
href={process.env.REACT_APP_HOMEPAGE}
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
</Box>
<IconButton onClick={toggleDarkMode} color="inherit">
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
</IconButton>
</Toolbar>

View File

@@ -24,7 +24,7 @@ import MenuItem from "@mui/material/MenuItem";
import Grid from "@mui/material/Grid";
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import FileUploadIcon from "@mui/icons-material/FileUpload";
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
import { useSetting } from "../../hooks/Setting";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Tabs from "@mui/material/Tabs";
@@ -35,11 +35,13 @@ import DeleteIcon from "@mui/icons-material/Delete";
import IconButton from "@mui/material/IconButton";
import ShareIcon from "@mui/icons-material/Share";
import SyncIcon from "@mui/icons-material/Sync";
import { useSubrules } from "../../hooks/Rules";
import { rulesCache, loadSubRules, syncSubRules } from "../../libs/rules";
import { useSubRules } from "../../hooks/SubRules";
import { syncSubRules } from "../../libs/subRules";
import { loadOrFetchSubRules } from "../../libs/subRules";
import { useAlert } from "../../hooks/Alert";
import { syncOpt, syncShareRules } from "../../libs/sync";
import { syncShareRules } from "../../libs/sync";
import { debounce } from "../../libs/utils";
import { delSubRules, getSyncWithDefault } from "../../libs/storage";
function RuleFields({ rule, rules, setShow, setKeyword }) {
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
@@ -409,12 +411,12 @@ function UploadButton({ onChange, text }) {
);
}
function ShareButton({ rules, injectRules, selectedSub }) {
function ShareButton({ rules, injectRules, selectedUrl }) {
const alert = useAlert();
const i18n = useI18n();
const handleClick = async () => {
try {
const { syncUrl, syncKey } = await syncOpt.load();
const { syncUrl, syncKey } = await getSyncWithDefault();
if (!syncUrl || !syncKey) {
alert.warning(i18n("error_sync_setting"));
return;
@@ -422,7 +424,7 @@ function ShareButton({ rules, injectRules, selectedSub }) {
const shareRules = [...rules.list];
if (injectRules) {
const subRules = await loadSubRules(selectedSub?.url);
const subRules = await loadOrFetchSubRules(selectedUrl);
shareRules.splice(-1, 0, ...subRules);
}
@@ -451,19 +453,15 @@ function ShareButton({ rules, injectRules, selectedSub }) {
);
}
function UserRules() {
function UserRules({ subRules }) {
const i18n = useI18n();
const rules = useRules();
const [showAdd, setShowAdd] = useState(false);
const setting = useSetting();
const updateSetting = useSettingUpdate();
const subrules = useSubrules();
const [subRules, setSubRules] = useState([]);
const { setting, updateSetting } = useSetting();
const [keyword, setKeyword] = useState("");
const selectedSub = subrules.list.find((item) => item.selected);
const injectRules = !!setting?.injectRules;
const { selectedUrl, selectedRules } = subRules;
const handleImport = (e) => {
const file = e.target.files[0];
@@ -493,19 +491,6 @@ function UserRules() {
});
};
useEffect(() => {
(async () => {
if (selectedSub?.url) {
try {
const rules = await loadSubRules(selectedSub?.url);
setSubRules(rules);
} catch (err) {
console.log("[load rules]", err);
}
}
})();
}, [selectedSub?.url]);
useEffect(() => {
if (!showAdd) {
setKeyword("");
@@ -514,7 +499,13 @@ function UserRules() {
return (
<Stack spacing={3}>
<Stack direction="row" alignItems="center" spacing={2} useFlexGap flexWrap="wrap">
<Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
<Button
size="small"
variant="contained"
@@ -536,7 +527,7 @@ function UserRules() {
<ShareButton
rules={rules}
injectRules={injectRules}
selectedSub={selectedSub}
selectedUrl={selectedUrl}
/>
<FormControlLabel
@@ -572,7 +563,7 @@ function UserRules() {
{injectRules && (
<Box>
{subRules
{selectedRules
.filter(
(rule) =>
rule.pattern.includes(keyword) || keyword.includes(rule.pattern)
@@ -586,13 +577,13 @@ function UserRules() {
);
}
function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
const [loading, setLoading] = useState(false);
const handleDel = async () => {
try {
await subrules.del(url);
await rulesCache.del(url);
await delSub(url);
await delSubRules(url);
} catch (err) {
console.log("[del subrules]", err);
}
@@ -603,7 +594,7 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
setLoading(true);
const rules = await syncSubRules(url);
if (rules.length > 0 && url === selectedUrl) {
setRules(rules);
setSelectedRules(rules);
}
} catch (err) {
console.log("[sync sub rules]", err);
@@ -633,7 +624,7 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
);
}
function SubRulesEdit({ subrules }) {
function SubRulesEdit({ subList, addSub }) {
const i18n = useI18n();
const [inputText, setInputText] = useState("");
const [inputError, setInputError] = useState("");
@@ -656,7 +647,7 @@ function SubRulesEdit({ subrules }) {
return;
}
if (subrules.list.find((item) => item.url === url)) {
if (subList.find((item) => item.url === url)) {
setInputError(i18n("error_duplicate_values"));
return;
}
@@ -667,7 +658,7 @@ function SubRulesEdit({ subrules }) {
if (rules.length === 0) {
throw new Error("empty rules");
}
await subrules.add(url);
await addSub(url);
setShowInput(false);
setInputText("");
} catch (err) {
@@ -735,47 +726,36 @@ function SubRulesEdit({ subrules }) {
);
}
function SubRules() {
const [loading, setLoading] = useState(false);
const [rules, setRules] = useState([]);
const subrules = useSubrules();
const selectedSub = subrules.list.find((item) => item.selected);
function SubRules({ subRules }) {
const {
subList,
selectSub,
addSub,
delSub,
selectedUrl,
selectedRules,
setSelectedRules,
loading,
} = subRules;
const handleSelect = (e) => {
const url = e.target.value;
subrules.select(url);
selectSub(url);
};
useEffect(() => {
(async () => {
if (selectedSub?.url) {
try {
setLoading(true);
const rules = await loadSubRules(selectedSub?.url);
setRules(rules);
} catch (err) {
console.log("[load rules]", err);
} finally {
setLoading(false);
}
}
})();
}, [selectedSub?.url]);
return (
<Stack spacing={3}>
<SubRulesEdit subrules={subrules} />
<SubRulesEdit subList={subList} addSub={addSub} />
<RadioGroup value={selectedSub?.url} onChange={handleSelect}>
{subrules.list.map((item, index) => (
<RadioGroup value={selectedUrl} onChange={handleSelect}>
{subList.map((item, index) => (
<SubRulesItem
key={item.url}
url={item.url}
index={index}
selectedUrl={selectedSub?.url}
subrules={subrules}
setRules={setRules}
selectedUrl={selectedUrl}
delSub={delSub}
setSelectedRules={setSelectedRules}
/>
))}
</RadioGroup>
@@ -786,7 +766,9 @@ function SubRules() {
<CircularProgress />
</center>
) : (
rules.map((rule) => <RuleAccordion key={rule.pattern} rule={rule} />)
selectedRules.map((rule) => (
<RuleAccordion key={rule.pattern} rule={rule} />
))
)}
</Box>
</Stack>
@@ -796,6 +778,7 @@ function SubRules() {
export default function Rules() {
const i18n = useI18n();
const [activeTab, setActiveTab] = useState(0);
const subRules = useSubRules();
const handleTabChange = (e, newValue) => {
setActiveTab(newValue);
@@ -816,8 +799,12 @@ export default function Rules() {
<Tab label={i18n("subscribe_rules")} />
</Tabs>
</Box>
<div hidden={activeTab !== 0}>{activeTab === 0 && <UserRules />}</div>
<div hidden={activeTab !== 1}>{activeTab === 1 && <SubRules />}</div>
<div hidden={activeTab !== 0}>
{activeTab === 0 && <UserRules subRules={subRules} />}
</div>
<div hidden={activeTab !== 1}>
{activeTab === 1 && <SubRules subRules={subRules} />}
</div>
</Stack>
</Box>
);

View File

@@ -5,47 +5,37 @@ import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
import { limitNumber, debounce } from "../../libs/utils";
import { useSetting } from "../../hooks/Setting";
import { limitNumber } from "../../libs/utils";
import { useI18n } from "../../hooks/I18n";
import { UI_LANGS } from "../../config";
import { useMemo } from "react";
export default function Settings() {
const i18n = useI18n();
const setting = useSetting();
const updateSetting = useSettingUpdate();
const { setting, updateSetting } = useSetting();
const handleChange = useMemo(
() =>
debounce((e) => {
e.preventDefault();
let { name, value } = e.target;
switch (name) {
case "fetchLimit":
value = limitNumber(value, 1, 100);
break;
case "fetchInterval":
value = limitNumber(value, 0, 5000);
break;
case "minLength":
value = limitNumber(value, 1, 100);
break;
case "maxLength":
value = limitNumber(value, 100, 10000);
break;
default:
}
updateSetting({
[name]: value,
});
}, 500),
[updateSetting]
);
if (!setting) {
return;
}
const handleChange = (e) => {
e.preventDefault();
let { name, value } = e.target;
switch (name) {
case "fetchLimit":
value = limitNumber(value, 1, 100);
break;
case "fetchInterval":
value = limitNumber(value, 0, 5000);
break;
case "minLength":
value = limitNumber(value, 1, 100);
break;
case "maxLength":
value = limitNumber(value, 100, 10000);
break;
default:
}
updateSetting({
[name]: value,
});
};
const {
uiLang,
@@ -85,7 +75,7 @@ export default function Settings() {
label={i18n("fetch_limit")}
type="number"
name="fetchLimit"
defaultValue={fetchLimit}
value={fetchLimit}
onChange={handleChange}
/>
@@ -94,7 +84,7 @@ export default function Settings() {
label={i18n("fetch_interval")}
type="number"
name="fetchInterval"
defaultValue={fetchInterval}
value={fetchInterval}
onChange={handleChange}
/>
@@ -103,7 +93,7 @@ export default function Settings() {
label={i18n("min_translate_length")}
type="number"
name="minLength"
defaultValue={minLength}
value={minLength}
onChange={handleChange}
/>
@@ -112,7 +102,7 @@ export default function Settings() {
label={i18n("max_translate_length")}
type="number"
name="maxLength"
defaultValue={maxLength}
value={maxLength}
onChange={handleChange}
/>
@@ -133,7 +123,7 @@ export default function Settings() {
size="small"
label={i18n("google_api")}
name="googleUrl"
defaultValue={googleUrl}
value={googleUrl}
onChange={handleChange}
/>
@@ -141,7 +131,7 @@ export default function Settings() {
size="small"
label={i18n("openai_api")}
name="openaiUrl"
defaultValue={openaiUrl}
value={openaiUrl}
onChange={handleChange}
/>
@@ -150,7 +140,7 @@ export default function Settings() {
type="password"
label={i18n("openai_key")}
name="openaiKey"
defaultValue={openaiKey}
value={openaiKey}
onChange={handleChange}
/>
@@ -158,7 +148,7 @@ export default function Settings() {
size="small"
label={i18n("openai_model")}
name="openaiModel"
defaultValue={openaiModel}
value={openaiModel}
onChange={handleChange}
/>
@@ -166,7 +156,7 @@ export default function Settings() {
size="small"
label={i18n("openai_prompt")}
name="openaiPrompt"
defaultValue={openaiPrompt}
value={openaiPrompt}
onChange={handleChange}
multiline
/>

View File

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

View File

@@ -4,14 +4,17 @@ import Rules from "./Rules";
import Setting from "./Setting";
import Layout from "./Layout";
import SyncSetting from "./SyncSetting";
import { StoragesProvider } from "../../hooks/Storage";
import { SettingProvider } from "../../hooks/Setting";
import ThemeProvider from "../../hooks/Theme";
import { useEffect, useState } from "react";
import { isGm } from "../../libs/browser";
import { isGm } from "../../libs/client";
import { sleep } from "../../libs/utils";
import CircularProgress from "@mui/material/CircularProgress";
import { trySyncAll } from "../../libs/sync";
import { trySyncSettingAndRules } from "../../libs/sync";
import { AlertProvider } from "../../hooks/Alert";
import Link from "@mui/material/Link";
import Divider from "@mui/material/Divider";
import Stack from "@mui/material/Stack";
export default function Options() {
const [error, setError] = useState(false);
@@ -24,6 +27,8 @@ export default function Options() {
let i = 0;
for (;;) {
if (window.APP_NAME === process.env.REACT_APP_NAME) {
// 同步数据
await trySyncSettingAndRules();
setReady(true);
break;
}
@@ -35,40 +40,65 @@ export default function Options() {
await sleep(1000);
}
} else {
// 同步数据
await trySyncSettingAndRules();
setReady(true);
}
// 同步数据
trySyncAll();
})();
}, []);
if (error) {
return (
<center>
<Divider>
<Link
href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider>
<h2>
Please confirm whether to install or enable{" "}
<a href={process.env.REACT_APP_HOMEPAGE}>KISS Translator</a>{" "}
Please confirm whether to install or enable KISS Translator
GreaseMonkey script?
</h2>
<h2>
<a href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>Click here</a>{" "}
to install, or <a href={process.env.REACT_APP_HOMEPAGE}>click here</a>{" "}
for help.
</h2>
<Stack spacing={2}>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
Install Userscript 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install Userscript 2
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install Userscript Safari 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install Userscript Safari 2
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
Open Options Page 1
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
Open Options Page 2
</Link>
</Stack>
</center>
);
}
if (isGm && !ready) {
if (!ready) {
return (
<center>
<Divider>
<Link
href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider>
<CircularProgress />
</center>
);
}
return (
<StoragesProvider>
<SettingProvider>
<ThemeProvider>
<AlertProvider>
<HashRouter>
@@ -83,6 +113,6 @@ export default function Options() {
</HashRouter>
</AlertProvider>
</ThemeProvider>
</StoragesProvider>
</SettingProvider>
);
}

View File

@@ -6,7 +6,8 @@ import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Button from "@mui/material/Button";
import { sendTabMsg } from "../../libs/msg";
import { browser, isExt } from "../../libs/browser";
import { browser } from "../../libs/browser";
import { isExt } from "../../libs/client";
import { useI18n } from "../../hooks/I18n";
import TextField from "@mui/material/TextField";
import {