Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
808b861dd1 | ||
|
|
17f1c487a8 | ||
|
|
8dc2c1a38f | ||
|
|
220a494692 | ||
|
|
db6bc10196 | ||
|
|
1880363aeb | ||
|
|
19c7b59883 | ||
|
|
749df89229 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "0.0.19",
|
"version": "0.0.20",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
|
|||||||
10
src-tauri/Cargo.lock
generated
10
src-tauri/Cargo.lock
generated
@@ -461,6 +461,7 @@ dependencies = [
|
|||||||
"dunce",
|
"dunce",
|
||||||
"log",
|
"log",
|
||||||
"log4rs",
|
"log4rs",
|
||||||
|
"nanoid",
|
||||||
"port_scanner",
|
"port_scanner",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2052,6 +2053,15 @@ dependencies = [
|
|||||||
"twoway",
|
"twoway",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nanoid"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
|
||||||
|
dependencies = [
|
||||||
|
"rand 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.8"
|
version = "0.2.8"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ tauri-build = { version = "1.0.0-rc.3", features = [] }
|
|||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
dirs = "4.0.0"
|
dirs = "4.0.0"
|
||||||
dunce = "1.0.2"
|
dunce = "1.0.2"
|
||||||
|
nanoid = "0.4.0"
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.8"
|
serde_yaml = "0.8"
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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};
|
||||||
@@ -125,8 +128,8 @@ impl Clash {
|
|||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
while let Some(event) = rx.recv().await {
|
while let Some(event) = rx.recv().await {
|
||||||
match event {
|
match event {
|
||||||
CommandEvent::Stdout(line) => log::info!("[stdout]: {}", line),
|
CommandEvent::Stdout(line) => log::info!("[clash]: {}", line),
|
||||||
CommandEvent::Stderr(err) => log::error!("[stderr]: {}", err),
|
CommandEvent::Stderr(err) => log::error!("[clash]: {}", err),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
mod clash;
|
mod clash;
|
||||||
mod prfitem;
|
|
||||||
mod profiles;
|
mod profiles;
|
||||||
mod verge;
|
mod verge;
|
||||||
|
|
||||||
|
|||||||
@@ -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}\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,168 @@ 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())
|
let mut profiles = config::read_yaml::<Self>(dirs::profiles_path());
|
||||||
|
|
||||||
|
if profiles.items.is_none() {
|
||||||
|
profiles.items = Some(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
profiles.items.as_mut().map(|items| {
|
||||||
|
for mut item in items.iter_mut() {
|
||||||
|
if item.uid.is_none() {
|
||||||
|
item.uid = Some(help::get_uid("d"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
profiles
|
||||||
}
|
}
|
||||||
|
|
||||||
/// save the config to the file
|
/// save the config to the file
|
||||||
@@ -92,303 +198,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,
|
each.extra = item.extra;
|
||||||
Err(e) => err = Some(e),
|
each.updated = 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}\"");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
core::Clash,
|
core::Clash,
|
||||||
|
log_if_err,
|
||||||
utils::{config, dirs, sysopt::SysProxyConfig},
|
utils::{config, dirs, sysopt::SysProxyConfig},
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
@@ -58,13 +59,18 @@ impl VergeConfig {
|
|||||||
/// Verge App abilities
|
/// Verge App abilities
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Verge {
|
pub struct Verge {
|
||||||
|
/// manage the verge config
|
||||||
pub config: VergeConfig,
|
pub config: VergeConfig,
|
||||||
|
|
||||||
pub old_sysproxy: Option<SysProxyConfig>,
|
/// current system proxy setting
|
||||||
|
|
||||||
pub cur_sysproxy: Option<SysProxyConfig>,
|
pub cur_sysproxy: Option<SysProxyConfig>,
|
||||||
|
|
||||||
pub auto_launch: Option<AutoLaunch>,
|
/// record the original system proxy
|
||||||
|
/// recover it when exit
|
||||||
|
old_sysproxy: Option<SysProxyConfig>,
|
||||||
|
|
||||||
|
/// helps to auto launch the app
|
||||||
|
auto_launch: Option<AutoLaunch>,
|
||||||
|
|
||||||
/// record whether the guard async is running or not
|
/// record whether the guard async is running or not
|
||||||
guard_state: Arc<Mutex<bool>>,
|
guard_state: Arc<Mutex<bool>>,
|
||||||
@@ -282,7 +288,7 @@ impl Verge {
|
|||||||
loop {
|
loop {
|
||||||
sleep(Duration::from_secs(wait_secs)).await;
|
sleep(Duration::from_secs(wait_secs)).await;
|
||||||
|
|
||||||
log::debug!("[Guard]: heartbeat detection");
|
log::debug!("guard heartbeat detection");
|
||||||
|
|
||||||
let verge = Verge::new();
|
let verge = Verge::new();
|
||||||
|
|
||||||
@@ -298,7 +304,7 @@ impl Verge {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("[Guard]: try to guard proxy");
|
log::info!("try to guard proxy");
|
||||||
|
|
||||||
let clash = Clash::new();
|
let clash = Clash::new();
|
||||||
|
|
||||||
@@ -307,12 +313,9 @@ impl Verge {
|
|||||||
let bypass = verge.config.system_proxy_bypass.clone();
|
let bypass = verge.config.system_proxy_bypass.clone();
|
||||||
let sysproxy = SysProxyConfig::new(true, port.clone(), bypass);
|
let sysproxy = SysProxyConfig::new(true, port.clone(), bypass);
|
||||||
|
|
||||||
if let Err(err) = sysproxy.set_sys() {
|
log_if_err!(sysproxy.set_sys());
|
||||||
log::error!("[Guard]: {err}");
|
|
||||||
log::error!("[Guard]: fail to set system proxy");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None => log::error!("[Guard]: fail to parse clash port"),
|
None => log::error!("fail to parse clash port"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
use crate::core::{ProfileExtra, ProfileResponse};
|
|
||||||
use std::{
|
|
||||||
str::FromStr,
|
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// parse the string
|
|
||||||
fn parse_string<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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// fetch and parse the profile url
|
|
||||||
/// maybe it contains some Subscription infomations, maybe not
|
|
||||||
pub async fn fetch_profile(url: &str, with_proxy: bool) -> Result<ProfileResponse, String> {
|
|
||||||
let builder = reqwest::ClientBuilder::new();
|
|
||||||
let client = match with_proxy {
|
|
||||||
true => builder.build(),
|
|
||||||
false => builder.no_proxy().build(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let resp = match client.unwrap().get(url).send().await {
|
|
||||||
Ok(res) => res,
|
|
||||||
Err(_) => return Err("failed to create https client".into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
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(ProfileExtra {
|
|
||||||
upload: parse_string(sub_info, "upload=").unwrap_or(0),
|
|
||||||
download: parse_string(sub_info, "download=").unwrap_or(0),
|
|
||||||
total: parse_string(sub_info, "total=").unwrap_or(0),
|
|
||||||
expire: parse_string(sub_info, "expire=").unwrap_or(0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let file = {
|
|
||||||
let now = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs();
|
|
||||||
format!("{}.yaml", now)
|
|
||||||
};
|
|
||||||
|
|
||||||
let name = match header.get("Content-Disposition") {
|
|
||||||
Some(name) => {
|
|
||||||
let name = name.to_str().unwrap();
|
|
||||||
parse_string::<String>(name, "filename=").unwrap_or(file.clone())
|
|
||||||
}
|
|
||||||
None => file.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// get the data
|
|
||||||
match resp.text_with_charset("utf-8").await {
|
|
||||||
Ok(data) => Ok(ProfileResponse {
|
|
||||||
file,
|
|
||||||
name,
|
|
||||||
data,
|
|
||||||
extra,
|
|
||||||
}),
|
|
||||||
Err(_) => Err("failed to parse the response data".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_string::<usize>(test_1, "upload=").unwrap(), 111);
|
|
||||||
assert_eq!(parse_string::<usize>(test_1, "download=").unwrap(), 2222);
|
|
||||||
assert_eq!(parse_string::<usize>(test_1, "total=").unwrap(), 3333);
|
|
||||||
assert_eq!(parse_string::<usize>(test_1, "expire=").unwrap(), 444);
|
|
||||||
assert_eq!(
|
|
||||||
parse_string::<String>(test_2, "filename=").unwrap(),
|
|
||||||
format!("Clash.yaml")
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(parse_string::<usize>(test_1, "aaa="), None);
|
|
||||||
assert_eq!(parse_string::<usize>(test_1, "upload1="), None);
|
|
||||||
assert_eq!(parse_string::<usize>(test_1, "expire1="), None);
|
|
||||||
assert_eq!(parse_string::<usize>(test_2, "attachment="), None);
|
|
||||||
}
|
|
||||||
94
src-tauri/src/utils/help.rs
Normal file
94
src-tauri/src/utils/help.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
use nanoid::nanoid;
|
||||||
|
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 _
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALPHABET: [char; 62] = [
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
|
||||||
|
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
|
||||||
|
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
|
||||||
|
'V', 'W', 'X', 'Y', 'Z',
|
||||||
|
];
|
||||||
|
|
||||||
|
/// generate the uid
|
||||||
|
pub fn get_uid(prefix: &str) -> String {
|
||||||
|
let id = nanoid!(11, &ALPHABET);
|
||||||
|
format!("{prefix}{id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod dirs;
|
pub mod dirs;
|
||||||
pub mod fetch;
|
pub mod help;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod resolve;
|
pub mod resolve;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "clash-verge",
|
"productName": "clash-verge",
|
||||||
"version": "0.0.19"
|
"version": "0.0.20"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"distDir": "../dist",
|
"distDir": "../dist",
|
||||||
|
|||||||
93
src/components/profile/profile-edit.tsx
Normal file
93
src/components/profile/profile-edit.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useLockFn } from "ahooks";
|
||||||
|
import { mutate } from "swr";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { CmdType } from "../../services/types";
|
||||||
|
import { patchProfile } from "../../services/cmds";
|
||||||
|
import Notice from "../base/base-notice";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
itemData: CmdType.ProfileItem;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// edit the profile item
|
||||||
|
const ProfileEdit = (props: Props) => {
|
||||||
|
const { open, itemData, onClose } = props;
|
||||||
|
|
||||||
|
// todo: more type
|
||||||
|
const [name, setName] = useState(itemData.name);
|
||||||
|
const [desc, setDesc] = useState(itemData.desc);
|
||||||
|
const [url, setUrl] = useState(itemData.url);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (itemData) {
|
||||||
|
setName(itemData.name);
|
||||||
|
setDesc(itemData.desc);
|
||||||
|
setUrl(itemData.url);
|
||||||
|
}
|
||||||
|
}, [itemData]);
|
||||||
|
|
||||||
|
const onUpdate = useLockFn(async () => {
|
||||||
|
try {
|
||||||
|
const { uid } = itemData;
|
||||||
|
await patchProfile(uid, { uid, name, desc, url });
|
||||||
|
mutate("getProfiles");
|
||||||
|
onClose();
|
||||||
|
} catch (err: any) {
|
||||||
|
Notice.error(err?.message || err?.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose}>
|
||||||
|
<DialogTitle>Edit Profile</DialogTitle>
|
||||||
|
<DialogContent sx={{ width: 360, pb: 0.5 }}>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
fullWidth
|
||||||
|
label="Name"
|
||||||
|
margin="dense"
|
||||||
|
variant="outlined"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Descriptions"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
value={desc}
|
||||||
|
onChange={(e) => setDesc(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Remote URL"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ px: 2, pb: 2 }}>
|
||||||
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
|
|
||||||
|
<Button onClick={onUpdate} variant="contained">
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileEdit;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import {
|
import {
|
||||||
alpha,
|
alpha,
|
||||||
@@ -16,9 +16,10 @@ import { useSWRConfig } from "swr";
|
|||||||
import { RefreshRounded } from "@mui/icons-material";
|
import { RefreshRounded } from "@mui/icons-material";
|
||||||
import { CmdType } from "../../services/types";
|
import { CmdType } from "../../services/types";
|
||||||
import { updateProfile, deleteProfile, viewProfile } from "../../services/cmds";
|
import { updateProfile, deleteProfile, viewProfile } from "../../services/cmds";
|
||||||
import Notice from "../base/base-notice";
|
|
||||||
import parseTraffic from "../../utils/parse-traffic";
|
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
import parseTraffic from "../../utils/parse-traffic";
|
||||||
|
import ProfileEdit from "./profile-edit";
|
||||||
|
import Notice from "../base/base-notice";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
@@ -38,18 +39,20 @@ const round = keyframes`
|
|||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// save the state of each item loading
|
||||||
|
const loadingCache: Record<string, boolean> = {};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
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(loadingCache[itemData.uid] ?? false);
|
||||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||||
const [position, setPosition] = useState({ left: 0, top: 0 });
|
const [position, setPosition] = useState({ left: 0, top: 0 });
|
||||||
|
|
||||||
@@ -66,10 +69,20 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
const hasUrl = !!itemData.url;
|
const hasUrl = !!itemData.url;
|
||||||
const hasExtra = !!extra; // only subscription url has extra info
|
const hasExtra = !!extra; // only subscription url has extra info
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadingCache[itemData.uid] = loading;
|
||||||
|
}, [itemData, loading]);
|
||||||
|
|
||||||
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
|
const onEdit = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
setEditOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
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,12 +98,12 @@ 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);
|
||||||
|
setLoading(false);
|
||||||
mutate("getProfiles");
|
mutate("getProfiles");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err.toString());
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
Notice.error(err?.message || err.toString());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,10 +111,10 @@ 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?.message || err.toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,6 +136,7 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const urlModeMenu = [
|
const urlModeMenu = [
|
||||||
{ label: "Select", handler: onForceSelect },
|
{ label: "Select", handler: onForceSelect },
|
||||||
|
{ label: "Edit", handler: onEdit },
|
||||||
{ label: "View", handler: onView },
|
{ label: "View", handler: onView },
|
||||||
{ label: "Update", handler: onUpdateWrapper(false) },
|
{ label: "Update", handler: onUpdateWrapper(false) },
|
||||||
{ label: "Update(Proxy)", handler: onUpdateWrapper(true) },
|
{ label: "Update(Proxy)", handler: onUpdateWrapper(true) },
|
||||||
@@ -130,7 +144,8 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
];
|
];
|
||||||
const fileModeMenu = [
|
const fileModeMenu = [
|
||||||
{ label: "Select", handler: onForceSelect },
|
{ label: "Select", handler: onForceSelect },
|
||||||
{ label: "Edit", handler: onView },
|
{ label: "Edit", handler: onEdit },
|
||||||
|
{ label: "View", handler: onView },
|
||||||
{ label: "Delete", handler: onDelete },
|
{ label: "Delete", handler: onDelete },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -261,6 +276,12 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
|
<ProfileEdit
|
||||||
|
open={editOpen}
|
||||||
|
itemData={itemData}
|
||||||
|
onClose={() => setEditOpen(false)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,10 +86,11 @@ 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;
|
||||||
mode?: string;
|
|
||||||
url?: string;
|
url?: string;
|
||||||
updated?: number;
|
updated?: number;
|
||||||
selected?: {
|
selected?: {
|
||||||
@@ -105,7 +106,8 @@ export namespace CmdType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ProfilesConfig {
|
export interface ProfilesConfig {
|
||||||
current?: number;
|
current?: string;
|
||||||
|
chain?: string[];
|
||||||
items?: ProfileItem[];
|
items?: ProfileItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user