Bläddra i källkod

feat: support seq editor

MystiPanda 11 månader sedan
förälder
incheckning
b854b5e1ac

+ 1 - 1
src-tauri/src/config/clash.rs

@@ -13,7 +13,7 @@ pub struct IClashTemp(pub Mapping);
 impl IClashTemp {
     pub fn new() -> Self {
         let template = Self::template();
-        match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) {
+        match dirs::clash_path().and_then(|path| help::read_mapping(&path)) {
             Ok(mut map) => {
                 template.0.keys().for_each(|key| {
                     if !map.contains_key(key) {

+ 105 - 10
src-tauri/src/config/prfitem.rs

@@ -152,8 +152,6 @@ impl PrfItem {
                 let desc = item.desc.unwrap_or("".into());
                 PrfItem::from_local(name, desc, file_data, item.option)
             }
-            "merge" => PrfItem::from_merge(),
-            "script" => PrfItem::from_script(),
             typ => bail!("invalid profile item type \"{typ}\""),
         }
     }
@@ -166,15 +164,15 @@ impl PrfItem {
         file_data: Option<String>,
         option: Option<PrfOption>,
     ) -> Result<PrfItem> {
-        let uid = help::get_uid("l");
+        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());
+        let mut rules = opt_ref.and_then(|o| o.rules.clone());
+        let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
+        let mut groups = opt_ref.and_then(|o| o.groups.clone());
 
         if merge.is_none() {
             let merge_item = PrfItem::from_merge()?;
@@ -186,6 +184,23 @@ impl PrfItem {
             Config::profiles().data().append_item(script_item.clone())?;
             script = script_item.uid;
         }
+        if rules.is_none() {
+            let rules_item = PrfItem::from_rules()?;
+            Config::profiles().data().append_item(rules_item.clone())?;
+            rules = rules_item.uid;
+        }
+        if proxies.is_none() {
+            let proxies_item = PrfItem::from_proxies()?;
+            Config::profiles()
+                .data()
+                .append_item(proxies_item.clone())?;
+            proxies = proxies_item.uid;
+        }
+        if groups.is_none() {
+            let groups_item = PrfItem::from_groups()?;
+            Config::profiles().data().append_item(groups_item.clone())?;
+            groups = groups_item.uid;
+        }
         Ok(PrfItem {
             uid: Some(uid),
             itype: Some("local".into()),
@@ -227,9 +242,9 @@ impl PrfItem {
         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 rules = opt_ref.and_then(|o| o.rules.clone());
+        let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
+        let mut groups = opt_ref.and_then(|o| o.groups.clone());
         let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
 
         if merge.is_none() {
@@ -242,6 +257,23 @@ impl PrfItem {
             Config::profiles().data().append_item(script_item.clone())?;
             script = script_item.uid;
         }
+        if rules.is_none() {
+            let rules_item = PrfItem::from_rules()?;
+            Config::profiles().data().append_item(rules_item.clone())?;
+            rules = rules_item.uid;
+        }
+        if proxies.is_none() {
+            let proxies_item = PrfItem::from_proxies()?;
+            Config::profiles()
+                .data()
+                .append_item(proxies_item.clone())?;
+            proxies = proxies_item.uid;
+        }
+        if groups.is_none() {
+            let groups_item = PrfItem::from_groups()?;
+            Config::profiles().data().append_item(groups_item.clone())?;
+            groups = groups_item.uid;
+        }
         // 使用软件自己的代理
         if self_proxy {
             let port = Config::verge()
@@ -352,7 +384,7 @@ impl PrfItem {
             None => None,
         };
 
-        let uid = help::get_uid("r");
+        let uid = help::get_uid("R");
         let file = format!("{uid}.yaml");
         let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
         let data = resp.text_with_charset("utf-8").await?;
@@ -436,6 +468,69 @@ impl PrfItem {
         })
     }
 
+    /// ## Rules type (enhance)
+    pub fn from_rules() -> Result<PrfItem> {
+        let uid = help::get_uid("r");
+        let file = format!("{uid}.yaml"); // yaml ext
+
+        Ok(PrfItem {
+            uid: Some(uid),
+            itype: Some("rules".into()),
+            name: None,
+            desc: None,
+            file: Some(file),
+            url: None,
+            home: None,
+            selected: None,
+            extra: None,
+            option: None,
+            updated: Some(chrono::Local::now().timestamp() as usize),
+            file_data: Some(tmpl::ITEM_RULES.into()),
+        })
+    }
+
+    /// ## Proxies type (enhance)
+    pub fn from_proxies() -> Result<PrfItem> {
+        let uid = help::get_uid("p");
+        let file = format!("{uid}.yaml"); // yaml ext
+
+        Ok(PrfItem {
+            uid: Some(uid),
+            itype: Some("proxies".into()),
+            name: None,
+            desc: None,
+            file: Some(file),
+            url: None,
+            home: None,
+            selected: None,
+            extra: None,
+            option: None,
+            updated: Some(chrono::Local::now().timestamp() as usize),
+            file_data: Some(tmpl::ITEM_PROXIES.into()),
+        })
+    }
+
+    /// ## Groups type (enhance)
+    pub fn from_groups() -> Result<PrfItem> {
+        let uid = help::get_uid("g");
+        let file = format!("{uid}.yaml"); // yaml ext
+
+        Ok(PrfItem {
+            uid: Some(uid),
+            itype: Some("groups".into()),
+            name: None,
+            desc: None,
+            file: Some(file),
+            url: None,
+            home: None,
+            selected: None,
+            extra: None,
+            option: None,
+            updated: Some(chrono::Local::now().timestamp() as usize),
+            file_data: Some(tmpl::ITEM_GROUPS.into()),
+        })
+    }
+
     /// get the file data
     pub fn read_file(&self) -> Result<String> {
         if self.file.is_none() {

+ 97 - 10
src-tauri/src/config/profiles.rs

@@ -246,9 +246,9 @@ impl IProfiles {
         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;
+        let mut rules_index = None;
+        let mut proxies_index = None;
+        let mut groups_index = None;
 
         // get the index
         for (i, _) in items.iter().enumerate() {
@@ -257,7 +257,6 @@ impl IProfiles {
                 break;
             }
         }
-
         if let Some(index) = index {
             if let Some(file) = items.remove(index).file {
                 let _ = dirs::app_profiles_dir().map(|path| {
@@ -268,7 +267,6 @@ impl IProfiles {
                 });
             }
         }
-
         // get the merge index
         for (i, _) in items.iter().enumerate() {
             if items[i].uid == merge_uid {
@@ -276,7 +274,6 @@ impl IProfiles {
                 break;
             }
         }
-
         if let Some(index) = merge_index {
             if let Some(file) = items.remove(index).file {
                 let _ = dirs::app_profiles_dir().map(|path| {
@@ -287,7 +284,6 @@ impl IProfiles {
                 });
             }
         }
-
         // get the script index
         for (i, _) in items.iter().enumerate() {
             if items[i].uid == script_uid {
@@ -295,7 +291,6 @@ impl IProfiles {
                 break;
             }
         }
-
         if let Some(index) = script_index {
             if let Some(file) = items.remove(index).file {
                 let _ = dirs::app_profiles_dir().map(|path| {
@@ -306,7 +301,57 @@ impl IProfiles {
                 });
             }
         }
-
+        // get the rules index
+        for (i, _) in items.iter().enumerate() {
+            if items[i].uid == rules_uid {
+                rules_index = Some(i);
+                break;
+            }
+        }
+        if let Some(index) = rules_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 proxies index
+        for (i, _) in items.iter().enumerate() {
+            if items[i].uid == proxies_uid {
+                proxies_index = Some(i);
+                break;
+            }
+        }
+        if let Some(index) = proxies_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 groups index
+        for (i, _) in items.iter().enumerate() {
+            if items[i].uid == groups_uid {
+                groups_index = Some(i);
+                break;
+            }
+        }
+        if let Some(index) = groups_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() {
@@ -329,7 +374,7 @@ impl IProfiles {
                         Some(file) => dirs::app_profiles_dir()?.join(file),
                         None => bail!("failed to get the file field"),
                     };
-                    return help::read_merge_mapping(&file_path);
+                    return help::read_mapping(&file_path);
                 }
                 bail!("failed to find the current profile \"uid:{current}\"");
             }
@@ -364,4 +409,46 @@ impl IProfiles {
             _ => None,
         }
     }
+
+    /// 获取current指向的订阅的rules
+    pub fn current_rules(&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 rules = item.option.as_ref().and_then(|e| e.rules.clone());
+                    return rules;
+                }
+                None
+            }
+            _ => None,
+        }
+    }
+
+    /// 获取current指向的订阅的proxies
+    pub fn current_proxies(&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 proxies = item.option.as_ref().and_then(|e| e.proxies.clone());
+                    return proxies;
+                }
+                None
+            }
+            _ => None,
+        }
+    }
+
+    /// 获取current指向的订阅的groups
+    pub fn current_groups(&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 groups = item.option.as_ref().and_then(|e| e.groups.clone());
+                    return groups;
+                }
+                None
+            }
+            _ => None,
+        }
+    }
 }

+ 17 - 1
src-tauri/src/enhance/chain.rs

@@ -1,3 +1,4 @@
+use super::SeqMap;
 use crate::{
     config::PrfItem,
     utils::{dirs, help},
@@ -15,6 +16,9 @@ pub struct ChainItem {
 pub enum ChainType {
     Merge(Mapping),
     Script(String),
+    Rules(SeqMap),
+    Proxies(SeqMap),
+    Groups(SeqMap),
 }
 
 #[derive(Debug, Clone)]
@@ -43,7 +47,19 @@ impl From<&PrfItem> for Option<ChainItem> {
             }),
             "merge" => Some(ChainItem {
                 uid,
-                data: ChainType::Merge(help::read_merge_mapping(&path).ok()?),
+                data: ChainType::Merge(help::read_mapping(&path).ok()?),
+            }),
+            "rules" => Some(ChainItem {
+                uid,
+                data: ChainType::Rules(help::read_seq_map(&path).ok()?),
+            }),
+            "proxies" => Some(ChainItem {
+                uid,
+                data: ChainType::Proxies(help::read_seq_map(&path).ok()?),
+            }),
+            "groups" => Some(ChainItem {
+                uid,
+                data: ChainType::Groups(help::read_seq_map(&path).ok()?),
             }),
             _ => None,
         }

+ 47 - 5
src-tauri/src/enhance/mod.rs

@@ -2,12 +2,14 @@ mod chain;
 pub mod field;
 mod merge;
 mod script;
+pub mod seq;
 mod tun;
 
 use self::chain::*;
 use self::field::*;
 use self::merge::*;
 use self::script::*;
+use self::seq::*;
 use self::tun::*;
 use crate::config::Config;
 use crate::utils::tmpl;
@@ -48,7 +50,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
     };
 
     // 从profiles里拿东西
-    let (mut config, merge_item, script_item) = {
+    let (mut config, merge_item, script_item, rules_item, proxies_item, groups_item) = {
         let profiles = Config::profiles();
         let profiles = profiles.latest();
 
@@ -59,9 +61,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
             .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(),
-                ),
+                data: ChainType::Merge(Mapping::new()),
             });
         let script = profiles
             .get_item(&profiles.current_script().unwrap_or_default())
@@ -71,14 +71,56 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
                 uid: "".into(),
                 data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
             });
+        let rules = profiles
+            .get_item(&profiles.current_rules().unwrap_or_default())
+            .ok()
+            .and_then(<Option<ChainItem>>::from)
+            .unwrap_or_else(|| ChainItem {
+                uid: "".into(),
+                data: ChainType::Rules(SeqMap::default()),
+            });
+        let proxies = profiles
+            .get_item(&profiles.current_proxies().unwrap_or_default())
+            .ok()
+            .and_then(<Option<ChainItem>>::from)
+            .unwrap_or_else(|| ChainItem {
+                uid: "".into(),
+                data: ChainType::Proxies(SeqMap::default()),
+            });
+        let groups = profiles
+            .get_item(&profiles.current_groups().unwrap_or_default())
+            .ok()
+            .and_then(<Option<ChainItem>>::from)
+            .unwrap_or_else(|| ChainItem {
+                uid: "".into(),
+                data: ChainType::Groups(SeqMap::default()),
+            });
 
-        (current, merge, script)
+        (current, merge, script, rules, proxies, groups)
     };
 
     let mut result_map = HashMap::new(); // 保存脚本日志
     let mut exists_keys = use_keys(&config); // 保存出现过的keys
 
     // 处理用户的profile
