chore: update

This commit is contained in:
huzibaca 2024-11-08 21:46:15 +08:00
parent 20d163cf3a
commit c3e24d7b96
No known key found for this signature in database
GPG Key ID: D4364EE4851DC302
20 changed files with 887 additions and 4 deletions

247
src-tauri/Cargo.lock generated
View File

@ -23,6 +23,17 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.11" version = "0.8.11"
@ -841,6 +852,27 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bzip2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.18.5" version = "0.18.5"
@ -973,6 +1005,16 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]] [[package]]
name = "clash-verge" name = "clash-verge"
version = "2.0.0" version = "2.0.0"
@ -995,6 +1037,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"port_scanner", "port_scanner",
"reqwest", "reqwest",
"reqwest_dav",
"runas", "runas",
"serde", "serde",
"serde_json", "serde_json",
@ -1020,6 +1063,7 @@ dependencies = [
"warp", "warp",
"window-shadows", "window-shadows",
"winreg 0.52.0", "winreg 0.52.0",
"zip",
] ]
[[package]] [[package]]
@ -1156,6 +1200,12 @@ dependencies = [
"tiny-keccak", "tiny-keccak",
] ]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@ -1245,6 +1295,21 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.4.2" version = "1.4.2"
@ -1420,6 +1485,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "deflate64"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
[[package]] [[package]]
name = "delay_timer" name = "delay_timer"
version = "0.11.6" version = "0.11.6"
@ -1555,6 +1626,20 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer 0.10.4", "block-buffer 0.10.4",
"crypto-common", "crypto-common",
"subtle",
]
[[package]]
name = "digest_auth"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3054f4e81d395e50822796c5e99ca522e6ba7be98947d6d4b0e5e61640bdb894"
dependencies = [
"digest 0.10.7",
"hex",
"md-5",
"rand 0.8.5",
"sha2 0.10.8",
] ]
[[package]] [[package]]
@ -2622,6 +2707,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest 0.10.7",
]
[[package]] [[package]]
name = "home" name = "home"
version = "0.5.9" version = "0.5.9"
@ -3077,6 +3171,15 @@ dependencies = [
"cfb", "cfb",
] ]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.13" version = "0.1.13"
@ -3461,6 +3564,12 @@ dependencies = [
"scopeguard", "scopeguard",
] ]
[[package]]
name = "lockfree-object-pool"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.22" version = "0.4.22"
@ -3522,6 +3631,16 @@ dependencies = [
"hashbrown 0.15.0", "hashbrown 0.15.0",
] ]
[[package]]
name = "lzma-rs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
dependencies = [
"byteorder",
"crc",
]
[[package]] [[package]]
name = "mac" name = "mac"
version = "0.1.1" version = "0.1.1"
@ -3594,6 +3713,16 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest 0.10.7",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@ -4459,6 +4588,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest 0.10.7",
"hmac",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -5294,6 +5433,26 @@ dependencies = [
"windows-registry 0.2.0", "windows-registry 0.2.0",
] ]
[[package]]
name = "reqwest_dav"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea79cbb695b7cc877ae9c0f0eeb8468e36cd03dc9c41a93bcf237396357c7b42"
dependencies = [
"async-trait",
"chrono",
"digest_auth",
"http 1.1.0",
"httpdate",
"reqwest",
"serde",
"serde-xml-rs",
"serde_derive",
"serde_json",
"tokio",
"url",
]
[[package]] [[package]]
name = "rfd" name = "rfd"
version = "0.15.0" version = "0.15.0"
@ -5664,6 +5823,18 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde-xml-rs"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782"
dependencies = [
"log",
"serde",
"thiserror",
"xml-rs",
]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.213" version = "1.0.213"
@ -8527,6 +8698,12 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "xml-rs"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.7.4" version = "0.7.4"
@ -8663,6 +8840,20 @@ name = "zeroize"
version = "1.8.1" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.85",
]
[[package]] [[package]]
name = "zerovec" name = "zerovec"
@ -8692,13 +8883,69 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494"
dependencies = [ dependencies = [
"aes",
"arbitrary", "arbitrary",
"bzip2",
"constant_time_eq",
"crc32fast", "crc32fast",
"crossbeam-utils", "crossbeam-utils",
"deflate64",
"displaydoc", "displaydoc",
"flate2",
"hmac",
"indexmap 2.6.0", "indexmap 2.6.0",
"lzma-rs",
"memchr", "memchr",
"pbkdf2",
"rand 0.8.5",
"sha1",
"thiserror", "thiserror",
"time",
"zeroize",
"zopfli",
"zstd",
]
[[package]]
name = "zopfli"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
dependencies = [
"bumpalo",
"crc32fast",
"lockfree-object-pool",
"log",
"once_cell",
"simd-adler32",
]
[[package]]
name = "zstd"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.13+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
dependencies = [
"cc",
"pkg-config",
] ]
[[package]] [[package]]

