Compare commits

..

9 Commits

Author SHA1 Message Date
Gabe Yuan
1191791447 v1.5.4 2023-08-22 17:52:12 +08:00
Gabe Yuan
5c510f2df2 add rules filter when add rule 2023-08-22 17:51:40 +08:00
Gabe Yuan
7c0aa23177 add rules filter when add rule 2023-08-22 17:46:57 +08:00
Gabe Yuan
4bc1c26653 add rules filter when add rule 2023-08-22 17:37:42 +08:00
Gabe Yuan
ca1e1148d6 sync subscribe rules when browser start or userscript run 2023-08-22 16:27:09 +08:00
Gabe Yuan
2224455a7f add text description for rules 2023-08-22 10:35:57 +08:00
Gabe Yuan
f463f3ce08 v1.5.3 2023-08-21 23:50:32 +08:00
Gabe Yuan
c0872db98c auto use unsafe fetch 2023-08-21 23:50:14 +08:00
Gabe Yuan
d3a5d91f01 auto use unsafe fetch 2023-08-21 23:46:42 +08:00
15 changed files with 304 additions and 143 deletions

3
.env
View File

@@ -2,11 +2,12 @@ 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.5.2 REACT_APP_VERSION=1.5.4
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
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
REACT_APP_LOGOURL=https://kiss-translator.rayjar.com/images/logo192.png 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_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_USERSCRIPT_DOWNLOADURL2=https://fishjar.github.io/kiss-translator/kiss-translator.user.js

View File

