Compare commits

..

11 Commits

14 changed files with 471 additions and 290 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -784,7 +784,7 @@ dependencies = [
[[package]]
name = "clash-verge"
version = "1.7.0"
version = "1.7.2"
dependencies = [
"anyhow",
"auto-launch",

View File

@@ -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"

View File

@@ -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()),

View File

@@ -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() {

View File

@@ -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);
}

View File

@@ -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",

View File

@@ -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"}

View File

@@ -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)}

View File

@@ -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 }) => ({

View File

@@ -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>

View File

@@ -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",

View File

@@ -64,6 +64,8 @@
"Delete Rule": "删除规则",
"Rule Condition Required": "规则条件缺失",
"Invalid Rule": "无效规则",
"Advanced": "高级",
"Visible": "可视化",
"DOMAIN": "匹配完整域名",
"DOMAIN-SUFFIX": "匹配域名后缀",
"DOMAIN-KEYWORD": "匹配域名关键字",