View File

@ -54,6 +54,8 @@ tauri-plugin-clipboard-manager = "2.0.1"
tauri-plugin-deep-link = "2.0.1" tauri-plugin-deep-link = "2.0.1"
tauri-plugin-devtools = "2.0.0-rc" tauri-plugin-devtools = "2.0.0-rc"
url = "2.5.2" url = "2.5.2"
zip = "2.2.0"
reqwest_dav = "0.1.14"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
runas = "=1.2.0" runas = "=1.2.0"
deelevate = "0.2.0" deelevate = "0.2.0"

View File

@ -11,6 +11,7 @@ use serde_yaml::Mapping;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use sysproxy::{Autoproxy, Sysproxy}; use sysproxy::{Autoproxy, Sysproxy};
type CmdResult<T = ()> = Result<T, String>; type CmdResult<T = ()> = Result<T, String>;
use reqwest_dav::list_cmd::ListFile;
use tauri::Manager; use tauri::Manager;
#[tauri::command] #[tauri::command]
@ -375,6 +376,37 @@ pub async fn exit_app() {
feat::quit(Some(0)); feat::quit(Some(0));
} }
#[tauri::command]
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
let patch = IVerge {
webdav_url: Some(url),
webdav_username: Some(username),
webdav_password: Some(password),
..IVerge::default()
};
Config::verge().draft().patch_config(patch.clone());
Config::verge().apply();
Config::verge()
.data()
.save_file()
.map_err(|err| err.to_string())?;
backup::WebDavClient::global().reset();
Ok(())
}
#[tauri::command]
pub async fn create_webdav_backup() -> CmdResult<()> {
feat::create_backup_and_upload_webdav()
.await
.map_err(|err| err.to_string())?;
Ok(())
}
#[tauri::command]
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
feat::list_wevdav_backup().await.map_err(|e| e.to_string())
}
pub mod service { pub mod service {
use super::*; use super::*;
use crate::core::service; use crate::core::service;

View File

@ -147,6 +147,10 @@ pub struct IVerge {
pub verge_port: Option<u16>, pub verge_port: Option<u16>,
pub verge_http_enabled: Option<bool>, pub verge_http_enabled: Option<bool>,
pub webdav_url: Option<String>,
pub webdav_username: Option<String>,
pub webdav_password: Option<String>,
} }
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
@ -304,6 +308,10 @@ impl IVerge {
patch!(proxy_layout_column); patch!(proxy_layout_column);
patch!(test_list); patch!(test_list);
patch!(auto_log_clean); patch!(auto_log_clean);
patch!(webdav_url);
patch!(webdav_username);
patch!(webdav_password);
} }
/// 在初始化前尝试拿到单例端口的值 /// 在初始化前尝试拿到单例端口的值

View File

