|
@@ -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::{Deserialize, Serialize};
|
|
use serde_yaml::{Mapping, Value};
|
|
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
|
|
/// profile name
|
|
pub name: Option<String>,
|
|
pub name: Option<String>,
|
|
|
|
|
|
@@ -32,53 +23,154 @@ pub struct ProfileItem {
|
|
/// profile file
|
|
/// profile file
|
|
pub file: Option<String>,
|
|
pub file: Option<String>,
|
|
|
|
|
|
- /// current mode
|
|
|
|
- #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
- pub mode: Option<String>,
|
|
|
|
-
|
|
|
|
/// source url
|
|
/// source url
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub url: Option<String>,
|
|
pub url: Option<String>,
|
|
|
|
|
|
/// selected infomation
|
|
/// selected infomation
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
- pub selected: Option<Vec<ProfileSelected>>,
|
|
|
|
|
|
+ pub selected: Option<Vec<PrfSelected>>,
|
|
|
|
|
|
/// user info
|
|
/// user info
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
- pub extra: Option<ProfileExtra>,
|
|
|
|
|
|
+ pub extra: Option<PrfExtra>,
|
|
|
|
|
|
/// updated time
|
|
/// updated time
|
|
pub updated: Option<usize>,
|
|
pub updated: Option<usize>,
|
|
|
|
+
|
|
|
|
+ /// the file data
|
|
|
|
+ #[serde(skip)]
|
|
|
|
+ pub file_data: Option<String>,
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
|
-pub struct ProfileSelected {
|
|
|
|
|
|
+pub struct PrfSelected {
|
|
pub name: Option<String>,
|
|
pub name: Option<String>,
|
|
pub now: Option<String>,
|
|
pub now: Option<String>,
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
|
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
|
-pub struct ProfileExtra {
|
|
|
|
|
|
+pub struct PrfExtra {
|
|
pub upload: usize,
|
|
pub upload: usize,
|
|
pub download: usize,
|
|
pub download: usize,
|
|
pub total: usize,
|
|
pub total: usize,
|
|
pub expire: 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)]
|
|
#[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 {
|
|
impl Profiles {
|
|
/// read the config from the file
|
|
/// read the config from the file
|
|
pub fn read_file() -> Self {
|
|
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
|
|
/// save the config to the file
|
|
@@ -92,303 +184,242 @@ impl Profiles {
|
|
|
|
|
|
/// sync the config between file and memory
|
|
/// sync the config between file and memory
|
|
pub fn sync_file(&mut self) -> Result<()> {
|
|
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
|
|
/// 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 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.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}\"");
|
|
|
|
+ }
|
|
}
|
|
}
|