diff --git a/src-tauri/src/cmds/profile.rs b/src-tauri/src/cmds/profile.rs index 39f28b0..576805c 100644 --- a/src-tauri/src/cmds/profile.rs +++ b/src-tauri/src/cmds/profile.rs @@ -1,60 +1,42 @@ use crate::{ config::{ProfileItem, ProfilesConfig}, - events::state::{ClashInfoState, ProfileLock}, - utils::{ - app_home_dir, - clash::put_clash_profile, - config::{read_profiles, save_profiles}, - fetch::fetch_profile, - }, + events::state::{ClashInfoState, ProfilesState}, + utils::{clash, fetch}, }; -use std::fs::File; -use std::io::Write; -use std::time::{SystemTime, UNIX_EPOCH}; use tauri::State; +/// get all profiles from `profiles.yaml` +/// do not acquire the lock of ProfileLock +#[tauri::command] +pub fn get_profiles(profiles: State<'_, ProfilesState>) -> Result { + match profiles.0.lock() { + Ok(profiles) => Ok(profiles.clone()), + Err(_) => Err("can not get profiles lock".into()), + } +} + +/// synchronize data irregularly +#[tauri::command] +pub fn sync_profiles(profiles: State<'_, ProfilesState>) -> Result<(), String> { + match profiles.0.lock() { + Ok(mut profiles) => profiles.sync_file(), + Err(_) => Err("can not get profiles lock".into()), + } +} + /// Import the profile from url /// and save to `profiles.yaml` #[tauri::command] -pub async fn import_profile(url: String, lock: State<'_, ProfileLock>) -> Result<(), String> { - let result = match fetch_profile(&url).await { +pub async fn import_profile(url: String, profiles: State<'_, ProfilesState>) -> Result<(), String> { + let result = match fetch::fetch_profile(&url).await { Some(r) => r, - None => { - log::error!("failed to fetch profile from `{}`", url); - return Err(format!("failed to fetch profile from `{}`", url)); - } + None => return Err(format!("failed to fetch profile from `{}`", url)), }; - // get lock - if lock.0.lock().is_err() { - return Err(format!("can not get file lock")); + match profiles.0.lock() { + Ok(mut profiles) => profiles.import_from_url(url, result), + Err(_) => Err("can not get profiles lock".into()), } - - // save the profile file - let path = app_home_dir().join("profiles").join(&result.file); - let file_data = result.data.as_bytes(); - File::create(path).unwrap().write(file_data).unwrap(); - - // update `profiles.yaml` - let mut profiles = read_profiles(); - let mut items = profiles.items.unwrap_or(vec![]); - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - items.push(ProfileItem { - name: Some(result.name), - file: Some(result.file), - mode: Some(format!("rule")), - url: Some(url), - selected: Some(vec![]), - extra: Some(result.extra), - updated: Some(now as usize), - }); - profiles.items = Some(items); - save_profiles(&profiles) } /// Update the profile @@ -62,129 +44,78 @@ pub async fn import_profile(url: String, lock: State<'_, ProfileLock>) -> Result /// http request firstly /// then acquire the lock of `profiles.yaml` #[tauri::command] -pub async fn update_profile(index: usize, lock: State<'_, ProfileLock>) -> Result<(), String> { - // get lock - if lock.0.lock().is_err() { - return Err(format!("can not get file lock")); - } - - // update `profiles.yaml` - let mut profiles = read_profiles(); - let mut items = profiles.items.unwrap_or(vec![]); - - if index >= items.len() { - return Err(format!("the index out of bound")); - } - - let url = match &items[index].url { - Some(u) => u, - None => return Err(format!("invalid url")), - }; - - let result = match fetch_profile(&url).await { - Some(r) => r, - None => { - log::error!("failed to fetch profile from `{}`", url); - return Err(format!("failed to fetch profile from `{}`", url)); +pub async fn update_profile( + index: usize, + profiles: State<'_, ProfilesState>, +) -> Result<(), String> { + // maybe we can get the url from the web app directly + let url = { + match profiles.0.lock() { + Ok(mut profile) => { + let items = profile.items.take().unwrap_or(vec![]); + if index >= items.len() { + return Err("the index out of bound".into()); + } + let url = match &items[index].url { + Some(u) => u.clone(), + None => return Err("failed to update profile for `invalid url`".into()), + }; + profile.items = Some(items); + url + } + Err(_) => return Err("can not get profiles lock".into()), } }; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as usize; + let result = match fetch::fetch_profile(&url).await { + Some(r) => r, + None => return Err(format!("failed to fetch profile from `{}`", url)), + }; - // update file - let file_path = &items[index].file.as_ref().unwrap(); - let file_path = app_home_dir().join("profiles").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 = Some(result.extra); - items[index].updated = Some(now); - profiles.items = Some(items); - save_profiles(&profiles) -} - -/// get all profiles from `profiles.yaml` -/// do not acquire the lock of ProfileLock -#[tauri::command] -pub fn get_profiles() -> Result { - Ok(read_profiles()) -} - -/// patch the profile config -#[tauri::command] -pub fn set_profiles( - index: usize, - profile: ProfileItem, - lock: State<'_, ProfileLock>, -) -> Result<(), String> { - // get lock - if lock.0.lock().is_err() { - return Err(format!("can not get file lock")); + match profiles.0.lock() { + Ok(mut profiles) => profiles.update_item(index, result), + Err(_) => Err("can not get profiles lock".into()), } - - let mut profiles = read_profiles(); - let mut items = profiles.items.unwrap_or(vec![]); - - if index >= items.len() { - return Err(format!("the index out of bound")); - } - - 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; - } - - profiles.items = Some(items); - save_profiles(&profiles) } /// change the current profile #[tauri::command] -pub async fn put_profiles( - current: usize, - lock: State<'_, ProfileLock>, +pub async fn select_profile( + index: usize, + profiles: State<'_, ProfilesState>, clash_info: State<'_, ClashInfoState>, ) -> Result<(), String> { - if lock.0.lock().is_err() { - return Err(format!("can not get file lock")); - } + match profiles.0.lock() { + Ok(mut profiles) => profiles.put_current(index)?, + Err(_) => return Err("can not get profiles lock".into()), + }; - let clash_info = match clash_info.0.lock() { + let arc = match clash_info.0.lock() { Ok(arc) => arc.clone(), - _ => return Err(format!("can not get clash info")), + _ => return Err("can not get clash info lock".into()), }; - let mut profiles = read_profiles(); - let items_len = match &profiles.items { - Some(list) => list.len(), - None => 0, - }; + clash::put_clash_profile(&arc).await +} - if current >= items_len { - return Err(format!("the index out of bound")); - } - - profiles.current = Some(current); - match save_profiles(&profiles) { - Ok(_) => put_clash_profile(&clash_info).await, - Err(err) => Err(err), +/// delete profile item +#[tauri::command] +pub fn delete_profile(index: usize, profiles: State<'_, ProfilesState>) -> Result<(), String> { + match profiles.0.lock() { + Ok(mut profiles) => profiles.delete_item(index), + Err(_) => Err("can not get profiles lock".into()), + } +} + +/// patch the profile config +#[tauri::command] +pub fn patch_profile( + index: usize, + profile: ProfileItem, + profiles: State<'_, ProfilesState>, +) -> Result<(), String> { + match profiles.0.lock() { + Ok(mut profiles) => profiles.patch_item(index, profile), + Err(_) => Err("can not get profiles lock".into()), } } diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index e83b35a..878590b 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -1,4 +1,8 @@ +use crate::utils::{app_home_dir, config}; use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::Write; +use std::time::{SystemTime, UNIX_EPOCH}; /// Define the `profiles.yaml` schema #[derive(Default, Debug, Clone, Deserialize, Serialize)] @@ -50,3 +54,156 @@ pub struct ProfileResponse { pub data: String, pub extra: ProfileExtra, } + +static PROFILE_YAML: &str = "profiles.yaml"; + +impl ProfilesConfig { + /// read the config from the file + pub fn read_file() -> Self { + config::read_yaml::(app_home_dir().join(PROFILE_YAML)) + } + + /// save the config to the file + pub fn save_file(&self) -> Result<(), String> { + config::save_yaml( + app_home_dir().join(PROFILE_YAML), + self, + Some("# Profiles Config for Clash Verge\n\n"), + ) + } + + /// sync the config between file and memory + pub fn sync_file(&mut self) -> Result<(), String> { + let data = config::read_yaml::(app_home_dir().join(PROFILE_YAML)); + if data.current.is_none() { + Err("failed to read profiles.yaml".into()) + } else { + self.current = data.current; + 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<(), String> { + // save the profile file + let path = app_home_dir().join("profiles").join(&result.file); + let file_data = result.data.as_bytes(); + File::create(path).unwrap().write(file_data).unwrap(); + + // update `profiles.yaml` + let data = ProfilesConfig::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), + file: Some(result.file), + mode: Some(format!("rule")), + url: Some(url), + selected: Some(vec![]), + extra: Some(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 + pub fn put_current(&mut self, index: usize) -> Result<(), String> { + let items = self.items.take().unwrap_or(vec![]); + + if index >= items.len() { + return Err("the index out of bound".into()); + } + + self.items = Some(items); + self.current = Some(index); + 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<(), String> { + 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 = app_home_dir().join("profiles").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 = Some(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<(), String> { + let mut items = self.items.take().unwrap_or(vec![]); + if index >= items.len() { + return Err("index out of bound".into()); + } + + 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<(), String> { + let mut current = self.current.clone().unwrap_or(0); + let mut items = self.items.clone().unwrap_or(vec![]); + + if index >= items.len() { + return Err("index out of bound".into()); + } + + items.remove(index); + + if current == index { + current = 0; + } else if current > index { + current = current - 1; + } + self.current = Some(current); + self.save_file() + } +} diff --git a/src-tauri/src/events/state.rs b/src-tauri/src/events/state.rs index 01b1b30..cfda63b 100644 --- a/src-tauri/src/events/state.rs +++ b/src-tauri/src/events/state.rs @@ -1,5 +1,8 @@ use super::emit::ClashInfoPayload; -use crate::{config::VergeConfig, utils::sysopt::SysProxyConfig}; +use crate::{ + config::{ProfilesConfig, VergeConfig}, + utils::sysopt::SysProxyConfig, +}; use std::sync::{Arc, Mutex}; use tauri::api::process::CommandChild; @@ -7,7 +10,7 @@ use tauri::api::process::CommandChild; pub struct ClashInfoState(pub Arc>); #[derive(Default)] -pub struct ProfileLock(pub Mutex); +pub struct ProfilesState(pub Arc>); #[derive(Default)] pub struct VergeConfLock(pub Arc>); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c425403..debbe79 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -35,7 +35,7 @@ fn main() -> std::io::Result<()> { .manage(state::VergeConfLock::default()) .manage(state::ClashInfoState::default()) .manage(state::SomthingState::default()) - .manage(state::ProfileLock::default()) + .manage(state::ProfilesState::default()) .setup(|app| Ok(resolve::resolve_setup(app))) .system_tray(SystemTray::new().with_menu(menu)) .on_system_tray_event(move |app_handle, event| match event { @@ -87,9 +87,11 @@ fn main() -> std::io::Result<()> { cmds::some::patch_verge_config, cmds::profile::import_profile, cmds::profile::update_profile, + cmds::profile::delete_profile, + cmds::profile::select_profile, + cmds::profile::patch_profile, + cmds::profile::sync_profiles, cmds::profile::get_profiles, - cmds::profile::set_profiles, - cmds::profile::put_profiles, ]) .build(tauri::generate_context!()) .expect("error while running tauri application") diff --git a/src-tauri/src/utils/config.rs b/src-tauri/src/utils/config.rs index 194344a..78b8030 100644 --- a/src-tauri/src/utils/config.rs +++ b/src-tauri/src/utils/config.rs @@ -1,5 +1,5 @@ use crate::{ - config::{ClashController, ProfilesConfig, VergeConfig}, + config::{ClashController, VergeConfig}, utils::app_home_dir, }; use serde::{de::DeserializeOwned, Serialize}; @@ -103,20 +103,6 @@ pub fn read_clash_controller() -> ClashController { } } -/// Get Profiles Config -pub fn read_profiles() -> ProfilesConfig { - read_yaml::(app_home_dir().join("profiles.yaml")) -} - -/// Save Verge App Config -pub fn save_profiles(profiles: &ProfilesConfig) -> Result<(), String> { - save_yaml( - app_home_dir().join("profiles.yaml"), - profiles, - Some("# Profiles Config for Clash Verge\n\n"), - ) -} - /// Get the `verge.yaml` pub fn read_verge() -> VergeConfig { read_yaml::(app_home_dir().join("verge.yaml")) diff --git a/src/components/proxy-group.tsx b/src/components/proxy-group.tsx index b9f1fa2..090539a 100644 --- a/src/components/proxy-group.tsx +++ b/src/components/proxy-group.tsx @@ -22,8 +22,7 @@ import { } from "@mui/icons-material"; import { updateProxy } from "../services/api"; import { ApiType } from "../services/types"; -import { getProfiles, setProfiles } from "../services/cmds"; -import noop from "../utils/noop"; +import { getProfiles, patchProfile } from "../services/cmds"; interface ItemProps { proxy: ApiType.ProxyItem; @@ -105,7 +104,7 @@ const ProxyGroup = ({ group }: Props) => { profile.selected[index] = { name: group.name, now: name }; } - setProfiles(profiles.current!, profile).catch(console.error); + patchProfile(profiles.current!, profile).catch(console.error); } catch { setNow(oldValue); // Todo diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index 11f1544..e374509 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -3,8 +3,8 @@ import useSWR, { useSWRConfig } from "swr"; import { Box, Button, Grid, TextField, Typography } from "@mui/material"; import { getProfiles, - putProfiles, - setProfiles, + selectProfile, + patchProfile, importProfile, } from "../services/cmds"; import { getProxies, updateProxy } from "../services/api"; @@ -52,7 +52,7 @@ const ProfilePage = () => { name, now, })); - setProfiles(profiles.current!, profile).catch(console.error); + patchProfile(profiles.current!, profile).catch(console.error); // update proxies cache if (hasChange) mutate("getProxies", getProxies()); }); @@ -66,7 +66,7 @@ const ProfilePage = () => { try { await importProfile(url); mutate("getProfiles", getProfiles()); - if (!profiles.items?.length) putProfiles(0).catch(noop); + if (!profiles.items?.length) selectProfile(0).catch(noop); notice.success("Successfully import profile."); } catch { notice.error("Failed to import profile."); @@ -80,7 +80,7 @@ const ProfilePage = () => { if (index === profiles.current || lockRef.current) return; if (lockRef.current) return; lockRef.current = true; - putProfiles(index) + selectProfile(index) .then(() => { mutate("getProfiles", { ...profiles, current: index }, true); }) diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 8100092..132cfb2 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -1,6 +1,37 @@ import { invoke } from "@tauri-apps/api/tauri"; import { ApiType, CmdType } from "./types"; +export async function getProfiles() { + return (await invoke("get_profiles")) ?? {}; +} + +export async function syncProfiles() { + return invoke("sync_profiles"); +} + +export async function importProfile(url: string) { + return invoke("import_profile", { url }); +} + +export async function updateProfile(index: number) { + return invoke("update_profile", { index }); +} + +export async function deleteProfile(index: number) { + return invoke("delete_profile", { index }); +} + +export async function patchProfile( + index: number, + profile: CmdType.ProfileItem +) { + return invoke("patch_profile", { index, profile }); +} + +export async function selectProfile(index: number) { + return invoke("select_profile", { index }); +} + export async function restartSidecar() { return invoke("restart_sidecar"); } @@ -13,26 +44,6 @@ export async function patchClashConfig(payload: Partial) { return invoke("patch_clash_config", { payload }); } -export async function importProfile(url: string) { - return invoke("import_profile", { url }); -} - -export async function updateProfile(index: number) { - return invoke("update_profile", { index }); -} - -export async function getProfiles() { - return (await invoke("get_profiles")) ?? {}; -} - -export async function setProfiles(index: number, profile: CmdType.ProfileItem) { - return invoke("set_profiles", { index, profile }); -} - -export async function putProfiles(current: number) { - return invoke("put_profiles", { current }); -} - export async function setSysProxy(enable: boolean) { return invoke("set_sys_proxy", { enable }); }