@ -0,0 +1,118 @@
use crate::config::Config;
use crate::utils::dirs;
use anyhow::Error;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use reqwest_dav::list_cmd::{ListEntity, ListFile};
use std::env::{consts::OS, temp_dir};
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use zip::write::SimpleFileOptions;
pub struct WebDavClient {
client: Arc<Mutex<Option<reqwest_dav::Client>>>,
}
impl WebDavClient {
pub fn global() -> &'static WebDavClient {
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
client: Arc::new(Mutex::new(None)),
})
}
async fn get_client(&self) -> Result<reqwest_dav::Client, Error> {
if self.client.lock().is_none() {
let verge = Config::verge().latest().clone();
if verge.webdav_url.is_none()
|| verge.webdav_username.is_none()
|| verge.webdav_password.is_none()
{
let msg =
"Unable to create web dav client, please make sure the webdav config is correct"
.to_string();
log::error!(target: "app","{}",msg);
return Err(anyhow::Error::msg(msg));
}
let url = verge.webdav_url.unwrap_or_default();
let username = verge.webdav_username.unwrap_or_default();
let password = verge.webdav_password.unwrap_or_default();
let client = reqwest_dav::ClientBuilder::new()
.set_host(url.to_owned())
.set_auth(reqwest_dav::Auth::Basic(
username.to_owned(),
password.to_owned(),
))
.build()?;
*self.client.lock() = Some(client.clone());
}
Ok(self.client.lock().clone().unwrap())
}
pub fn reset(&self) {
if !self.client.lock().is_none() {
self.client.lock().take();
}
}
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
let client = self.get_client().await?;
if client.get(dirs::BACKUP_DIR).await.is_err() {
client.mkcol(dirs::BACKUP_DIR).await?;
}
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name);
client
.put(webdav_path.as_ref(), fs::read(file_path)?)
.await?;
Ok(())
}
pub async fn list_files(&self) -> Result<Vec<ListFile>, Error> {
let client = self.get_client().await?;
let files = client
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(1))
.await?;
let mut final_files = Vec::new();
for file in files {
if let ListEntity::File(file) = file {
final_files.push(file);
}
}
Ok(final_files)
}
}
pub fn create_backup() -> Result<(String, PathBuf), Error> {
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
let zip_file_name = format!("{}-backup-{}.zip", OS, now);
let zip_path = temp_dir().join(&zip_file_name);
let file = fs::File::create(&zip_path)?;
let mut zip = zip::ZipWriter::new(file);
zip.add_directory("profiles/", SimpleFileOptions::default())?;
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) {
for entry in entries {
let entry = entry.unwrap();
let path = entry.path();
if path.is_file() {
let backup_path = format!("profiles/{}", entry.file_name().to_str().unwrap());
zip.start_file(backup_path, options)?;
zip.write_all(fs::read(path).unwrap().as_slice())?;
}
}
}
zip.start_file(dirs::CLASH_CONFIG, options)?;
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
zip.start_file(dirs::VERGE_CONFIG, options)?;
zip.write_all(fs::read(dirs::verge_path()?)?.as_slice())?;
zip.start_file(dirs::PROFILE_YAML, options)?;
zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
zip.finish()?;
Ok((zip_file_name, zip_path))
}

View File

@ -1,3 +1,4 @@
pub mod backup;
pub mod clash_api; pub mod clash_api;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
mod core; mod core;

View File

@ -9,6 +9,7 @@ use crate::core::*;
use crate::log_err; use crate::log_err;
use crate::utils::resolve; use crate::utils::resolve;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use reqwest_dav::list_cmd::ListFile;
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use tauri_plugin_clipboard_manager::ClipboardExt; use tauri_plugin_clipboard_manager::ClipboardExt;
@ -401,3 +402,42 @@ pub async fn test_delay(url: String) -> Result<u32> {
} }
} }
} }
pub async fn create_backup_and_upload_webdav() -> Result<()> {
if let Err(err) = async {
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
log::error!(target: "app", "Failed to create backup: {:#?}", err);
err
})?;
backup::WebDavClient::global()
.upload(temp_file_path.clone(), file_name)
.await
.map_err(|err| {
log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err);
err
})?;
std::fs::remove_file(&temp_file_path).map_err(|err| {
log::warn!(target: "app", "Failed to remove temp file: {:#?}", err);
err
})?;
Ok(())
}
.await
{
return Err(err);
}
Ok(())
}
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
backup::WebDavClient::global()
.list_files()
.await
.map_err(|err| {
log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err);
err
})
}