@@ -88,6 +88,7 @@ const userscriptWebpack = (config, env) => {
// @grant GM.setValue // @grant GM.setValue
// @grant GM.getValue // @grant GM.getValue
// @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

View File

@@ -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.5.2", "version": "1.5.4",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"private": true, "private": true,
"dependencies": { "dependencies": {

View File

@@ -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.5.2", "version": "1.5.4",
"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",

View File

@@ -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.5.2", "version": "1.5.4",
"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",

View File

@@ -20,19 +20,16 @@ import { sha256 } from "../libs/utils";
* @param {*} data * @param {*} data
* @returns * @returns
*/ */
export const apiSyncData = async (url, key, data) => export const apiSyncData = async (url, key, data, isBg = false) =>
fetchPolyfill( fetchPolyfill(url, {
url, headers: {
{ "Content-type": "application/json",
headers: { Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
"Content-type": "application/json",
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
},
method: "POST",
body: JSON.stringify(data),
}, },
{ useUnsafe: true } method: "POST",
); body: JSON.stringify(data),
isBg,
});
/** /**
* 谷歌翻译 * 谷歌翻译
@@ -53,15 +50,14 @@ const apiGoogleTranslate = async (translator, text, to, from) => {
}; };
const { googleUrl } = await getSetting(); const { googleUrl } = await getSetting();
const input = `${googleUrl}?${queryString.stringify(params)}`; const input = `${googleUrl}?${queryString.stringify(params)}`;
return fetchPolyfill( return fetchPolyfill(input, {
input, headers: {
{ "Content-type": "application/json",
headers: {
"Content-type": "application/json",
},
}, },
{ useCache: true, usePool: true, translator } useCache: true,
); usePool: true,
translator,
});
}; };
/** /**
@@ -78,17 +74,16 @@ const apiMicrosoftTranslate = (translator, text, to, from) => {
"api-version": "3.0", "api-version": "3.0",
}; };
const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`; const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`;
return fetchPolyfill( return fetchPolyfill(input, {
input, headers: {
{ "Content-type": "application/json",
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify([{ Text: text }]),
}, },
{ useCache: true, usePool: true, translator } method: "POST",
); body: JSON.stringify([{ Text: text }]),
useCache: true,
usePool: true,
translator,
});
}; };
/** /**
@@ -104,31 +99,31 @@ const apiOpenaiTranslate = async (translator, text, to, from) => {
let prompt = openaiPrompt let prompt = openaiPrompt
.replaceAll(PROMPT_PLACE_FROM, from) .replaceAll(PROMPT_PLACE_FROM, from)
.replaceAll(PROMPT_PLACE_TO, to); .replaceAll(PROMPT_PLACE_TO, to);
return fetchPolyfill( return fetchPolyfill(openaiUrl, {
openaiUrl, headers: {
{ "Content-type": "application/json",
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify({
model: openaiModel,
messages: [
{
role: "system",
content: prompt,
},
{
role: "user",
content: text,
},
],
temperature: 0,
max_tokens: 256,
}),
}, },
{ useCache: true, usePool: true, translator, token: openaiKey } method: "POST",
); body: JSON.stringify({
model: openaiModel,
messages: [
{
role: "system",
content: prompt,
},
{
role: "user",
content: text,
},
],
temperature: 0,
max_tokens: 256,
}),
useCache: true,
usePool: true,
translator,
token: openaiKey,
});
}; };
/** /**

View File

@@ -14,12 +14,15 @@ import {
STOKEY_RULES, STOKEY_RULES,
STOKEY_SYNC, STOKEY_SYNC,
CACHE_NAME, CACHE_NAME,
STOKEY_RULESCACHE_PREFIX,
BUILTIN_RULES,
} from "./config"; } from "./config";
import storage from "./libs/storage"; 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"; import { sendTabMsg } from "./libs/msg";
import { trySyncAllSubRules } from "./libs/rules";
/** /**
* 插件安装 * 插件安装
@@ -29,7 +32,10 @@ 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 storage.trySetObj(
`${STOKEY_RULESCACHE_PREFIX}${process.env.REACT_APP_RULESURL}`,
BUILTIN_RULES
);
}); });
/** /**
@@ -39,13 +45,16 @@ browser.runtime.onStartup.addListener(async () => {
console.log("browser onStartup"); console.log("browser onStartup");
// 同步数据 // 同步数据
await syncAll(); await syncAll(true);
// 清除缓存 // 清除缓存
const { clearCache } = await getSetting(); const setting = await getSetting();
if (clearCache) { if (setting.clearCache) {
caches.delete(CACHE_NAME); caches.delete(CACHE_NAME);
} }
// 同步订阅规则
trySyncAllSubRules(setting, true);
}); });
/** /**
@@ -55,8 +64,8 @@ browser.runtime.onMessage.addListener(
({ action, args }, sender, sendResponse) => { ({ action, args }, sender, sendResponse) => {
switch (action) { switch (action) {
case MSG_FETCH: case MSG_FETCH:
const { input, init, opts } = args; const { input, opts } = args;
fetchData(input, init, opts) fetchData(input, opts)
.then((data) => { .then((data) => {
sendResponse({ data }); sendResponse({ data });
}) })

View File

@@ -108,9 +108,9 @@ export const I18N = {
zh: `注入订阅规则`, zh: `注入订阅规则`,
en: `Inject Subscribe Rules`, en: `Inject Subscribe Rules`,
}, },
edit_rules: { personal_rules: {
zh: `编辑规则`, zh: `个人规则`,
en: `Edit Rules`, en: `Personal Rules`,
}, },
subscribe_rules: { subscribe_rules: {
zh: `订阅规则`, zh: `订阅规则`,
@@ -120,6 +120,14 @@ export const I18N = {
zh: `订阅地址`, zh: `订阅地址`,
en: `Subscribe URL`, en: `Subscribe URL`,
}, },
rules_warn_1: {
zh: `1、“个人规则”一直生效选择“注入订阅规则”后“订阅规则”才会生效。`,
en: `1. The "Personal Rules" are always in effect. After selecting "Inject Subscription Rules", the "Subscription Rules" will take effect.`,
},
rules_warn_2: {
zh: `2、“订阅规则”的注入位置是倒数第二的位置因此除全局规则(*)外,“个人规则”优先级比“订阅规则”高,“个人规则”填写同样的网址会覆盖”订阅规则“的条目。`,
en: `2. The injection position of "Subscription Rules" is the penultimate position. Therefore, except for the global rules (*), the priority of "Personal Rules" is higher than that of "Subscription Rules". Filling in the same url in "Personal Rules" will overwrite "Subscription Rules" entry.`,
},
sync_warn: { sync_warn: {
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`, zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`, en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`,

View File

@@ -157,7 +157,7 @@ export const GLOBLA_RULE = {
// 订阅列表 // 订阅列表
export const DEFAULT_SUBRULES_LIST = [ export const DEFAULT_SUBRULES_LIST = [
{ {
url: "https://kiss-translator.rayjar.com/kiss-translator-rules.json", url: process.env.REACT_APP_RULESURL,
selected: true, selected: true,
}, },
{ {
@@ -192,4 +192,5 @@ export const DEFAULT_SYNC = {
settingSyncAt: 0, settingSyncAt: 0,
rulesUpdateAt: 0, rulesUpdateAt: 0,
rulesSyncAt: 0, rulesSyncAt: 0,
subRulesSyncAt: 0, // 订阅规则同步时间
}; };

View File

@@ -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);
}; };
@@ -98,14 +103,12 @@ export const fetchPool = taskPool(
/** /**
* 请求数据统一接口 * 请求数据统一接口
* @param {*} input * @param {*} input
* @param {*} init
* @param {*} opts * @param {*} opts
* @returns * @returns
*/ */
export const fetchData = async ( export const fetchData = async (
input, input,
init, { useCache, usePool, translator, token, ...init } = {}
{ useCache, usePool, translator, useUnsafe, 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 +126,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) {
@@ -152,22 +155,21 @@ export const fetchData = async (
/** /**
* fetch 兼容性封装 * fetch 兼容性封装
* @param {*} input * @param {*} input
* @param {*} init
* @param {*} opts * @param {*} opts
* @returns * @returns
*/ */
export const fetchPolyfill = async (input, init, opts) => { export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
// 插件 // 插件
if (isExt) { if (isExt && !isBg) {
const res = await sendMsg(MSG_FETCH, { input, init, opts }); const res = await sendMsg(MSG_FETCH, { input, opts });
if (res.error) { if (res.error) {
throw new Error(res.error); throw new Error(res.error);
} }
return res.data; return res.data;
} }
// 油猴/网页 // 油猴/网页/BackgroundPage
return await fetchData(input, init, opts); return await fetchData(input, opts);
}; };
/** /**

View File

@@ -10,7 +10,7 @@ import {
} from "../config"; } from "../config";
import { browser } from "./browser"; import { browser } from "./browser";
import { isMatch } from "./utils"; import { isMatch } from "./utils";
import { tryLoadRules } from "./rules"; import { loadSubRules } from "./rules";
/** /**
* 获取节点列表并转为数组 * 获取节点列表并转为数组
@@ -57,13 +57,14 @@ export const setFab = async (obj) => await storage.setObj(STOKEY_FAB, obj);
export const matchRule = async ( export const matchRule = async (
rules, rules,
href, href,
{ injectRules, subrulesList = DEFAULT_SUBRULES_LIST } { injectRules = true, subrulesList = DEFAULT_SUBRULES_LIST }
) => { ) => {
rules = [...rules];
if (injectRules) { if (injectRules) {
try { try {
const selectedSub = subrulesList.find((item) => item.selected); const selectedSub = subrulesList.find((item) => item.selected);
if (selectedSub?.url) { if (selectedSub?.url) {
const subRules = await tryLoadRules(selectedSub.url); const subRules = await loadSubRules(selectedSub.url);
rules.splice(-1, 0, ...subRules); rules.splice(-1, 0, ...subRules);
} }
} catch (err) { } catch (err) {
@@ -71,13 +72,13 @@ export const matchRule = async (
} }
} }
const rule = rules.find((rule) => const rule = rules.find((r) =>
rule.pattern.split(",").some((p) => isMatch(href, p.trim())) r.pattern.split(",").some((p) => isMatch(href, p.trim()))
); );
const globalRule = const globalRule =
rules.find((rule) => rules.find((r) => r.pattern.split(",").some((p) => p.trim() === "*")) ||
rule.pattern.split(",").some((p) => p.trim() === "*") GLOBLA_RULE;
) || GLOBLA_RULE;
if (!rule) { if (!rule) {
return globalRule; return globalRule;

View File

@@ -9,6 +9,7 @@ import {
OPT_LANGS_FROM, OPT_LANGS_FROM,
OPT_LANGS_TO, OPT_LANGS_TO,
} from "../config"; } from "../config";
import { syncOpt } from "./sync";
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]); const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
const toLangs = OPT_LANGS_TO.map((item) => item[0]); const toLangs = OPT_LANGS_TO.map((item) => item[0]);
@@ -62,11 +63,11 @@ export const checkRules = (rules) => {
}; };
/** /**
* 本地rules缓存 * 订阅规则的本地缓存
*/ */
export const rulesCache = { export const rulesCache = {
fetch: async (url) => { fetch: async (url, isBg = false) => {
const res = await fetchPolyfill(url, null, { useUnsafe: true }); const res = await fetchPolyfill(url, { isBg });
const rules = checkRules(res).filter( const rules = checkRules(res).filter(
(rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== "" (rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== ""
); );
@@ -84,16 +85,61 @@ export const rulesCache = {
}; };
/** /**
* 从缓存或远程加载订阅的rules * 同步订阅规则
* @param {*} url * @param {*} url
* @returns * @returns
*/ */
export const tryLoadRules = async (url) => { export const syncSubRules = async (url, isBg = false) => {
let rules = await rulesCache.get(url); 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) { if (rules?.length) {
return rules; return rules;
} }
rules = await rulesCache.fetch(url); return await syncSubRules(url);
await rulesCache.set(url, rules);
return rules;
}; };

View File

@@ -13,69 +13,95 @@ import { getSetting, getRules } from ".";
import { apiSyncData } from "../apis"; import { apiSyncData } from "../apis";
import { sha256 } from "./utils"; import { sha256 } from "./utils";
export const loadSyncOpt = async () => /**
(await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC; * 同步相关数据
*/
export const syncOpt = {
load: async () => (await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC,
update: async (obj) => {
await storage.putObj(STOKEY_SYNC, obj);
},
};
export const syncSetting = async () => { /**
* 同步设置
* @returns
*/
export const syncSetting = async (isBg = false) => {
try { try {
const { syncUrl, syncKey, settingUpdateAt } = await loadSyncOpt(); const { syncUrl, syncKey, settingUpdateAt } = await syncOpt.load();
if (!syncUrl || !syncKey) { if (!syncUrl || !syncKey) {
return; return;
} }
const setting = await getSetting(); const setting = await getSetting();
const res = await apiSyncData(syncUrl, syncKey, { const res = await apiSyncData(
key: KV_SETTING_KEY, syncUrl,
value: setting, syncKey,
updateAt: settingUpdateAt, {
}); key: KV_SETTING_KEY,
value: setting,
updateAt: settingUpdateAt,
},
isBg
);
if (res && res.updateAt > settingUpdateAt) { if (res && res.updateAt > settingUpdateAt) {
await storage.putObj(STOKEY_SYNC, { await syncOpt.update({
settingUpdateAt: res.updateAt, settingUpdateAt: res.updateAt,
settingSyncAt: res.updateAt, settingSyncAt: res.updateAt,
}); });
await storage.setObj(STOKEY_SETTING, res.value); await storage.setObj(STOKEY_SETTING, res.value);
} else { } else {
await storage.putObj(STOKEY_SYNC, { await syncOpt.update({ settingSyncAt: res.updateAt });
settingSyncAt: res.updateAt,
});
} }
} catch (err) { } catch (err) {
console.log("[sync setting]", err); console.log("[sync setting]", err);
} }
}; };
export const syncRules = async () => { /**
* 同步规则
* @returns
*/
export const syncRules = async (isBg = false) => {
try { try {
const { syncUrl, syncKey, rulesUpdateAt } = await loadSyncOpt(); const { syncUrl, syncKey, rulesUpdateAt } = await syncOpt.load();
if (!syncUrl || !syncKey) { if (!syncUrl || !syncKey) {
return; return;
} }
const rules = await getRules(); const rules = await getRules();
const res = await apiSyncData(syncUrl, syncKey, { const res = await apiSyncData(
key: KV_RULES_KEY, syncUrl,
value: rules, syncKey,
updateAt: rulesUpdateAt, {
}); key: KV_RULES_KEY,
value: rules,
updateAt: rulesUpdateAt,
},
isBg
);
if (res && res.updateAt > rulesUpdateAt) { if (res && res.updateAt > rulesUpdateAt) {
await storage.putObj(STOKEY_SYNC, { await syncOpt.update({
rulesUpdateAt: res.updateAt, rulesUpdateAt: res.updateAt,
rulesSyncAt: res.updateAt, rulesSyncAt: res.updateAt,
}); });
await storage.setObj(STOKEY_RULES, res.value); await storage.setObj(STOKEY_RULES, res.value);
} else { } else {
await storage.putObj(STOKEY_SYNC, { await syncOpt.update({ rulesSyncAt: res.updateAt });
rulesSyncAt: res.updateAt,
});
} }
} catch (err) { } catch (err) {
console.log("[sync user rules]", err); console.log("[sync user rules]", err);
} }
}; };
/**
* 同步分享规则
* @param {*} param0
* @returns
*/
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => { export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
await apiSyncData(syncUrl, syncKey, { await apiSyncData(syncUrl, syncKey, {
key: KV_RULES_SHARE_KEY, key: KV_RULES_SHARE_KEY,
@@ -87,7 +113,11 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
return shareUrl; return shareUrl;
}; };
export const syncAll = async () => { /**
await syncSetting(); * 同步个人设置和规则
await syncRules(); * @returns
*/
export const syncAll = async (isBg = false) => {
await syncSetting(isBg);
await syncRules(isBg);
}; };

View File

@@ -5,6 +5,7 @@ import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react"; import { CacheProvider } from "@emotion/react";
import { getSetting, getRules, matchRule, getFab } from "./libs"; import { getSetting, getRules, matchRule, getFab } from "./libs";
import { Translator } from "./libs/translator"; import { Translator } from "./libs/translator";
import { trySyncAllSubRules } from "./libs/rules";
/** /**
* 入口函数 * 入口函数
@@ -70,4 +71,7 @@ import { Translator } from "./libs/translator";
}, },
"C" "C"
); );
// 同步订阅规则
trySyncAllSubRules(setting);
})(); })();

View File

@@ -3,6 +3,7 @@ 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 CircularProgress from "@mui/material/CircularProgress";
import Alert from "@mui/material/Alert";
import { import {
GLOBAL_KEY, GLOBAL_KEY,
DEFAULT_RULE, DEFAULT_RULE,
@@ -11,7 +12,7 @@ import {
OPT_TRANS_ALL, OPT_TRANS_ALL,
OPT_STYLE_ALL, OPT_STYLE_ALL,
} from "../../config"; } from "../../config";
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect, useMemo } 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";
@@ -35,11 +36,12 @@ import IconButton from "@mui/material/IconButton";
import ShareIcon from "@mui/icons-material/Share"; import ShareIcon from "@mui/icons-material/Share";
import SyncIcon from "@mui/icons-material/Sync"; import SyncIcon from "@mui/icons-material/Sync";
import { useSubrules } from "../../hooks/Rules"; import { useSubrules } from "../../hooks/Rules";
import { rulesCache, tryLoadRules } from "../../libs/rules"; import { rulesCache, loadSubRules, syncSubRules } from "../../libs/rules";
import { useAlert } from "../../hooks/Alert"; import { useAlert } from "../../hooks/Alert";
import { loadSyncOpt, syncShareRules } from "../../libs/sync"; import { syncOpt, syncShareRules } from "../../libs/sync";
import { debounce } from "../../libs/utils";
function RuleFields({ rule, rules, setShow }) { function RuleFields({ rule, rules, setShow, setKeyword }) {
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" }; const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
const editMode = !!rule; const editMode = !!rule;
@@ -73,10 +75,21 @@ function RuleFields({ rule, rules, setShow }) {
setErrors((pre) => ({ ...pre, [name]: "" })); setErrors((pre) => ({ ...pre, [name]: "" }));
}; };
const handlePatternChange = useMemo(
() =>
debounce(async (patterns) => {
setKeyword(patterns.trim());
}, 500),
[setKeyword]
);
const handleChange = (e) => { const handleChange = (e) => {
e.preventDefault(); e.preventDefault();
const { name, value } = e.target; const { name, value } = e.target;
setFormValues((pre) => ({ ...pre, [name]: value })); setFormValues((pre) => ({ ...pre, [name]: value }));
if (name === "pattern" && !editMode) {
handlePatternChange(value);
}
}; };
const handleCancel = (e) => { const handleCancel = (e) => {
@@ -401,7 +414,7 @@ function ShareButton({ rules, injectRules, selectedSub }) {
const i18n = useI18n(); const i18n = useI18n();
const handleClick = async () => { const handleClick = async () => {
try { try {
const { syncUrl, syncKey } = await loadSyncOpt(); const { syncUrl, syncKey } = await syncOpt.load();
if (!syncUrl || !syncKey) { if (!syncUrl || !syncKey) {
alert.warning(i18n("error_sync_setting")); alert.warning(i18n("error_sync_setting"));
return; return;
@@ -409,7 +422,7 @@ function ShareButton({ rules, injectRules, selectedSub }) {
const shareRules = [...rules.list]; const shareRules = [...rules.list];
if (injectRules) { if (injectRules) {
const subRules = await tryLoadRules(selectedSub?.url); const subRules = await loadSubRules(selectedSub?.url);
shareRules.splice(-1, 0, ...subRules); shareRules.splice(-1, 0, ...subRules);
} }
@@ -445,6 +458,9 @@ function UserRules() {
const setting = useSetting(); const setting = useSetting();
const updateSetting = useSettingUpdate(); const updateSetting = useSettingUpdate();
const subrules = useSubrules(); const subrules = useSubrules();
const [subRules, setSubRules] = useState([]);
const [keyword, setKeyword] = useState("");
const selectedSub = subrules.list.find((item) => item.selected); const selectedSub = subrules.list.find((item) => item.selected);
const injectRules = !!setting?.injectRules; const injectRules = !!setting?.injectRules;
@@ -477,6 +493,25 @@ 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("");
}
}, [showAdd]);
return ( return (
<Stack spacing={3}> <Stack spacing={3}>
<Stack direction="row" spacing={2} useFlexGap flexWrap="wrap"> <Stack direction="row" spacing={2} useFlexGap flexWrap="wrap">
@@ -516,13 +551,37 @@ function UserRules() {
/> />
</Stack> </Stack>
{showAdd && <RuleFields rules={rules} setShow={setShowAdd} />} {showAdd && (
<RuleFields
rules={rules}
setShow={setShowAdd}
setKeyword={setKeyword}
/>
)}
<Box> <Box>
{rules.list.map((rule) => ( {rules.list
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} /> .filter(
))} (rule) =>
rule.pattern.includes(keyword) || keyword.includes(rule.pattern)
)
.map((rule) => (
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} />
))}
</Box> </Box>
{injectRules && (
<Box>
{subRules
.filter(
(rule) =>
rule.pattern.includes(keyword) || keyword.includes(rule.pattern)
)
.map((rule) => (
<RuleAccordion key={rule.pattern} rule={rule} />
))}
</Box>
)}
</Stack> </Stack>
); );
} }
@@ -542,9 +601,8 @@ function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
const handleSync = async () => { const handleSync = async () => {
try { try {
setLoading(true); setLoading(true);
const rules = await rulesCache.fetch(url); const rules = await syncSubRules(url);
await rulesCache.set(url, rules); if (rules.length > 0 && url === selectedUrl) {
if (url === selectedUrl) {
setRules(rules); setRules(rules);
} }
} catch (err) { } catch (err) {
@@ -603,11 +661,10 @@ function SubRulesEdit({ subrules }) {
} }
try { try {
const rules = await rulesCache.fetch(url); const rules = await syncSubRules(url);
if (rules.length === 0) { if (rules.length === 0) {
throw new Error("empty rules"); throw new Error("empty rules");
} }
await rulesCache.set(url, rules);
await subrules.add(url); await subrules.add(url);
setShowInput(false); setShowInput(false);
setInputText(""); setInputText("");
@@ -686,7 +743,7 @@ function SubRules() {
try { try {
setLoading(true); setLoading(true);
const rules = await tryLoadRules(selectedSub?.url); const rules = await loadSubRules(selectedSub?.url);
setRules(rules); setRules(rules);
} catch (err) { } catch (err) {
console.log("[load rules]", err); console.log("[load rules]", err);
@@ -738,9 +795,15 @@ export default function Rules() {
return ( return (
<Box> <Box>
<Stack spacing={3}> <Stack spacing={3}>
<Alert severity="info">
{i18n("rules_warn_1")}
<br />
{i18n("rules_warn_2")}
</Alert>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}> <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs value={activeTab} onChange={handleTabChange}> <Tabs value={activeTab} onChange={handleTabChange}>
<Tab label={i18n("edit_rules")} /> <Tab label={i18n("personal_rules")} />
<Tab label={i18n("subscribe_rules")} /> <Tab label={i18n("subscribe_rules")} />
</Tabs> </Tabs>
</Box> </Box>