Subscribe Rules
This commit is contained in:
@@ -94,6 +94,9 @@ const userscriptWebpack = (config, env) => {
|
||||
// @connect api.openai.com
|
||||
// @connect openai.azure.com
|
||||
// @connect workers.dev
|
||||
// @connect github.io
|
||||
// @connect githubusercontent.com
|
||||
// @connect kiss-translator.rayjar.com
|
||||
// @run-at document-end
|
||||
// ==/UserScript==
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ browser.runtime.onInstalled.addListener(() => {
|
||||
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
|
||||
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES);
|
||||
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
|
||||
// todo:缓存内置rules
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -105,8 +105,20 @@ export const I18N = {
|
||||
en: `Add`,
|
||||
},
|
||||
inject_rules: {
|
||||
zh: `注入内置规则`,
|
||||
en: `Inject Built-in Rules`,
|
||||
zh: `注入订阅规则`,
|
||||
en: `Inject Subscribe Rules`,
|
||||
},
|
||||
edit_rules: {
|
||||
zh: `编辑规则`,
|
||||
en: `Edit Rules`,
|
||||
},
|
||||
subscribe_rules: {
|
||||
zh: `订阅规则`,
|
||||
en: `Subscribe Rules`,
|
||||
},
|
||||
subscribe_url: {
|
||||
zh: `订阅地址`,
|
||||
en: `Subscribe URL`,
|
||||
},
|
||||
sync_warn: {
|
||||
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
|
||||
@@ -196,6 +208,10 @@ export const I18N = {
|
||||
zh: `错误的文件类型`,
|
||||
en: `Wrong file type`,
|
||||
},
|
||||
error_fetch_url: {
|
||||
zh: `请检查url地址是否正确或稍后再试。`,
|
||||
en: `Please check if the url address is correct or try again later.`,
|
||||
},
|
||||
openai_api: {
|
||||
zh: `OpenAI 接口`,
|
||||
en: `OpenAI API`,
|
||||
|
||||
@@ -16,6 +16,7 @@ export const STOKEY_SETTING = `${APP_NAME}_setting`;
|
||||
export const STOKEY_RULES = `${APP_NAME}_rules`;
|
||||
export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
||||
export const STOKEY_FAB = `${APP_NAME}_fab`;
|
||||
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
|
||||
|
||||
export const CLIENT_WEB = "web";
|
||||
export const CLIENT_CHROME = "chrome";
|
||||
@@ -147,13 +148,25 @@ export const GLOBLA_RULE = {
|
||||
bgColor: "",
|
||||
};
|
||||
|
||||
// 订阅列表
|
||||
export const DEFAULT_SUBRULES_LIST = [
|
||||
{
|
||||
url: "https://kiss-translator.rayjar.com/kiss-translator-rules.json",
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
url: "https://fishjar.github.io/kiss-translator/kiss-translator-rules.json",
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_SETTING = {
|
||||
darkMode: false, // 深色模式
|
||||
uiLang: "en", // 界面语言
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||
injectRules: true, // 是否注入内置规则
|
||||
injectRules: true, // 是否注入订阅规则
|
||||
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
||||
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口
|
||||
openaiUrl: "https://api.openai.com/v1/chat/completions",
|
||||
openaiKey: "",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Translator } from "./libs/translator";
|
||||
(async () => {
|
||||
const setting = await getSetting();
|
||||
const rules = await getRules();
|
||||
const rule = matchRule(rules, document.location.href, setting);
|
||||
const rule = await matchRule(rules, document.location.href, setting);
|
||||
const translator = new Translator(rule, setting);
|
||||
|
||||
// 监听消息
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import {
|
||||
STOKEY_RULES,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
GLOBAL_KEY,
|
||||
} from "../config";
|
||||
import { STOKEY_RULES, DEFAULT_SUBRULES_LIST } from "../config";
|
||||
import storage from "../libs/storage";
|
||||
import { useStorages } from "./Storage";
|
||||
import { matchValue } from "../libs/utils";
|
||||
import { syncRules } from "../libs/sync";
|
||||
import { useSync } from "./Sync";
|
||||
import { useSetting, useSettingUpdate } from "./Setting";
|
||||
import { checkRules } from "../libs/rules";
|
||||
|
||||
/**
|
||||
* 匹配规则增删改查 hook
|
||||
@@ -61,35 +55,9 @@ export function useRules() {
|
||||
|
||||
const merge = async (newRules) => {
|
||||
const rules = [...list];
|
||||
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
|
||||
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
|
||||
newRules
|
||||
.filter(({ pattern }) => pattern && typeof pattern === "string")
|
||||
.map(
|
||||
({
|
||||
pattern,
|
||||
selector,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
}) => ({
|
||||
pattern,
|
||||
selector: typeof selector === "string" ? selector : "",
|
||||
bgColor: typeof bgColor === "string" ? bgColor : "",
|
||||
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
|
||||
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
|
||||
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
|
||||
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
|
||||
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
|
||||
})
|
||||
)
|
||||
.forEach((newRule) => {
|
||||
const rule = rules.find(
|
||||
(oldRule) => oldRule.pattern === newRule.pattern
|
||||
);
|
||||
newRules = checkRules(newRules);
|
||||
newRules.forEach((newRule) => {
|
||||
const rule = rules.find((oldRule) => oldRule.pattern === newRule.pattern);
|
||||
if (rule) {
|
||||
Object.assign(rule, newRule);
|
||||
} else {
|
||||
@@ -101,3 +69,39 @@ export function useRules() {
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ import {
|
||||
STOKEY_FAB,
|
||||
GLOBLA_RULE,
|
||||
GLOBAL_KEY,
|
||||
BUILTIN_RULES,
|
||||
DEFAULT_SUBRULES_LIST,
|
||||
} from "../config";
|
||||
import { browser } from "./browser";
|
||||
import { isMatch } from "./utils";
|
||||
import { tryLoadRules } from "./rules";
|
||||
|
||||
/**
|
||||
* 获取节点列表并转为数组
|
||||
@@ -53,9 +54,21 @@ export const setFab = async (obj) => await storage.setObj(STOKEY_FAB, obj);
|
||||
* @param {string} href
|
||||
* @returns
|
||||
*/
|
||||
export const matchRule = (rules, href, { injectRules }) => {
|
||||
export const matchRule = async (
|
||||
rules,
|
||||
href,
|
||||
{ injectRules, subrulesList = DEFAULT_SUBRULES_LIST }
|
||||
) => {
|
||||
if (injectRules) {
|
||||
rules.splice(-1, 0, ...BUILTIN_RULES);
|
||||
try {
|
||||
const selectedSub = subrulesList.find((item) => item.selected);
|
||||
if (selectedSub?.url) {
|
||||
const subRules = await tryLoadRules(selectedSub.url);
|
||||
rules.splice(-1, 0, ...subRules);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[load injectRules]", err);
|
||||
}
|
||||
}
|
||||
|
||||
const rule = rules.find((rule) =>
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* 任务池
|
||||
* @param {*} fn
|
||||
* @param {*} preFn
|
||||
* @param {*} _interval
|
||||
* @param {*} _limit
|
||||
* @returns
|
||||
*/
|
||||
export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
||||
const pool = [];
|
||||
const maxRetry = 2; // 最大重试次数
|
||||
@@ -6,11 +14,6 @@ export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
||||
let interval = _interval; // 间隔时间
|
||||
let timer = null;
|
||||
|
||||
/**
|
||||
* 任务池
|
||||
* @param {*} item
|
||||
* @param {*} preArgs
|
||||
*/
|
||||
const handleTask = async (item, preArgs) => {
|
||||
curCount++;
|
||||
const { args, resolve, reject, retry } = item;
|
||||
|
||||
99
src/libs/rules.js
Normal file
99
src/libs/rules.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import storage from "./storage";
|
||||
import { fetchPolyfill } from "./fetch";
|
||||
import { matchValue, type } from "./utils";
|
||||
import {
|
||||
STOKEY_RULESCACHE_PREFIX,
|
||||
GLOBAL_KEY,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
} from "../config";
|
||||
|
||||
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
|
||||
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
|
||||
|
||||
/**
|
||||
* 检查过滤rules
|
||||
* @param {*} rules
|
||||
* @returns
|
||||
*/
|
||||
export const checkRules = (rules) => {
|
||||
if (type(rules) === "string") {
|
||||
rules = JSON.parse(rules);
|
||||
}
|
||||
if (type(rules) !== "array") {
|
||||
throw new Error("data error");
|
||||
}
|
||||
|
||||
const patternSet = new Set();
|
||||
rules = rules
|
||||
.filter((rule) => type(rule) === "object")
|
||||
.filter(({ pattern }) => {
|
||||
if (type(pattern) !== "string" || patternSet.has(pattern.trim())) {
|
||||
return false;
|
||||
}
|
||||
patternSet.add(pattern.trim());
|
||||
return true;
|
||||
})
|
||||
.map(
|
||||
({
|
||||
pattern,
|
||||
selector,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
}) => ({
|
||||
pattern: pattern.trim(),
|
||||
selector: type(selector) === "string" ? selector : "",
|
||||
bgColor: type(bgColor) === "string" ? bgColor : "",
|
||||
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
|
||||
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
|
||||
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
|
||||
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
|
||||
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
|
||||
})
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
/**
|
||||
* 本地rules缓存
|
||||
*/
|
||||
export const rulesCache = {
|
||||
fetch: async (url) => {
|
||||
const res = await fetchPolyfill(url);
|
||||
const rules = checkRules(res).filter(
|
||||
(rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== ""
|
||||
);
|
||||
return rules;
|
||||
},
|
||||
set: async (url, rules) => {
|
||||
await storage.setObj(`${STOKEY_RULESCACHE_PREFIX}${url}`, rules);
|
||||
},
|
||||
get: async (url) => {
|
||||
return await storage.getObj(`${STOKEY_RULESCACHE_PREFIX}${url}`);
|
||||
},
|
||||
del: async (url) => {
|
||||
await storage.del(`${STOKEY_RULESCACHE_PREFIX}${url}`);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 从缓存或远程加载订阅的rules
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const tryLoadRules = async (url) => {
|
||||
let rules = await rulesCache.get(url);
|
||||
if (rules?.length) {
|
||||
return rules;
|
||||
}
|
||||
rules = await rulesCache.fetch(url);
|
||||
await rulesCache.set(url, rules);
|
||||
return rules;
|
||||
};
|
||||
@@ -88,3 +88,13 @@ export const isMatch = (s, p) => {
|
||||
|
||||
return p.slice(pIndex).replaceAll("*", "") === "";
|
||||
};
|
||||
|
||||
/**
|
||||
* 类型检查
|
||||
* @param {*} o
|
||||
* @returns
|
||||
*/
|
||||
export const type = (o) => {
|
||||
const s = Object.prototype.toString.call(o);
|
||||
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ import { Translator } from "./libs/translator";
|
||||
// 翻译页面
|
||||
const setting = await getSetting();
|
||||
const rules = await getRules();
|
||||
const rule = matchRule(rules, document.location.href, setting);
|
||||
const rule = await matchRule(rules, document.location.href, setting);
|
||||
const translator = new Translator(rule, setting);
|
||||
|
||||
// 浮球按钮
|
||||
|
||||
@@ -2,6 +2,7 @@ import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Button from "@mui/material/Button";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import {
|
||||
GLOBAL_KEY,
|
||||
DEFAULT_RULE,
|
||||
@@ -9,9 +10,8 @@ import {
|
||||
OPT_LANGS_TO,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
BUILTIN_RULES,
|
||||
} from "../../config";
|
||||
import { useState, useRef } from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
@@ -26,6 +26,15 @@ import FileUploadIcon from "@mui/icons-material/FileUpload";
|
||||
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import Radio from "@mui/material/Radio";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import { useSubrules } from "../../hooks/Rules";
|
||||
import { rulesCache, tryLoadRules } from "../../libs/rules";
|
||||
|
||||
function RuleFields({ rule, rules, setShow }) {
|
||||
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
|
||||
@@ -384,12 +393,13 @@ function UploadButton({ onChange, text }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function Rules() {
|
||||
function UserRules() {
|
||||
const i18n = useI18n();
|
||||
const rules = useRules();
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const setting = useSetting();
|
||||
const updateSetting = useSettingUpdate();
|
||||
|
||||
const injectRules = !!setting?.injectRules;
|
||||
|
||||
const handleImport = (e) => {
|
||||
@@ -421,7 +431,6 @@ export default function Rules() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Button
|
||||
@@ -461,14 +470,229 @@ export default function Rules() {
|
||||
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} />
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
{injectRules && (
|
||||
<Box>
|
||||
{BUILTIN_RULES.map((rule) => (
|
||||
<RuleAccordion key={rule.pattern} rule={rule} />
|
||||
))}
|
||||
</Box>
|
||||
function SubRulesItem({ index, url, selectedUrl, subrules, setRules }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleDel = async () => {
|
||||
try {
|
||||
await subrules.del(url);
|
||||
await rulesCache.del(url);
|
||||
} catch (err) {
|
||||
console.log("[del subrules]", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const rules = await rulesCache.fetch(url);
|
||||
await rulesCache.set(url, rules);
|
||||
if (url === selectedUrl) {
|
||||
setRules(rules);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[sync rules]", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<FormControlLabel value={url} control={<Radio />} label={url} />
|
||||
|
||||
{loading ? (
|
||||
<CircularProgress size={16} />
|
||||
) : (
|
||||
<IconButton size="small" onClick={handleSync}>
|
||||
<SyncIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{index !== 0 && selectedUrl !== url && (
|
||||
<IconButton size="small" onClick={handleDel}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function SubRulesEdit({ subrules }) {
|
||||
const i18n = useI18n();
|
||||
const [inputText, setInputText] = useState("");
|
||||
const [inputError, setInputError] = useState("");
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
|
||||
const handleCancel = (e) => {
|
||||
e.preventDefault();
|
||||
setShowInput(false);
|
||||
setInputText("");
|
||||
setInputError("");
|
||||
};
|
||||
|
||||
const handleSave = async (e) => {
|
||||
e.preventDefault();
|
||||
const url = inputText.trim();
|
||||
|
||||
if (!url) {
|
||||
setInputError(i18n("error_cant_be_blank"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (subrules.list.find((item) => item.url === url)) {
|
||||
setInputError(i18n("error_duplicate_values"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const rules = await rulesCache.fetch(url);
|
||||
if (rules.length === 0) {
|
||||
throw new Error("empty rules");
|
||||
}
|
||||
await rulesCache.set(url, rules);
|
||||
await subrules.add(url);
|
||||
setShowInput(false);
|
||||
setInputText("");
|
||||
} catch (err) {
|
||||
console.log("[fetch rules]", err);
|
||||
setInputError(i18n("error_fetch_url"));
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (e) => {
|
||||
e.preventDefault();
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
|
||||
const handleFocus = (e) => {
|
||||
e.preventDefault();
|
||||
setInputError("");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
disabled={showInput}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowInput(true);
|
||||
}}
|
||||
>
|
||||
{i18n("add")}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{showInput && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
value={inputText}
|
||||
error={!!inputError}
|
||||
helperText={inputError}
|
||||
onChange={handleInput}
|
||||
onFocus={handleFocus}
|
||||
label={i18n("subscribe_url")}
|
||||
/>
|
||||
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<Button size="small" variant="contained" onClick={handleSave}>
|
||||
{i18n("save")}
|
||||
</Button>
|
||||
<Button size="small" variant="outlined" onClick={handleCancel}>
|
||||
{i18n("cancel")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SubRules() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [rules, setRules] = useState([]);
|
||||
const subrules = useSubrules();
|
||||
const selectedSub = subrules.list.find((item) => item.selected);
|
||||
|
||||
const handleSelect = (e) => {
|
||||
const url = e.target.value;
|
||||
subrules.select(url);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (selectedSub?.url) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const rules = await tryLoadRules(selectedSub?.url);
|
||||
setRules(rules);
|
||||
} catch (err) {
|
||||
console.log("[load rules]", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [selectedSub?.url]);
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<SubRulesEdit subrules={subrules} />
|
||||
|
||||
<RadioGroup value={selectedSub?.url} onChange={handleSelect}>
|
||||
{subrules.list.map((item, index) => (
|
||||
<SubRulesItem
|
||||
key={item.url}
|
||||
url={item.url}
|
||||
index={index}
|
||||
selectedUrl={selectedSub?.url}
|
||||
subrules={subrules}
|
||||
setRules={setRules}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
|
||||
<Box>
|
||||
{loading ? (
|
||||
<center>
|
||||
<CircularProgress />
|
||||
</center>
|
||||
) : (
|
||||
rules.map((rule) => <RuleAccordion key={rule.pattern} rule={rule} />)
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Rules() {
|
||||
const i18n = useI18n();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
const handleTabChange = (e, newValue) => {
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<Tab label={i18n("edit_rules")} />
|
||||
<Tab label={i18n("subscribe_rules")} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<div hidden={activeTab !== 0}>{activeTab === 0 && <UserRules />}</div>
|
||||
<div hidden={activeTab !== 1}>{activeTab === 1 && <SubRules />}</div>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user