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

View File

@ -11,9 +11,6 @@ pub struct IProfiles {
/// same as PrfConfig.current /// same as PrfConfig.current
pub current: Option<String>, pub current: Option<String>,
/// same as PrfConfig.chain
pub chain: Option<Vec<String>>,
/// profile list /// profile list
pub items: Option<Vec<PrfItem>>, pub items: Option<Vec<PrfItem>>,
} }
@ -80,10 +77,6 @@ impl IProfiles {
} }
} }
if let Some(chain) = patch.chain {
self.chain = Some(chain);
}
Ok(()) Ok(())
} }
@ -243,9 +236,19 @@ impl IProfiles {
pub fn delete_item(&mut self, uid: String) -> Result<bool> { pub fn delete_item(&mut self, uid: String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(&uid); let current = self.current.as_ref().unwrap_or(&uid);
let current = current.clone(); 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 items = self.items.take().unwrap_or_default();
let mut index = None; 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 // get the index
for (i, _) in items.iter().enumerate() { 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 // delete the original uid
if current == uid { if current == uid {
self.current = match !items.is_empty() { self.current = match !items.is_empty() {
@ -295,4 +336,32 @@ impl IProfiles {
_ => Ok(Mapping::new()), _ => 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::script::*;
use self::tun::*; use self::tun::*;
use crate::config::Config; use crate::config::Config;
use crate::utils::tmpl;
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::HashSet; use std::collections::HashSet;
@ -47,33 +48,45 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
}; };
// 从profiles里拿东西 // 从profiles里拿东西
let (mut config, chain) = { let (mut config, merge_item, script_item) = {
let profiles = Config::profiles(); let profiles = Config::profiles();
let profiles = profiles.latest(); let profiles = profiles.latest();
let current = profiles.current_mapping().unwrap_or_default(); 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() { (current, merge, script)
Some(chain) => chain
.iter()
.filter_map(|uid| profiles.get_item(uid).ok())
.filter_map(<Option<ChainItem>>::from)
.collect::<Vec<ChainItem>>(),
None => vec![],
};
(current, chain)
}; };
let mut result_map = HashMap::new(); // 保存脚本日志 let mut result_map = HashMap::new(); // 保存脚本日志
let mut exists_keys = use_keys(&config); // 保存出现过的keys let mut exists_keys = use_keys(&config); // 保存出现过的keys
// 处理用户的profile // 处理用户的profile
chain.into_iter().for_each(|item| match item.data { match merge_item.data {
ChainType::Merge(merge) => { ChainType::Merge(merge) => {
exists_keys.extend(use_keys(&merge)); exists_keys.extend(use_keys(&merge));
config = use_merge(merge, config.to_owned()); config = use_merge(merge, config.to_owned());
} }
_ => {}
}
match script_item.data {
ChainType::Script(script) => { ChainType::Script(script) => {
let mut logs = vec![]; 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())), Err(err) => logs.push(("exception".into(), err.to_string())),
} }
result_map.insert(item.uid, logs); result_map.insert(script_item.uid, logs);
} }
}); _ => {}
}
// 合并默认的config // 合并默认的config
for (key, value) in clash_config.into_iter() { 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)) => { Some((url, opt)) => {
let merged_opt = PrfOption::merge(opt, option); let merged_opt = PrfOption::merge(opt, option);
let item = PrfItem::from_url(&url, None, None, merged_opt).await?; let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
let profiles = Config::profiles(); let profiles = Config::profiles();
let mut profiles = profiles.latest(); let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?; profiles.update_item(uid.clone(), item)?;

View File

@ -1,10 +1,6 @@
use crate::config::{IVerge, PrfOption}; use crate::cmds::import_profile;
use crate::{ use crate::config::IVerge;
config::{Config, PrfItem}, use crate::{config::Config, core::*, utils::init, utils::server};
core::*,
utils::init,
utils::server,
};
use crate::{log_err, trace_err}; use crate::{log_err, trace_err};
use anyhow::Result; use anyhow::Result;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -102,7 +98,7 @@ pub async fn resolve_setup(app: &mut App) {
if argvs.len() > 1 { if argvs.len() > 1 {
let param = argvs[1].as_str(); let param = argvs[1].as_str();
if param.starts_with("clash:") { 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(()) Ok(())
} }
pub async fn resolve_scheme(param: String) { pub async fn resolve_scheme(param: String) -> Result<()> {
let url = param let url = param
.trim_start_matches("clash://install-config/?url=") .trim_start_matches("clash://install-config/?url=")
.trim_start_matches("clash://install-config?url="); .trim_start_matches("clash://install-config?url=");
let option = PrfOption { match import_profile(url.to_string(), None).await {
user_agent: None, Ok(_) => {
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() {
notification::Notification::new(crate::utils::dirs::APP_ID) notification::Notification::new(crate::utils::dirs::APP_ID)
.title("Clash Verge") .title("Clash Verge")
.body("Import profile success") .body("Import profile success")
.show() .show()
.unwrap(); .unwrap();
}; }
} else { Err(e) => {
notification::Notification::new(crate::utils::dirs::APP_ID) notification::Notification::new(crate::utils::dirs::APP_ID)
.title("Clash Verge") .title("Clash Verge")
.body("Import profile failed") .body(format!("Import profile failed: {e}"))
.show() .show()
.unwrap(); .unwrap();
log::error!("failed to parse url: {}", url); log::error!("Import profile failed: {e}");
}
} }
Ok(())
} }

View File

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

View File

@ -59,7 +59,7 @@ export const ProfileItem = (props: Props) => {
const loadingCache = useLoadingCache(); const loadingCache = useLoadingCache();
const setLoadingCache = useSetLoadingCache(); const setLoadingCache = useSetLoadingCache();
const { uid, name = "Profile", extra, updated = 0 } = itemData; const { uid, name = "Profile", extra, updated = 0, option } = itemData;
// local file mode // local file mode
// remote file mode // remote file mode
@ -105,6 +105,8 @@ export const ProfileItem = (props: Props) => {
}, [hasUrl, updated]); }, [hasUrl, updated]);
const [fileOpen, setFileOpen] = useState(false); const [fileOpen, setFileOpen] = useState(false);
const [mergeOpen, setMergeOpen] = useState(false);
const [scriptOpen, setScriptOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false);
const onOpenHome = () => { const onOpenHome = () => {
@ -122,6 +124,16 @@ export const ProfileItem = (props: Props) => {
setFileOpen(true); setFileOpen(true);
}; };
const onEditMerge = () => {
setAnchorEl(null);
setMergeOpen(true);
};
const onEditScript = () => {
setAnchorEl(null);
setScriptOpen(true);
};
const onForceSelect = () => { const onForceSelect = () => {
setAnchorEl(null); setAnchorEl(null);
onSelect(true); onSelect(true);
@ -174,33 +186,55 @@ export const ProfileItem = (props: Props) => {
}); });
const urlModeMenu = ( const urlModeMenu = (
hasHome ? [{ label: "Home", handler: onOpenHome }] : [] hasHome ? [{ label: "Home", handler: onOpenHome, disabled: false }] : []
).concat([ ).concat([
{ label: "Select", handler: onForceSelect }, { label: "Select", handler: onForceSelect, disabled: false },
{ label: "Edit Info", handler: onEditInfo }, { label: "Edit Info", handler: onEditInfo, disabled: false },
{ label: "Edit File", handler: onEditFile }, { label: "Edit File", handler: onEditFile, disabled: false },
{ label: "Open File", handler: onOpenFile }, {
{ label: "Update", handler: () => onUpdate(0) }, label: "Edit Merge",
{ label: "Update(Proxy)", handler: () => onUpdate(2) }, 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", label: "Delete",
handler: () => { handler: () => {
setAnchorEl(null); setAnchorEl(null);
setConfirmOpen(true); setConfirmOpen(true);
}, },
disabled: false,
}, },
]); ]);
const fileModeMenu = [ const fileModeMenu = [
{ label: "Select", handler: onForceSelect }, { label: "Select", handler: onForceSelect, disabled: false },
{ label: "Edit Info", handler: onEditInfo }, { label: "Edit Info", handler: onEditInfo, disabled: false },
{ label: "Edit File", handler: onEditFile }, { label: "Edit File", handler: onEditFile, disabled: false },
{ label: "Open File", handler: onOpenFile }, {
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", label: "Delete",
handler: () => { handler: () => {
setAnchorEl(null); setAnchorEl(null);
setConfirmOpen(true); setConfirmOpen(true);
}, },
disabled: false,
}, },
]; ];
@ -369,6 +403,7 @@ export const ProfileItem = (props: Props) => {
<MenuItem <MenuItem
key={item.label} key={item.label}
onClick={item.handler} onClick={item.handler}
disabled={item.disabled}
sx={[ sx={[
{ {
minWidth: 120, minWidth: 120,
@ -398,6 +433,24 @@ export const ProfileItem = (props: Props) => {
onChange={onChange} onChange={onChange}
onClose={() => setFileOpen(false)} 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 <ConfirmViewer
title={t("Confirm deletion")} title={t("Confirm deletion")}
message={t("This operation is not reversible")} 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 // create or edit the profile
// remote / local / merge / script // remote / local
export const ProfileViewer = forwardRef<ProfileViewerRef, Props>( export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
(props, ref) => { (props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -92,9 +92,6 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
if (form.type === "remote" && !form.url) { if (form.type === "remote" && !form.url) {
throw new Error("The URL should not be null"); throw new Error("The URL should not be null");
} }
if (form.type !== "remote" && form.type !== "local") {
delete form.option;
}
if (form.option?.update_interval) { if (form.option?.update_interval) {
form.option.update_interval = +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")}> <Select {...field} autoFocus label={t("Type")}>
<MenuItem value="remote">Remote</MenuItem> <MenuItem value="remote">Remote</MenuItem>
<MenuItem value="local">Local</MenuItem> <MenuItem value="local">Local</MenuItem>
<MenuItem value="script">Script</MenuItem>
<MenuItem value="merge">Merge</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
)} )}

View File

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

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": "پراکسی‌ها", "Label-Proxies": "پراکسی‌ها",
"Label-Profiles": "پروفایل‌ها", "Label-Profiles": "پروفایل‌ها",
"Label-Connections": "اتصالات", "Label-Connections": "اتصالات",
@ -22,7 +19,6 @@
"Label-Logs": "لاگ‌ها", "Label-Logs": "لاگ‌ها",
"Label-Test": "آزمون", "Label-Test": "آزمون",
"Label-Settings": "تنظیمات", "Label-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 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": "آدرس IP مقصد", "Destination IP": "آدرس 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", "Tun Mode": "حالت Tun",
@ -157,7 +149,6 @@
"Auto Launch": "راه‌اندازی خودکار", "Auto Launch": "راه‌اندازی خودکار",
"Silent Start": "شروع بی‌صدا", "Silent Start": "شروع بی‌صدا",
"Silent Start Info": "برنامه را در حالت پس‌زمینه بدون نمایش پانل اجرا کنید", "Silent Start Info": "برنامه را در حالت پس‌زمینه بدون نمایش پانل اجرا کنید",
"Clash Setting": "تنظیمات Clash", "Clash Setting": "تنظیمات Clash",
"Allow Lan": "اجازه LAN", "Allow Lan": "اجازه LAN",
"IPv6": "IPv6", "IPv6": "IPv6",
@ -186,7 +177,6 @@
"Open UWP tool": "باز کردن ابزار UWP", "Open UWP tool": "باز کردن ابزار UWP",
"Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود", "Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود",
"Update GeoData": "به‌روزرسانی GeoData", "Update GeoData": "به‌روزرسانی GeoData",
"TG Channel": "کانال تلگرام", "TG Channel": "کانال تلگرام",
"Manual": "راهنما", "Manual": "راهنما",
"Github Repo": "مخزن GitHub", "Github Repo": "مخزن GitHub",
@ -258,7 +248,6 @@
"Open Dev Tools": "باز کردن ابزارهای توسعه‌دهنده", "Open Dev Tools": "باز کردن ابزارهای توسعه‌دهنده",
"Exit": "خروج", "Exit": "خروج",
"Verge Version": "نسخه Verge", "Verge Version": "نسخه Verge",
"ReadOnly": "فقط خواندنی", "ReadOnly": "فقط خواندنی",
"ReadOnlyMessage": "نمی‌توان در ویرایشگر فقط خواندنی ویرایش کرد", "ReadOnlyMessage": "نمی‌توان در ویرایشگر فقط خواندنی ویرایش کرد",
"Filter": "فیلتر", "Filter": "فیلتر",
@ -266,7 +255,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 به‌روزرسانی شد", "Clash Config Updated": "پیکربندی Clash به‌روزرسانی شد",
"Profile Switched": "پروفایل تغییر یافت", "Profile Switched": "پروفایل تغییر یافت",

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": "Прокси", "Label-Proxies": "Прокси",
"Label-Profiles": "Профили", "Label-Profiles": "Профили",
"Label-Connections": "Соединения", "Label-Connections": "Соединения",
@ -22,7 +19,6 @@
"Label-Logs": "Логи", "Label-Logs": "Логи",
"Label-Test": "Тест", "Label-Test": "Тест",
"Label-Settings": "Настройки", "Label-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": "Изменить Merge.",
"Edit Script": "Изменить 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": "Tablichnyy vid", "Table View": "Tablichnyy vid",
"List View": "Spiskovyy vid", "List View": "Spiskovyy vid",
@ -97,21 +93,17 @@
"Source": "Исходный адрес", "Source": "Исходный адрес",
"Destination IP": "IP-адрес назначения", "Destination IP": "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": "Тестовый URL", "Test URL": "Тестовый 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": "Запускать программу в фоновом режиме без отображения панели", "Silent Start Info": "Запускать программу в фоновом режиме без отображения панели",
"Clash Setting": "Настройки Clash", "Clash Setting": "Настройки Clash",
"Allow Lan": "Разрешить локальную сеть", "Allow Lan": "Разрешить локальную сеть",
"IPv6": "IPv6", "IPv6": "IPv6",
@ -186,7 +177,6 @@
"Open UWP tool": "Открыть UWP инструмент", "Open UWP tool": "Открыть UWP инструмент",
"Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение", "Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение",
"Update GeoData": "Обновление GeoData", "Update GeoData": "Обновление GeoData",
"TG Channel": "Канал Telegram", "TG Channel": "Канал Telegram",
"Manual": "Документация", "Manual": "Документация",
"Github Repo": "GitHub репозиторий", "Github Repo": "GitHub репозиторий",
@ -258,7 +248,6 @@
"Open Dev Tools": "Открыть инструменты разработчика", "Open Dev Tools": "Открыть инструменты разработчика",
"Exit": "Выход", "Exit": "Выход",
"Verge Version": "Версия Verge", "Verge Version": "Версия Verge",
"ReadOnly": "Только для чтения", "ReadOnly": "Только для чтения",
"ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения", "ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения",
"Filter": "Фильтр", "Filter": "Фильтр",
@ -266,7 +255,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 конфигурация Обновлена", "Clash Config Updated": "Clash конфигурация Обновлена",
"Profile Switched": "Профиль изменен", "Profile Switched": "Профиль изменен",

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": "代 理", "Label-Proxies": "代 理",
"Label-Profiles": "订 阅", "Label-Profiles": "订 阅",
"Label-Connections": "连 接", "Label-Connections": "连 接",
@ -22,7 +19,6 @@
"Label-Logs": "日 志", "Label-Logs": "日 志",
"Label-Test": "测 试", "Label-Test": "测 试",
"Label-Settings": "设 置", "Label-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": "编辑 Merge",
"Edit Script": "编辑 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 模式", "Tun Mode": "Tun 模式",
@ -157,7 +149,6 @@
"Auto Launch": "开机自启", "Auto Launch": "开机自启",
"Silent Start": "静默启动", "Silent Start": "静默启动",
"Silent Start Info": "程序启动时以后台模式运行,不显示程序面板", "Silent Start Info": "程序启动时以后台模式运行,不显示程序面板",
"TG Channel": "Telegram 频道", "TG Channel": "Telegram 频道",
"Manual": "使用手册", "Manual": "使用手册",
"Github Repo": "GitHub 项目地址", "Github Repo": "GitHub 项目地址",
@ -189,7 +180,6 @@
"Open UWP tool": "UWP 工具", "Open UWP tool": "UWP 工具",
"Open UWP tool Info": "Windows 8开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制", "Open UWP tool Info": "Windows 8开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制",
"Update GeoData": "更新 GeoData", "Update GeoData": "更新 GeoData",
"Verge Setting": "Verge 设置", "Verge Setting": "Verge 设置",
"Language": "语言设置", "Language": "语言设置",
"Theme Mode": "主题模式", "Theme Mode": "主题模式",
@ -258,7 +248,6 @@
"Open Dev Tools": "打开开发者工具", "Open Dev Tools": "打开开发者工具",
"Exit": "退出", "Exit": "退出",
"Verge Version": "Verge 版本", "Verge Version": "Verge 版本",
"ReadOnly": "只读", "ReadOnly": "只读",
"ReadOnlyMessage": "无法在只读模式下编辑", "ReadOnlyMessage": "无法在只读模式下编辑",
"Filter": "过滤节点", "Filter": "过滤节点",
@ -266,7 +255,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 配置已更新", "Clash Config Updated": "Clash 配置已更新",
"Profile Switched": "订阅已切换", "Profile Switched": "订阅已切换",

View File

@ -42,7 +42,6 @@ import {
ProfileViewerRef, ProfileViewerRef,
} from "@/components/profile/profile-viewer"; } from "@/components/profile/profile-viewer";
import { ProfileItem } from "@/components/profile/profile-item"; import { ProfileItem } from "@/components/profile/profile-item";
import { ProfileMore } from "@/components/profile/profile-more";
import { useProfiles } from "@/hooks/use-profiles"; import { useProfiles } from "@/hooks/use-profiles";
import { ConfigViewer } from "@/components/setting/mods/config-viewer"; import { ConfigViewer } from "@/components/setting/mods/config-viewer";
import { throttle } from "lodash-es"; import { throttle } from "lodash-es";
@ -105,31 +104,22 @@ const ProfilePage = () => {
getRuntimeLogs getRuntimeLogs
); );
const chain = profiles.chain || [];
const viewerRef = useRef<ProfileViewerRef>(null); const viewerRef = useRef<ProfileViewerRef>(null);
const configRef = useRef<DialogRef>(null); const configRef = useRef<DialogRef>(null);
// distinguish type // distinguish type
const { regularItems, enhanceItems } = useMemo(() => { const profileItems = useMemo(() => {
const items = profiles.items || []; const items = profiles.items || [];
const chain = profiles.chain || [];
const type1 = ["local", "remote"]; const type1 = ["local", "remote"];
const type2 = ["merge", "script"];
const regularItems = items.filter((i) => i && type1.includes(i.type!)); const profileItems = 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)));
return { regularItems, enhanceItems }; return profileItems;
}, [profiles]); }, [profiles]);
const currentActivatings = () => { const currentActivatings = () => {
return [...new Set([profiles.current ?? "", ...chain])].filter(Boolean); return [...new Set([profiles.current ?? ""])].filter(Boolean);
}; };
const onImport = async () => { 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 onDelete = useLockFn(async (uid: string) => {
const current = profiles.current === uid; const current = profiles.current === uid;
try { try {
await onDisable(uid);
setActivatings([...(current ? currentActivatings() : []), uid]); setActivatings([...(current ? currentActivatings() : []), uid]);
await deleteProfile(uid); await deleteProfile(uid);
mutateProfiles(); 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 setLoadingCache = useSetLoadingCache();
const onUpdateAll = useLockFn(async () => { const onUpdateAll = useLockFn(async () => {
@ -281,7 +228,7 @@ const ProfilePage = () => {
return new Promise((resolve) => { return new Promise((resolve) => {
setLoadingCache((cache) => { setLoadingCache((cache) => {
// 获取没有正在更新的订阅 // 获取没有正在更新的订阅
const items = regularItems.filter( const items = profileItems.filter(
(e) => e.type === "remote" && !cache[e.uid] (e) => e.type === "remote" && !cache[e.uid]
); );
const change = Object.fromEntries(items.map((e) => [e.uid, true])); const change = Object.fromEntries(items.map((e) => [e.uid, true]));
@ -296,11 +243,6 @@ const ProfilePage = () => {
const text = await readText(); const text = await readText();
if (text) setUrl(text); 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 ( return (
<BasePage <BasePage
@ -415,11 +357,11 @@ const ProfilePage = () => {
<Box sx={{ mb: 1.5 }}> <Box sx={{ mb: 1.5 }}>
<Grid container spacing={{ xs: 1, lg: 1 }}> <Grid container spacing={{ xs: 1, lg: 1 }}>
<SortableContext <SortableContext
items={regularItems.map((x) => { items={profileItems.map((x) => {
return x.uid; return x.uid;
})} })}
> >
{regularItems.map((item) => ( {profileItems.map((item) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}> <Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
<ProfileItem <ProfileItem
id={item.uid} id={item.uid}
@ -441,43 +383,6 @@ const ProfilePage = () => {
</Grid> </Grid>
</Box> </Box>
</DndContext> </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> </Box>
<ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} /> <ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
<ConfigViewer ref={configRef} /> <ConfigViewer ref={configRef} />

View File

@ -178,11 +178,15 @@ interface IProfileOption {
self_proxy?: boolean; self_proxy?: boolean;
update_interval?: number; update_interval?: number;
danger_accept_invalid_certs?: boolean; danger_accept_invalid_certs?: boolean;
merge?: string;
script?: string;
rules?: string;
proxies?: string;
groups?: string;
} }
interface IProfilesConfig { interface IProfilesConfig {
current?: string; current?: string;
chain?: string[];
valid?: string[]; valid?: string[];
items?: IProfileItem[]; items?: IProfileItem[];
} }
@ -254,75 +258,3 @@ interface IVergeConfig {
proxy_layout_column?: number; proxy_layout_column?: number;
test_list?: IVergeTestItem[]; 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;
}