Compare commits
11 Commits
80
UPDATELOG.md
80
UPDATELOG.md
@@ -1,3 +1,83 @@
|
||||
## v1.7.2
|
||||
|
||||
### Break Changes
|
||||
|
||||
- 更新后请务必重新导入所有订阅,包括 Remote 和 Local
|
||||
- 此版本重构了 Merge/Script,更新前请先备份好自定义 Merge 和 Script(更新并不会删除配置文件,但是旧版 Merge 和 Script 在更新后无法从前端访问,备份以防万一)
|
||||
- Merge 改名为 `扩展配置`,分为 `全局扩展配置` 和 `订阅扩展配置`,全局扩展配置对所有订阅生效,订阅扩展配置只对关联的订阅生效
|
||||
- Script 改名为 `扩展脚本`,同样分为 `全局扩展脚本` 和 `订阅扩展脚本`
|
||||
- 订阅扩展配置在订阅右键菜单里进入
|
||||
- 执行优先级为: 全局扩展配置 -> 全局扩展脚本 -> 订阅扩展配置 ->订阅扩展脚本
|
||||
- 扩展配置删除了 `prepend/append` 能力,请使用 右键订阅 -> `编辑规则`/`编辑节点`/`编辑代理组` 来代替
|
||||
- MacOS 用户更新后请重新安装服务模式
|
||||
|
||||
### Features
|
||||
|
||||
- 升级内核到 1.18.6
|
||||
- 移除内核授权,改为服务模式实现
|
||||
- 自动填充本地订阅名称
|
||||
- 添加重大更新处理逻辑
|
||||
- 订阅单独指定扩展配置/脚本(需要重新导入订阅)
|
||||
- 添加可视化规则编辑器(需要重新导入订阅)
|
||||
- 编辑器新增工具栏按钮(格式化、最大化/最小化)
|
||||
- WEBUI 使用最新版 metacubex,并解决无法自动登陆问问题
|
||||
- 禁用部分 Webview2 快捷键
|
||||
- 热键配置新增连接符 + 号
|
||||
- 新增部分悬浮提示按钮,用于解释说明
|
||||
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 设置页面右上角新增 TG 频道链接
|
||||
- 各种细节优化和界面性能优化
|
||||
|
||||
### Bugs Fixes
|
||||
|
||||
- 修复代理绕过格式检查
|
||||
- 通过进程名称关闭进程
|
||||
- 退出软件时恢复 DNS 设置
|
||||
- 修复创建本地订阅时更新间隔无法保存
|
||||
- 连接页面列宽无法调整
|
||||
|
||||
---
|
||||
|
||||
## v1.7.1
|
||||
|
||||
### Break Changes
|
||||
|
||||
- 更新后请务必重新导入所有订阅,包括 Remote 和 Local
|
||||
- 此版本重构了 Merge/Script,更新前请先备份好自定义 Merge 和 Script(更新并不会删除配置文件,但是旧版 Merge 和 Script 在更新后无法从前端访问,备份以防万一)
|
||||
- Merge 改名为 `扩展配置`,分为 `全局扩展配置` 和 `订阅扩展配置`,全局扩展配置对所有订阅生效,订阅扩展配置只对关联的订阅生效
|
||||
- Script 改名为 `扩展脚本`,同样分为 `全局扩展脚本` 和 `订阅扩展脚本`
|
||||
- 订阅扩展配置在订阅右键菜单里进入
|
||||
- 执行优先级为: 全局扩展配置 -> 全局扩展脚本 -> 订阅扩展配置 ->订阅扩展脚本
|
||||
- 扩展配置删除了 `prepend/append` 能力,请使用 右键订阅 -> `编辑规则`/`编辑节点`/`编辑代理组` 来代替
|
||||
- MacOS 用户更新后请重新安装服务模式
|
||||
|
||||
### Features
|
||||
|
||||
- 升级内核到 1.18.6
|
||||
- 移除内核授权,改为服务模式实现
|
||||
- 自动填充本地订阅名称
|
||||
- 添加重大更新处理逻辑
|
||||
- 订阅单独指定扩展配置/脚本(需要重新导入订阅)
|
||||
- 添加可视化规则编辑器(需要重新导入订阅)
|
||||
- 编辑器新增工具栏按钮(格式化、最大化/最小化)
|
||||
- WEBUI 使用最新版 metacubex,并解决无法自动登陆问问题
|
||||
- 禁用部分 Webview2 快捷键
|
||||
- 热键配置新增连接符 + 号
|
||||
- 新增部分悬浮提示按钮,用于解释说明
|
||||
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 设置页面右上角新增 TG 频道链接
|
||||
- 各种细节优化和界面性能优化
|
||||
|
||||
### Bugs Fixes
|
||||
|
||||
- 修复代理绕过格式检查
|
||||
- 通过进程名称关闭进程
|
||||
- 退出软件时恢复 DNS 设置
|
||||
- 修复创建本地订阅时更新间隔无法保存
|
||||
- 连接页面列宽无法调整
|
||||
|
||||
---
|
||||
|
||||
## v1.7.0
|
||||
|
||||
### Break Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.2",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -784,7 +784,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clash-verge"
|
||||
version = "1.7.0"
|
||||
version = "1.7.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auto-launch",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "1.7.0"
|
||||
version = "1.7.2"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
|
||||
@@ -247,33 +247,6 @@ impl PrfItem {
|
||||
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = PrfItem::from_merge(None)?;
|
||||
Config::profiles().data().append_item(merge_item.clone())?;
|
||||
merge = merge_item.uid;
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = PrfItem::from_script(None)?;
|
||||
Config::profiles().data().append_item(script_item.clone())?;
|
||||
script = script_item.uid;
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = PrfItem::from_rules()?;
|
||||
Config::profiles().data().append_item(rules_item.clone())?;
|
||||
rules = rules_item.uid;
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = PrfItem::from_proxies()?;
|
||||
Config::profiles()
|
||||
.data()
|
||||
.append_item(proxies_item.clone())?;
|
||||
proxies = proxies_item.uid;
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = PrfItem::from_groups()?;
|
||||
Config::profiles().data().append_item(groups_item.clone())?;
|
||||
groups = groups_item.uid;
|
||||
}
|
||||
// 使用软件自己的代理
|
||||
if self_proxy {
|
||||
let port = Config::verge()
|
||||
@@ -400,6 +373,34 @@ impl PrfItem {
|
||||
bail!("profile does not contain `proxies` or `proxy-providers`");
|
||||
}
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = PrfItem::from_merge(None)?;
|
||||
Config::profiles().data().append_item(merge_item.clone())?;
|
||||
merge = merge_item.uid;
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = PrfItem::from_script(None)?;
|
||||
Config::profiles().data().append_item(script_item.clone())?;
|
||||
script = script_item.uid;
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = PrfItem::from_rules()?;
|
||||
Config::profiles().data().append_item(rules_item.clone())?;
|
||||
rules = rules_item.uid;
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = PrfItem::from_proxies()?;
|
||||
Config::profiles()
|
||||
.data()
|
||||
.append_item(proxies_item.clone())?;
|
||||
proxies = proxies_item.uid;
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = PrfItem::from_groups()?;
|
||||
Config::profiles().data().append_item(groups_item.clone())?;
|
||||
groups = groups_item.uid;
|
||||
}
|
||||
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("remote".into()),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::prfitem::PrfItem;
|
||||
use super::{prfitem::PrfItem, PrfOption};
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -205,6 +205,7 @@ impl IProfiles {
|
||||
each.extra = item.extra;
|
||||
each.updated = item.updated;
|
||||
each.home = item.home;
|
||||
each.option = PrfOption::merge(each.option.clone(), item.option);
|
||||
// save the file data
|
||||
// move the field value after save
|
||||
if let Some(file_data) = item.file_data.take() {
|
||||
|
||||
@@ -8,15 +8,15 @@ pub struct SeqMap {
|
||||
}
|
||||
|
||||
pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping {
|
||||
let prepend = seq_map.prepend;
|
||||
let mut prepend = seq_map.prepend;
|
||||
let append = seq_map.append;
|
||||
let delete = seq_map.delete;
|
||||
|
||||
let origin_seq = config.get(&name).map_or(Sequence::default(), |val| {
|
||||
val.as_sequence().unwrap().clone()
|
||||
val.as_sequence().unwrap_or(&Sequence::default()).clone()
|
||||
});
|
||||
let mut seq = origin_seq.clone();
|
||||
|
||||
prepend.reverse();
|
||||
for item in prepend {
|
||||
seq.insert(0, item);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"package": {
|
||||
"productName": "Clash Verge",
|
||||
"version": "1.7.0"
|
||||
"version": "1.7.2"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
|
||||
@@ -162,7 +162,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ width: "auto", height: "100vh" }}>
|
||||
<DialogContent sx={{ width: "auto", height: "calc(100vh - 185px)" }}>
|
||||
<MonacoEditor
|
||||
language={language}
|
||||
theme={themeMode === "light" ? "vs" : "vs-dark"}
|
||||
|
||||
@@ -71,7 +71,7 @@ export const ProfileItem = (props: Props) => {
|
||||
const from = parseUrl(itemData.url);
|
||||
const description = itemData.desc;
|
||||
const expire = parseExpire(extra?.expire);
|
||||
const progress = Math.round(((download + upload) * 100) / (total + 0.1));
|
||||
const progress = Math.round(((download + upload) * 100) / (total + 0.01) + 1);
|
||||
|
||||
const loading = loadingCache[itemData.uid] ?? false;
|
||||
|
||||
@@ -211,27 +211,27 @@ export const ProfileItem = (props: Props) => {
|
||||
{
|
||||
label: "Edit Rules",
|
||||
handler: onEditRules,
|
||||
disabled: option?.rules === null,
|
||||
disabled: !option?.rules,
|
||||
},
|
||||
{
|
||||
label: "Edit Proxies",
|
||||
handler: onEditProxies,
|
||||
disabled: !option?.proxies,
|
||||
},
|
||||
{
|
||||
label: "Edit Groups",
|
||||
handler: onEditGroups,
|
||||
disabled: !option?.groups,
|
||||
},
|
||||
// {
|
||||
// label: "Edit Proxies",
|
||||
// handler: onEditProxies,
|
||||
// disabled: option?.proxies === null,
|
||||
// },
|
||||
// {
|
||||
// label: "Edit Groups",
|
||||
// handler: onEditGroups,
|
||||
// disabled: option?.groups === null,
|
||||
// },
|
||||
{
|
||||
label: "Extend Config",
|
||||
handler: onEditMerge,
|
||||
disabled: option?.merge === null,
|
||||
disabled: !option?.merge,
|
||||
},
|
||||
{
|
||||
label: "Extend Script",
|
||||
handler: onEditScript,
|
||||
disabled: option?.script === null,
|
||||
disabled: !option?.script,
|
||||
},
|
||||
{ label: "Open File", handler: onOpenFile, disabled: false },
|
||||
{ label: "Update", handler: () => onUpdate(0), disabled: false },
|
||||
@@ -252,27 +252,27 @@ export const ProfileItem = (props: Props) => {
|
||||
{
|
||||
label: "Edit Rules",
|
||||
handler: onEditRules,
|
||||
disabled: option?.rules === null,
|
||||
disabled: !option?.rules,
|
||||
},
|
||||
{
|
||||
label: "Edit Proxies",
|
||||
handler: onEditProxies,
|
||||
disabled: !option?.proxies,
|
||||
},
|
||||
{
|
||||
label: "Edit Groups",
|
||||
handler: onEditGroups,
|
||||
disabled: !option?.groups,
|
||||
},
|
||||
// {
|
||||
// label: "Edit Proxies",
|
||||
// handler: onEditProxies,
|
||||
// disabled: option?.proxies === null,
|
||||
// },
|
||||
// {
|
||||
// label: "Edit Groups",
|
||||
// handler: onEditGroups,
|
||||
// disabled: option?.groups === null,
|
||||
// },
|
||||
{
|
||||
label: "Extend Config",
|
||||
handler: onEditMerge,
|
||||
disabled: option?.merge === null,
|
||||
disabled: !option?.merge,
|
||||
},
|
||||
{
|
||||
label: "Extend Script",
|
||||
handler: onEditScript,
|
||||
disabled: option?.script === null,
|
||||
disabled: !option?.script,
|
||||
},
|
||||
{ label: "Open File", handler: onOpenFile, disabled: false },
|
||||
{
|
||||
@@ -429,7 +429,7 @@ export const ProfileItem = (props: Props) => {
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
style={{ opacity: progress > 0 ? 1 : 0 }}
|
||||
style={{ opacity: total > 0 ? 1 : 0 }}
|
||||
/>
|
||||
</ProfileBox>
|
||||
|
||||
@@ -501,10 +501,10 @@ export const ProfileItem = (props: Props) => {
|
||||
/>
|
||||
<EditorViewer
|
||||
open={groupsOpen}
|
||||
initialData={readProfileFile(option?.proxies ?? "")}
|
||||
initialData={readProfileFile(option?.groups ?? "")}
|
||||
language="yaml"
|
||||
onSave={async (prev, curr) => {
|
||||
await saveProfileFile(option?.proxies ?? "", curr ?? "");
|
||||
await saveProfileFile(option?.groups ?? "", curr ?? "");
|
||||
onSave && onSave(prev, curr);
|
||||
}}
|
||||
onClose={() => setGroupsOpen(false)}
|
||||
|
||||
@@ -18,9 +18,17 @@ interface Props {
|
||||
|
||||
export const RuleItem = (props: Props) => {
|
||||
let { type, ruleRaw, onDelete } = props;
|
||||
const sortable = type === "prepend" || type === "append";
|
||||
const rule = ruleRaw.replace(",no-resolve", "").split(",");
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: ruleRaw });
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = sortable
|
||||
? useSortable({ id: ruleRaw })
|
||||
: {
|
||||
attributes: {},
|
||||
listeners: {},
|
||||
setNodeRef: null,
|
||||
transform: null,
|
||||
transition: null,
|
||||
};
|
||||
return (
|
||||
<ListItem
|
||||
sx={({ palette }) => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import yaml from "js-yaml";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -36,6 +36,8 @@ import getSystem from "@/utils/get-system";
|
||||
import { RuleItem } from "@/components/profile/rule-item";
|
||||
import { BaseSearchBox } from "../base/base-search-box";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import MonacoEditor from "react-monaco-editor";
|
||||
import { useThemeMode } from "@/services/states";
|
||||
|
||||
interface Props {
|
||||
profileUid: string;
|
||||
@@ -230,8 +232,11 @@ const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
|
||||
export const RulesEditorViewer = (props: Props) => {
|
||||
const { title, profileUid, property, open, onClose, onSave } = props;
|
||||
const { t } = useTranslation();
|
||||
const themeMode = useThemeMode();
|
||||
|
||||
const [prevData, setPrevData] = useState("");
|
||||
const [currData, setCurrData] = useState("");
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [match, setMatch] = useState(() => (_: string) => true);
|
||||
|
||||
const [ruleType, setRuleType] = useState<(typeof rules)[number]>(rules[0]);
|
||||
@@ -247,6 +252,11 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
const [appendSeq, setAppendSeq] = useState<string[]>([]);
|
||||
const [deleteSeq, setDeleteSeq] = useState<string[]>([]);
|
||||
|
||||
const filteredRuleList = useMemo(
|
||||
() => ruleList.filter((rule) => match(rule)),
|
||||
[ruleList, match]
|
||||
);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
@@ -286,9 +296,28 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
setPrependSeq(obj.prepend || []);
|
||||
setAppendSeq(obj.append || []);
|
||||
setDeleteSeq(obj.delete || []);
|
||||
|
||||
setPrevData(data);
|
||||
setCurrData(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currData === "") return;
|
||||
if (visible !== true) return;
|
||||
|
||||
let obj = yaml.load(currData) as { prepend: []; append: []; delete: [] };
|
||||
setPrependSeq(obj.prepend || []);
|
||||
setAppendSeq(obj.append || []);
|
||||
setDeleteSeq(obj.delete || []);
|
||||
}, [visible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prependSeq && appendSeq && deleteSeq)
|
||||
setCurrData(
|
||||
yaml.dump({ prepend: prependSeq, append: appendSeq, delete: deleteSeq })
|
||||
);
|
||||
}, [prependSeq, appendSeq, deleteSeq]);
|
||||
|
||||
const fetchProfile = async () => {
|
||||
let data = await readProfileFile(profileUid);
|
||||
let groupsObj = yaml.load(data) as { "proxy-groups": [] };
|
||||
@@ -333,11 +362,6 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
let currData = yaml.dump({
|
||||
prepend: prependSeq,
|
||||
append: appendSeq,
|
||||
delete: deleteSeq,
|
||||
});
|
||||
await saveProfileFile(property, currData);
|
||||
onSave?.(prevData, currData);
|
||||
onClose();
|
||||
@@ -348,229 +372,292 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
|
||||
<DialogTitle>{title ?? t("Edit Rules")}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
{t("Edit Rules")}
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setVisible((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
{visible ? t("Advanced") : t("Visible")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ display: "flex", width: "auto", height: "100vh" }}>
|
||||
<List
|
||||
sx={{
|
||||
height: "calc(100% - 16px)",
|
||||
width: "50%",
|
||||
padding: "0 10px",
|
||||
}}
|
||||
>
|
||||
<Item>
|
||||
<ListItemText primary={t("Rule Type")} />
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
options={rules}
|
||||
value={ruleType}
|
||||
getOptionLabel={(option) => option.name}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} title={t(option.name)}>
|
||||
{option.name}
|
||||
</li>
|
||||
)}
|
||||
onChange={(_, value) => value && setRuleType(value)}
|
||||
/>
|
||||
</Item>
|
||||
<Item sx={{ display: !(ruleType.required ?? true) ? "none" : "" }}>
|
||||
<ListItemText primary={t("Rule Content")} />
|
||||
|
||||
{ruleType.name === "RULE-SET" && (
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
options={ruleSetList}
|
||||
value={ruleContent}
|
||||
onChange={(_, value) => value && setRuleContent(value)}
|
||||
/>
|
||||
)}
|
||||
{ruleType.name === "SUB-RULE" && (
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
options={subRuleList}
|
||||
value={ruleContent}
|
||||
onChange={(_, value) => value && setRuleContent(value)}
|
||||
/>
|
||||
)}
|
||||
{ruleType.name !== "RULE-SET" && ruleType.name !== "SUB-RULE" && (
|
||||
<TextField
|
||||
autoComplete="off"
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
value={ruleContent}
|
||||
required={ruleType.required ?? true}
|
||||
error={(ruleType.required ?? true) && !ruleContent}
|
||||
placeholder={ruleType.example}
|
||||
onChange={(e) => setRuleContent(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Item>
|
||||
<Item>
|
||||
<ListItemText primary={t("Proxy Policy")} />
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
options={proxyPolicyList}
|
||||
value={proxyPolicy}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} title={t(option)}>
|
||||
{option}
|
||||
</li>
|
||||
)}
|
||||
onChange={(_, value) => value && setProxyPolicy(value)}
|
||||
/>
|
||||
</Item>
|
||||
{ruleType.noResolve && (
|
||||
<Item>
|
||||
<ListItemText primary={t("No Resolve")} />
|
||||
<Switch
|
||||
checked={noResolve}
|
||||
onChange={() => setNoResolve(!noResolve)}
|
||||
/>
|
||||
</Item>
|
||||
)}
|
||||
<Item>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
try {
|
||||
let raw = validateRule();
|
||||
if (prependSeq.includes(raw)) return;
|
||||
setPrependSeq([...prependSeq, raw]);
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
}
|
||||
<DialogContent
|
||||
sx={{ display: "flex", width: "auto", height: "calc(100vh - 185px)" }}
|
||||
>
|
||||
{visible ? (
|
||||
<>
|
||||
<List
|
||||
sx={{
|
||||
width: "50%",
|
||||
padding: "0 10px",
|
||||
}}
|
||||
>
|
||||
{t("Prepend Rule")}
|
||||
</Button>
|
||||
</Item>
|
||||
<Item>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
try {
|
||||
let raw = validateRule();
|
||||
if (appendSeq.includes(raw)) return;
|
||||
setAppendSeq([...appendSeq, raw]);
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Append Rule")}
|
||||
</Button>
|
||||
</Item>
|
||||
</List>
|
||||
<Item>
|
||||
<ListItemText primary={t("Rule Type")} />
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
options={rules}
|
||||
value={ruleType}
|
||||
getOptionLabel={(option) => option.name}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} title={t(option.name)}>
|
||||
{option.name}
|
||||
</li>
|
||||
)}
|
||||
onChange={(_, value) => value && setRuleType(value)}
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
sx={{ display: !(ruleType.required ?? true) ? "none" : "" }}
|
||||
>
|
||||
<ListItemText primary={t("Rule Content")} />
|
||||
|
||||
<List
|
||||
sx={{ height: "calc(100% - 16px)", width: "50%", padding: "0 10px" }}
|
||||
>
|
||||
<BaseSearchBox
|
||||
matchCase={false}
|
||||
onSearch={(match) => setMatch(() => match)}
|
||||
/>
|
||||
<Virtuoso
|
||||
style={{ height: "calc(100% - 16px)", marginTop: "8px" }}
|
||||
totalCount={
|
||||
ruleList.length +
|
||||
(prependSeq.length > 0 ? 1 : 0) +
|
||||
(appendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
increaseViewportBy={256}
|
||||
itemContent={(index) => {
|
||||
let shift = prependSeq.length > 0 ? 1 : 0;
|
||||
if (prependSeq.length > 0 && index === 0) {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onPrependDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={prependSeq.map((x) => {
|
||||
return x;
|
||||
})}
|
||||
>
|
||||
{prependSeq.map((item, index) => {
|
||||
return (
|
||||
<RuleItem
|
||||
key={`${item}-${index}`}
|
||||
type="prepend"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(
|
||||
prependSeq.filter((v) => v !== item)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
} else if (index < ruleList.length + shift) {
|
||||
let newIndex = index - shift;
|
||||
return (
|
||||
<RuleItem
|
||||
key={`${ruleList[newIndex]}-${index}`}
|
||||
type={
|
||||
deleteSeq.includes(ruleList[newIndex])
|
||||
? "delete"
|
||||
: "original"
|
||||
}
|
||||
ruleRaw={ruleList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(ruleList[newIndex])) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter((v) => v !== ruleList[newIndex])
|
||||
);
|
||||
} else {
|
||||
setDeleteSeq((prev) => [...prev, ruleList[newIndex]]);
|
||||
}
|
||||
}}
|
||||
{ruleType.name === "RULE-SET" && (
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
options={ruleSetList}
|
||||
value={ruleContent}
|
||||
onChange={(_, value) => value && setRuleContent(value)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onAppendDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={appendSeq.map((x) => {
|
||||
return x;
|
||||
})}
|
||||
>
|
||||
{appendSeq.map((item, index) => {
|
||||
return (
|
||||
<RuleItem
|
||||
key={`${item}-${index}`}
|
||||
type="append"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v !== item));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
)}
|
||||
{ruleType.name === "SUB-RULE" && (
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
options={subRuleList}
|
||||
value={ruleContent}
|
||||
onChange={(_, value) => value && setRuleContent(value)}
|
||||
/>
|
||||
)}
|
||||
{ruleType.name !== "RULE-SET" &&
|
||||
ruleType.name !== "SUB-RULE" && (
|
||||
<TextField
|
||||
autoComplete="off"
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
value={ruleContent}
|
||||
required={ruleType.required ?? true}
|
||||
error={(ruleType.required ?? true) && !ruleContent}
|
||||
placeholder={ruleType.example}
|
||||
onChange={(e) => setRuleContent(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Item>
|
||||
<Item>
|
||||
<ListItemText primary={t("Proxy Policy")} />
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
options={proxyPolicyList}
|
||||
value={proxyPolicy}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} title={t(option)}>
|
||||
{option}
|
||||
</li>
|
||||
)}
|
||||
onChange={(_, value) => value && setProxyPolicy(value)}
|
||||
/>
|
||||
</Item>
|
||||
{ruleType.noResolve && (
|
||||
<Item>
|
||||
<ListItemText primary={t("No Resolve")} />
|
||||
<Switch
|
||||
checked={noResolve}
|
||||
onChange={() => setNoResolve(!noResolve)}
|
||||
/>
|
||||
</Item>
|
||||
)}
|
||||
<Item>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
try {
|
||||
let raw = validateRule();
|
||||
if (prependSeq.includes(raw)) return;
|
||||
setPrependSeq([...prependSeq, raw]);
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Prepend Rule")}
|
||||
</Button>
|
||||
</Item>
|
||||
<Item>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
try {
|
||||
let raw = validateRule();
|
||||
if (appendSeq.includes(raw)) return;
|
||||
setAppendSeq([...appendSeq, raw]);
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Append Rule")}
|
||||
</Button>
|
||||
</Item>
|
||||
</List>
|
||||
|
||||
<List
|
||||
sx={{
|
||||
width: "50%",
|
||||
padding: "0 10px",
|
||||
}}
|
||||
>
|
||||
<BaseSearchBox
|
||||
matchCase={false}
|
||||
onSearch={(match) => setMatch(() => match)}
|
||||
/>
|
||||
<Virtuoso
|
||||
style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
|
||||
totalCount={
|
||||
filteredRuleList.length +
|
||||
(prependSeq.length > 0 ? 1 : 0) +
|
||||
(appendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
increaseViewportBy={256}
|
||||
itemContent={(index) => {
|
||||
let shift = prependSeq.length > 0 ? 1 : 0;
|
||||
if (prependSeq.length > 0 && index === 0) {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onPrependDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={prependSeq.map((x) => {
|
||||
return x;
|
||||
})}
|
||||
>
|
||||
{prependSeq.map((item, index) => {
|
||||
return (
|
||||
<RuleItem
|
||||
key={`${item}-${index}`}
|
||||
type="prepend"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(
|
||||
prependSeq.filter((v) => v !== item)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
} else if (index < filteredRuleList.length + shift) {
|
||||
let newIndex = index - shift;
|
||||
return (
|
||||
<RuleItem
|
||||
key={`${filteredRuleList[newIndex]}-${index}`}
|
||||
type={
|
||||
deleteSeq.includes(filteredRuleList[newIndex])
|
||||
? "delete"
|
||||
: "original"
|
||||
}
|
||||
ruleRaw={filteredRuleList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredRuleList[newIndex])) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter(
|
||||
(v) => v !== filteredRuleList[newIndex]
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
...prev,
|
||||
filteredRuleList[newIndex],
|
||||
]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onAppendDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={appendSeq.map((x) => {
|
||||
return x;
|
||||
})}
|
||||
>
|
||||
{appendSeq.map((item, index) => {
|
||||
return (
|
||||
<RuleItem
|
||||
key={`${item}-${index}`}
|
||||
type="append"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(
|
||||
appendSeq.filter((v) => v !== item)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
<MonacoEditor
|
||||
height="100%"
|
||||
language="yaml"
|
||||
value={currData}
|
||||
theme={themeMode === "light" ? "vs" : "vs-dark"}
|
||||
options={{
|
||||
tabSize: 2, // 根据语言类型设置缩进大小
|
||||
minimap: {
|
||||
enabled: document.documentElement.clientWidth >= 1500, // 超过一定宽度显示minimap滚动条
|
||||
},
|
||||
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
|
||||
quickSuggestions: {
|
||||
strings: true, // 字符串类型的建议
|
||||
comments: true, // 注释类型的建议
|
||||
other: true, // 其他类型的建议
|
||||
},
|
||||
padding: {
|
||||
top: 33, // 顶部padding防止遮挡snippets
|
||||
},
|
||||
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
|
||||
getSystem() === "windows" ? ", twemoji mozilla" : ""
|
||||
}`,
|
||||
fontLigatures: true, // 连字符
|
||||
smoothScrolling: true, // 平滑滚动
|
||||
}}
|
||||
onChange={(value) => setCurrData(value)}
|
||||
/>
|
||||
</List>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
"Delete Rule": "Delete Rule",
|
||||
"Rule Condition Required": "Rule Condition Required",
|
||||
"Invalid Rule": "Invalid Rule",
|
||||
"Advanced": "Advanced",
|
||||
"Visible": "Visible",
|
||||
"DOMAIN": "Matches the full domain name",
|
||||
"DOMAIN-SUFFIX": "Matches the domain suffix",
|
||||
"DOMAIN-KEYWORD": "Matches the domain keyword",
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
"Delete Rule": "删除规则",
|
||||
"Rule Condition Required": "规则条件缺失",
|
||||
"Invalid Rule": "无效规则",
|
||||
"Advanced": "高级",
|
||||
"Visible": "可视化",
|
||||
"DOMAIN": "匹配完整域名",
|
||||
"DOMAIN-SUFFIX": "匹配域名后缀",
|
||||
"DOMAIN-KEYWORD": "匹配域名关键字",
|
||||
|
||||
Reference in New Issue
Block a user