From 749df8922993364a5321d1e9fb8e22bee5c2aaa8 Mon Sep 17 00:00:00 2001
From: GyDi <segydi@foxmail.com>
Date: Tue, 1 Mar 2022 08:58:47 +0800
Subject: [PATCH] refactor: profile config

---
 src-tauri/src/cmds.rs                   | 118 ++---
 src-tauri/src/core/clash.rs             |  59 ++-
 src-tauri/src/core/mod.rs               |   1 -
 src-tauri/src/core/prfitem.rs           | 332 --------------
 src-tauri/src/core/profiles.rs          | 585 +++++++++++++-----------
 src-tauri/src/utils/help.rs             |  86 ++++
 src-tauri/src/utils/init.rs             |  12 +-
 src-tauri/src/utils/mod.rs              |   3 +-
 src-tauri/src/utils/resolve.rs          |  14 +-
 src-tauri/src/utils/tmpl.rs             |   4 +-
 src/components/profile/profile-item.tsx |  10 +-
 src/components/proxy/proxy-group.tsx    |   9 +-
 src/pages/profiles.tsx                  |  30 +-
 src/services/cmds.ts                    |  10 +-
 src/services/types.ts                   |   5 +-
 15 files changed, 543 insertions(+), 735 deletions(-)
 delete mode 100644 src-tauri/src/core/prfitem.rs
 create mode 100644 src-tauri/src/utils/help.rs

diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs
index 8eb1609..4026c22 100644
--- a/src-tauri/src/cmds.rs
+++ b/src-tauri/src/cmds.rs
@@ -1,37 +1,18 @@
 use crate::{
-  core::{ClashInfo, ProfileItem, Profiles, VergeConfig},
+  core::{ClashInfo, PrfItem, Profiles, VergeConfig},
+  ret_err,
   states::{ClashState, ProfilesState, VergeState},
-  utils::{dirs, fetch::fetch_profile, sysopt::SysProxyConfig},
+  utils::{dirs, sysopt::SysProxyConfig},
+  wrap_err,
 };
 use anyhow::Result;
 use serde_yaml::Mapping;
 use std::{path::PathBuf, process::Command};
 use tauri::{api, State};
 
-/// wrap the anyhow error
-/// transform the error to String
-macro_rules! wrap_err {
-  ($stat: expr) => {
-    match $stat {
-      Ok(a) => Ok(a),
-      Err(err) => {
-        log::error!("{}", err.to_string());
-        Err(format!("{}", err.to_string()))
-      }
-    }
-  };
-}
-
-/// return the string literal error
-macro_rules! ret_err {
-  ($str: literal) => {
-    return Err($str.into())
-  };
-}
-
 /// get all profiles from `profiles.yaml`
 #[tauri::command]
-pub fn get_profiles(profiles_state: State<'_, ProfilesState>) -> Result<Profiles, String> {
+pub fn get_profiles<'a>(profiles_state: State<'_, ProfilesState>) -> Result<Profiles, String> {
   let profiles = profiles_state.0.lock().unwrap();
   Ok(profiles.clone())
 }
@@ -51,9 +32,10 @@ pub async fn import_profile(
   with_proxy: bool,
   profiles_state: State<'_, ProfilesState>,
 ) -> Result<(), String> {
-  let result = fetch_profile(&url, with_proxy).await?;
+  let item = wrap_err!(PrfItem::from_url(&url, with_proxy).await)?;
+
   let mut profiles = profiles_state.0.lock().unwrap();
-  wrap_err!(profiles.import_from_url(url, result))
+  wrap_err!(profiles.append_item(item))
 }
 
 /// new a profile
