refactor: Associate Profile with Merge/Script.

This commit is contained in:
MystiPanda 2024-06-29 23:07:44 +08:00
parent b85929772e
commit 3efef52398
15 changed files with 286 additions and 613 deletions

View File

@ -94,6 +94,16 @@ pub struct PrfOption {
/// default is `false`
#[serde(skip_serializing_if = "Option::is_none")]
pub danger_accept_invalid_certs: Option<bool>,
pub merge: Option<String>,
pub script: Option<String>,
pub rules: Option<String>,
pub proxies: Option<String>,
pub groups: Option<String>,
}
impl PrfOption {
@ -107,6 +117,11 @@ impl PrfOption {
.danger_accept_invalid_certs
.or(a.danger_accept_invalid_certs);
a.update_interval = b.update_interval.or(a.update_interval);
a.merge = b.merge.or(a.merge);
a.script = b.script.or(a.script);
a.rules = b.rules.or(a.rules);
a.proxies = b.proxies.or(a.proxies);
a.groups = b.groups.or(a.groups);
Some(a)
}
t => t.0.or(t.1),
@ -137,16 +152,8 @@ impl PrfItem {
let desc = item.desc.unwrap_or("".into());
PrfItem::from_local(name, desc, file_data, item.option)
}
"merge" => {
let name = item.name.unwrap_or("Merge".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_merge(name, desc)
}
"script" => {
let name = item.name.unwrap_or("Script".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_script(name, desc)
}
"merge" => PrfItem::from_merge(),
"script" => PrfItem::from_script(),
typ => bail!("invalid profile item type \"{typ}\""),
}
}
@ -161,7 +168,24 @@ impl PrfItem {
) -> Result<PrfItem> {
let uid = help::get_uid("l");
let file = format!("{uid}.yaml");
let opt_ref = option.as_ref();
let update_interval = opt_ref.and_then(|o| o.update_interval);
let mut merge = opt_ref.and_then(|o| o.merge.clone());
let mut script = opt_ref.and_then(|o| o.script.clone());
let rules = opt_ref.and_then(|o| o.rules.clone());
let proxies = opt_ref.and_then(|o| o.proxies.clone());
let groups = opt_ref.and_then(|o| o.groups.clone());
if merge.is_none() {
let merge_item = PrfItem::from_merge()?;
Config::profiles().data().append_item(merge_item.clone())?;
merge = merge_item.uid;
}
if script.is_none() {
let script_item = PrfItem::from_script()?;
Config::profiles().data().append_item(script_item.clone())?;
script = script_item.uid;
}
Ok(PrfItem {
uid: Some(uid),
itype: Some("local".into()),
@ -172,7 +196,12 @@ impl PrfItem {
selected: None,
extra: None,
option: Some(PrfOption {
update_interval: option.unwrap_or_default().update_interval,
update_interval,
merge,
script,
rules,
proxies,
groups,
..PrfOption::default()
}),
home: None,
@ -196,9 +225,23 @@ impl PrfItem {
opt_ref.map_or(false, |o| o.danger_accept_invalid_certs.unwrap_or(false));
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
let update_interval = opt_ref.and_then(|o| o.update_interval);
let mut merge = opt_ref.and_then(|o| o.merge.clone());
let mut script = opt_ref.and_then(|o| o.script.clone());
let rules = opt_ref.and_then(|o| o.rules.clone());
let proxies = opt_ref.and_then(|o| o.proxies.clone());
let 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()?;
Config::profiles().data().append_item(merge_item.clone())?;
merge = merge_item.uid;
}
if script.is_none() {
let script_item = PrfItem::from_script()?;
Config::profiles().data().append_item(script_item.clone())?;
script = script_item.uid;
}
// 使用软件自己的代理
if self_proxy {
let port = Config::verge()
@ -290,17 +333,11 @@ impl PrfItem {
crate::utils::help::get_last_part_and_decode(url).unwrap_or("Remote File".into()),
),
};
let option = match update_interval {
Some(val) => Some(PrfOption {
update_interval: Some(val),
..PrfOption::default()
}),
let update_interval = match update_interval {
Some(val) => Some(val),
None => match header.get("profile-update-interval") {
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
Ok(val) => Some(PrfOption {
update_interval: Some(val * 60), // hour -> min
..PrfOption::default()
}),
Ok(val) => Some(val * 60), // hour -> min
Err(_) => None,
},
None => None,
@ -340,7 +377,15 @@ impl PrfItem {
url: Some(url.into()),
selected: None,
extra,
option,
option: Some(PrfOption {
update_interval,
merge,
script,
rules,
proxies,
groups,
..PrfOption::default()
}),
home,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(data.into()),
@ -349,15 +394,15 @@ impl PrfItem {
/// ## Merge type (enhance)
/// create the enhanced item by using `merge` rule
pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
pub fn from_merge() -> Result<PrfItem> {
let uid = help::get_uid("m");
let file = format!("{uid}.yaml");
Ok(PrfItem {
uid: Some(uid),
itype: Some("merge".into()),
name: Some(name),
desc: Some(desc),
name: None,
desc: None,
file: Some(file),
url: None,
selected: None,
@ -371,15 +416,15 @@ impl PrfItem {
/// ## Script type (enhance)
/// create the enhanced item by using javascript quick.js
pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
pub fn from_script() -> Result<PrfItem> {
let uid = help::get_uid("s");
let file = format!("{uid}.js"); // js ext
Ok(PrfItem {
uid: Some(uid),
itype: Some("script".into()),
name: Some(name),
desc: Some(desc),
name: None,
desc: None,
file: Some(file),
url: None,
home: None,

View File

@ -11,9 +11,6 @@ pub struct IProfiles {
/// same as PrfConfig.current
pub current: Option<String>,
/// same as PrfConfig.chain
pub chain: Option<Vec<String>>,
/// profile list
pub items: Option<Vec<PrfItem>>,
}
@ -80,10 +77,6 @@ impl IProfiles {
}
}
if let Some(chain) = patch.chain {
self.chain = Some(chain);
}
Ok(())
}
@ -243,9 +236,19 @@ impl IProfiles {
pub fn delete_item(&mut self, uid: String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(&uid);
let current = current.clone();
let item = self.get_item(&uid)?;
let merge_uid = item.option.as_ref().and_then(|e| e.merge.clone());
let script_uid = item.option.as_ref().and_then(|e| e.script.clone());
let rules_uid = item.option.as_ref().and_then(|e| e.rules.clone());
let proxies_uid = item.option.as_ref().and_then(|e| e.proxies.clone());
let groups_uid = item.option.as_ref().and_then(|e| e.groups.clone());
let mut items = self.items.take().unwrap_or_default();
let mut index = None;
let mut merge_index = None;
let mut script_index = None;
// let mut rules_index = None;
// let mut proxies_index = None;
// let mut groups_index = None;
// get the index
for (i, _) in items.iter().enumerate() {
@ -266,6 +269,44 @@ impl IProfiles {
}
}
// get the merge index
for (i, _) in items.iter().enumerate() {
if items[i].uid == merge_uid {
merge_index = Some(i);
break;
}
}
if let Some(index) = merge_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
}
});
}
}
// get the script index
for (i, _) in items.iter().enumerate() {
if items[i].uid == script_uid {
script_index = Some(i);
break;
}
}
if let Some(index) = script_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
}
});
}
}
// delete the original uid
if current == uid {
self.current = match !items.is_empty() {
@ -295,4 +336,32 @@ impl IProfiles {
_ => Ok(Mapping::new()),
}
}
/// 获取current指向的订阅的merge
pub fn current_merge(&self) -> Option<String> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let merge = item.option.as_ref().and_then(|e| e.merge.clone());
return merge;
}
None
}
_ => None,
}
}
/// 获取current指向的订阅的script
pub fn current_script(&self) -> Option<String> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let script = item.option.as_ref().and_then(|e| e.script.clone());
return script;
}
None
}
_ => None,
}
}
}