View File

@ -123,7 +123,11 @@ pub fn run() {
// service mode // service mode
cmds::service::check_service, cmds::service::check_service,
// clash api // clash api
cmds::clash_api_get_proxy_delay cmds::clash_api_get_proxy_delay,
// backup
cmds::create_webdav_backup,
cmds::save_webdav_config,
cmds::list_webdav_backup,
]); ]);
#[cfg(debug_assertions)] #[cfg(debug_assertions)]

View File

@ -6,14 +6,19 @@ use tauri::Manager;
#[cfg(not(feature = "verge-dev"))] #[cfg(not(feature = "verge-dev"))]
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev"; pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev";
#[cfg(not(feature = "verge-dev"))]
pub static BACKUP_DIR: &str = "clash-verge-rev-backup";
#[cfg(feature = "verge-dev")] #[cfg(feature = "verge-dev")]
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev"; pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev";
#[cfg(feature = "verge-dev")]
pub static BACKUP_DIR: &str = "clash-verge-rev-backup-dev";
pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new(); pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
static CLASH_CONFIG: &str = "config.yaml"; pub static CLASH_CONFIG: &str = "config.yaml";
static VERGE_CONFIG: &str = "verge.yaml"; pub static VERGE_CONFIG: &str = "verge.yaml";
static PROFILE_YAML: &str = "profiles.yaml"; pub static PROFILE_YAML: &str = "profiles.yaml";
/// init portable flag /// init portable flag
pub fn init_portable_flag() -> Result<()> { pub fn init_portable_flag() -> Result<()> {

View File

@ -0,0 +1,33 @@
import React from "react";
import { Box, CircularProgress } from "@mui/material";
export interface BaseLoadingOverlayProps {
isLoading: boolean;
}
export const BaseLoadingOverlay: React.FC<BaseLoadingOverlayProps> = ({
isLoading,
}) => {
if (!isLoading) return null;
return (
<Box
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(255, 255, 255, 0.7)",
zIndex: 1000,
}}
>
<CircularProgress />
</Box>
);
};
export default BaseLoadingOverlay;

View File

@ -5,3 +5,4 @@ export { BaseLoading } from "./base-loading";
export { BaseErrorBoundary } from "./base-error-boundary"; export { BaseErrorBoundary } from "./base-error-boundary";
export { Notice } from "./base-notice"; export { Notice } from "./base-notice";
export { Switch } from "./base-switch"; export { Switch } from "./base-switch";
export { BaseLoadingOverlay } from "./base-loading-overlay";

View File