@@ -65,59 +47,50 @@ pub async fn new_profile(
   desc: String,
   profiles_state: State<'_, ProfilesState>,
 ) -> Result<(), String> {
+  let item = wrap_err!(PrfItem::from_local(name, desc))?;
   let mut profiles = profiles_state.0.lock().unwrap();
-  wrap_err!(profiles.append_item(name, desc))?;
-  Ok(())
+
+  wrap_err!(profiles.append_item(item))
 }
 
 /// Update the profile
 #[tauri::command]
 pub async fn update_profile(
-  index: usize,
+  index: String,
   with_proxy: bool,
   clash_state: State<'_, ClashState>,
   profiles_state: State<'_, ProfilesState>,
 ) -> Result<(), String> {
-  // maybe we can get the url from the web app directly
-  let url = match profiles_state.0.lock() {
-    Ok(mut profile) => {
-      let items = profile.items.take().unwrap_or(vec![]);
-      if index >= items.len() {
-        ret_err!("the index out of bound");
-      }
-      let url = match &items[index].url {
-        Some(u) => u.clone(),
-        None => ret_err!("failed to update profile for `invalid url`"),
-      };
-      profile.items = Some(items);
-      url
+  let url = {
+    // must release the lock here
+    let profiles = profiles_state.0.lock().unwrap();
+    let item = wrap_err!(profiles.get_item(&index))?;
+
+    if item.url.is_none() {
+      ret_err!("failed to get the item url");
     }
-    Err(_) => ret_err!("failed to get profiles lock"),
+
+    item.url.clone().unwrap()
   };
 
-  let result = fetch_profile(&url, with_proxy).await?;
+  let item = wrap_err!(PrfItem::from_url(&url, with_proxy).await)?;
 
-  match profiles_state.0.lock() {
-    Ok(mut profiles) => {
-      wrap_err!(profiles.update_item(index, result))?;
+  let mut profiles = profiles_state.0.lock().unwrap();
+  wrap_err!(profiles.update_item(index.clone(), item))?;
 
-      // reactivate the profile
-      let current = profiles.current.clone().unwrap_or(0);
-      if current == index {
-        let clash = clash_state.0.lock().unwrap();
-        wrap_err!(profiles.activate(&clash))
-      } else {
-        Ok(())
-      }
-    }
-    Err(_) => ret_err!("failed to get profiles lock"),
+  // reactivate the profile
+  if Some(index) == profiles.get_current() {
+    let clash = clash_state.0.lock().unwrap();
+    wrap_err!(clash.activate(&profiles))?;
   }
+
+  Ok(())
 }
 
 /// change the current profile
 #[tauri::command]
 pub fn select_profile(
-  index: usize,
+  index: String,
   clash_state: State<'_, ClashState>,
   profiles_state: State<'_, ProfilesState>,
 ) -> Result<(), String> {
@@ -125,13 +98,13 @@ pub fn select_profile(
   wrap_err!(profiles.put_current(index))?;
 
   let clash = clash_state.0.lock().unwrap();
-  wrap_err!(profiles.activate(&clash))
+  wrap_err!(clash.activate(&profiles))
 }
 
 /// delete profile item
 #[tauri::command]
 pub fn delete_profile(
-  index: usize,
+  index: String,
   clash_state: State<'_, ClashState>,
   profiles_state: State<'_, ProfilesState>,
 ) -> Result<(), String> {
@@ -139,7 +112,7 @@ pub fn delete_profile(
 
   if wrap_err!(profiles.delete_item(index))? {
     let clash = clash_state.0.lock().unwrap();
-    wrap_err!(profiles.activate(&clash))?;
+    wrap_err!(clash.activate(&profiles))?;
   }
 
   Ok(())
@@ -148,8 +121,8 @@ pub fn delete_profile(
 /// patch the profile config
 #[tauri::command]
 pub fn patch_profile(
-  index: usize,
-  profile: ProfileItem,
+  index: String,
+  profile: PrfItem,
   profiles_state: State<'_, ProfilesState>,
 ) -> Result<(), String> {
   let mut profiles = profiles_state.0.lock().unwrap();
@@ -158,19 +131,16 @@ pub fn patch_profile(
 
 /// run vscode command to edit the profile
 #[tauri::command]
-pub fn view_profile(index: usize, profiles_state: State<'_, ProfilesState>) -> Result<(), String> {
-  let mut profiles = profiles_state.0.lock().unwrap();
-  let items = profiles.items.take().unwrap_or(vec![]);
+pub fn view_profile(index: String, profiles_state: State<'_, ProfilesState>) -> Result<(), String> {
+  let profiles = profiles_state.0.lock().unwrap();
+  let item = wrap_err!(profiles.get_item(&index))?;
 
-  if index >= items.len() {
-    profiles.items = Some(items);
-    ret_err!("the index out of bound");
+  let file = item.file.clone();
+  if file.is_none() {
+    ret_err!("the file is null");
   }
 
-  let file = items[index].file.clone().unwrap_or("".into());
-  profiles.items = Some(items);
-
-  let path = dirs::app_profiles_dir().join(file);
+  let path = dirs::app_profiles_dir().join(file.unwrap());
   if !path.exists() {
     ret_err!("the file not found");
   }
@@ -285,7 +255,7 @@ pub fn patch_verge_config(
 
     wrap_err!(clash.tun_mode(tun_mode.unwrap()))?;
     clash.update_config();
-    wrap_err!(profiles.activate(&clash))?;
+    wrap_err!(clash.activate(&profiles))?;
   }
 
   Ok(())
diff --git a/src-tauri/src/core/clash.rs b/src-tauri/src/core/clash.rs
index d6a1d8c..811890b 100644
--- a/src-tauri/src/core/clash.rs
+++ b/src-tauri/src/core/clash.rs
@@ -1,6 +1,9 @@
+use std::collections::HashMap;
+
 use super::{Profiles, Verge};
 use crate::utils::{config, dirs};
 use anyhow::{bail, Result};
+use reqwest::header::HeaderMap;
 use serde::{Deserialize, Serialize};
 use serde_yaml::{Mapping, Value};
 use tauri::api::process::{Command, CommandChild, CommandEvent};
@@ -153,7 +156,7 @@ impl Clash {
     self.update_config();
     self.drop_sidecar()?;
     self.run_sidecar()?;
-    profiles.activate(&self)
+    self.activate(profiles)
   }
 
   /// update the clash info
@@ -191,11 +194,7 @@ impl Clash {
         verge.init_sysproxy(port);
       }
 
-      if self.config.contains_key(key) {
-        self.config[key] = value;
-      } else {
-        self.config.insert(key.clone(), value);
-      }
+      self.config.insert(key.clone(), value);
     }
     self.save_config()
   }
@@ -241,6 +240,54 @@ impl Clash {
 
     self.save_config()
   }
+
+  /// activate the profile
+  pub fn activate(&self, profiles: &Profiles) -> Result<()> {
+    let temp_path = dirs::profiles_temp_path();
+    let info = self.info.clone();
+    let mut config = self.config.clone();
+    let gen_config = profiles.gen_activate()?;
+
+    for (key, value) in gen_config.into_iter() {
+      config.insert(key, value);
+    }
+
+    config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?;
+
+    tauri::async_runtime::spawn(async move {
+      let server = info.server.clone().unwrap();
+      let server = format!("http://{server}/configs");
+
+      let mut headers = HeaderMap::new();
+      headers.insert("Content-Type", "application/json".parse().unwrap());
+
+      if let Some(secret) = info.secret.as_ref() {
+        let secret = format!("Bearer {}", secret.clone()).parse().unwrap();
+        headers.insert("Authorization", secret);
+      }
+
+      let mut data = HashMap::new();
+      data.insert("path", temp_path.as_os_str().to_str().unwrap());
+
+      for _ in 0..5 {
+        match reqwest::ClientBuilder::new().no_proxy().build() {
+          Ok(client) => match client
+            .put(&server)
+            .headers(headers.clone())
+            .json(&data)
+            .send()
+            .await
+          {
+            Ok(_) => break,
+            Err(err) => log::error!("failed to activate for `{err}`"),
+          },
+          Err(err) => log::error!("failed to activate for `{err}`"),
+        }
+      }
+    });
+
+    Ok(())
+  }
 }
 
 impl Default for Clash {
diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs
index 1075ba5..1cf8f1e 100644
--- a/src-tauri/src/core/mod.rs
+++ b/src-tauri/src/core/mod.rs
@@ -1,5 +1,4 @@
 mod clash;
-mod prfitem;
 mod profiles;
 mod verge;
 
diff --git a/src-tauri/src/core/prfitem.rs b/src-tauri/src/core/prfitem.rs
deleted file mode 100644
index fa3be88..0000000
--- a/src-tauri/src/core/prfitem.rs
+++ /dev/null
@@ -1,332 +0,0 @@
-//! Todos
-//! refactor the profiles
-
-use crate::utils::{config, dirs};
-use anyhow::{bail, Result};
-use serde::{Deserialize, Serialize};
-use serde_yaml::{Mapping, Value};
-use std::{fs, str::FromStr};
-
-#[derive(Default, Debug, Clone, Deserialize, Serialize)]
-pub struct PrfItem {
-  pub uid: Option<String>,
-
-  /// profile item type
-  /// enum value: remote | local | script | merge
-  #[serde(rename = "type")]
-  pub itype: Option<String>,
-
-  /// profile name
-  pub name: Option<String>,
-
-  /// profile description
-  #[serde(skip_serializing_if = "Option::is_none")]
-  pub desc: Option<String>,
-
-  /// profile file
-  pub file: Option<String>,
-
-  /// source url
-  #[serde(skip_serializing_if = "Option::is_none")]
-  pub url: Option<String>,
-
-  /// selected infomation
-  #[serde(skip_serializing_if = "Option::is_none")]
-  pub selected: Option<Vec<PrfSelected>>,
-
-  /// user info
-  #[serde(skip_serializing_if = "Option::is_none")]
-  pub extra: Option<PrfExtra>,
-
-  /// updated time
-  pub updated: Option<usize>,
-}
-
-#[derive(Default, Debug, Clone, Deserialize, Serialize)]
-pub struct PrfSelected {
-  pub name: Option<String>,
-  pub now: Option<String>,
-}
-
-#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
-pub struct PrfExtra {
-  pub upload: usize,
-  pub download: usize,
-  pub total: usize,
-  pub expire: usize,
-}
-
-type FileData = String;
-
-impl PrfItem {
-  pub fn gen_now() -> usize {
-    use std::time::{SystemTime, UNIX_EPOCH};
-
-    SystemTime::now()
-      .duration_since(UNIX_EPOCH)
-      .unwrap()
-      .as_secs() as _
-  }
-
-  /// generate the uid
-  pub fn gen_uid(prefix: &str) -> String {
-    let now = Self::gen_now();
-    format!("{prefix}{now}")
-  }
-
-  /// parse the string
-  fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
-    match target.find(key) {
-      Some(idx) => {
-        let idx = idx + key.len();
-        let value = &target[idx..];
-        match match value.split(';').nth(0) {
-          Some(value) => value.trim().parse(),
-          None => value.trim().parse(),
-        } {
-          Ok(r) => Some(r),
-          Err(_) => None,
-        }
-      }
-      None => None,
-    }
-  }
-
-  pub async fn from_url(url: &str, with_proxy: bool) -> Result<(Self, FileData)> {
-    let mut builder = reqwest::ClientBuilder::new();
-
-    if !with_proxy {
-      builder = builder.no_proxy();
-    }
-
-    let resp = builder.build()?.get(url).send().await?;
-    let header = resp.headers();
-
-    // parse the Subscription Userinfo
-    let extra = match header.get("Subscription-Userinfo") {
-      Some(value) => {
-        let sub_info = value.to_str().unwrap_or("");
-
-        Some(PrfExtra {
-          upload: PrfItem::parse_str(sub_info, "upload=").unwrap_or(0),
-          download: PrfItem::parse_str(sub_info, "download=").unwrap_or(0),
-          total: PrfItem::parse_str(sub_info, "total=").unwrap_or(0),
-          expire: PrfItem::parse_str(sub_info, "expire=").unwrap_or(0),
-        })
-      }
-      None => None,
-    };
-
-    let uid = PrfItem::gen_uid("r");
-    let file = format!("{uid}.yaml");
-    let name = uid.clone();
-    let data = resp.text_with_charset("utf-8").await?;
-
-    let item = PrfItem {
-      uid: Some(uid),
-      itype: Some("remote".into()),
-      name: Some(name),
-      desc: None,
-      file: Some(file),
-      url: Some(url.into()),
-      selected: None,
-      extra,
-      updated: Some(PrfItem::gen_now()),
-    };
-
-    Ok((item, data))
-  }
-}
-
-///
-/// ## Profiles Config
-///
-/// Define the `profiles.yaml` schema
-///
-#[derive(Default, Debug, Clone, Deserialize, Serialize)]
-pub struct Profiles {
-  /// same as PrfConfig.current
-  current: Option<String>,
-
-  /// same as PrfConfig.chain
-  chain: Option<Vec<String>>,
-
-  /// profile list
-  items: Option<Vec<PrfItem>>,
-}
-
-impl Profiles {
-  pub fn new() -> Profiles {
-    Profiles::read_file()
-  }
-
-  /// read the config from the file
-  pub fn read_file() -> Self {
-    config::read_yaml::<Self>(dirs::profiles_path())
-  }
-
-  /// save the config to the file
-  pub fn save_file(&self) -> Result<()> {
-    config::save_yaml(
-      dirs::profiles_path(),
-      self,
-      Some("# Profiles Config for Clash Verge\n\n"),
-    )
-  }
-
-  /// get the current uid
-  pub fn get_current(&self) -> Option<String> {
-    self.current.clone()
-  }
-
-  /// only change the main to the target id
-  pub fn put_current(&mut self, uid: String) -> Result<()> {
-    if self.items.is_none() {
-      self.items = Some(vec![]);
-    }
-
-    let items = self.items.as_ref().unwrap();
-    let some_uid = Some(uid.clone());
-
-    for each in items.iter() {
-      if each.uid == some_uid {
-        self.current = some_uid;
-        return self.save_file();
-      }
-    }
-
-    bail!("invalid uid \"{uid}\"");
-  }
-
-  /// append new item
-  /// return the new item's uid
-  pub fn append_item(&mut self, item: PrfItem) -> Result<()> {
-    if item.uid.is_none() {
-      bail!("the uid should not be null");
-    }
-
-    let mut items = self.items.take().unwrap_or(vec![]);
-    items.push(item);
-    self.items = Some(items);
-    self.save_file()
-  }
-
-  /// update the item's value
-  pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
-    let mut items = self.items.take().unwrap_or(vec![]);
-
-    macro_rules! patch {
-      ($lv: expr, $rv: expr, $key: tt) => {
-        if ($rv.$key).is_some() {
-          $lv.$key = $rv.$key;
-        }
-      };
-    }
-
-    for mut each in items.iter_mut() {
-      if each.uid == Some(uid.clone()) {
-        patch!(each, item, itype);
-        patch!(each, item, name);
-        patch!(each, item, desc);
-        patch!(each, item, file);
-        patch!(each, item, url);
-        patch!(each, item, selected);
-        patch!(each, item, extra);
-
-        each.updated = Some(PrfItem::gen_now());
-
-        self.items = Some(items);
-        return self.save_file();
-      }
-    }
-
-    self.items = Some(items);
-    bail!("failed to found the uid \"{uid}\"")
-  }
-
-  /// delete item
-  /// if delete the main then return true
-  pub fn delete_item(&mut self, uid: String) -> Result<bool> {
-    let current = self.current.as_ref().unwrap_or(&uid);
-    let current = current.clone();
-
-    let mut items = self.items.take().unwrap_or(vec![]);
-    let mut index = None;
-
-    // get the index
-    for i in 0..items.len() {
-      if items[i].uid == Some(uid.clone()) {
-        index = Some(i);
-        break;
-      }
-    }
-
-    if let Some(index) = index {
-      items.remove(index).file.map(|file| {
-        let path = dirs::app_profiles_dir().join(file);
-        if path.exists() {
-          let _ = fs::remove_file(path);
-        }
-      });
-    }
-
-    // delete the original uid
-    if current == uid {
-      self.current = match items.len() > 0 {
-        true => items[0].uid.clone(),
-        false => None,
-      };
-    }
-
-    self.items = Some(items);
-    self.save_file()?;
-    Ok(current == uid)
-  }
-
-  /// only generate config mapping
-  pub fn gen_activate(&self) -> Result<Mapping> {
-    if self.current.is_none() {
-      bail!("invalid main uid on profiles");
-    }
-
-    let current = self.current.clone().unwrap();
-
-    for item in self.items.as_ref().unwrap().iter() {
-      if item.uid == Some(current.clone()) {
-        let file_path = match item.file.clone() {
-          Some(file) => dirs::app_profiles_dir().join(file),
-          None => bail!("failed to get the file field"),
-        };
-
-        if !file_path.exists() {
-          bail!("failed to read the file \"{}\"", file_path.display());
-        }
-
-        let mut new_config = Mapping::new();
-        let def_config = config::read_yaml::<Mapping>(file_path.clone());
-
-        // Only the following fields are allowed:
-        // proxies/proxy-providers/proxy-groups/rule-providers/rules
-        let valid_keys = vec![
-          "proxies",
-          "proxy-providers",
-          "proxy-groups",
-          "rule-providers",
-          "rules",
-        ];
-
-        valid_keys.iter().for_each(|key| {
-          let key = Value::String(key.to_string());
-          if def_config.contains_key(&key) {
-            let value = def_config[&key].clone();
-            new_config.insert(key, value);
-          }
-        });
-
-        return Ok(new_config);
-      }
-    }
-
-    bail!("failed to found the uid \"{current}\"");
-  }
-}
diff --git a/src-tauri/src/core/profiles.rs b/src-tauri/src/core/profiles.rs
index 4fdae29..0b872d7 100644
--- a/src-tauri/src/core/profiles.rs
+++ b/src-tauri/src/core/profiles.rs
@@ -1,27 +1,18 @@
-use super::{Clash, ClashInfo};
-use crate::utils::{config, dirs, tmpl};
-use anyhow::{bail, Result};
-use reqwest::header::HeaderMap;
+use crate::utils::{config, dirs, help, tmpl};
+use anyhow::{bail, Context, Result};
 use serde::{Deserialize, Serialize};
 use serde_yaml::{Mapping, Value};
-use std::collections::HashMap;
-use std::fs::{remove_file, File};
-use std::io::Write;
-use std::path::PathBuf;
-use std::time::{SystemTime, UNIX_EPOCH};
+use std::{fs, io::Write};
 
-/// Define the `profiles.yaml` schema
-#[derive(Default, Debug, Clone, Deserialize, Serialize)]
-pub struct Profiles {
-  /// current profile's name
-  pub current: Option<usize>,
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct PrfItem {
+  pub uid: Option<String>,
 
-  /// profile list
-  pub items: Option<Vec<ProfileItem>>,
-}
+  /// profile item type
+  /// enum value: remote | local | script | merge
+  #[serde(rename = "type")]
+  pub itype: Option<String>,
 
-#[derive(Default, Debug, Clone, Deserialize, Serialize)]
-pub struct ProfileItem {
   /// profile name
   pub name: Option<String>,
 
@@ -32,53 +23,154 @@ pub struct ProfileItem {
   /// profile file
   pub file: Option<String>,
 
-  /// current mode
-  #[serde(skip_serializing_if = "Option::is_none")]
-  pub mode: Option<String>,
-
   /// source url
   #[serde(skip_serializing_if = "Option::is_none")]
   pub url: Option<String>,
 
   /// selected infomation
   #[serde(skip_serializing_if = "Option::is_none")]
-  pub selected: Option<Vec<ProfileSelected>>,
+  pub selected: Option<Vec<PrfSelected>>,
 
   /// user info
   #[serde(skip_serializing_if = "Option::is_none")]
-  pub extra: Option<ProfileExtra>,
+  pub extra: Option<PrfExtra>,
 
   /// updated time
   pub updated: Option<usize>,
+
+  /// the file data
+  #[serde(skip)]
+  pub file_data: Option<String>,
 }
 
 #[derive(Default, Debug, Clone, Deserialize, Serialize)]
-pub struct ProfileSelected {
+pub struct PrfSelected {
   pub name: Option<String>,
   pub now: Option<String>,
 }
 
 #[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
-pub struct ProfileExtra {
+pub struct PrfExtra {
   pub upload: usize,
   pub download: usize,
   pub total: usize,
   pub expire: usize,
 }
 
+impl Default for PrfItem {
+  fn default() -> Self {
+    PrfItem {
+      uid: None,
+      itype: None,
+      name: None,
+      desc: None,
+      file: None,
+      url: None,
+      selected: None,
+      extra: None,
+      updated: None,
+      file_data: None,
+    }
+  }
+}
+
+impl PrfItem {
+  /// ## Local type
+  /// create a new item from name/desc
+  pub fn from_local(name: String, desc: String) -> Result<PrfItem> {
+    let uid = help::get_uid("l");
+    let file = format!("{uid}.yaml");
+
+    Ok(PrfItem {
+      uid: Some(uid),
+      itype: Some("local".into()),
+      name: Some(name),
+      desc: Some(desc),
+      file: Some(file),
+      url: None,
+      selected: None,
+      extra: None,
+      updated: Some(help::get_now()),
+      file_data: Some(tmpl::ITEM_CONFIG.into()),
+    })
+  }
+
+  /// ## Remote type
+  /// create a new item from url
+  pub async fn from_url(url: &str, with_proxy: bool) -> Result<PrfItem> {
+    let mut builder = reqwest::ClientBuilder::new();
+
+    if !with_proxy {
+      builder = builder.no_proxy();
+    }
+
+    let resp = builder.build()?.get(url).send().await?;
+    let header = resp.headers();
+
+    // parse the Subscription Userinfo
+    let extra = match header.get("Subscription-Userinfo") {
+      Some(value) => {
+        let sub_info = value.to_str().unwrap_or("");
+
+        Some(PrfExtra {
+          upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
+          download: help::parse_str(sub_info, "download=").unwrap_or(0),
+          total: help::parse_str(sub_info, "total=").unwrap_or(0),
+          expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
+        })
+      }
+      None => None,
+    };
+
+    let uid = help::get_uid("r");
+    let file = format!("{uid}.yaml");
+    let name = uid.clone();
+    let data = resp.text_with_charset("utf-8").await?;
+
+    Ok(PrfItem {
+      uid: Some(uid),
+      itype: Some("remote".into()),
+      name: Some(name),
+      desc: None,
+      file: Some(file),
+      url: Some(url.into()),
+      selected: None,
+      extra,
+      updated: Some(help::get_now()),
+      file_data: Some(data),
+    })
+  }
+}
+
+///
+/// ## Profiles Config
+///
+/// Define the `profiles.yaml` schema
+///
 #[derive(Default, Debug, Clone, Deserialize, Serialize)]
-/// the result from url
-pub struct ProfileResponse {
-  pub name: String,
-  pub file: String,
-  pub data: String,
-  pub extra: Option<ProfileExtra>,
+pub struct Profiles {
+  /// same as PrfConfig.current
+  current: Option<String>,
+
+  /// same as PrfConfig.chain
+  chain: Option<Vec<String>>,
+
+  /// profile list
+  items: Option<Vec<PrfItem>>,
+}
+
+macro_rules! patch {
+  ($lv: expr, $rv: expr, $key: tt) => {
+    if ($rv.$key).is_some() {
+      $lv.$key = $rv.$key;
+    }
+  };
 }
 
 impl Profiles {
   /// read the config from the file
   pub fn read_file() -> Self {
-    config::read_yaml::<Profiles>(dirs::profiles_path())
+    config::read_yaml::<Self>(dirs::profiles_path())
   }
 
   /// save the config to the file
@@ -92,303 +184,242 @@ impl Profiles {
 
   /// sync the config between file and memory
   pub fn sync_file(&mut self) -> Result<()> {
-    let data = config::read_yaml::<Self>(dirs::profiles_path());
-    if data.current.is_none() {
-      bail!("failed to read profiles.yaml")
-    } else {
-      self.current = data.current;
-      self.items = data.items;
-      Ok(())
+    let data = Self::read_file();
+    if data.current.is_none() && data.items.is_none() {
+      bail!("failed to read profiles.yaml");
     }
+
+    self.current = data.current;
+    self.chain = data.chain;
+    self.items = data.items;
+    Ok(())
   }
 
-  /// import the new profile from the url
-  /// and update the config file
-  pub fn import_from_url(&mut self, url: String, result: ProfileResponse) -> Result<()> {
-    // save the profile file
-    let path = dirs::app_profiles_dir().join(&result.file);
-    let file_data = result.data.as_bytes();
-    File::create(path).unwrap().write(file_data).unwrap();
-
-    // update `profiles.yaml`
-    let data = Profiles::read_file();
-    let mut items = data.items.unwrap_or(vec![]);
-
-    let now = SystemTime::now()
-      .duration_since(UNIX_EPOCH)
-      .unwrap()
-      .as_secs();
-
-    items.push(ProfileItem {
-      name: Some(result.name),
-      desc: Some("imported url".into()),
-      file: Some(result.file),
-      mode: Some(format!("rule")),
-      url: Some(url),
-      selected: Some(vec![]),
-      extra: result.extra,
-      updated: Some(now as usize),
-    });
+  /// get the current uid
+  pub fn get_current(&self) -> Option<String> {
+    self.current.clone()
+  }
 
-    self.items = Some(items);
-    if data.current.is_none() {
-      self.current = Some(0);
+  /// only change the main to the target id
+  pub fn put_current(&mut self, uid: String) -> Result<()> {
+    if self.items.is_none() {
+      self.items = Some(vec![]);
     }
 
-    self.save_file()
+    let items = self.items.as_ref().unwrap();
+    let some_uid = Some(uid.clone());
+
+    for each in items.iter() {
+      if each.uid == some_uid {
+        self.current = some_uid;
+        return self.save_file();
+      }
+    }
+
+    bail!("invalid uid \"{uid}\"");
   }
 
-  /// set the current and save to file
-  pub fn put_current(&mut self, index: usize) -> Result<()> {
-    let items = self.items.take().unwrap_or(vec![]);
+  /// find the item by the uid
+  pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
+    if self.items.is_some() {
+      let items = self.items.as_ref().unwrap();
+      let some_uid = Some(uid.clone());
 
-    if index >= items.len() {
-      bail!("the index out of bound");
+      for each in items.iter() {
+        if each.uid == some_uid {
+          return Ok(each);
+        }
+      }
     }
 
-    self.items = Some(items);
-    self.current = Some(index);
-    self.save_file()
+    bail!("failed to get the item by \"{}\"", uid);
   }
 
   /// append new item
-  /// return the new item's index
-  pub fn append_item(&mut self, name: String, desc: String) -> Result<(usize, PathBuf)> {
-    let mut items = self.items.take().unwrap_or(vec![]);
-
-    // create a new profile file
-    let now = SystemTime::now()
-      .duration_since(UNIX_EPOCH)
-      .unwrap()
-      .as_secs();
-    let file = format!("{}.yaml", now);
-    let path = dirs::app_profiles_dir().join(&file);
-
-    match File::create(&path).unwrap().write(tmpl::ITEM_CONFIG) {
-      Ok(_) => {
-        items.push(ProfileItem {
-          name: Some(name),
-          desc: Some(desc),
-          file: Some(file),
-          mode: None,
-          url: None,
-          selected: Some(vec![]),
-          extra: None,
-          updated: Some(now as usize),
-        });
+  /// if the file_data is some
+  /// then should save the data to file
+  pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
+    if item.uid.is_none() {
+      bail!("the uid should not be null");
+    }
 
-        let index = items.len();
-        self.items = Some(items);
-        Ok((index, path))
+    // save the file data
+    // move the field value after save
+    if let Some(file_data) = item.file_data.take() {
+      if item.file.is_none() {
+        bail!("the file should not be null");
       }
-      Err(_) => bail!("failed to create file"),
+
+      let file = item.file.clone().unwrap();
+      let path = dirs::app_profiles_dir().join(&file);
+
+      fs::File::create(path)
+        .context(format!("failed to create file \"{}\"", file))?
+        .write(file_data.as_bytes())
+        .context(format!("failed to write to file \"{}\"", file))?;
+    }
+
+    if self.items.is_none() {
+      self.items = Some(vec![]);
     }
+
+    self.items.as_mut().map(|items| items.push(item));
+    self.save_file()
   }
 
-  /// update the target profile
-  /// and save to config file
-  /// only support the url item
-  pub fn update_item(&mut self, index: usize, result: ProfileResponse) -> Result<()> {
+  /// update the item's value
+  pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
     let mut items = self.items.take().unwrap_or(vec![]);
 
-    let now = SystemTime::now()
-      .duration_since(UNIX_EPOCH)
-      .unwrap()
-      .as_secs() as usize;
+    for mut each in items.iter_mut() {
+      if each.uid == Some(uid.clone()) {
+        patch!(each, item, itype);
+        patch!(each, item, name);
+        patch!(each, item, desc);
+        patch!(each, item, file);
+        patch!(each, item, url);
+        patch!(each, item, selected);
+        patch!(each, item, extra);
 
-    // update file
-    let file_path = &items[index].file.as_ref().unwrap();
-    let file_path = dirs::app_profiles_dir().join(file_path);
-    let file_data = result.data.as_bytes();
-    File::create(file_path).unwrap().write(file_data).unwrap();
+        each.updated = Some(help::get_now());
 
-    items[index].name = Some(result.name);
-    items[index].extra = result.extra;
-    items[index].updated = Some(now);
+        self.items = Some(items);
+        return self.save_file();
+      }
+    }
 
     self.items = Some(items);
-    self.save_file()
+    bail!("failed to found the uid \"{uid}\"")
   }
 
-  /// patch item
-  pub fn patch_item(&mut self, index: usize, profile: ProfileItem) -> Result<()> {
-    let mut items = self.items.take().unwrap_or(vec![]);
-    if index >= items.len() {
-      bail!("index out of range");
+  /// be used to update the remote item
+  /// only patch `updated` `extra` `file_data`
+  pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
+    if self.items.is_none() {
+      self.items = Some(vec![]);
     }
 
-    if profile.name.is_some() {
-      items[index].name = profile.name;
-    }
-    if profile.file.is_some() {
-      items[index].file = profile.file;
-    }
-    if profile.mode.is_some() {
-      items[index].mode = profile.mode;
-    }
-    if profile.url.is_some() {
-      items[index].url = profile.url;
-    }
-    if profile.selected.is_some() {
-      items[index].selected = profile.selected;
-    }
-    if profile.extra.is_some() {
-      items[index].extra = profile.extra;
-    }
+    // find the item
+    let _ = self.get_item(&uid)?;
 
-    self.items = Some(items);
-    self.save_file()
-  }
+    self.items.as_mut().map(|items| {
+      let some_uid = Some(uid.clone());
 
-  /// delete the item
-  pub fn delete_item(&mut self, index: usize) -> Result<bool> {
-    let mut current = self.current.clone().unwrap_or(0);
-    let mut items = self.items.clone().unwrap_or(vec![]);
+      for mut each in items.iter_mut() {
+        if each.uid == some_uid {
+          patch!(each, item, extra);
+          patch!(each, item, updated);
 
-    if index >= items.len() {
-      bail!("index out of range");
-    }
+          // save the file data
+          // move the field value after save
+          if let Some(file_data) = item.file_data.take() {
+            let file = each.file.take();
+            let file = file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid)));
 
-    let mut rm_item = items.remove(index);
+            // the file must exists
+            each.file = Some(file.clone());
 
-    // delete the file
-    if let Some(file) = rm_item.file.take() {
-      let file_path = dirs::app_profiles_dir().join(file);
+            let path = dirs::app_profiles_dir().join(&file);
 
-      if file_path.exists() {
-        if let Err(err) = remove_file(file_path) {
-          log::error!("{err}");
+            fs::File::create(path)
+              .unwrap()
+              .write(file_data.as_bytes())
+              .unwrap();
+          }
+
+          break;
         }
       }
-    }
+    });
 
-    let mut should_change = false;
+    self.save_file()
+  }
 
-    if current == index {
-      current = 0;
-      should_change = true;
-    } else if current > index {
-      current = current - 1;
-    }
+  /// delete item
+  /// if delete the current then return true
+  pub fn delete_item(&mut self, uid: String) -> Result<bool> {
+    let current = self.current.as_ref().unwrap_or(&uid);
+    let current = current.clone();
 
-    self.current = Some(current);
-    self.items = Some(items);
+    let mut items = self.items.take().unwrap_or(vec![]);
+    let mut index = None;
 
-    match self.save_file() {
-      Ok(_) => Ok(should_change),
-      Err(err) => Err(err),
+    // get the index
+    for i in 0..items.len() {
+      if items[i].uid == Some(uid.clone()) {
+        index = Some(i);
+        break;
+      }
     }
-  }
 
-  /// activate current profile
-  pub fn activate(&self, clash: &Clash) -> Result<()> {
-    let current = self.current.unwrap_or(0);
-    match self.items.clone() {
-      Some(items) => {
-        if current >= items.len() {
-          bail!("the index out of bound");
+    if let Some(index) = index {
+      items.remove(index).file.map(|file| {
+        let path = dirs::app_profiles_dir().join(file);
+        if path.exists() {
+          let _ = fs::remove_file(path);
         }
+      });
+    }
 
-        let profile = items[current].clone();
-        let clash_config = clash.config.clone();
-        let clash_info = clash.info.clone();
-
-        tauri::async_runtime::spawn(async move {
-          let mut count = 5; // retry times
-          let mut err = None;
-          while count > 0 {
-            match activate_profile(&profile, &clash_config, &clash_info).await {
-              Ok(_) => return,
-              Err(e) => err = Some(e),
-            }
-            count -= 1;
-          }
-          log::error!("failed to activate for `{}`", err.unwrap());
-        });
-
-        Ok(())
-      }
-      None => bail!("empty profiles"),
+    // delete the original uid
+    if current == uid {
+      self.current = match items.len() > 0 {
+        true => items[0].uid.clone(),
+        false => None,
+      };
     }
+
+    self.items = Some(items);
+    self.save_file()?;
+    Ok(current == uid)
   }
-}
 
-/// put the profile to clash
-pub async fn activate_profile(
-  profile_item: &ProfileItem,
-  clash_config: &Mapping,
-  clash_info: &ClashInfo,
-) -> Result<()> {
-  // temp profile's path
-  let temp_path = dirs::profiles_temp_path();
-
-  // generate temp profile
-  {
-    let file_name = match profile_item.file.clone() {
-      Some(file_name) => file_name,
-      None => bail!("profile item should have `file` field"),
-    };
+  /// only generate config mapping
+  pub fn gen_activate(&self) -> Result<Mapping> {
+    let config = Mapping::new();
 
-    let file_path = dirs::app_profiles_dir().join(file_name);
-    if !file_path.exists() {
-      bail!(
-        "profile `{}` not exists",
-        file_path.as_os_str().to_str().unwrap()
-      );
+    if self.current.is_none() || self.items.is_none() {
+      return Ok(config);
     }
 
-    // begin to generate the new profile config
-    let def_config = config::read_yaml::<Mapping>(file_path.clone());
-
-    // use the clash config except 5 keys below
-    let mut new_config = clash_config.clone();
-
-    // Only the following fields are allowed:
-    // proxies/proxy-providers/proxy-groups/rule-providers/rules
-    let valid_keys = vec![
-      "proxies",
-      "proxy-providers",
-      "proxy-groups",
-      "rule-providers",
-      "rules",
-    ];
-    valid_keys.iter().for_each(|key| {
-      let key = Value::String(key.to_string());
-      if def_config.contains_key(&key) {
-        let value = def_config[&key].clone();
-        new_config.insert(key, value);
-      }
-    });
+    let current = self.current.clone().unwrap();
 
-    config::save_yaml(
-      temp_path.clone(),
-      &new_config,
-      Some("# Clash Verge Temp File"),
-    )?
-  };
+    for item in self.items.as_ref().unwrap().iter() {
+      if item.uid == Some(current.clone()) {
+        let file_path = match item.file.clone() {
+          Some(file) => dirs::app_profiles_dir().join(file),
+          None => bail!("failed to get the file field"),
+        };
 
-  let server = format!("http://{}/configs", clash_info.server.clone().unwrap());
-
-  let mut headers = HeaderMap::new();
-  headers.insert("Content-Type", "application/json".parse().unwrap());
-
-  if let Some(secret) = clash_info.secret.clone() {
-    headers.insert(
-      "Authorization",
-      format!("Bearer {}", secret).parse().unwrap(),
-    );
-  }
+        if !file_path.exists() {
+          bail!("failed to read the file \"{}\"", file_path.display());
+        }
 
-  let mut data = HashMap::new();
-  data.insert("path", temp_path.as_os_str().to_str().unwrap());
+        let mut new_config = Mapping::new();
+        let def_config = config::read_yaml::<Mapping>(file_path.clone());
+
+        // Only the following fields are allowed:
+        // proxies/proxy-providers/proxy-groups/rule-providers/rules
+        let valid_keys = vec![
+          "proxies",
+          "proxy-providers",
+          "proxy-groups",
+          "rule-providers",
+          "rules",
+        ];
+
+        valid_keys.iter().for_each(|key| {
+          let key = Value::String(key.to_string());
+          if def_config.contains_key(&key) {
+            let value = def_config[&key].clone();
+            new_config.insert(key, value);
+          }
+        });
 
-  let client = reqwest::ClientBuilder::new().no_proxy().build()?;
+        return Ok(new_config);
+      }
+    }
 
-  client
-    .put(server)
-    .headers(headers)
-    .json(&data)
-    .send()
-    .await?;
-  Ok(())
+    bail!("failed to found the uid \"{current}\"");
+  }
 }
diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs
new file mode 100644
index 0000000..3ddf1bd
--- /dev/null
+++ b/src-tauri/src/utils/help.rs
@@ -0,0 +1,86 @@
+use std::str::FromStr;
+use std::time::{SystemTime, UNIX_EPOCH};
+
+pub fn get_now() -> usize {
+  SystemTime::now()
+    .duration_since(UNIX_EPOCH)
+    .unwrap()
+    .as_secs() as _
+}
+
+/// generate the uid
+pub fn get_uid(prefix: &str) -> String {
+  let now = get_now();
+  format!("{prefix}{now}")
+}
+
+/// parse the string
+/// xxx=123123; => 123123
+pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
+  match target.find(key) {
+    Some(idx) => {
+      let idx = idx + key.len();
+      let value = &target[idx..];
+      match match value.split(';').nth(0) {
+        Some(value) => value.trim().parse(),
+        None => value.trim().parse(),
+      } {
+        Ok(r) => Some(r),
+        Err(_) => None,
+      }
+    }
+    None => None,
+  }
+}
+
+#[macro_export]
+macro_rules! log_if_err {
+  ($result: expr) => {
+    if let Err(err) = $result {
+      log::error!("{err}");
+    }
+  };
+}
+
+/// wrap the anyhow error
+/// transform the error to String
+#[macro_export]
+macro_rules! wrap_err {
+  ($stat: expr) => {
+    match $stat {
+      Ok(a) => Ok(a),
+      Err(err) => {
+        log::error!("{}", err.to_string());
+        Err(format!("{}", err.to_string()))
+      }
+    }
+  };
+}
+
+/// return the string literal error
+#[macro_export]
+macro_rules! ret_err {
+  ($str: literal) => {
+    return Err($str.into())
+  };
+}
+
+#[test]
+fn test_parse_value() {
+  let test_1 = "upload=111; download=2222; total=3333; expire=444";
+  let test_2 = "attachment; filename=Clash.yaml";
+
+  assert_eq!(parse_str::<usize>(test_1, "upload=").unwrap(), 111);
+  assert_eq!(parse_str::<usize>(test_1, "download=").unwrap(), 2222);
+  assert_eq!(parse_str::<usize>(test_1, "total=").unwrap(), 3333);
+  assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444);
+  assert_eq!(
+    parse_str::<String>(test_2, "filename=").unwrap(),
+    format!("Clash.yaml")
+  );
+
+  assert_eq!(parse_str::<usize>(test_1, "aaa="), None);
+  assert_eq!(parse_str::<usize>(test_1, "upload1="), None);
+  assert_eq!(parse_str::<usize>(test_1, "expire1="), None);
+  assert_eq!(parse_str::<usize>(test_2, "attachment="), None);
+}
diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs
index 0fc16fd..facb277 100644
--- a/src-tauri/src/utils/init.rs
+++ b/src-tauri/src/utils/init.rs
@@ -3,7 +3,7 @@ use chrono::Local;
 use log::LevelFilter;
 use log4rs::append::console::ConsoleAppender;
 use log4rs::append::file::FileAppender;
-use log4rs::config::{Appender, Config, Root};
+use log4rs::config::{Appender, Config, Logger, Root};
 use log4rs::encode::pattern::PatternEncoder;
 use std::fs;
 use std::io::Write;
@@ -28,11 +28,13 @@ fn init_log(log_dir: &PathBuf) {
   let config = Config::builder()
     .appender(Appender::builder().build("stdout", Box::new(stdout)))
     .appender(Appender::builder().build("file", Box::new(tofile)))
-    .build(
-      Root::builder()
-        .appenders(["stdout", "file"])
-        .build(LevelFilter::Debug),
+    .logger(
+      Logger::builder()
+        .appender("file")
+        .additive(false)
+        .build("app", LevelFilter::Info),
     )
+    .build(Root::builder().appender("stdout").build(LevelFilter::Info))
     .unwrap();
 
   log4rs::init_config(config).unwrap();
diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs
index be971d5..3db4e7c 100644
--- a/src-tauri/src/utils/mod.rs
+++ b/src-tauri/src/utils/mod.rs
@@ -1,6 +1,7 @@
 pub mod config;
 pub mod dirs;
-pub mod fetch;
+// pub mod fetch;
+pub mod help;
 pub mod init;
 pub mod resolve;
 pub mod server;
diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs
index 9fe90ce..9753940 100644
--- a/src-tauri/src/utils/resolve.rs
+++ b/src-tauri/src/utils/resolve.rs
@@ -1,5 +1,5 @@
 use super::{init, server};
-use crate::{core::Profiles, states};
+use crate::{core::Profiles, log_if_err, states};
 use tauri::{App, AppHandle, Manager};
 
 /// handle something when start app
@@ -21,14 +21,10 @@ pub fn resolve_setup(app: &App) {
   let mut verge = verge_state.0.lock().unwrap();
   let mut profiles = profiles_state.0.lock().unwrap();
 
-  if let Err(err) = clash.run_sidecar() {
-    log::error!("{err}");
-  }
+  log_if_err!(clash.run_sidecar());
 
   *profiles = Profiles::read_file();
-  if let Err(err) = profiles.activate(&clash) {
-    log::error!("{err}");
-  }
+  log_if_err!(clash.activate(&profiles));
 
   verge.init_sysproxy(clash.info.port.clone());
   // enable tun mode
@@ -41,9 +37,7 @@ pub fn resolve_setup(app: &App) {
   }
 
   verge.init_launch();
-  if let Err(err) = verge.sync_launch() {
-    log::error!("{err}");
-  }
+  log_if_err!(verge.sync_launch());
 }
 
 /// reset system proxy
diff --git a/src-tauri/src/utils/tmpl.rs b/src-tauri/src/utils/tmpl.rs
index 2b9c04a..2462c2d 100644
--- a/src-tauri/src/utils/tmpl.rs
+++ b/src-tauri/src/utils/tmpl.rs
@@ -14,7 +14,7 @@ secret: ""
 /// template for `profiles.yaml`
 pub const PROFILES_CONFIG: &[u8] = b"# Profiles Config for Clash Verge
 
-current: 0
+current: ~
 items: ~
 ";
 
@@ -32,7 +32,7 @@ system_proxy_bypass: localhost;127.*;10.*;192.168.*;<local>
 ";
 
 /// template for new a profile item
-pub const ITEM_CONFIG: &[u8] = b"# Profile Template for clash verge\n\n
+pub const ITEM_CONFIG: &str = "# Profile Template for clash verge\n\n
 # proxies defination (optional, the same as clash)
 proxies:\n
 # proxy-groups (optional, the same as clash)
diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx
index e42acc4..b39459d 100644
--- a/src/components/profile/profile-item.tsx
+++ b/src/components/profile/profile-item.tsx
@@ -39,14 +39,14 @@ const round = keyframes`
 `;
 
 interface Props {
-  index: number;
+  // index: number;
   selected: boolean;
   itemData: CmdType.ProfileItem;
   onSelect: (force: boolean) => void;
 }
 
 const ProfileItem: React.FC<Props> = (props) => {
-  const { index, selected, itemData, onSelect } = props;
+  const { selected, itemData, onSelect } = props;
 
   const { mutate } = useSWRConfig();
   const [loading, setLoading] = useState(false);
@@ -69,7 +69,7 @@ const ProfileItem: React.FC<Props> = (props) => {
   const onView = async () => {
     setAnchorEl(null);
     try {
-      await viewProfile(index);
+      await viewProfile(itemData.uid);
     } catch (err: any) {
       Notice.error(err.toString());
     }
@@ -85,7 +85,7 @@ const ProfileItem: React.FC<Props> = (props) => {
     if (loading) return;
     setLoading(true);
     try {
-      await updateProfile(index, withProxy);
+      await updateProfile(itemData.uid, withProxy);
       mutate("getProfiles");
     } catch (err: any) {
       Notice.error(err.toString());
@@ -98,7 +98,7 @@ const ProfileItem: React.FC<Props> = (props) => {
     setAnchorEl(null);
 
     try {
-      await deleteProfile(index);
+      await deleteProfile(itemData.uid);
       mutate("getProfiles");
     } catch (err: any) {
       Notice.error(err.toString());
diff --git a/src/components/proxy/proxy-group.tsx b/src/components/proxy/proxy-group.tsx
index 2a3d61d..acd516d 100644
--- a/src/components/proxy/proxy-group.tsx
+++ b/src/components/proxy/proxy-group.tsx
@@ -1,5 +1,5 @@
+import useSWR, { useSWRConfig } from "swr";
 import { useEffect, useRef, useState } from "react";
-import { useSWRConfig } from "swr";
 import { useLockFn } from "ahooks";
 import { Virtuoso } from "react-virtuoso";
 import {
@@ -46,6 +46,8 @@ const ProxyGroup = ({ group }: Props) => {
   const virtuosoRef = useRef<any>();
   const filterProxies = useFilterProxy(proxies, group.name, filterText);
 
+  const { data: profiles } = useSWR("getProfiles", getProfiles);
+
   const onChangeProxy = useLockFn(async (name: string) => {
     // Todo: support another proxy group type
     if (group.type !== "Selector") return;
@@ -60,8 +62,7 @@ const ProxyGroup = ({ group }: Props) => {
     }
 
     try {
-      const profiles = await getProfiles();
-      const profile = profiles.items![profiles.current!]!;
+      const profile = profiles?.items?.find((p) => p.uid === profiles.current);
       if (!profile) return;
       if (!profile.selected) profile.selected = [];
 
@@ -74,7 +75,7 @@ const ProxyGroup = ({ group }: Props) => {
       } else {
         profile.selected[index] = { name: group.name, now: name };
       }
-      await patchProfile(profiles.current!, profile);
+      await patchProfile(profiles!.current!, profile);
     } catch (err) {
       console.error(err);
     }
diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx
index 802e34c..b6491ab 100644
--- a/src/pages/profiles.tsx
+++ b/src/pages/profiles.tsx
@@ -10,7 +10,6 @@ import {
   newProfile,
 } from "../services/cmds";
 import { getProxies, updateProxy } from "../services/api";
-import noop from "../utils/noop";
 import Notice from "../components/base/base-notice";
 import BasePage from "../components/base/base-page";
 import ProfileItem from "../components/profile/profile-item";
@@ -28,7 +27,7 @@ const ProfilePage = () => {
     if (!profiles.items) profiles.items = [];
 
     const current = profiles.current;
-    const profile = profiles.items![current];
+    const profile = profiles.items.find((p) => p.uid === current);
     if (!profile) return;
 
     setTimeout(async () => {
@@ -72,9 +71,17 @@ const ProfilePage = () => {
 
     try {
       await importProfile(url);
-      mutate("getProfiles", getProfiles());
-      if (!profiles.items?.length) selectProfile(0).catch(noop);
       Notice.success("Successfully import profile.");
+
+      getProfiles().then((newProfiles) => {
+        mutate("getProfiles", newProfiles);
+
+        if (!newProfiles.current && newProfiles.items?.length) {
+          const current = newProfiles.items[0].uid;
+          selectProfile(current);
+          mutate("getProfiles", { ...newProfiles, current }, true);
+        }
+      });
     } catch {
       Notice.error("Failed to import profile.");
     } finally {
@@ -82,12 +89,12 @@ const ProfilePage = () => {
     }
   };
 
-  const onSelect = useLockFn(async (index: number, force: boolean) => {
-    if (!force && index === profiles.current) return;
+  const onSelect = useLockFn(async (current: string, force: boolean) => {
+    if (!force && current === profiles.current) return;
 
     try {
-      await selectProfile(index);
-      mutate("getProfiles", { ...profiles, current: index }, true);
+      await selectProfile(current);
+      mutate("getProfiles", { ...profiles, current: current }, true);
     } catch (err: any) {
       err && Notice.error(err.toString());
     }
@@ -131,13 +138,12 @@ const ProfilePage = () => {
       </Box>
 
       <Grid container spacing={3}>
-        {profiles?.items?.map((item, idx) => (
+        {profiles?.items?.map((item) => (
           <Grid item xs={12} sm={6} key={item.file}>
             <ProfileItem
-              index={idx}
-              selected={profiles.current === idx}
+              selected={profiles.current === item.uid}
               itemData={item}
-              onSelect={(f) => onSelect(idx, f)}
+              onSelect={(f) => onSelect(item.uid, f)}
             />
           </Grid>
         ))}
diff --git a/src/services/cmds.ts b/src/services/cmds.ts
index b8d9d4a..595a4db 100644
--- a/src/services/cmds.ts
+++ b/src/services/cmds.ts
@@ -13,7 +13,7 @@ export async function newProfile(name: string, desc: string) {
   return invoke<void>("new_profile", { name, desc });
 }
 
-export async function viewProfile(index: number) {
+export async function viewProfile(index: string) {
   return invoke<void>("view_profile", { index });
 }
 
@@ -21,22 +21,22 @@ export async function importProfile(url: string) {
   return invoke<void>("import_profile", { url, withProxy: true });
 }
 
-export async function updateProfile(index: number, withProxy: boolean) {
+export async function updateProfile(index: string, withProxy: boolean) {
   return invoke<void>("update_profile", { index, withProxy });
 }
 
-export async function deleteProfile(index: number) {
+export async function deleteProfile(index: string) {
   return invoke<void>("delete_profile", { index });
 }
 
 export async function patchProfile(
-  index: number,
+  index: string,
   profile: CmdType.ProfileItem
 ) {
   return invoke<void>("patch_profile", { index, profile });
 }
 
-export async function selectProfile(index: number) {
+export async function selectProfile(index: string) {
   return invoke<void>("select_profile", { index });
 }
 
diff --git a/src/services/types.ts b/src/services/types.ts
index 8843766..07ace0e 100644
--- a/src/services/types.ts
+++ b/src/services/types.ts
@@ -86,6 +86,8 @@ export namespace CmdType {
   }
 
   export interface ProfileItem {
+    uid: string;
+    type?: string;
     name?: string;
     desc?: string;
     file?: string;
@@ -105,7 +107,8 @@ export namespace CmdType {
   }
 
   export interface ProfilesConfig {
-    current?: number;
+    current?: string;
+    chain?: string[];
     items?: ProfileItem[];
   }
 
-- 
GitLab