mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 01:33:45 +08:00
refactor: Associate Profile with Merge/Script.
This commit is contained in:
parent
b85929772e
commit
3efef52398
@ -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,
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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)?;
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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")}
|
||||
|
@ -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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
)}
|
||||
|
@ -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",
|
||||
|
@ -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": "پروفایل تغییر یافت",
|
||||
|
@ -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": "Профиль изменен",
|
||||
|
@ -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": "订阅已切换",
|
||||
|
@ -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} />
|
||||
|
78
src/services/types.d.ts
vendored
78
src/services/types.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user