@ -0,0 +1,330 @@
import { forwardRef, useImperativeHandle, useState, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { Typography } from "@mui/material";
import { useForm } from "react-hook-form";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { isValidUrl } from "@/utils/helper";
import { BaseLoadingOverlay } from "@/components/base";
import {
TextField,
Button,
Grid,
Box,
Paper,
Stack,
IconButton,
InputAdornment,
Divider,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import DeleteIcon from "@mui/icons-material/Delete";
import RestoreIcon from "@mui/icons-material/Restore";
import { createWebdavBackup, saveWebdavConfig } from "@/services/cmds";
import { save } from "@tauri-apps/plugin-dialog";
export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const { verge, mutateVerge } = useVerge();
const { webdav_url, webdav_username, webdav_password } = verge || {};
const [showPassword, setShowPassword] = useState(false);
const usernameRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const urlRef = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false);
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
defaultValues: {
url: webdav_url,
username: webdav_username,
password: webdav_password,
},
});
const url = watch("url");
const username = watch("username");
const password = watch("password");
const webdavChanged =
webdav_url !== url ||
webdav_username !== username ||
webdav_password !== password;
// const backups = [] as any[];
const backups = [
{ name: "backup1.zip" },
{ name: "backup2.zip" },
{ name: "backup3.zip" },
];
useImperativeHandle(ref, () => ({
open: () => {
setOpen(true);
},
close: () => setOpen(false),
}));
const checkForm = () => {
const username = usernameRef.current?.value;
const password = passwordRef.current?.value;
const url = urlRef.current?.value;
if (!url) {
Notice.error(t("Webdav url cannot be empty"));
urlRef.current?.focus();
return;
} else if (!isValidUrl(url)) {
Notice.error(t("Webdav address must be url"));
urlRef.current?.focus();
return;
}
if (!username) {
Notice.error(t("Username cannot be empty"));
usernameRef.current?.focus();
return;
}
if (!password) {
Notice.error(t("Password cannot be empty"));
passwordRef.current?.focus();
return;
}
};
const submit = async (data: IWebDavConfig) => {
checkForm();
setIsLoading(true);
await saveWebdavConfig(data.url, data.username, data.password)
.then(() => {
mutateVerge(
{
webdav_url: data.url,
webdav_username: data.username,
webdav_password: data.password,
},
false
);
Notice.success(t("Webdav Config Saved Successfully"), 1500);
})
.catch((e) => {
Notice.error(t("Webdav Config Save Failed", { error: e }), 3000);
})
.finally(() => {
setIsLoading(false);
});
};
const handleClickShowPassword = () => {
setShowPassword(!showPassword);
};
const handleBackup = useLockFn(async () => {
checkForm();
setIsLoading(true);
await createWebdavBackup()
.then(() => {
Notice.success(t("Backup Successfully"), 1500);
})
.finally(() => {
setIsLoading(false);
})
.catch((e) => {
console.log(e, "backup failed");
Notice.error(t("Backup Failed", { error: e }), 3000);
});
});
return (
<BaseDialog
open={open}
title={t("Backup Setting")}
contentSx={{ width: 600, maxHeight: 800 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
disableFooter={true}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
<Box sx={{ maxWidth: 800 }}>
<BaseLoadingOverlay isLoading={isLoading} />
<Paper elevation={2} sx={{ padding: 2 }}>
<form onSubmit={handleSubmit(submit)}>
<Grid container spacing={2}>
<Grid item xs={12} sm={9}>
<Grid container spacing={2}>
{/* WebDAV Server Address */}
<Grid item xs={12}>
<TextField
fullWidth
label="WebDAV Server URL"
variant="outlined"
size="small"
{...register("url")}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={urlRef}
/>
</Grid>
{/* Username and Password */}
<Grid item xs={6}>
<TextField
label="Username"
variant="outlined"
size="small"
{...register("username")}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={usernameRef}
/>
</Grid>
<Grid item xs={6}>
<TextField
label="Password"
type={showPassword ? "text" : "password"}
variant="outlined"
size="small"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={passwordRef}
{...register("password")}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowPassword}
edge="end"
>
{showPassword ? (
<VisibilityOff />
) : (
<Visibility />
)}
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
</Grid>
<Grid item xs={12} sm={3}>
<Stack
direction="column"
justifyContent="center"
alignItems="stretch"
sx={{ height: "100%" }}
>
{webdavChanged ||
webdav_url === null ||
webdav_username == null ||
webdav_password == null ? (
<Button
variant="contained"
color="primary"
sx={{ height: "100%" }}
type="submit"
>
Save
</Button>
) : (
<Button
variant="contained"
color="success"
sx={{ height: "100%" }}
onClick={handleBackup}
type="button"
>
Backup
</Button>
)}
</Stack>
</Grid>
</Grid>
</form>
<Divider sx={{ marginY: 2 }} />
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell></TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{backups.length > 0 ? (
backups?.map((backup, index) => (
<TableRow key={index}>
<TableCell component="th" scope="row">
{backup.name}
</TableCell>
<TableCell align="right">
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<IconButton
color="secondary"
aria-label="delete"
size="small"
>
<DeleteIcon />
</IconButton>
<Divider
orientation="vertical"
flexItem
sx={{ mx: 1, height: 24 }}
/>
<IconButton
color="primary"
aria-label="restore"
size="small"
>
<RestoreIcon />
</IconButton>
</Box>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={2} align="center">
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: 150,
}}
>
<Typography variant="body1" color="textSecondary">
</Typography>
</Box>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</Paper>
</Box>
</BaseDialog>
);
});

View File

@ -23,6 +23,7 @@ import { ThemeViewer } from "./mods/theme-viewer";
import { GuardState } from "./mods/guard-state"; import { GuardState } from "./mods/guard-state";
import { LayoutViewer } from "./mods/layout-viewer"; import { LayoutViewer } from "./mods/layout-viewer";
import { UpdateViewer } from "./mods/update-viewer"; import { UpdateViewer } from "./mods/update-viewer";
import { BackupViewer } from "./mods/backup-viewer";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import { routers } from "@/pages/_routers"; import { routers } from "@/pages/_routers";
import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { TooltipIcon } from "@/components/base/base-tooltip-icon";
@ -52,6 +53,7 @@ const SettingVerge = ({ onError }: Props) => {
const themeRef = useRef<DialogRef>(null); const themeRef = useRef<DialogRef>(null);
const layoutRef = useRef<DialogRef>(null); const layoutRef = useRef<DialogRef>(null);
const updateRef = useRef<DialogRef>(null); const updateRef = useRef<DialogRef>(null);
const backupRef = useRef<DialogRef>(null);
const onChangeData = (patch: Partial<IVergeConfig>) => { const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false); mutateVerge({ ...verge, ...patch }, false);
@ -83,6 +85,7 @@ const SettingVerge = ({ onError }: Props) => {
<MiscViewer ref={miscRef} /> <MiscViewer ref={miscRef} />
<LayoutViewer ref={layoutRef} /> <LayoutViewer ref={layoutRef} />
<UpdateViewer ref={updateRef} /> <UpdateViewer ref={updateRef} />
<BackupViewer ref={backupRef} />
<SettingItem label={t("Language")}> <SettingItem label={t("Language")}>
<GuardState <GuardState
@ -238,6 +241,11 @@ const SettingVerge = ({ onError }: Props) => {
label={t("Hotkey Setting")} label={t("Hotkey Setting")}
/> />
<SettingItem
onClick={() => backupRef.current?.open()}
label={t("Backup Setting")}
/>
<SettingItem <SettingItem
onClick={() => configRef.current?.open()} onClick={() => configRef.current?.open()}
label={t("Runtime Config")} label={t("Runtime Config")}

View File

@ -332,6 +332,7 @@
"clash_mode_direct": "Direct Mode", "clash_mode_direct": "Direct Mode",
"toggle_system_proxy": "Enable/Disable System Proxy", "toggle_system_proxy": "Enable/Disable System Proxy",
"toggle_tun_mode": "Enable/Disable Tun Mode", "toggle_tun_mode": "Enable/Disable Tun Mode",
"Backup Setting": "Backup Setting",
"Runtime Config": "Runtime Config", "Runtime Config": "Runtime Config",
"Open Conf Dir": "Open Conf Dir", "Open Conf Dir": "Open Conf Dir",
"Open Core Dir": "Open Core Dir", "Open Core Dir": "Open Core Dir",

View File

@ -330,6 +330,7 @@
"clash_mode_direct": "حالت مستقیم", "clash_mode_direct": "حالت مستقیم",
"toggle_system_proxy": "فعال/غیرفعال کردن پراکسی سیستم", "toggle_system_proxy": "فعال/غیرفعال کردن پراکسی سیستم",
"toggle_tun_mode": "فعال/غیرفعال کردن حالت Tun", "toggle_tun_mode": "فعال/غیرفعال کردن حالت Tun",
"Backup Setting": "تنظیمات پشتیبان",
"Runtime Config": "پیکربندی زمان اجرا", "Runtime Config": "پیکربندی زمان اجرا",
"Open Conf Dir": "باز کردن پوشه برنامه", "Open Conf Dir": "باز کردن پوشه برنامه",
"Open Core Dir": "باز کردن پوشه هسته", "Open Core Dir": "باز کردن پوشه هسته",

View File

@ -330,6 +330,7 @@
"clash_mode_direct": "Прямой режим", "clash_mode_direct": "Прямой режим",
"toggle_system_proxy": "Включить/Отключить системный прокси", "toggle_system_proxy": "Включить/Отключить системный прокси",
"toggle_tun_mode": "Включить/Отключить режим туннеля", "toggle_tun_mode": "Включить/Отключить режим туннеля",
"Backup Setting": "Настройки резервного копирования",
"Runtime Config": "Используемый конфиг", "Runtime Config": "Используемый конфиг",
"Open Conf Dir": "Открыть папку приложения", "Open Conf Dir": "Открыть папку приложения",
"Open Core Dir": "Открыть папку ядра", "Open Core Dir": "Открыть папку ядра",

View File

@ -332,6 +332,7 @@
"clash_mode_direct": "直连模式", "clash_mode_direct": "直连模式",
"toggle_system_proxy": "打开/关闭系统代理", "toggle_system_proxy": "打开/关闭系统代理",
"toggle_tun_mode": "打开/关闭 Tun 模式", "toggle_tun_mode": "打开/关闭 Tun 模式",
"Backup Setting": "备份设置",
"Runtime Config": "当前配置", "Runtime Config": "当前配置",
"Open Conf Dir": "配置目录", "Open Conf Dir": "配置目录",
"Open Core Dir": "内核目录", "Open Core Dir": "内核目录",

View File

@ -236,3 +236,26 @@ export async function getNetworkInterfaces() {
export async function getNetworkInterfacesInfo() { export async function getNetworkInterfacesInfo() {
return invoke<INetworkInterface[]>("get_network_interfaces_info"); return invoke<INetworkInterface[]>("get_network_interfaces_info");
} }
export async function createWebdavBackup() {
return invoke<void>("create_webdav_backup");
}
export async function saveWebdavConfig(
url: string,
username: string,
password: String
) {
return invoke<void>("save_webdav_config", {
url,
username,
password,
});
}
export async function listWebDavBackup() {
let list: IWebDavFile[] = await invoke<IWebDavFile[]>("list_webdav_backup");
list.map((item) => {
item.filename = item.href.split("/").pop() as string;
});
return list;
}

View File

@ -744,4 +744,22 @@ interface IVergeConfig {
auto_log_clean?: 0 | 1 | 2 | 3; auto_log_clean?: 0 | 1 | 2 | 3;
proxy_layout_column?: number; proxy_layout_column?: number;
test_list?: IVergeTestItem[]; test_list?: IVergeTestItem[];
webdav_url?: string;
webdav_username?: string;
webdav_password?: string;
}
interface IWebDavFile {
filename: string;
href: string;
last_modified: string;
content_length: number;
content_type: string;
tag: string;
}
interface IWebDavConfig {
url: string;
username: string;
password: string;
} }

9
src/utils/helper.ts Normal file
View File

@ -0,0 +1,9 @@
export const isValidUrl = (url: string) => {
try {
new URL(url);
return true;
} catch (e) {
console.log(e);
return false;
}
};