View File

@ -10,6 +10,7 @@ use self::merge::*;
use self::script::*;
use self::tun::*;
use crate::config::Config;
use crate::utils::tmpl;
use serde_yaml::Mapping;
use std::collections::HashMap;
use std::collections::HashSet;
@ -47,33 +48,45 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
};
// 从profiles里拿东西
let (mut config, chain) = {
let (mut config, merge_item, script_item) = {
let profiles = Config::profiles();
let profiles = profiles.latest();
let current = profiles.current_mapping().unwrap_or_default();
let merge = profiles
.get_item(&profiles.current_merge().unwrap_or_default())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Merge(
serde_yaml::from_str::<Mapping>(tmpl::ITEM_MERGE).unwrap_or_default(),
),
});
let script = profiles
.get_item(&profiles.current_script().unwrap_or_default())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
let chain = match profiles.chain.as_ref() {
Some(chain) => chain
.iter()
.filter_map(|uid| profiles.get_item(uid).ok())
.filter_map(<Option<ChainItem>>::from)
.collect::<Vec<ChainItem>>(),
None => vec![],
};
(current, chain)
(current, merge, script)
};
let mut result_map = HashMap::new(); // 保存脚本日志
let mut exists_keys = use_keys(&config); // 保存出现过的keys
// 处理用户的profile
chain.into_iter().for_each(|item| match item.data {
match merge_item.data {
ChainType::Merge(merge) => {
exists_keys.extend(use_keys(&merge));
config = use_merge(merge, config.to_owned());
}
_ => {}
}
match script_item.data {
ChainType::Script(script) => {
let mut logs = vec![];
@ -86,9 +99,10 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
Err(err) => logs.push(("exception".into(), err.to_string())),
}
result_map.insert(item.uid, logs);
result_map.insert(script_item.uid, logs);
}
});
_ => {}
}
// 合并默认的config
for (key, value) in clash_config.into_iter() {

View File

@ -292,7 +292,6 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
Some((url, opt)) => {
let merged_opt = PrfOption::merge(opt, option);
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
let profiles = Config::profiles();
let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?;

View File

@ -1,10 +1,6 @@
use crate::config::{IVerge, PrfOption};
use crate::{
config::{Config, PrfItem},
core::*,
utils::init,
utils::server,
};
use crate::cmds::import_profile;
use crate::config::IVerge;
use crate::{config::Config, core::*, utils::init, utils::server};
use crate::{log_err, trace_err};
use anyhow::Result;
use once_cell::sync::OnceCell;
@ -102,7 +98,7 @@ pub async fn resolve_setup(app: &mut App) {
if argvs.len() > 1 {
let param = argvs[1].as_str();
if param.starts_with("clash:") {
resolve_scheme(argvs[1].to_owned()).await;
log_err!(resolve_scheme(argvs[1].to_owned()).await);
}
}
}
@ -240,31 +236,26 @@ pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) ->
Ok(())
}
pub async fn resolve_scheme(param: String) {
pub async fn resolve_scheme(param: String) -> Result<()> {
let url = param
.trim_start_matches("clash://install-config/?url=")
.trim_start_matches("clash://install-config?url=");
let option = PrfOption {
user_agent: None,
with_proxy: Some(true),
self_proxy: None,
danger_accept_invalid_certs: None,
update_interval: None,
};
if let Ok(item) = PrfItem::from_url(url, None, None, Some(option)).await {
if Config::profiles().data().append_item(item).is_ok() {
match import_profile(url.to_string(), None).await {
Ok(_) => {
notification::Notification::new(crate::utils::dirs::APP_ID)
.title("Clash Verge")
.body("Import profile success")
.show()
.unwrap();
};
} else {
notification::Notification::new(crate::utils::dirs::APP_ID)
.title("Clash Verge")
.body("Import profile failed")
.show()
.unwrap();
log::error!("failed to parse url: {}", url);
}
Err(e) => {
notification::Notification::new(crate::utils::dirs::APP_ID)
.title("Clash Verge")
.body(format!("Import profile failed: {e}"))
.show()
.unwrap();
log::error!("Import profile failed: {e}");
}
}
Ok(())
}

View File

@ -1,7 +1,10 @@
extern crate warp;
use super::resolve;
use crate::config::{Config, IVerge, DEFAULT_PAC};
use crate::{
config::{Config, IVerge, DEFAULT_PAC},
log_err,
};
use anyhow::{bail, Result};
use port_scanner::local_port_available;
use std::convert::Infallible;
@ -85,7 +88,7 @@ pub fn embed_server(app_handle: AppHandle) {
.and_then(scheme_handler);
async fn scheme_handler(query: QueryParam) -> Result<impl warp::Reply, Infallible> {
resolve::resolve_scheme(query.param).await;
log_err!(resolve::resolve_scheme(query.param).await);
Ok("ok")
}
let commands = ping.or(visible).or(pac).or(scheme);

View File

@ -59,7 +59,7 @@ export const ProfileItem = (props: Props) => {
const loadingCache = useLoadingCache();
const setLoadingCache = useSetLoadingCache();
const { uid, name = "Profile", extra, updated = 0 } = itemData;
const { uid, name = "Profile", extra, updated = 0, option } = itemData;
// local file mode
// remote file mode
@ -105,6 +105,8 @@ export const ProfileItem = (props: Props) => {
}, [hasUrl, updated]);
const [fileOpen, setFileOpen] = useState(false);
const [mergeOpen, setMergeOpen] = useState(false);
const [scriptOpen, setScriptOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const onOpenHome = () => {
@ -122,6 +124,16 @@ export const ProfileItem = (props: Props) => {
setFileOpen(true);
};
const onEditMerge = () => {
setAnchorEl(null);
setMergeOpen(true);
};
const onEditScript = () => {
setAnchorEl(null);
setScriptOpen(true);
};
const onForceSelect = () => {
setAnchorEl(null);
onSelect(true);
@ -174,33 +186,55 @@ export const ProfileItem = (props: Props) => {
});
const urlModeMenu = (
hasHome ? [{ label: "Home", handler: onOpenHome }] : []
hasHome ? [{ label: "Home", handler: onOpenHome, disabled: false }] : []
).concat([
{ label: "Select", handler: onForceSelect },
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "Update", handler: () => onUpdate(0) },
{ label: "Update(Proxy)", handler: () => onUpdate(2) },
{ label: "Select", handler: onForceSelect, disabled: false },
{ label: "Edit Info", handler: onEditInfo, disabled: false },
{ label: "Edit File", handler: onEditFile, disabled: false },
{
label: "Edit Merge",
handler: onEditMerge,
disabled: option?.merge === null,
},
{
label: "Edit Script",
handler: onEditScript,
disabled: option?.script === null,
},
{ label: "Open File", handler: onOpenFile, disabled: false },
{ label: "Update", handler: () => onUpdate(0), disabled: false },
{ label: "Update(Proxy)", handler: () => onUpdate(2), disabled: false },
{
label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
},
disabled: false,
},
]);
const fileModeMenu = [
{ label: "Select", handler: onForceSelect },
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "Select", handler: onForceSelect, disabled: false },
{ label: "Edit Info", handler: onEditInfo, disabled: false },
{ label: "Edit File", handler: onEditFile, disabled: false },
{
label: "Edit Merge",
handler: onEditMerge,
disabled: option?.merge === null,
},
{
label: "Edit Script",
handler: onEditScript,
disabled: option?.script === null,
},
{ label: "Open File", handler: onOpenFile, disabled: false },
{
label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
},
disabled: false,
},
];
@ -369,6 +403,7 @@ export const ProfileItem = (props: Props) => {
<MenuItem
key={item.label}
onClick={item.handler}
disabled={item.disabled}
sx={[
{
minWidth: 120,
@ -398,6 +433,24 @@ export const ProfileItem = (props: Props) => {
onChange={onChange}
onClose={() => setFileOpen(false)}
/>
<EditorViewer
mode="profile"
property={option?.merge ?? "123"}
open={mergeOpen}
language="yaml"
schema="merge"
onChange={onChange}
onClose={() => setMergeOpen(false)}
/>
<EditorViewer
mode="profile"
property={option?.script ?? ""}
open={scriptOpen}
language="javascript"
schema={undefined}
onChange={onChange}
onClose={() => setScriptOpen(false)}
/>
<ConfirmViewer
title={t("Confirm deletion")}
message={t("This operation is not reversible")}

View File

@ -1,285 +0,0 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import {
Box,
Badge,
Chip,
Typography,
MenuItem,
Menu,
IconButton,
CircularProgress,
} from "@mui/material";
import { FeaturedPlayListRounded } from "@mui/icons-material";
import { viewProfile } from "@/services/cmds";
import { Notice } from "@/components/base";
import { EditorViewer } from "@/components/profile/editor-viewer";
import { ProfileBox } from "./profile-box";
import { LogViewer } from "./log-viewer";
import { ConfirmViewer } from "./confirm-viewer";
interface Props {
selected: boolean;
activating: boolean;
itemData: IProfileItem;
enableNum: number;
logInfo?: [string, string][];
onEnable: () => void;
onDisable: () => void;
onMoveTop: () => void;
onMoveEnd: () => void;
onEdit: () => void;
onChange?: (prev?: string, curr?: string) => void;
onDelete: () => void;
}
// profile enhanced item
export const ProfileMore = (props: Props) => {
const {
selected,
activating,
itemData,
enableNum,
logInfo = [],
onEnable,
onDisable,
onMoveTop,
onMoveEnd,
onDelete,
onEdit,
onChange,
} = props;
const { uid, type } = itemData;
const { t, i18n } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 });
const [fileOpen, setFileOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [logOpen, setLogOpen] = useState(false);
const onEditInfo = () => {
setAnchorEl(null);
onEdit();
};
const onEditFile = () => {
setAnchorEl(null);
setFileOpen(true);
};
const onOpenFile = useLockFn(async () => {
setAnchorEl(null);
try {
await viewProfile(itemData.uid);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
const fnWrapper = (fn: () => void) => () => {
setAnchorEl(null);
return fn();
};
const hasError = !!logInfo.find((e) => e[0] === "exception");
const showMove = enableNum > 1 && !hasError;
const enableMenu = [
{ label: "Disable", handler: fnWrapper(onDisable) },
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "To Top", show: showMove, handler: fnWrapper(onMoveTop) },
{ label: "To End", show: showMove, handler: fnWrapper(onMoveEnd) },
{
label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
},
},
];
const disableMenu = [
{ label: "Enable", handler: fnWrapper(onEnable) },
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{
label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
},
},
];
const boxStyle = {
height: 26,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
lineHeight: 1,
};
return (
<>
<ProfileBox
aria-selected={selected}
onDoubleClick={onEditFile}
// onClick={() => onSelect(false)}
onContextMenu={(event) => {
const { clientX, clientY } = event;
setPosition({ top: clientY, left: clientX });
setAnchorEl(event.currentTarget);
event.preventDefault();
}}
>
{activating && (
<Box
sx={{
position: "absolute",
display: "flex",
justifyContent: "center",
alignItems: "center",
top: 10,
left: 10,
right: 10,
bottom: 2,
zIndex: 10,
backdropFilter: "blur(2px)",
}}
>
<CircularProgress color="inherit" size={20} />
</Box>
)}
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
mb={0.5}
>
<Typography
width="calc(100% - 52px)"
variant="h6"
component="h2"
noWrap
title={itemData.name}
>
{itemData.name}
</Typography>
<Chip
label={type}
color="primary"
size="small"
variant="outlined"
sx={{ height: 20, textTransform: "capitalize" }}
/>
</Box>
<Box sx={boxStyle}>
{selected && type === "script" ? (
hasError ? (
<Badge color="error" variant="dot" overlap="circular">
<IconButton
size="small"
edge="start"
color="error"
title={t("Script Console")}
onClick={() => setLogOpen(true)}
>
<FeaturedPlayListRounded fontSize="inherit" />
</IconButton>
</Badge>
) : (
<IconButton
size="small"
edge="start"
color="inherit"
title={t("Script Console")}
onClick={() => setLogOpen(true)}
>
<FeaturedPlayListRounded fontSize="inherit" />
</IconButton>
)
) : (
<Typography
noWrap
title={itemData.desc}
sx={i18n.language === "zh" ? { width: "calc(100% - 75px)" } : {}}
>
{itemData.desc}
</Typography>
)}
</Box>
</ProfileBox>
<Menu
open={!!anchorEl}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorPosition={position}
anchorReference="anchorPosition"
transitionDuration={225}
MenuListProps={{ sx: { py: 0.5 } }}
onContextMenu={(e) => {
setAnchorEl(null);
e.preventDefault();
}}
>
{(selected ? enableMenu : disableMenu)
.filter((item: any) => item.show !== false)
.map((item) => (
<MenuItem
key={item.label}
onClick={item.handler}
sx={[
{ minWidth: 120 },
(theme) => {
return {
color:
item.label === "Delete"
? theme.palette.error.main
: undefined,
};
},
]}
dense
>
{t(item.label)}
</MenuItem>
))}
</Menu>
<EditorViewer
mode="profile"
property={uid}
open={fileOpen}
language={type === "merge" ? "yaml" : "javascript"}
schema={type === "merge" ? "merge" : undefined}
onChange={onChange}
onClose={() => setFileOpen(false)}
/>
<ConfirmViewer
title={t("Confirm deletion")}
message={t("This operation is not reversible")}
open={confirmOpen}
onClose={() => setConfirmOpen(false)}
onConfirm={() => {
onDelete();
setConfirmOpen(false);
}}
/>
{selected && (
<LogViewer
open={logOpen}
logInfo={logInfo}
onClose={() => setLogOpen(false)}
/>
)}
</>
);
};

View File

@ -33,7 +33,7 @@ export interface ProfileViewerRef {
}
// create or edit the profile
// remote / local / merge / script
// remote / local
export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
(props, ref) => {
const { t } = useTranslation();
@ -92,9 +92,6 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
if (form.type === "remote" && !form.url) {
throw new Error("The URL should not be null");
}
if (form.type !== "remote" && form.type !== "local") {
delete form.option;
}
if (form.option?.update_interval) {
form.option.update_interval = +form.option.update_interval;
@ -168,8 +165,6 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
<Select {...field} autoFocus label={t("Type")}>
<MenuItem value="remote">Remote</MenuItem>
<MenuItem value="local">Local</MenuItem>
<MenuItem value="script">Script</MenuItem>
<MenuItem value="merge">Merge</MenuItem>
</Select>
</FormControl>
)}

View File

@ -1,20 +1,17 @@
{
"millis": "millis",
"mins": "mins",
"Back": "Back",
"Close": "Close",
"Cancel": "Cancel",
"Confirm": "Confirm",
"Empty": "Empty",
"New": "New",
"Edit": "Edit",
"Save": "Save",
"Delete": "Delete",
"Enable": "Enable",
"Disable": "Disable",
"Label-Proxies": "Proxies",
"Label-Profiles": "Profiles",
"Label-Connections": "Connections",
@ -22,7 +19,6 @@
"Label-Logs": "Logs",
"Label-Test": "Test",
"Label-Settings": "Settings",
"Proxies": "Proxies",
"Proxy Groups": "Proxy Groups",
"Proxy Provider": "Proxy Provider",
@ -41,7 +37,6 @@
"Delay check to cancel fixed": "Delay check to cancel fixed",
"Proxy basic": "Proxy basic",
"Proxy detail": "Proxy detail",
"Profiles": "Profiles",
"Update All Profiles": "Update All Profiles",
"View Runtime Config": "View Runtime Config",
@ -55,6 +50,8 @@
"Expire Time": "Expire Time",
"Create Profile": "Create Profile",
"Edit Profile": "Edit Profile",
"Edit Merge": "Edit Merge",
"Edit Script": "Edit Script",
"Type": "Type",
"Name": "Name",
"Descriptions": "Descriptions",
@ -77,7 +74,6 @@
"Script Console": "Script Console",
"To Top": "To Top",
"To End": "To End",
"Connections": "Connections",
"Table View": "Table View",
"List View": "List View",
@ -97,21 +93,17 @@
"Source": "Source",
"Destination IP": "Destination IP",
"Close Connection": "Close Connection",
"Rules": "Rules",
"Rule Provider": "Rule Provider",
"Logs": "Logs",
"Pause": "Pause",
"Clear": "Clear",
"Test": "Test",
"Test All": "Test All",
"Create Test": "Create Test",
"Edit Test": "Edit Test",
"Icon": "Icon",
"Test URL": "Test URL",
"Settings": "Settings",
"System Setting": "System Setting",
"Tun Mode": "Tun Mode",
@ -157,7 +149,6 @@
"Auto Launch": "Auto Launch",
"Silent Start": "Silent Start",
"Silent Start Info": "Start the program in background mode without displaying the panel",
"Clash Setting": "Clash Setting",
"Allow Lan": "Allow Lan",
"IPv6": "IPv6",
@ -181,7 +172,6 @@
"Open UWP tool": "Open UWP tool",
"Open UWP tool Info": "Since Windows 8, UWP apps (such as Microsoft Store) are restricted from directly accessing local host network services, and this tool can be used to bypass this restriction",
"Update GeoData": "Update GeoData",
"TG Channel": "Telegram Channel",
"Manual": "Manual",
"Github Repo": "Github Repo",
@ -253,7 +243,6 @@
"Open Dev Tools": "Open Dev Tools",
"Exit": "Exit",
"Verge Version": "Verge Version",
"ReadOnly": "ReadOnly",
"ReadOnlyMessage": "Cannot edit in read-only editor",
"Filter": "Filter",
@ -261,7 +250,6 @@
"Match Case": "Match Case",
"Match Whole Word": "Match Whole Word",
"Use Regular Expression": "Use Regular Expression",
"Profile Imported Successfully": "Profile Imported Successfully",
"Clash Config Updated": "Clash Config Updated",
"Profile Switched": "Profile Switched",

View File

@ -1,20 +1,17 @@
{
"millis": "میلی‌ثانیه",
"mins": "دقیقه",
"Back": "بازگشت",
"Close": "بستن",
"Cancel": "لغو",
"Confirm": "تأیید",
"Empty": "خالی خالی",
"New": "جدید",
"Edit": "ویرایش",
"Save": "ذخیره",
"Delete": "حذف",
"Enable": "فعال کردن",
"Disable": "غیرفعال کردن",
"Label-Proxies": "پراکسی‌ها",
"Label-Profiles": "پروفایل‌ها",
"Label-Connections": "اتصالات",
@ -22,7 +19,6 @@
"Label-Logs": "لاگ‌ها",
"Label-Test": "آزمون",
"Label-Settings": "تنظیمات",
"Proxies": "پراکسی‌ها",
"Proxy Groups": "گروه‌های پراکسی",
"Proxy Provider": "تأمین‌کننده پروکسی",
@ -41,7 +37,6 @@
"Delay check to cancel fixed": "بررسی تأخیر برای لغو ثابت",
"Proxy basic": "پراکسی پایه",
"Proxy detail": "جزئیات پراکسی",
"Profiles": "پروفایل‌ها",
"Update All Profiles": "به‌روزرسانی همه پروفایل‌ها",
"View Runtime Config": "مشاهده پیکربندی زمان اجرا",
@ -55,6 +50,8 @@
"Expire Time": "زمان انقضا",
"Create Profile": "ایجاد پروفایل",
"Edit Profile": "ویرایش پروفایل",
"Edit Merge": "ادغام ویرایش",
"Edit Script": "ویرایش اسکریپت",
"Type": "نوع",
"Name": "نام",
"Descriptions": "توضیحات",
@ -77,7 +74,6 @@
"Script Console": "کنسول اسکریپت",
"To Top": "به بالا",
"To End": "به پایان",
"Connections": "اتصالات",
"Table View": "نمای جدولی",
"List View": "نمای لیستی",
@ -97,21 +93,17 @@
"Source": "منبع",
"Destination IP": "آدرس IP مقصد",
"Close Connection": "بستن اتصال",
"Rules": "قوانین",
"Rule Provider": "تأمین‌کننده قانون",
"Logs": "لاگ‌ها",
"Pause": "توقف",
"Clear": "پاک کردن",
"Test": "آزمون",
"Test All": "آزمون همه",
"Create Test": "ایجاد آزمون",
"Edit Test": "ویرایش آزمون",
"Icon": "آیکون",
"Test URL": "آدرس آزمون",
"Settings": "تنظیمات",
"System Setting": "تنظیمات سیستم",
"Tun Mode": "حالت Tun",
@ -157,7 +149,6 @@
"Auto Launch": "راه‌اندازی خودکار",
"Silent Start": "شروع بی‌صدا",
"Silent Start Info": "برنامه را در حالت پس‌زمینه بدون نمایش پانل اجرا کنید",
"Clash Setting": "تنظیمات Clash",
"Allow Lan": "اجازه LAN",
"IPv6": "IPv6",
@ -186,7 +177,6 @@
"Open UWP tool": "باز کردن ابزار UWP",
"Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود",
"Update GeoData": "به‌روزرسانی GeoData",
"TG Channel": "کانال تلگرام",
"Manual": "راهنما",
"Github Repo": "مخزن GitHub",
@ -258,7 +248,6 @@
"Open Dev Tools": "باز کردن ابزارهای توسعه‌دهنده",
"Exit": "خروج",
"Verge Version": "نسخه Verge",
"ReadOnly": "فقط خواندنی",
"ReadOnlyMessage": "نمی‌توان در ویرایشگر فقط خواندنی ویرایش کرد",
"Filter": "فیلتر",
@ -266,7 +255,6 @@
"Match Case": "تطبیق حروف کوچک و بزرگ",
"Match Whole Word": "تطبیق کل کلمه",
"Use Regular Expression": "استفاده از عبارت منظم",
"Profile Imported Successfully": "پروفایل با موفقیت وارد شد",
"Clash Config Updated": "پیکربندی Clash به‌روزرسانی شد",
"Profile Switched": "پروفایل تغییر یافت",

View File

@ -1,20 +1,17 @@
{
"millis": "миллисекунды",
"mins": "минуты",
"Back": "Назад",
"Close": "Закрыть",
"Cancel": "Отмена",
"Confirm": "Подтвердить",
"Empty": "Пусто",
"New": "Новый",
"Edit": "Редактировать",
"Save": "Сохранить",
"Delete": "Удалить",
"Enable": "Включить",
"Disable": "Отключить",
"Label-Proxies": "Прокси",
"Label-Profiles": "Профили",
"Label-Connections": "Соединения",
@ -22,7 +19,6 @@
"Label-Logs": "Логи",
"Label-Test": "Тест",
"Label-Settings": "Настройки",
"Proxies": "Прокси",
"Proxy Groups": "Группы прокси",
"Proxy Provider": "Провайдер прокси",
@ -41,7 +37,6 @@
"Delay check to cancel fixed": "Проверка задержки для отмены фиксированного",
"Proxy basic": "Резюме о прокси",
"Proxy detail": "Подробности о прокси",
"Profiles": "Профили",
"Update All Profiles": "Обновить все профили",
"View Runtime Config": "Просмотреть используемый конфиг",
@ -55,6 +50,8 @@
"Expire Time": "Время окончания",
"Create Profile": "Создать профиль",
"Edit Profile": "Изменить профиль",
"Edit Merge": "Изменить Merge.",
"Edit Script": "Изменить Script",
"Type": "Тип",
"Name": "Название",
"Descriptions": "Описания",
@ -77,7 +74,6 @@
"Script Console": "Консоль скрипта",
"To Top": "Наверх",
"To End": "Вниз",
"Connections": "Соединения",
"Table View": "Tablichnyy vid",
"List View": "Spiskovyy vid",
@ -97,21 +93,17 @@
"Source": "Исходный адрес",
"Destination IP": "IP-адрес назначения",
"Close Connection": "Закрыть соединение",
"Rules": "Правила",
"Rule Provider": "Провайдер правило",
"Logs": "Логи",
"Pause": "Пауза",
"Clear": "Очистить",
"Test": "Тест",
"Test All": "Тест Все",
"Create Test": "Создать тест",
"Edit Test": "Редактировать тест",
"Icon": "Икона",
"Test URL": "Тестовый URL",
"Settings": "Настройки",
"System Setting": "Настройки системы",
"Tun Mode": "Режим туннеля",
@ -157,7 +149,6 @@
"Auto Launch": "Автозапуск",
"Silent Start": "Тихий запуск",
"Silent Start Info": "Запускать программу в фоновом режиме без отображения панели",
"Clash Setting": "Настройки Clash",
"Allow Lan": "Разрешить локальную сеть",
"IPv6": "IPv6",
@ -186,7 +177,6 @@
"Open UWP tool": "Открыть UWP инструмент",
"Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение",
"Update GeoData": "Обновление GeoData",
"TG Channel": "Канал Telegram",
"Manual": "Документация",
"Github Repo": "GitHub репозиторий",
@ -258,7 +248,6 @@
"Open Dev Tools": "Открыть инструменты разработчика",
"Exit": "Выход",
"Verge Version": "Версия Verge",
"ReadOnly": "Только для чтения",
"ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения",
"Filter": "Фильтр",
@ -266,7 +255,6 @@
"Match Case": "Учитывать регистр",
"Match Whole Word": "Полное совпадение слова",
"Use Regular Expression": "Использовать регулярные выражения",
"Profile Imported Successfully": "Профиль успешно импортирован",
"Clash Config Updated": "Clash конфигурация Обновлена",
"Profile Switched": "Профиль изменен",

View File

@ -1,20 +1,17 @@
{
"millis": "毫秒",
"mins": "分钟",
"Back": "返回",
"Close": "关闭",
"Cancel": "取消",
"Confirm": "确认",
"Empty": "空空如也",
"New": "新建",
"Edit": "编辑",
"Save": "保存",
"Delete": "删除",
"Enable": "启用",
"Disable": "禁用",
"Label-Proxies": "代 理",
"Label-Profiles": "订 阅",
"Label-Connections": "连 接",
@ -22,7 +19,6 @@
"Label-Logs": "日 志",
"Label-Test": "测 试",
"Label-Settings": "设 置",
"Proxies": "代理",
"Proxy Groups": "代理组",
"Proxy Provider": "代理集合",
@ -41,7 +37,6 @@
"Delay check to cancel fixed": "进行延迟测试,以取消固定",
"Proxy basic": "隐藏节点细节",
"Proxy detail": "展示节点细节",
"Profiles": "订阅",
"Update All Profiles": "更新所有订阅",
"View Runtime Config": "查看运行时订阅",
@ -55,6 +50,8 @@
"Expire Time": "到期时间",
"Create Profile": "新建配置",
"Edit Profile": "编辑配置",
"Edit Merge": "编辑 Merge",
"Edit Script": "编辑 Script",
"Type": "类型",
"Name": "名称",
"Descriptions": "描述",
@ -77,7 +74,6 @@
"Script Console": "脚本控制台输出",
"To Top": "移到最前",
"To End": "移到末尾",
"Connections": "连接",
"Table View": "表格视图",
"List View": "列表视图",
@ -97,21 +93,17 @@
"Source": "源地址",
"Destination IP": "目标地址",
"Close Connection": "关闭连接",
"Rules": "规则",
"Rule Provider": "规则集合",
"Logs": "日志",
"Pause": "暂停",
"Clear": "清除",
"Test": "测试",
"Test All": "测试全部",
"Create Test": "新建测试",
"Edit Test": "编辑测试",
"Icon": "图标",
"Test URL": "测试地址",
"Settings": "设置",
"System Setting": "系统设置",
"Tun Mode": "Tun 模式",
@ -157,7 +149,6 @@
"Auto Launch": "开机自启",
"Silent Start": "静默启动",
"Silent Start Info": "程序启动时以后台模式运行,不显示程序面板",
"TG Channel": "Telegram 频道",
"Manual": "使用手册",
"Github Repo": "GitHub 项目地址",
@ -189,7 +180,6 @@
"Open UWP tool": "UWP 工具",
"Open UWP tool Info": "Windows 8开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制",
"Update GeoData": "更新 GeoData",
"Verge Setting": "Verge 设置",
"Language": "语言设置",
"Theme Mode": "主题模式",
@ -258,7 +248,6 @@
"Open Dev Tools": "打开开发者工具",
"Exit": "退出",
"Verge Version": "Verge 版本",
"ReadOnly": "只读",
"ReadOnlyMessage": "无法在只读模式下编辑",
"Filter": "过滤节点",
@ -266,7 +255,6 @@
"Match Case": "区分大小写",
"Match Whole Word": "全字匹配",
"Use Regular Expression": "使用正则表达式",
"Profile Imported Successfully": "导入订阅成功",
"Clash Config Updated": "Clash 配置已更新",
"Profile Switched": "订阅已切换",

View File

@ -42,7 +42,6 @@ import {
ProfileViewerRef,
} from "@/components/profile/profile-viewer";
import { ProfileItem } from "@/components/profile/profile-item";
import { ProfileMore } from "@/components/profile/profile-more";
import { useProfiles } from "@/hooks/use-profiles";
import { ConfigViewer } from "@/components/setting/mods/config-viewer";
import { throttle } from "lodash-es";
@ -105,31 +104,22 @@ const ProfilePage = () => {
getRuntimeLogs
);
const chain = profiles.chain || [];
const viewerRef = useRef<ProfileViewerRef>(null);
const configRef = useRef<DialogRef>(null);
// distinguish type
const { regularItems, enhanceItems } = useMemo(() => {
const profileItems = useMemo(() => {
const items = profiles.items || [];
const chain = profiles.chain || [];
const type1 = ["local", "remote"];
const type2 = ["merge", "script"];
const regularItems = items.filter((i) => i && type1.includes(i.type!));
const restItems = items.filter((i) => i && type2.includes(i.type!));
const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i]));
const enhanceItems = chain
.map((i) => restMap[i]!)
.filter(Boolean)
.concat(restItems.filter((i) => !chain.includes(i.uid)));
const profileItems = items.filter((i) => i && type1.includes(i.type!));
return { regularItems, enhanceItems };
return profileItems;
}, [profiles]);
const currentActivatings = () => {
return [...new Set([profiles.current ?? "", ...chain])].filter(Boolean);
return [...new Set([profiles.current ?? ""])].filter(Boolean);
};
const onImport = async () => {
@ -205,38 +195,9 @@ const ProfilePage = () => {
}
});
const onEnable = useLockFn(async (uid: string) => {
if (chain.includes(uid)) return;
try {
setActivatings([...currentActivatings(), uid]);
const newChain = [...chain, uid];
await patchProfiles({ chain: newChain });
mutateLogs();
} catch (err: any) {
Notice.error(err.message || err.toString(), 3000);
} finally {
setActivatings([]);
}
});
const onDisable = useLockFn(async (uid: string) => {
if (!chain.includes(uid)) return;
try {
setActivatings([...currentActivatings(), uid]);
const newChain = chain.filter((i) => i !== uid);
await patchProfiles({ chain: newChain });
mutateLogs();
} catch (err: any) {
Notice.error(err.message || err.toString(), 3000);
} finally {
setActivatings([]);
}
});
const onDelete = useLockFn(async (uid: string) => {
const current = profiles.current === uid;
try {
await onDisable(uid);
setActivatings([...(current ? currentActivatings() : []), uid]);
await deleteProfile(uid);
mutateProfiles();
@ -249,20 +210,6 @@ const ProfilePage = () => {
}
});
const onMoveTop = useLockFn(async (uid: string) => {
if (!chain.includes(uid)) return;
const newChain = [uid].concat(chain.filter((i) => i !== uid));
await patchProfiles({ chain: newChain });
mutateLogs();
});
const onMoveEnd = useLockFn(async (uid: string) => {
if (!chain.includes(uid)) return;
const newChain = chain.filter((i) => i !== uid).concat([uid]);
await patchProfiles({ chain: newChain });
mutateLogs();
});
// 更新所有订阅
const setLoadingCache = useSetLoadingCache();
const onUpdateAll = useLockFn(async () => {
@ -281,7 +228,7 @@ const ProfilePage = () => {
return new Promise((resolve) => {
setLoadingCache((cache) => {
// 获取没有正在更新的订阅
const items = regularItems.filter(
const items = profileItems.filter(
(e) => e.type === "remote" && !cache[e.uid]
);
const change = Object.fromEntries(items.map((e) => [e.uid, true]));
@ -296,11 +243,6 @@ const ProfilePage = () => {
const text = await readText();
if (text) setUrl(text);
};
const mode = useThemeMode();
const islight = mode === "light" ? true : false;
const dividercolor = islight
? "rgba(0, 0, 0, 0.06)"
: "rgba(255, 255, 255, 0.06)";
return (
<BasePage
@ -415,11 +357,11 @@ const ProfilePage = () => {
<Box sx={{ mb: 1.5 }}>
<Grid container spacing={{ xs: 1, lg: 1 }}>
<SortableContext
items={regularItems.map((x) => {
items={profileItems.map((x) => {
return x.uid;
})}
>
{regularItems.map((item) => (
{profileItems.map((item) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
<ProfileItem
id={item.uid}
@ -441,43 +383,6 @@ const ProfilePage = () => {
</Grid>
</Box>
</DndContext>
{enhanceItems.length > 0 && (
<Divider
variant="middle"
flexItem
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
></Divider>
)}
{enhanceItems.length > 0 && (
<Box sx={{ mt: 1.5 }}>
<Grid container spacing={{ xs: 1, lg: 1 }}>
{enhanceItems.map((item) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
<ProfileMore
selected={!!chain.includes(item.uid)}
activating={activatings.includes(item.uid)}
itemData={item}
enableNum={chain.length || 0}
logInfo={chainLogs[item.uid]}
onEnable={() => onEnable(item.uid)}
onDisable={() => onDisable(item.uid)}
onDelete={() => onDelete(item.uid)}
onMoveTop={() => onMoveTop(item.uid)}
onMoveEnd={() => onMoveEnd(item.uid)}
onEdit={() => viewerRef.current?.edit(item)}
onChange={async (prev, curr) => {
if (prev !== curr && chain.includes(item.uid)) {
await onEnhance();
}
}}
/>
</Grid>
))}
</Grid>
</Box>
)}
</Box>
<ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
<ConfigViewer ref={configRef} />

View File

@ -178,11 +178,15 @@ interface IProfileOption {
self_proxy?: boolean;
update_interval?: number;
danger_accept_invalid_certs?: boolean;
merge?: string;
script?: string;
rules?: string;
proxies?: string;
groups?: string;
}
interface IProfilesConfig {
current?: string;
chain?: string[];
valid?: string[];
items?: IProfileItem[];
}
@ -254,75 +258,3 @@ interface IVergeConfig {
proxy_layout_column?: number;
test_list?: IVergeTestItem[];
}
type IClashConfigValue = any;
interface IProfileMerge {
// clash config fields (default supports)
rules?: IClashConfigValue;
proxies?: IClashConfigValue;
"proxy-groups"?: IClashConfigValue;
"proxy-providers"?: IClashConfigValue;
"rule-providers"?: IClashConfigValue;
// clash config fields (use flag)
tun?: IClashConfigValue;
dns?: IClashConfigValue;
hosts?: IClashConfigValue;
script?: IClashConfigValue;
profile?: IClashConfigValue;
payload?: IClashConfigValue;
"interface-name"?: IClashConfigValue;
"routing-mark"?: IClashConfigValue;
// functional fields
use?: string[];
"prepend-rules"?: any[];
"append-rules"?: any[];
"prepend-proxies"?: any[];
"append-proxies"?: any[];
"prepend-proxy-groups"?: any[];
"append-proxy-groups"?: any[];
// fix
ebpf?: any;
experimental?: any;
iptables?: any;
sniffer?: any;
authentication?: any;
"bind-address"?: any;
"external-ui"?: any;
"auto-redir"?: any;
"socks-port"?: any;
"redir-port"?: any;
"tproxy-port"?: any;
"geodata-mode"?: any;
"tcp-concurrent"?: any;
}
// partial of the clash config
type IProfileData = Partial<{
rules: any[];
proxies: any[];
"proxy-groups": any[];
"proxy-providers": any[];
"rule-providers": any[];
[k: string]: any;
}>;
interface IChainItem {
item: IProfileItem;
merge?: IProfileMerge;
script?: string;
}
interface IEnhancedPayload {
chain: IChainItem[];
valid: string[];
current: IProfileData;
callback: string;
}
interface IEnhancedResult {
data: IProfileData;
status: string;
error?: string;
}