refactor: profile config

This commit is contained in:
GyDi 2022-03-01 08:58:47 +08:00
parent 444f2172fa
commit 749df89229
No known key found for this signature in database
GPG Key ID: 1C95E0D3467B3084
15 changed files with 567 additions and 759 deletions

View File

@ -1,37 +1,18 @@
use crate::{ use crate::{
core::{ClashInfo, ProfileItem, Profiles, VergeConfig}, core::{ClashInfo, PrfItem, Profiles, VergeConfig},
ret_err,
states::{ClashState, ProfilesState, VergeState}, states::{ClashState, ProfilesState, VergeState},
utils::{dirs, fetch::fetch_profile, sysopt::SysProxyConfig}, utils::{dirs, sysopt::SysProxyConfig},
wrap_err,
}; };
use anyhow::Result; use anyhow::Result;
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::{path::PathBuf, process::Command}; use std::{path::PathBuf, process::Command};
use tauri::{api, State}; 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` /// get all profiles from `profiles.yaml`
#[tauri::command] #[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(); let profiles = profiles_state.0.lock().unwrap();
Ok(profiles.clone()) Ok(profiles.clone())
} }
@ -51,9 +32,10 @@ pub async fn import_profile(
with_proxy: bool, with_proxy: bool,
profiles_state: State<'_, ProfilesState>, profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> { ) -> 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(); 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 /// new a profile
@ -65,59 +47,50 @@ pub async fn new_profile(
desc: String, desc: String,
profiles_state: State<'_, ProfilesState>, profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> { ) -> Result<(), String> {
let item = wrap_err!(PrfItem::from_local(name, desc))?;
let mut profiles = profiles_state.0.lock().unwrap(); 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 /// Update the profile
#[tauri::command] #[tauri::command]
pub async fn update_profile( pub async fn update_profile(
index: usize, index: String,
with_proxy: bool, with_proxy: bool,
clash_state: State<'_, ClashState>, clash_state: State<'_, ClashState>,
profiles_state: State<'_, ProfilesState>, profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> { ) -> Result<(), String> {
// maybe we can get the url from the web app directly let url = {
let url = match profiles_state.0.lock() { // must release the lock here
Ok(mut profile) => { let profiles = profiles_state.0.lock().unwrap();
let items = profile.items.take().unwrap_or(vec![]); let item = wrap_err!(profiles.get_item(&index))?;
if index >= items.len() {
ret_err!("the index out of bound"); if item.url.is_none() {
} ret_err!("failed to get the item url");
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
} }
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() { let mut profiles = profiles_state.0.lock().unwrap();
Ok(mut profiles) => { wrap_err!(profiles.update_item(index.clone(), item))?;
wrap_err!(profiles.update_item(index, result))?;
// reactivate the profile // reactivate the profile
let current = profiles.current.clone().unwrap_or(0); if Some(index) == profiles.get_current() {
if current == index { let clash = clash_state.0.lock().unwrap();
let clash = clash_state.0.lock().unwrap(); wrap_err!(clash.activate(&profiles))?;
wrap_err!(profiles.activate(&clash))
} else {
Ok(())
}
}
Err(_) => ret_err!("failed to get profiles lock"),
} }
Ok(())
} }
/// change the current profile /// change the current profile
#[tauri::command] #[tauri::command]
pub fn select_profile( pub fn select_profile(
index: usize, index: String,
clash_state: State<'_, ClashState>, clash_state: State<'_, ClashState>,
profiles_state: State<'_, ProfilesState>, profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> { ) -> Result<(), String> {
@ -125,13 +98,13 @@ pub fn select_profile(
wrap_err!(profiles.put_current(index))?; wrap_err!(profiles.put_current(index))?;
let clash = clash_state.0.lock().unwrap(); let clash = clash_state.0.lock().unwrap();
wrap_err!(profiles.activate(&clash)) wrap_err!(clash.activate(&profiles))
} }
/// delete profile item /// delete profile item
#[tauri::command] #[tauri::command]
pub fn delete_profile( pub fn delete_profile(
index: usize, index: String,
clash_state: State<'_, ClashState>, clash_state: State<'_, ClashState>,
profiles_state: State<'_, ProfilesState>, profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> { ) -> Result<(), String> {
@ -139,7 +112,7 @@ pub fn delete_profile(
if wrap_err!(profiles.delete_item(index))? { if wrap_err!(profiles.delete_item(index))? {
let clash = clash_state.0.lock().unwrap(); let clash = clash_state.0.lock().unwrap();
wrap_err!(profiles.activate(&clash))?; wrap_err!(clash.activate(&profiles))?;
} }
Ok(()) Ok(())
@ -148,8 +121,8 @@ pub fn delete_profile(
/// patch the profile config /// patch the profile config
#[tauri::command] #[tauri::command]
pub fn patch_profile( pub fn patch_profile(
index: usize, index: String,
profile: ProfileItem, profile: PrfItem,
profiles_state: State<'_, ProfilesState>, profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> { ) -> Result<(), String> {
let mut profiles = profiles_state.0.lock().unwrap(); let mut profiles = profiles_state.0.lock().unwrap();
@ -158,19 +131,16 @@ pub fn patch_profile(
/// run vscode command to edit the profile /// run vscode command to edit the profile
#[tauri::command] #[tauri::command]
pub fn view_profile(index: usize, profiles_state: State<'_, ProfilesState>) -> Result<(), String> { pub fn view_profile(index: String, profiles_state: State<'_, ProfilesState>) -> Result<(), String> {
let mut profiles = profiles_state.0.lock().unwrap(); let profiles = profiles_state.0.lock().unwrap();
let items = profiles.items.take().unwrap_or(vec![]); let item = wrap_err!(profiles.get_item(&index))?;
if index >= items.len() { let file = item.file.clone();
profiles.items = Some(items); if file.is_none() {
ret_err!("the index out of bound"); ret_err!("the file is null");
} }
let file = items[index].file.clone().unwrap_or("".into()); let path = dirs::app_profiles_dir().join(file.unwrap());
profiles.items = Some(items);
let path = dirs::app_profiles_dir().join(file);
if !path.exists() { if !path.exists() {
ret_err!("the file not found"); ret_err!("the file not found");
} }
@ -285,7 +255,7 @@ pub fn patch_verge_config(
wrap_err!(clash.tun_mode(tun_mode.unwrap()))?; wrap_err!(clash.tun_mode(tun_mode.unwrap()))?;
clash.update_config(); clash.update_config();
wrap_err!(profiles.activate(&clash))?; wrap_err!(clash.activate(&profiles))?;
} }
Ok(()) Ok(())

View File

@ -1,6 +1,9 @@
use std::collections::HashMap;
use super::{Profiles, Verge}; use super::{Profiles, Verge};
use crate::utils::{config, dirs}; use crate::utils::{config, dirs};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use reqwest::header::HeaderMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use tauri::api::process::{Command, CommandChild, CommandEvent}; use tauri::api::process::{Command, CommandChild, CommandEvent};
@ -153,7 +156,7 @@ impl Clash {
self.update_config(); self.update_config();
self.drop_sidecar()?; self.drop_sidecar()?;
self.run_sidecar()?; self.run_sidecar()?;
profiles.activate(&self) self.activate(profiles)
} }
/// update the clash info /// update the clash info
@ -191,11 +194,7 @@ impl Clash {
verge.init_sysproxy(port); verge.init_sysproxy(port);
} }
if self.config.contains_key(key) { self.config.insert(key.clone(), value);
self.config[key] = value;
} else {
self.config.insert(key.clone(), value);
}
} }
self.save_config() self.save_config()
} }
@ -241,6 +240,54 @@ impl Clash {
self.save_config() 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 { impl Default for Clash {

View File

@ -1,5 +1,4 @@
mod clash; mod clash;
mod prfitem;
mod profiles; mod profiles;
mod verge; mod verge;

View File

@ -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}\"");
}
}

View File

@ -1,27 +1,18 @@
use super::{Clash, ClashInfo}; use crate::utils::{config, dirs, help, tmpl};
use crate::utils::{config, dirs, tmpl}; use anyhow::{bail, Context, Result};
use anyhow::{bail, Result};
use reqwest::header::HeaderMap;
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, io::Write};
use std::fs::{remove_file, File};
use std::io::Write;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
/// Define the `profiles.yaml` schema #[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Default, Debug, Clone, Deserialize, Serialize)] pub struct PrfItem {
pub struct Profiles { pub uid: Option<String>,
/// current profile's name
pub current: Option<usize>,
/// profile list /// profile item type
pub items: Option<Vec<ProfileItem>>, /// 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 Profiles {
pub struct ProfileResponse { /// same as PrfConfig.current
pub name: String, current: Option<String>,
pub file: String,
pub data: String, /// same as PrfConfig.chain
pub extra: Option<ProfileExtra>, 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()); let data = Self::read_file();
if data.current.is_none() { if data.current.is_none() && data.items.is_none() {
bail!("failed to read profiles.yaml") bail!("failed to read profiles.yaml");
} else {
self.current = data.current;
self.items = data.items;
Ok(())
} }
self.current = data.current;
self.chain = data.chain;
self.items = data.items;
Ok(())
} }
/// import the new profile from the url /// get the current uid
/// and update the config file pub fn get_current(&self) -> Option<String> {
pub fn import_from_url(&mut self, url: String, result: ProfileResponse) -> Result<()> { self.current.clone()
// 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),
});
self.items = Some(items);
if data.current.is_none() {
self.current = Some(0);
}
self.save_file()
} }
/// set the current and save to file /// only change the main to the target id
pub fn put_current(&mut self, index: usize) -> Result<()> { pub fn put_current(&mut self, uid: String) -> Result<()> {
let items = self.items.take().unwrap_or(vec![]); if self.items.is_none() {
self.items = Some(vec![]);
if index >= items.len() {
bail!("the index out of bound");
} }
self.items = Some(items); let items = self.items.as_ref().unwrap();
self.current = Some(index); let some_uid = Some(uid.clone());
self.save_file()
for each in items.iter() {
if each.uid == some_uid {
self.current = some_uid;
return self.save_file();
}
}
bail!("invalid uid \"{uid}\"");
}
/// 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());
for each in items.iter() {
if each.uid == some_uid {
return Ok(each);
}
}
}
bail!("failed to get the item by \"{}\"", uid);
} }
/// append new item /// append new item
/// return the new item's index /// if the file_data is some
pub fn append_item(&mut self, name: String, desc: String) -> Result<(usize, PathBuf)> { /// 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");
}
// 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");
}
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 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![]);
// create a new profile file for mut each in items.iter_mut() {
let now = SystemTime::now() if each.uid == Some(uid.clone()) {
.duration_since(UNIX_EPOCH) patch!(each, item, itype);
.unwrap() patch!(each, item, name);
.as_secs(); patch!(each, item, desc);
let file = format!("{}.yaml", now); patch!(each, item, file);
let path = dirs::app_profiles_dir().join(&file); patch!(each, item, url);
patch!(each, item, selected);
patch!(each, item, extra);
match File::create(&path).unwrap().write(tmpl::ITEM_CONFIG) { each.updated = Some(help::get_now());
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),
});
let index = items.len();
self.items = Some(items); self.items = Some(items);
Ok((index, path)) return self.save_file();
}
Err(_) => bail!("failed to create 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<()> {
let mut items = self.items.take().unwrap_or(vec![]);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as usize;
// 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();
items[index].name = Some(result.name);
items[index].extra = result.extra;
items[index].updated = Some(now);
self.items = Some(items);
self.save_file()
}
/// 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");
}
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;
}
self.items = Some(items);
self.save_file()
}
/// 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![]);
if index >= items.len() {
bail!("index out of range");
}
let mut rm_item = items.remove(index);
// delete the file
if let Some(file) = rm_item.file.take() {
let file_path = dirs::app_profiles_dir().join(file);
if file_path.exists() {
if let Err(err) = remove_file(file_path) {
log::error!("{err}");
}
} }
} }
let mut should_change = false;
if current == index {
current = 0;
should_change = true;
} else if current > index {
current = current - 1;
}
self.current = Some(current);
self.items = Some(items); self.items = Some(items);
bail!("failed to found the uid \"{uid}\"")
match self.save_file() {
Ok(_) => Ok(should_change),
Err(err) => Err(err),
}
} }
/// activate current profile /// be used to update the remote item
pub fn activate(&self, clash: &Clash) -> Result<()> { /// only patch `updated` `extra` `file_data`
let current = self.current.unwrap_or(0); pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
match self.items.clone() { if self.items.is_none() {
Some(items) => { self.items = Some(vec![]);
if current >= items.len() { }
bail!("the index out of bound");
}
let profile = items[current].clone(); // find the item
let clash_config = clash.config.clone(); let _ = self.get_item(&uid)?;
let clash_info = clash.info.clone();
tauri::async_runtime::spawn(async move { self.items.as_mut().map(|items| {
let mut count = 5; // retry times let some_uid = Some(uid.clone());
let mut err = None;
while count > 0 { for mut each in items.iter_mut() {
match activate_profile(&profile, &clash_config, &clash_info).await { if each.uid == some_uid {
Ok(_) => return, patch!(each, item, extra);
Err(e) => err = Some(e), patch!(each, item, updated);
}
count -= 1; // 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)));
// the file must exists
each.file = Some(file.clone());
let path = dirs::app_profiles_dir().join(&file);
fs::File::create(path)
.unwrap()
.write(file_data.as_bytes())
.unwrap();
} }
log::error!("failed to activate for `{}`", err.unwrap());
});
Ok(()) break;
} }
None => bail!("empty profiles"),
}
}
}
/// 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"),
};
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()
);
}
// 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);
} }
}); });
config::save_yaml( self.save_file()
temp_path.clone(),
&new_config,
Some("# Clash Verge Temp File"),
)?
};
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(),
);
} }
let mut data = HashMap::new(); /// delete item
data.insert("path", temp_path.as_os_str().to_str().unwrap()); /// 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();
let client = reqwest::ClientBuilder::new().no_proxy().build()?; let mut items = self.items.take().unwrap_or(vec![]);
let mut index = None;
client // get the index
.put(server) for i in 0..items.len() {
.headers(headers) if items[i].uid == Some(uid.clone()) {
.json(&data) index = Some(i);
.send() break;
.await?; }
Ok(()) }
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> {
let config = Mapping::new();
if self.current.is_none() || self.items.is_none() {
return Ok(config);
}
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}\"");
}
} }

View File

@ -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);
}

View File

@ -3,7 +3,7 @@ use chrono::Local;
use log::LevelFilter; use log::LevelFilter;
use log4rs::append::console::ConsoleAppender; use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender; use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Config, Root}; use log4rs::config::{Appender, Config, Logger, Root};
use log4rs::encode::pattern::PatternEncoder; use log4rs::encode::pattern::PatternEncoder;
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
@ -28,11 +28,13 @@ fn init_log(log_dir: &PathBuf) {
let config = Config::builder() let config = Config::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout))) .appender(Appender::builder().build("stdout", Box::new(stdout)))
.appender(Appender::builder().build("file", Box::new(tofile))) .appender(Appender::builder().build("file", Box::new(tofile)))
.build( .logger(
Root::builder() Logger::builder()
.appenders(["stdout", "file"]) .appender("file")
.build(LevelFilter::Debug), .additive(false)
.build("app", LevelFilter::Info),
) )
.build(Root::builder().appender("stdout").build(LevelFilter::Info))
.unwrap(); .unwrap();
log4rs::init_config(config).unwrap(); log4rs::init_config(config).unwrap();

View File

@ -1,6 +1,7 @@
pub mod config; pub mod config;
pub mod dirs; pub mod dirs;
pub mod fetch; // pub mod fetch;
pub mod help;
pub mod init; pub mod init;
pub mod resolve; pub mod resolve;
pub mod server; pub mod server;

View File

@ -1,5 +1,5 @@
use super::{init, server}; use super::{init, server};
use crate::{core::Profiles, states}; use crate::{core::Profiles, log_if_err, states};
use tauri::{App, AppHandle, Manager}; use tauri::{App, AppHandle, Manager};
/// handle something when start app /// 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 verge = verge_state.0.lock().unwrap();
let mut profiles = profiles_state.0.lock().unwrap(); let mut profiles = profiles_state.0.lock().unwrap();
if let Err(err) = clash.run_sidecar() { log_if_err!(clash.run_sidecar());
log::error!("{err}");
}
*profiles = Profiles::read_file(); *profiles = Profiles::read_file();
if let Err(err) = profiles.activate(&clash) { log_if_err!(clash.activate(&profiles));
log::error!("{err}");
}
verge.init_sysproxy(clash.info.port.clone()); verge.init_sysproxy(clash.info.port.clone());
// enable tun mode // enable tun mode
@ -41,9 +37,7 @@ pub fn resolve_setup(app: &App) {
} }
verge.init_launch(); verge.init_launch();
if let Err(err) = verge.sync_launch() { log_if_err!(verge.sync_launch());
log::error!("{err}");
}
} }
/// reset system proxy /// reset system proxy

View File

@ -14,7 +14,7 @@ secret: ""
/// template for `profiles.yaml` /// template for `profiles.yaml`
pub const PROFILES_CONFIG: &[u8] = b"# Profiles Config for Clash Verge pub const PROFILES_CONFIG: &[u8] = b"# Profiles Config for Clash Verge
current: 0 current: ~
items: ~ items: ~
"; ";
@ -32,7 +32,7 @@ system_proxy_bypass: localhost;127.*;10.*;192.168.*;<local>
"; ";
/// template for new a profile item /// 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 defination (optional, the same as clash)
proxies:\n proxies:\n
# proxy-groups (optional, the same as clash) # proxy-groups (optional, the same as clash)

View File

@ -39,14 +39,14 @@ const round = keyframes`
`; `;
interface Props { interface Props {
index: number; // index: number;
selected: boolean; selected: boolean;
itemData: CmdType.ProfileItem; itemData: CmdType.ProfileItem;
onSelect: (force: boolean) => void; onSelect: (force: boolean) => void;
} }
const ProfileItem: React.FC<Props> = (props) => { const ProfileItem: React.FC<Props> = (props) => {
const { index, selected, itemData, onSelect } = props; const { selected, itemData, onSelect } = props;
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -69,7 +69,7 @@ const ProfileItem: React.FC<Props> = (props) => {
const onView = async () => { const onView = async () => {
setAnchorEl(null); setAnchorEl(null);
try { try {
await viewProfile(index); await viewProfile(itemData.uid);
} catch (err: any) { } catch (err: any) {
Notice.error(err.toString()); Notice.error(err.toString());
} }
@ -85,7 +85,7 @@ const ProfileItem: React.FC<Props> = (props) => {
if (loading) return; if (loading) return;
setLoading(true); setLoading(true);
try { try {
await updateProfile(index, withProxy); await updateProfile(itemData.uid, withProxy);
mutate("getProfiles"); mutate("getProfiles");
} catch (err: any) { } catch (err: any) {
Notice.error(err.toString()); Notice.error(err.toString());
@ -98,7 +98,7 @@ const ProfileItem: React.FC<Props> = (props) => {
setAnchorEl(null); setAnchorEl(null);
try { try {
await deleteProfile(index); await deleteProfile(itemData.uid);
mutate("getProfiles"); mutate("getProfiles");
} catch (err: any) { } catch (err: any) {
Notice.error(err.toString()); Notice.error(err.toString());

View File

@ -1,5 +1,5 @@
import useSWR, { useSWRConfig } from "swr";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useSWRConfig } from "swr";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
import { import {
@ -46,6 +46,8 @@ const ProxyGroup = ({ group }: Props) => {
const virtuosoRef = useRef<any>(); const virtuosoRef = useRef<any>();
const filterProxies = useFilterProxy(proxies, group.name, filterText); const filterProxies = useFilterProxy(proxies, group.name, filterText);
const { data: profiles } = useSWR("getProfiles", getProfiles);
const onChangeProxy = useLockFn(async (name: string) => { const onChangeProxy = useLockFn(async (name: string) => {
// Todo: support another proxy group type // Todo: support another proxy group type
if (group.type !== "Selector") return; if (group.type !== "Selector") return;
@ -60,8 +62,7 @@ const ProxyGroup = ({ group }: Props) => {
} }
try { try {
const profiles = await getProfiles(); const profile = profiles?.items?.find((p) => p.uid === profiles.current);
const profile = profiles.items![profiles.current!]!;
if (!profile) return; if (!profile) return;
if (!profile.selected) profile.selected = []; if (!profile.selected) profile.selected = [];
@ -74,7 +75,7 @@ const ProxyGroup = ({ group }: Props) => {
} else { } else {
profile.selected[index] = { name: group.name, now: name }; profile.selected[index] = { name: group.name, now: name };
} }
await patchProfile(profiles.current!, profile); await patchProfile(profiles!.current!, profile);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }

View File

@ -10,7 +10,6 @@ import {
newProfile, newProfile,
} from "../services/cmds"; } from "../services/cmds";
import { getProxies, updateProxy } from "../services/api"; import { getProxies, updateProxy } from "../services/api";
import noop from "../utils/noop";
import Notice from "../components/base/base-notice"; import Notice from "../components/base/base-notice";
import BasePage from "../components/base/base-page"; import BasePage from "../components/base/base-page";
import ProfileItem from "../components/profile/profile-item"; import ProfileItem from "../components/profile/profile-item";
@ -28,7 +27,7 @@ const ProfilePage = () => {
if (!profiles.items) profiles.items = []; if (!profiles.items) profiles.items = [];
const current = profiles.current; const current = profiles.current;
const profile = profiles.items![current]; const profile = profiles.items.find((p) => p.uid === current);
if (!profile) return; if (!profile) return;
setTimeout(async () => { setTimeout(async () => {
@ -72,9 +71,17 @@ const ProfilePage = () => {
try { try {
await importProfile(url); await importProfile(url);
mutate("getProfiles", getProfiles());
if (!profiles.items?.length) selectProfile(0).catch(noop);
Notice.success("Successfully import profile."); 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 { } catch {
Notice.error("Failed to import profile."); Notice.error("Failed to import profile.");
} finally { } finally {
@ -82,12 +89,12 @@ const ProfilePage = () => {
} }
}; };
const onSelect = useLockFn(async (index: number, force: boolean) => { const onSelect = useLockFn(async (current: string, force: boolean) => {
if (!force && index === profiles.current) return; if (!force && current === profiles.current) return;
try { try {
await selectProfile(index); await selectProfile(current);
mutate("getProfiles", { ...profiles, current: index }, true); mutate("getProfiles", { ...profiles, current: current }, true);
} catch (err: any) { } catch (err: any) {
err && Notice.error(err.toString()); err && Notice.error(err.toString());
} }
@ -131,13 +138,12 @@ const ProfilePage = () => {
</Box> </Box>
<Grid container spacing={3}> <Grid container spacing={3}>
{profiles?.items?.map((item, idx) => ( {profiles?.items?.map((item) => (
<Grid item xs={12} sm={6} key={item.file}> <Grid item xs={12} sm={6} key={item.file}>
<ProfileItem <ProfileItem
index={idx} selected={profiles.current === item.uid}
selected={profiles.current === idx}
itemData={item} itemData={item}
onSelect={(f) => onSelect(idx, f)} onSelect={(f) => onSelect(item.uid, f)}
/> />
</Grid> </Grid>
))} ))}

View File

@ -13,7 +13,7 @@ export async function newProfile(name: string, desc: string) {
return invoke<void>("new_profile", { name, desc }); 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 }); return invoke<void>("view_profile", { index });
} }
@ -21,22 +21,22 @@ export async function importProfile(url: string) {
return invoke<void>("import_profile", { url, withProxy: true }); 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 }); 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 }); return invoke<void>("delete_profile", { index });
} }
export async function patchProfile( export async function patchProfile(
index: number, index: string,
profile: CmdType.ProfileItem profile: CmdType.ProfileItem
) { ) {
return invoke<void>("patch_profile", { index, profile }); 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 }); return invoke<void>("select_profile", { index });
} }

View File

@ -86,6 +86,8 @@ export namespace CmdType {
} }
export interface ProfileItem { export interface ProfileItem {
uid: string;
type?: string;
name?: string; name?: string;
desc?: string; desc?: string;
file?: string; file?: string;
@ -105,7 +107,8 @@ export namespace CmdType {
} }
export interface ProfilesConfig { export interface ProfilesConfig {
current?: number; current?: string;
chain?: string[];
items?: ProfileItem[]; items?: ProfileItem[];
} }