From 3efef52398f38976c30df6706b6ff5cde807bf6b Mon Sep 17 00:00:00 2001 From: MystiPanda Date: Sat, 29 Jun 2024 23:07:44 +0800 Subject: [PATCH] refactor: Associate Profile with Merge/Script. --- src-tauri/src/config/prfitem.rs | 101 +++++--- src-tauri/src/config/profiles.rs | 85 ++++++- src-tauri/src/enhance/mod.rs | 42 ++-- src-tauri/src/feat.rs | 1 - src-tauri/src/utils/resolve.rs | 43 ++-- src-tauri/src/utils/server.rs | 7 +- src/components/profile/profile-item.tsx | 77 +++++- src/components/profile/profile-more.tsx | 285 ---------------------- src/components/profile/profile-viewer.tsx | 7 +- src/locales/en.json | 16 +- src/locales/fa.json | 16 +- src/locales/ru.json | 16 +- src/locales/zh.json | 16 +- src/pages/profiles.tsx | 109 +-------- src/services/types.d.ts | 78 +----- 15 files changed, 286 insertions(+), 613 deletions(-) delete mode 100644 src/components/profile/profile-more.tsx diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index d2a7955a..22a378f8 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -94,6 +94,16 @@ pub struct PrfOption { /// default is `false` #[serde(skip_serializing_if = "Option::is_none")] pub danger_accept_invalid_certs: Option, + + pub merge: Option, + + pub script: Option, + + pub rules: Option, + + pub proxies: Option, + + pub groups: Option, } 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 { 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::() { - 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 { + pub fn from_merge() -> Result { 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 { + pub fn from_script() -> Result { 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, diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 30966516..2283c224 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -11,9 +11,6 @@ pub struct IProfiles { /// same as PrfConfig.current pub current: Option, - /// same as PrfConfig.chain - pub chain: Option>, - /// profile list pub items: Option>, } @@ -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 { 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 { + 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 { + 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, + } + } } diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 5a6b5348..fc9d2c90 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -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, HashMap) { }; // 从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(>::from) + .unwrap_or_else(|| ChainItem { + uid: "".into(), + data: ChainType::Merge( + serde_yaml::from_str::(tmpl::ITEM_MERGE).unwrap_or_default(), + ), + }); + let script = profiles + .get_item(&profiles.current_script().unwrap_or_default()) + .ok() + .and_then(>::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(>::from) - .collect::>(), - 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, HashMap) { 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() { diff --git a/src-tauri/src/feat.rs b/src-tauri/src/feat.rs index 93c388f1..57a19287 100644 --- a/src-tauri/src/feat.rs +++ b/src-tauri/src/feat.rs @@ -292,7 +292,6 @@ pub async fn update_profile(uid: String, option: Option) -> 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)?; diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs index 01afcf0e..8d22c297 100644 --- a/src-tauri/src/utils/resolve.rs +++ b/src-tauri/src/utils/resolve.rs @@ -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(()) } diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index 4a02f103..ac16b67f 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -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 { - 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); diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 1487cb82..9f392e43 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -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) => { { onChange={onChange} onClose={() => setFileOpen(false)} /> + setMergeOpen(false)} + /> + setScriptOpen(false)} + /> 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(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 ( - <> - onSelect(false)} - onContextMenu={(event) => { - const { clientX, clientY } = event; - setPosition({ top: clientY, left: clientX }); - setAnchorEl(event.currentTarget); - event.preventDefault(); - }} - > - {activating && ( - - - - )} - - - {itemData.name} - - - - - - - {selected && type === "script" ? ( - hasError ? ( - - setLogOpen(true)} - > - - - - ) : ( - setLogOpen(true)} - > - - - ) - ) : ( - - {itemData.desc} - - )} - - - - 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) => ( - { - return { - color: - item.label === "Delete" - ? theme.palette.error.main - : undefined, - }; - }, - ]} - dense - > - {t(item.label)} - - ))} - - - setFileOpen(false)} - /> - setConfirmOpen(false)} - onConfirm={() => { - onDelete(); - setConfirmOpen(false); - }} - /> - {selected && ( - setLogOpen(false)} - /> - )} - - ); -}; diff --git a/src/components/profile/profile-viewer.tsx b/src/components/profile/profile-viewer.tsx index 0a675231..06879d0d 100644 --- a/src/components/profile/profile-viewer.tsx +++ b/src/components/profile/profile-viewer.tsx @@ -33,7 +33,7 @@ export interface ProfileViewerRef { } // create or edit the profile -// remote / local / merge / script +// remote / local export const ProfileViewer = forwardRef( (props, ref) => { const { t } = useTranslation(); @@ -92,9 +92,6 @@ export const ProfileViewer = forwardRef( 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( )} diff --git a/src/locales/en.json b/src/locales/en.json index c431e0ba..45b38c22 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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", diff --git a/src/locales/fa.json b/src/locales/fa.json index ec9287dd..c9bea1f9 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -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": "پروفایل تغییر یافت", diff --git a/src/locales/ru.json b/src/locales/ru.json index 3fb89aef..dee452cd 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -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": "Профиль изменен", diff --git a/src/locales/zh.json b/src/locales/zh.json index 498e4af7..8fa1a652 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -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": "订阅已切换", diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 1bd7f62f..9c641e64 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -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(null); const configRef = useRef(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 ( { { + items={profileItems.map((x) => { return x.uid; })} > - {regularItems.map((item) => ( + {profileItems.map((item) => ( { - - {enhanceItems.length > 0 && ( - - )} - - {enhanceItems.length > 0 && ( - - - {enhanceItems.map((item) => ( - - 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(); - } - }} - /> - - ))} - - - )} mutateProfiles()} /> diff --git a/src/services/types.d.ts b/src/services/types.d.ts index 13734526..9cdc4cb1 100644 --- a/src/services/types.d.ts +++ b/src/services/types.d.ts @@ -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; -}