+    match rules_item.data {
+        ChainType::Rules(rules) => {
+            config = use_seq(rules, config.to_owned(), "rules");
+        }
+        _ => {}
+    }
+    match proxies_item.data {
+        ChainType::Proxies(proxies) => {
+            config = use_seq(proxies, config.to_owned(), "proxies");
+        }
+        _ => {}
+    }
+    match groups_item.data {
+        ChainType::Groups(groups) => {
+            config = use_seq(groups, config.to_owned(), "proxy-groups");
+        }
+        _ => {}
+    }
     match merge_item.data {
         ChainType::Merge(merge) => {
             exists_keys.extend(use_keys(&merge));

+ 34 - 0
src-tauri/src/enhance/seq.rs

@@ -0,0 +1,34 @@
+use serde::{Deserialize, Serialize};
+use serde_yaml::{Mapping, Sequence, Value};
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+pub struct SeqMap {
+    prepend: Sequence,
+    append: Sequence,
+    delete: Sequence,
+}
+
+pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping {
+    let prepend = seq_map.prepend;
+    let append = seq_map.append;
+    let delete = seq_map.delete;
+
+    let origin_seq = config.get(&name).map_or(Sequence::default(), |val| {
+        val.as_sequence().unwrap().clone()
+    });
+    let mut seq = origin_seq.clone();
+
+    for item in prepend {
+        seq.insert(0, item);
+    }
+
+    for item in append {
+        seq.push(item);
+    }
+
+    for item in delete {
+        seq.retain(|x| x != &item);
+    }
+    let mut config = config.clone();
+    config.insert(Value::from(name), Value::from(seq));
+    return config;
+}

+ 9 - 1
src-tauri/src/utils/help.rs

@@ -1,3 +1,4 @@
+use crate::enhance::seq::SeqMap;
 use anyhow::{anyhow, bail, Context, Result};
 use nanoid::nanoid;
 use serde::{de::DeserializeOwned, Serialize};
@@ -26,7 +27,7 @@ pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
 }
 
 /// read mapping from yaml fix #165
-pub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> {
+pub fn read_mapping(path: &PathBuf) -> Result<Mapping> {
     let mut val: Value = read_yaml(path)?;
     val.apply_merge()
         .with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
@@ -40,6 +41,13 @@ pub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> {
         .to_owned())
 }
 
+/// read mapping from yaml fix #165
+pub fn read_seq_map(path: &PathBuf) -> Result<SeqMap> {
+    let val: SeqMap = read_yaml(path)?;
+
+    Ok(val)
+}
+
 /// save the data to the file
 /// can set `prefix` string to add some comments
 pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {

+ 30 - 0
src-tauri/src/utils/tmpl.rs

@@ -33,3 +33,33 @@ function main(config) {
   return config;
 }
 ";
+
+/// enhanced profile
+pub const ITEM_RULES: &str = "# Profile Enhancement Rules Template for Clash Verge
+
+prepend: []
+
+append: []
+
+delete: []
+";
+
+/// enhanced profile
+pub const ITEM_PROXIES: &str = "# Profile Enhancement Proxies Template for Clash Verge
+
+prepend: []
+
+append: []
+
+delete: []
+";
+
+/// enhanced profile
+pub const ITEM_GROUPS: &str = "# Profile Enhancement Groups Template for Clash Verge
+
+prepend: []
+
+append: []
+
+delete: []
+";

+ 76 - 1
src/components/profile/profile-item.tsx

@@ -105,6 +105,9 @@ export const ProfileItem = (props: Props) => {
   }, [hasUrl, updated]);
 
   const [fileOpen, setFileOpen] = useState(false);
+  const [rulesOpen, setRulesOpen] = useState(false);
+  const [proxiesOpen, setProxiesOpen] = useState(false);
+  const [groupsOpen, setGroupsOpen] = useState(false);
   const [mergeOpen, setMergeOpen] = useState(false);
   const [scriptOpen, setScriptOpen] = useState(false);
   const [confirmOpen, setConfirmOpen] = useState(false);
@@ -124,6 +127,21 @@ export const ProfileItem = (props: Props) => {
     setFileOpen(true);
   };
 
+  const onEditRules = () => {
+    setAnchorEl(null);
+    setRulesOpen(true);
+  };
+
+  const onEditProxies = () => {
+    setAnchorEl(null);
+    setProxiesOpen(true);
+  };
+
+  const onEditGroups = () => {
+    setAnchorEl(null);
+    setGroupsOpen(true);
+  };
+
   const onEditMerge = () => {
     setAnchorEl(null);
     setMergeOpen(true);
@@ -191,6 +209,21 @@ export const ProfileItem = (props: Props) => {
     { label: "Select", handler: onForceSelect, disabled: false },
     { label: "Edit Info", handler: onEditInfo, disabled: false },
     { label: "Edit File", handler: onEditFile, disabled: false },
+    {
+      label: "Edit Rules",
+      handler: onEditRules,
+      disabled: option?.rules === null,
+    },
+    {
+      label: "Edit Proxies",
+      handler: onEditProxies,
+      disabled: option?.proxies === null,
+    },
+    {
+      label: "Edit Groups",
+      handler: onEditGroups,
+      disabled: option?.groups === null,
+    },
     {
       label: "Edit Merge",
       handler: onEditMerge,
@@ -217,6 +250,21 @@ export const ProfileItem = (props: Props) => {
     { label: "Select", handler: onForceSelect, disabled: false },
     { label: "Edit Info", handler: onEditInfo, disabled: false },
     { label: "Edit File", handler: onEditFile, disabled: false },
+    {
+      label: "Edit Rules",
+      handler: onEditRules,
+      disabled: option?.rules === null,
+    },
+    {
+      label: "Edit Proxies",
+      handler: onEditProxies,
+      disabled: option?.proxies === null,
+    },
+    {
+      label: "Edit Groups",
+      handler: onEditGroups,
+      disabled: option?.groups === null,
+    },
     {
       label: "Edit Merge",
       handler: onEditMerge,
@@ -435,7 +483,34 @@ export const ProfileItem = (props: Props) => {
       />
       <EditorViewer
         mode="profile"
-        property={option?.merge ?? "123"}
+        property={option?.rules ?? ""}
+        open={rulesOpen}
+        language="yaml"
+        schema={undefined}
+        onChange={onChange}
+        onClose={() => setRulesOpen(false)}
+      />
+      <EditorViewer
+        mode="profile"
+        property={option?.proxies ?? ""}
+        open={proxiesOpen}
+        language="yaml"
+        schema={undefined}
+        onChange={onChange}
+        onClose={() => setProxiesOpen(false)}
+      />
+      <EditorViewer
+        mode="profile"
+        property={option?.groups ?? ""}
+        open={groupsOpen}
+        language="yaml"
+        schema={undefined}
+        onChange={onChange}
+        onClose={() => setGroupsOpen(false)}
+      />
+      <EditorViewer
+        mode="profile"
+        property={option?.merge ?? ""}
         open={mergeOpen}
         language="yaml"
         schema="merge"

+ 3 - 0
src/locales/en.json

@@ -50,6 +50,9 @@
   "Expire Time": "Expire Time",
   "Create Profile": "Create Profile",
   "Edit Profile": "Edit Profile",
+  "Edit Proxies": "Edit Proxies",
+  "Edit Rules": "Edit Rules",
+  "Edit Groups": "Edit Proxy Groups",
   "Edit Merge": "Edit Merge",
   "Edit Script": "Edit Script",
   "Type": "Type",

+ 3 - 0
src/locales/zh.json

@@ -50,6 +50,9 @@
   "Expire Time": "到期时间",
   "Create Profile": "新建配置",
   "Edit Profile": "编辑配置",
+  "Edit Proxies": "编辑代理",
+  "Edit Rules": "编辑规则",
+  "Edit Groups": "编辑代理组",
   "Edit Merge": "编辑 Merge",
   "Edit Script": "编辑 Script",
   "Type": "类型",