From 17f931963aff892434287c27e532b9b1dc83e202 Mon Sep 17 00:00:00 2001 From: Jonson Petard <41122242+greenhat616@users.noreply.github.com> Date: Wed, 14 Feb 2024 02:03:18 +0800 Subject: [PATCH] feat(backend)!: add tray proxies selector support (#417) * feat(backend): provide a proxies endpoint test * chore: update deps * feat: first impl * chore: update deps * fix: confilict * fix: lint --- backend/Cargo.lock | 154 ++++++++----- backend/tauri/Cargo.toml | 1 + backend/tauri/src/cmds.rs | 36 ++- backend/tauri/src/core/clash/api.rs | 81 ++++++- backend/tauri/src/core/clash/mod.rs | 10 + backend/tauri/src/core/clash/proxies.rs | 265 +++++++++++++++++++++++ backend/tauri/src/core/handle.rs | 6 + backend/tauri/src/core/tasks/utils.rs | 1 + backend/tauri/src/core/tray/mod.rs | 24 +- backend/tauri/src/core/tray/proxies.rs | 140 ++++++++++++ backend/tauri/src/feat.rs | 28 ++- backend/tauri/src/main.rs | 3 + backend/tauri/src/utils/config.rs | 10 + backend/tauri/src/utils/resolve.rs | 31 ++- src/components/proxy/provider-button.tsx | 15 +- src/components/proxy/proxy-groups.tsx | 23 +- src/components/proxy/use-render-list.ts | 15 +- src/hooks/use-profiles.ts | 8 +- src/pages/_layout.tsx | 5 + src/services/api.ts | 2 + src/services/cmds.ts | 13 ++ 21 files changed, 756 insertions(+), 115 deletions(-) create mode 100644 backend/tauri/src/core/clash/proxies.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 093293e052..74815caf2c 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ "getrandom 0.2.12", "once_cell", @@ -41,9 +41,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" +checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" dependencies = [ "cfg-if", "getrandom 0.2.12", @@ -126,13 +126,13 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" +checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" dependencies = [ "concurrent-queue", - "event-listener 4.0.3", - "event-listener-strategy", + "event-listener 5.0.0", + "event-listener-strategy 0.5.0", "futures-core", "pin-project-lite", ] @@ -218,7 +218,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ "event-listener 4.0.3", - "event-listener-strategy", + "event-listener-strategy 0.4.0", "pin-project-lite", ] @@ -343,6 +343,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backon" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a6197b2120bb2185a267f6515038558b019e92b832bb0320e96d66268dcf9" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "pin-project", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -439,7 +451,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.0", "async-lock 3.3.0", "async-task", "fastrand 2.0.1", @@ -488,9 +500,9 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytemuck" -version = "1.14.2" +version = "1.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea31d69bda4949c1c1562c1e6f042a1caefac98cdc8a298260a2ff41c1e2d42b" +checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" [[package]] name = "byteorder" @@ -609,9 +621,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.6" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6100bc57b6209840798d95cb2775684849d332f7bd788db2a8c8caf7ef82a41a" +checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" dependencies = [ "smallvec", "target-lexicon", @@ -666,6 +678,7 @@ dependencies = [ "anyhow", "async-trait", "auto-launch", + "backon", "chrono", "ctrlc", "deelevate", @@ -868,9 +881,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] @@ -1224,9 +1237,9 @@ checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "embed-resource" @@ -1259,9 +1272,9 @@ dependencies = [ [[package]] name = "enumflags2" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939" +checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" dependencies = [ "enumflags2_derive", "serde", @@ -1269,9 +1282,9 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" +checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", @@ -1322,6 +1335,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "event-listener" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72557800024fabbaa2449dd4bf24e37b93702d457a4d4f2b0dd1f0f039f20c1" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "event-listener-strategy" version = "0.4.0" @@ -1332,6 +1356,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "event-listener-strategy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +dependencies = [ + "event-listener 5.0.0", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -1931,7 +1965,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.2", + "indexmap 2.2.3", "slab", "tokio", "tokio-util", @@ -1968,7 +2002,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.7", + "ahash 0.7.8", ] [[package]] @@ -1977,7 +2011,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.7", + "ahash 0.8.8", ] [[package]] @@ -2256,9 +2290,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -2340,7 +2374,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c416c05ba2a10240e022887617af3128fccdbf69713214da0fc81a5690d00df7" dependencies = [ - "ahash 0.8.7", + "ahash 0.8.8", "once_cell", "regex", ] @@ -2421,9 +2455,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] @@ -3065,9 +3099,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] @@ -3218,9 +3252,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.2.2+3.2.1" +version = "300.2.3+3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bbfad0063610ac26ee79f7484739e2b07555a75c42453b89263830b5c8103bc" +checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" dependencies = [ "cc", ] @@ -3640,7 +3674,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" dependencies = [ "base64 0.21.7", - "indexmap 2.2.2", + "indexmap 2.2.3", "line-wrap", "quick-xml 0.31.0", "serde", @@ -4497,16 +4531,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.6.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b0ed1662c5a68664f45b76d18deb0e234aff37207086803165c961eb695e981" +checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" dependencies = [ "base64 0.21.7", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.2", + "indexmap 2.2.3", "serde", + "serde_derive", "serde_json", "serde_with_macros", "time", @@ -4514,9 +4549,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.6.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "568577ff0ef47b879f736cd66740e022f3672788cdf002a05a4e609ea5a6fb15" +checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" dependencies = [ "darling", "proc-macro2", @@ -4542,7 +4577,7 @@ version = "0.9.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adf8a49373e98a4c5f0ceb5d05aa7c648d75f63774981ed95b7c7443bbd50c6e" dependencies = [ - "indexmap 2.2.2", + "indexmap 2.2.3", "itoa 1.0.10", "ryu", "serde", @@ -4956,7 +4991,7 @@ version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" dependencies = [ - "cfg-expr 0.15.6", + "cfg-expr 0.15.7", "heck 0.4.1", "pkg-config", "toml 0.8.10", @@ -5537,7 +5572,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.4", + "toml_edit 0.22.5", ] [[package]] @@ -5555,24 +5590,24 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.2", + "indexmap 2.2.3", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.4" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9ffdf896f8daaabf9b66ba8e77ea1ed5ed0f72821b398aba62352e95062951" +checksum = "99e68c159e8f5ba8a28c4eb7b0c0c190d77bb479047ca713270048145a9ad28a" dependencies = [ - "indexmap 2.2.2", + "indexmap 2.2.3", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.0", ] [[package]] @@ -5752,9 +5787,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unsafe-any-ors" @@ -6600,9 +6635,18 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.39" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5389a154b01683d28c77f8f68f49dea75f0a4da32557a58f68ee51ebba472d29" +checksum = "6b1dbce9e90e5404c5a52ed82b1d13fc8cfbdad85033b6f57546ffd1265f8451" dependencies = [ "memchr", ] diff --git a/backend/tauri/Cargo.toml b/backend/tauri/Cargo.toml index 9af3b4ec7c..4e7cbc1458 100644 --- a/backend/tauri/Cargo.toml +++ b/backend/tauri/Cargo.toml @@ -65,6 +65,7 @@ rocksdb = "0.21" thiserror = { workspace = true } simd-json = "0.13.8" runas = "=1.0.0" # blocked by https://github.com/mitsuhiko/rust-runas/issues/13 +backon = "0.4.1" rust-i18n = "3" [target.'cfg(windows)'.dependencies] diff --git a/backend/tauri/src/cmds.rs b/backend/tauri/src/cmds.rs index 7686121fc0..d075fc2dd9 100644 --- a/backend/tauri/src/cmds.rs +++ b/backend/tauri/src/cmds.rs @@ -176,7 +176,9 @@ pub fn get_runtime_logs() -> CmdResult>> { #[tauri::command] pub async fn patch_clash_config(payload: Mapping) -> CmdResult { - wrap_err!(feat::patch_clash(payload).await) + wrap_err!(feat::patch_clash(payload).await)?; + feat::update_proxies_buff(None); + Ok(()) } #[tauri::command] @@ -324,6 +326,38 @@ pub async fn clash_api_get_proxy_delay( } } +#[tauri::command] +pub async fn get_proxies() -> CmdResult { + use crate::core::clash::proxies::ProxiesGuard; + use crate::core::clash::proxies::ProxiesGuardExt; + match ProxiesGuard::global().update().await { + Ok(_) => { + let proxies = ProxiesGuard::global().read().inner().clone(); + Ok(proxies) + } + Err(err) => Err(err.to_string()), + } +} + +#[tauri::command] +pub async fn select_proxy(group: String, name: String) -> CmdResult<()> { + use crate::core::clash::proxies::ProxiesGuard; + use crate::core::clash::proxies::ProxiesGuardExt; + wrap_err!(ProxiesGuard::global().select_proxy(&group, &name).await)?; + Ok(()) +} + +#[tauri::command] +pub async fn update_proxy_provider(name: String) -> CmdResult<()> { + use crate::core::clash::{ + api, + proxies::{ProxiesGuard, ProxiesGuardExt}, + }; + wrap_err!(api::update_providers_proxies_group(&name).await)?; + wrap_err!(ProxiesGuard::global().update().await)?; + Ok(()) +} + #[cfg(windows)] pub mod uwp { use super::*; diff --git a/backend/tauri/src/core/clash/api.rs b/backend/tauri/src/core/clash/api.rs index f7b0167c18..2ed3e10099 100644 --- a/backend/tauri/src/core/clash/api.rs +++ b/backend/tauri/src/core/clash/api.rs @@ -3,7 +3,10 @@ use anyhow::{bail, Result}; use reqwest::header::HeaderMap; use serde::{Deserialize, Serialize}; use serde_yaml::Mapping; -use std::collections::HashMap; +use std::{ + collections::HashMap, + fmt::{self, Display, Formatter}, +}; /// PUT /configs /// path 是绝对路径 @@ -43,14 +46,14 @@ pub struct ProxiesRes { pub proxies: Option>, } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, Default)] #[serde(rename_all = "camelCase")] pub struct ProxyItemHistory { pub time: String, pub delay: i64, } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, Default)] #[serde(rename_all = "camelCase")] pub struct ProxyItem { pub name: String, @@ -60,13 +63,52 @@ pub struct ProxyItem { pub all: Option>, pub now: Option, // 当前选中的代理 pub provider: Option, - pub alive: Option, // Mihomo Or Premium Only - pub xudp: Option, // Mihomo Only - pub tfo: Option, // Mihomo Only + pub alive: Option, // Mihomo Or Premium Only + #[serde(skip_serializing_if = "Option::is_none")] + pub xudp: Option, // Mihomo Only + #[serde(skip_serializing_if = "Option::is_none")] + pub tfo: Option, // Mihomo Only + #[serde(skip_serializing_if = "Option::is_none")] pub icon: Option, // Mihomo Only #[serde(default)] pub hidden: bool, // Mihomo Only - // extra: {}, // Mihomo Only + // extra: {}, // Mihomo Only +} + +impl From for ProxyItem { + fn from(item: ProxyProviderItem) -> Self { + let ProxyProviderItem { + name, + r#type, + proxies, + vehicle_type: _, + test_url: _, + expected_status: _, + } = item; + + let now = proxies + .iter() + .find(|p| p.now.is_some()) + .map(|p| p.name.clone()) + .unwrap_or_default(); + + let all = proxies.iter().map(|p| p.name.clone()).collect(); + + Self { + name, + r#type: r#type.to_string(), + udp: false, + history: vec![], + all: Some(all), + now: Some(now), + provider: None, + alive: None, + xudp: None, + tfo: None, + icon: None, + hidden: false, + } + } } /// GET /proxies @@ -87,6 +129,7 @@ pub async fn get_proxies() -> Result { /// name: 代理名称 /// 返回代理的配置 /// +#[allow(dead_code)] pub async fn get_proxy(name: String) -> Result { let (url, headers) = clash_client_info()?; let url = format!("{url}/proxies/{name}"); @@ -102,7 +145,7 @@ pub async fn get_proxy(name: String) -> Result { /// 选择代理 /// group: 代理分组名称 /// name: 代理名称 -pub async fn update_proxy(group: String, name: String) -> Result<()> { +pub async fn update_proxy(group: &str, name: &str) -> Result<()> { let (url, headers) = clash_client_info()?; let url = format!("{url}/proxies/{group}"); @@ -137,6 +180,16 @@ pub enum ProviderType { Unknown, } +impl Display for ProviderType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + ProviderType::Proxy => write!(f, "Proxy"), + ProviderType::Rule => write!(f, "Rule"), + ProviderType::Unknown => write!(f, "Unknown"), + } + } +} + #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProxyProviderItem { @@ -144,7 +197,9 @@ pub struct ProxyProviderItem { pub r#type: ProviderType, pub proxies: Vec, pub vehicle_type: VehicleType, - pub test_url: Option, // Mihomo Only + #[serde(skip_serializing_if = "Option::is_none")] + pub test_url: Option, // Mihomo Only + #[serde(skip_serializing_if = "Option::is_none")] pub expected_status: Option, // Mihomo Only } @@ -156,7 +211,7 @@ pub struct ProvidersProxiesRes { /// GET /providers/proxies /// 获取所有代理集合的所有代理信息 -pub async fn get_providers_proxies() -> Result { +pub async fn get_providers_proxies() -> Result { let (url, headers) = clash_client_info()?; let url = format!("{url}/providers/proxies"); @@ -164,12 +219,13 @@ pub async fn get_providers_proxies() -> Result { let builder = client.get(&url).headers(headers); let response = builder.send().await?; - Ok(response.json::().await?) + Ok(response.json::().await?) } /// GET /providers/proxies/:name /// 获取单个代理集合的所有代理信息 /// group: 代理集合名称 +#[allow(dead_code)] pub async fn get_providers_proxies_group(group: String) -> Result { let (url, headers) = clash_client_info()?; let url = format!("{url}/providers/proxies/{group}"); @@ -184,7 +240,7 @@ pub async fn get_providers_proxies_group(group: String) -> Result Result<()> { +pub async fn update_providers_proxies_group(name: &str) -> Result<()> { let (url, headers) = clash_client_info()?; let url = format!("{url}/providers/proxies/{name}"); @@ -203,6 +259,7 @@ pub async fn update_providers_proxies_group(name: String) -> Result<()> { /// GET /providers/proxies/:name/healthcheck /// 获取代理集合的健康检查 /// name: 代理集合名称 +#[allow(dead_code)] pub async fn get_providers_proxies_healthcheck(name: String) -> Result { let (url, headers) = clash_client_info()?; let url = format!("{url}/providers/proxies/{name}/healthcheck"); diff --git a/backend/tauri/src/core/clash/mod.rs b/backend/tauri/src/core/clash/mod.rs index b3442cde4c..3906797cc0 100644 --- a/backend/tauri/src/core/clash/mod.rs +++ b/backend/tauri/src/core/clash/mod.rs @@ -1,2 +1,12 @@ +use backon::ExponentialBuilder; +use once_cell::sync::Lazy; pub mod api; pub mod core; +pub mod proxies; + +pub static CLASH_API_DEFAULT_BACKOFF_STRATEGY: Lazy = Lazy::new(|| { + ExponentialBuilder::default() + .with_min_delay(std::time::Duration::from_millis(50)) + .with_max_delay(std::time::Duration::from_secs(5)) + .with_max_times(5) +}); diff --git a/backend/tauri/src/core/clash/proxies.rs b/backend/tauri/src/core/clash/proxies.rs new file mode 100644 index 0000000000..a60b8ded5b --- /dev/null +++ b/backend/tauri/src/core/clash/proxies.rs @@ -0,0 +1,265 @@ +/// This module is used to manage the proxies for the Tauri application. +/// It is used to provide the unite interface between tray and frontend. +/// TODO: add a diff algorithm to reduce the data transfer, and the rerendering of the tray menu. +use super::{api, CLASH_API_DEFAULT_BACKOFF_STRATEGY}; +use anyhow::Result; +use backon::Retryable; +use log::warn; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + sync::{Arc, OnceLock}, +}; +use tokio::{sync::broadcast, try_join}; + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ProxyGroupItem { + pub name: String, + pub r#type: String, // TODO: 考虑改成枚举 + pub udp: bool, + pub history: Vec, + pub all: Vec, + pub now: Option, // 当前选中的代理 + pub provider: Option, + pub alive: Option, // Mihomo Or Premium Only + #[serde(skip_serializing_if = "Option::is_none")] + pub xudp: Option, // Mihomo Only + #[serde(skip_serializing_if = "Option::is_none")] + pub tfo: Option, // Mihomo Only + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, // Mihomo Only + #[serde(default)] + pub hidden: bool, // Mihomo Only + // extra: {}, // Mihomo Only +} + +impl From for ProxyGroupItem { + fn from(item: api::ProxyItem) -> Self { + let all = vec![]; + ProxyGroupItem { + name: item.name, + r#type: item.r#type, + udp: item.udp, + history: item.history, + all, + now: item.now, + provider: item.provider, + alive: item.alive, + xudp: item.xudp, + tfo: item.tfo, + icon: item.icon, + hidden: item.hidden, + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct Proxies { + pub global: ProxyGroupItem, + pub direct: api::ProxyItem, + pub groups: Vec, + pub records: HashMap, + pub proxies: Vec, +} + +async fn fetch_proxies() -> Result<(api::ProxiesRes, api::ProvidersProxiesRes)> { + try_join!(api::get_proxies(), api::get_providers_proxies()) +} + +impl Proxies { + pub async fn fetch() -> Result { + let (inner_proxies, providers_proxies) = fetch_proxies + .retry(&*CLASH_API_DEFAULT_BACKOFF_STRATEGY) + .await?; + let inner_proxies = inner_proxies.proxies.unwrap_or_default(); + // 1. filter out the Http or File type provider proxies + let providers_proxies: HashMap = { + let records = providers_proxies.providers.unwrap_or_default(); + records + .into_iter() + .filter(|(_k, v)| { + matches!( + v.vehicle_type, + api::VehicleType::Http | api::VehicleType::File + ) + }) + .collect() + }; + + // 2. mapping provider => providerProxiesItem to name => ProxyItem + let mut provider_map = HashMap::::new(); + for (provider, record) in providers_proxies.iter() { + let name = record.name.clone(); + let mut record: api::ProxyItem = record.clone().into(); + record.provider = Some(provider.clone()); + provider_map.insert(name, record); + } + let generate_item = |name: &str| { + if let Some(r) = inner_proxies.get(name) { + r.clone() + } else if let Some(r) = provider_map.get(name) { + r.clone() + } else { + api::ProxyItem { + name: name.to_string(), + r#type: "Unknown".to_string(), + udp: false, + history: vec![], + ..Default::default() + } + } + }; + + let global = inner_proxies.get("GLOBAL"); + let direct = inner_proxies + .get("DIRECT") + .ok_or(anyhow::anyhow!("DIRECT is missing in /proxies"))? + .clone(); // It should be always exists + let reject = inner_proxies + .get("REJECT") + .ok_or(anyhow::anyhow!("REJECT is missing in /proxies"))? + .clone(); // It should be always exists + + // 3. generate the proxies groups + let groups: Vec = match global { + Some(api::ProxyItem { all: Some(all), .. }) => { + let all = all.clone(); + all.into_iter() + .filter(|name| { + matches!( + inner_proxies.get(name), + Some(api::ProxyItem { all: Some(_), .. }) + ) + }) + .map(|name| { + let item = inner_proxies + .get(&name) + .unwrap_or(&api::ProxyItem::default()) + .clone(); + let item_all = item.all.clone().unwrap_or_default(); + let mut item: ProxyGroupItem = item.into(); + item.all = item_all + .into_iter() + .map(|name| generate_item(&name)) + .collect(); + item + }) + .collect() + } + _ => { + let mut groups: Vec = inner_proxies + .clone() + .into_values() + .filter(|v| v.name == "GLOBAL" && v.all.is_some()) + .map(|v| { + let all = v.all.clone().unwrap_or_default(); + let mut item: ProxyGroupItem = v.clone().into(); + item.all = all.into_iter().map(|name| generate_item(&name)).collect(); + item + }) + .collect(); + groups.sort_by(|a, b| b.name.to_lowercase().cmp(&a.name.to_lowercase())); + groups + } + }; + + // 4. generate the proxies + let mut proxies: Vec = vec![direct.clone(), reject]; + proxies.extend(inner_proxies.clone().into_values().filter(|v| { + matches!(v.name.as_str(), "DIRECT" | "REJECT") + && (v.all.is_none() || v.all.as_ref().unwrap().is_empty()) + })); + + // 5. generate the global + let global: Option = global.map(|v| { + let all = v.all.clone().unwrap_or_default(); + let mut item: ProxyGroupItem = v.clone().into(); + item.all = all.into_iter().map(|name| generate_item(&name)).collect(); + item + }); + + Ok(Proxies { + global: global.unwrap_or_default(), + direct, + groups, + records: inner_proxies, + proxies, + }) + } +} + +pub struct ProxiesGuard { + inner: Proxies, + updated_at: u64, + sender: broadcast::Sender<()>, +} + +impl ProxiesGuard { + pub fn global() -> &'static Arc> { + static PROXIES: OnceLock>> = OnceLock::new(); + PROXIES.get_or_init(|| { + let (tx, _) = broadcast::channel(5); // 默认提供 5 个消费位置,提供一定的缓冲 + Arc::new(RwLock::new(ProxiesGuard { + sender: tx, + inner: Proxies::default(), + updated_at: 0, + })) + }) + } + + pub fn get_receiver(&self) -> broadcast::Receiver<()> { + self.sender.subscribe() + } + + pub fn replace(&mut self, proxies: Proxies) { + let now = tokio::time::Instant::now().elapsed().as_secs(); + self.inner = proxies; + self.updated_at = now; + + if let Err(e) = self.sender.send(()) { + warn!( + target: "clash::proxies", + "send update signal failed: {:?}", e + ); + } + } + + // pub async fn select_proxy(&mut self, group: &str, name: &str) -> Result<()> { + // api::update_proxy(group, name).await?; + // self.update().await?; + // Ok(()) + // } + + pub fn inner(&self) -> &Proxies { + &self.inner + } + + pub fn updated_at(&self) -> u64 { + self.updated_at + } +} + +pub trait ProxiesGuardExt { + async fn update(&self) -> Result<()>; + async fn select_proxy(&self, group: &str, name: &str) -> Result<()>; +} + +type ProxiesGuardSingleton = &'static Arc>; +impl ProxiesGuardExt for ProxiesGuardSingleton { + async fn update(&self) -> Result<()> { + let proxies = Proxies::fetch().await?; + { + self.write().replace(proxies); + } + Ok(()) + } + + async fn select_proxy(&self, group: &str, name: &str) -> Result<()> { + api::update_proxy(group, name).await?; + self.update().await?; + Ok(()) + } +} diff --git a/backend/tauri/src/core/handle.rs b/backend/tauri/src/core/handle.rs index d12d459114..359c4fecf0 100644 --- a/backend/tauri/src/core/handle.rs +++ b/backend/tauri/src/core/handle.rs @@ -50,6 +50,12 @@ impl Handle { } } + pub fn mutate_proxies() { + if let Some(window) = Self::global().get_window() { + log_err!(window.emit("verge://mutate-proxies", "yes")); + } + } + pub fn notice_message, M: Into>(status: S, msg: M) { if let Some(window) = Self::global().get_window() { log_err!(window.emit("verge://notice-message", (status.into(), msg.into()))); diff --git a/backend/tauri/src/core/tasks/utils.rs b/backend/tauri/src/core/tasks/utils.rs index 23d5acdcec..a89e905d64 100644 --- a/backend/tauri/src/core/tasks/utils.rs +++ b/backend/tauri/src/core/tasks/utils.rs @@ -41,5 +41,6 @@ impl Error { } pub trait ConfigChangedNotifier { + #[allow(dead_code)] fn notify_config_changed(&self, task_name: &str) -> Result<()>; } diff --git a/backend/tauri/src/core/tray/mod.rs b/backend/tauri/src/core/tray/mod.rs index 80173c387c..1ce536eb2f 100644 --- a/backend/tauri/src/core/tray/mod.rs +++ b/backend/tauri/src/core/tray/mod.rs @@ -10,17 +10,19 @@ use super::storage; pub struct Tray {} -mod proxies; +pub mod proxies; + +use self::proxies::SystemTrayMenuProxiesExt; impl Tray { pub fn tray_menu(_app_handle: &AppHandle) -> SystemTrayMenu { - let zh = { Config::verge().latest().language == Some("zh".into()) }; - let version = env!("NYANPASU_VERSION"); SystemTrayMenu::new() .add_item(CustomMenuItem::new("open_window", t!("tray.dashboard"))) .add_native_item(SystemTrayMenuItem::Separator) + .setup_proxies() // Setup the proxies menu + .add_native_item(SystemTrayMenuItem::Separator) .add_item(CustomMenuItem::new("rule_mode", t!("tray.rule_mode"))) .add_item(CustomMenuItem::new("global_mode", t!("tray.global_mode"))) .add_item(CustomMenuItem::new("direct_mode", t!("tray.direct_mode"))) @@ -75,15 +77,7 @@ impl Tray { } pub fn update_part(app_handle: &AppHandle) -> Result<()> { - let mode = { - Config::clash() - .latest() - .0 - .get("mode") - .map(|val| val.as_str().unwrap_or("rule")) - .unwrap_or("rule") - .to_owned() - }; + let mode = crate::utils::config::get_current_clash_mode(); let tray = app_handle.tray_handle(); @@ -115,8 +109,6 @@ impl Tray { #[cfg(not(target_os = "linux"))] { - let zh = { verge.language == Some("zh".into()) }; - let switch_map = { let mut map = std::collections::HashMap::new(); map.insert(true, t!("tray.proxy_action.on")); @@ -167,7 +159,9 @@ impl Tray { storage::Storage::global().destroy().unwrap(); std::process::exit(0); } - _ => {} + _ => { + proxies::on_system_tray_event(&id); + } }, #[cfg(target_os = "windows")] SystemTrayEvent::LeftClick { .. } => { diff --git a/backend/tauri/src/core/tray/proxies.rs b/backend/tauri/src/core/tray/proxies.rs index 8b13789179..19be465dc3 100644 --- a/backend/tauri/src/core/tray/proxies.rs +++ b/backend/tauri/src/core/tray/proxies.rs @@ -1 +1,141 @@ +use crate::core::{ + clash::proxies::{ProxiesGuard, ProxiesGuardExt}, + handle::Handle, +}; +use log::{debug, error, warn}; +use tauri::SystemTrayMenu; +async fn loop_task() { + loop { + match ProxiesGuard::global().update().await { + Ok(_) => { + debug!(target: "tray", "update proxies success"); + } + Err(e) => { + warn!(target: "tray", "update proxies failed: {:?}", e); + } + } + { + let guard = ProxiesGuard::global().read(); + if guard.updated_at() == 0 { + error!(target: "tray", "proxies not updated yet!!!!"); + // TODO: add a error dialog or notification, and panic? + } else { + let proxies = guard.inner(); + let str = simd_json::to_string_pretty(proxies).unwrap(); + debug!(target: "tray", "proxies info: {:?}", str); + } + } + tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; // TODO: add a config to control the interval + } +} + +pub async fn proxies_updated_receiver() { + let mut rx = ProxiesGuard::global().read().get_receiver(); + loop { + match rx.recv().await { + Ok(_) => { + debug!(target: "tray::proxies", "proxies updated"); + if Handle::global().app_handle.lock().is_none() { + warn!(target: "tray::proxies", "app handle not found"); + continue; + } + Handle::mutate_proxies(); + match Handle::update_systray() { + Ok(_) => { + debug!(target: "tray::proxies", "update systray success"); + } + Err(e) => { + warn!(target: "tray::proxies", "update systray failed: {:?}", e); + } + } + } + Err(e) => { + warn!(target: "tray::proxies", "proxies updated receiver failed: {:?}", e); + } + } + } +} + +pub fn setup_proxies() { + tauri::async_runtime::spawn(loop_task()); + tauri::async_runtime::spawn(proxies_updated_receiver()); +} + +mod platform_impl { + use crate::core::clash::proxies::{ProxiesGuard, ProxyGroupItem}; + use tauri::{CustomMenuItem, SystemTrayMenu, SystemTraySubmenu}; + + pub fn generate_group_selector(group: &ProxyGroupItem) -> SystemTraySubmenu { + let mut group_menu = SystemTrayMenu::new(); + for item in group.all.iter() { + let mut sub_item = CustomMenuItem::new( + format!("select_proxy_{}_{}", group.name, item.name.clone()), + item.name.clone(), + ); + if let Some(now) = group.now.clone() { + if now == item.name { + sub_item = sub_item.selected(); + } + } + group_menu = group_menu.add_item(sub_item); + } + SystemTraySubmenu::new(group.name.clone(), group_menu) + } + + pub fn setup_tray(menu: &mut SystemTrayMenu) -> SystemTrayMenu { + let proxies = ProxiesGuard::global().read().inner().to_owned(); + let mode = crate::utils::config::get_current_clash_mode(); + let mut menu = menu.to_owned(); + match mode.as_str() { + "rule" | "script" | "global" => { + if mode == "global" { + let group_selector = generate_group_selector(&proxies.global); + menu = menu.add_submenu(group_selector); + } + for group in proxies.groups.iter() { + let group_selector = generate_group_selector(group); + menu = menu.add_submenu(group_selector); + } + menu + } + _ => { + menu.add_item(CustomMenuItem::new("no_proxy", "NO PROXY COULD SELECTED").disabled()) + // DIRECT + } + } + } +} + +pub trait SystemTrayMenuProxiesExt { + fn setup_proxies(&mut self) -> Self; +} + +impl SystemTrayMenuProxiesExt for SystemTrayMenu { + fn setup_proxies(&mut self) -> Self { + platform_impl::setup_tray(self) + } +} + +pub fn on_system_tray_event(event: &str) { + if !event.starts_with("select_proxy_") { + return; // bypass non-select event + } + let parts: Vec<&str> = event.split('_').collect(); + if parts.len() != 4 { + return; // bypass invalid event + } + let group = parts[2].to_owned(); + let name = parts[3].to_owned(); + tauri::async_runtime::spawn(async move { + match ProxiesGuard::global().select_proxy(&group, &name).await { + Ok(_) => { + debug!(target: "tray", "select proxy success: {} {}", group, name); + } + Err(e) => { + warn!(target: "tray", "select proxy failed, {} {}, cause: {:?}", group, name, e); + // TODO: add a error dialog or notification + } + } + }); +} diff --git a/backend/tauri/src/feat.rs b/backend/tauri/src/feat.rs index 16d73ae959..6671c309f9 100644 --- a/backend/tauri/src/feat.rs +++ b/backend/tauri/src/feat.rs @@ -61,7 +61,7 @@ pub fn restart_clash_core() { pub fn change_clash_mode(mode: String) { let mut mapping = Mapping::new(); mapping.insert(Value::from("mode"), mode.clone().into()); - + let (tx, rx) = tokio::sync::oneshot::channel(); tauri::async_runtime::spawn(async move { log::debug!(target: "app", "change clash mode to {mode}"); @@ -77,7 +77,13 @@ pub fn change_clash_mode(mode: String) { } Err(err) => log::error!(target: "app", "{err}"), } + if tx.send(()).is_err() { + log::error!(target: "app::change_clash_mode", "failed to send tx"); + } }); + + // refresh proxies + update_proxies_buff(Some(rx)); } // 切换系统代理 @@ -379,3 +385,23 @@ pub fn copy_clash_env(option: &str) { _ => log::error!(target: "app", "copy_clash_env: Invalid option! {option}"), } } + +pub fn update_proxies_buff(rx: Option>) { + use crate::core::clash::proxies::{ProxiesGuard, ProxiesGuardExt}; + + tauri::async_runtime::spawn(async move { + if let Some(rx) = rx { + if let Err(e) = rx.await { + log::error!(target: "app::clash::proxies", "update proxies buff by rx failed: {e}"); + } + } + match ProxiesGuard::global().update().await { + Ok(_) => { + log::debug!(target: "app::clash::proxies", "update proxies buff success"); + } + Err(e) => { + log::error!(target: "app::clash::proxies", "update proxies buff failed: {e}"); + } + } + }); +} diff --git a/backend/tauri/src/main.rs b/backend/tauri/src/main.rs index 99c84d41bb..73fc904cc2 100644 --- a/backend/tauri/src/main.rs +++ b/backend/tauri/src/main.rs @@ -94,6 +94,9 @@ fn main() -> std::io::Result<()> { cmds::service::install_service, cmds::service::uninstall_service, cmds::is_portable, + cmds::get_proxies, + cmds::select_proxy, + cmds::update_proxy_provider, ]); #[cfg(target_os = "macos")] diff --git a/backend/tauri/src/utils/config.rs b/backend/tauri/src/utils/config.rs index a97ab53849..8b1ed79a20 100644 --- a/backend/tauri/src/utils/config.rs +++ b/backend/tauri/src/utils/config.rs @@ -23,6 +23,16 @@ pub fn get_system_proxy() -> Result> { Ok(None) } +pub fn get_current_clash_mode() -> String { + Config::clash() + .latest() + .0 + .get("mode") + .map(|val| val.as_str().unwrap_or("rule")) + .unwrap_or("rule") + .to_owned() +} + pub trait NyanpasuReqwestProxyExt { fn swift_set_proxy(self, url: &str) -> Self; diff --git a/backend/tauri/src/utils/resolve.rs b/backend/tauri/src/utils/resolve.rs index d5b0a06dc4..269ce1a569 100644 --- a/backend/tauri/src/utils/resolve.rs +++ b/backend/tauri/src/utils/resolve.rs @@ -2,6 +2,7 @@ use crate::{ config::{ClashCore, Config, IVerge, WindowState}, core::{ tasks::{jobs::ProfilesJobGuard, JobsManager}, + tray::proxies, *, }, log_err, trace_err, @@ -96,6 +97,9 @@ pub fn resolve_setup(app: &mut App) { // setup jobs log_err!(JobsManager::global_register()); // init task manager log_err!(ProfilesJobGuard::global().lock().init()); + + // test job + proxies::setup_proxies(); } /// reset system proxy @@ -203,17 +207,34 @@ pub fn create_window(app_handle: &AppHandle) { // log::error!(target: "app", "failed to create window, get_window is None") // } // }); + #[cfg(debug_assertions)] + { + win.open_devtools(); + } } Err(err) => log::error!(target: "app", "failed to create window, {err}"), } } #[cfg(target_os = "macos")] - crate::log_err!(builder - .decorations(true) - .hidden_title(true) - .title_bar_style(tauri::TitleBarStyle::Overlay) - .build()); + { + match builder + .decorations(true) + .hidden_title(true) + .title_bar_style(tauri::TitleBarStyle::Overlay) + .build() + { + Ok(win) => { + #[cfg(debug_assertions)] + { + win.open_devtools(); + } + } + Err(err) => { + log::error!(target: "app", "failed to create window, {err}"); + } + } + } #[cfg(target_os = "linux")] crate::log_err!(builder.decorations(true).transparent(false).build()); diff --git a/src/components/proxy/provider-button.tsx b/src/components/proxy/provider-button.tsx index 5a63072ef4..4dd0e46c0c 100644 --- a/src/components/proxy/provider-button.tsx +++ b/src/components/proxy/provider-button.tsx @@ -1,6 +1,6 @@ -import dayjs from "dayjs"; -import useSWR, { mutate } from "swr"; -import { useState } from "react"; +import { getProviders } from "@/services/api"; +import { updateProxyProvider } from "@/services/cmds"; +import { RefreshRounded } from "@mui/icons-material"; import { Button, IconButton, @@ -8,10 +8,11 @@ import { ListItem, ListItemText, } from "@mui/material"; -import { RefreshRounded } from "@mui/icons-material"; -import { useTranslation } from "react-i18next"; import { useLockFn } from "ahooks"; -import { getProviders, providerUpdate } from "@/services/api"; +import dayjs from "dayjs"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import useSWR, { mutate } from "swr"; import { BaseDialog } from "../base"; export const ProviderButton = () => { @@ -23,7 +24,7 @@ export const ProviderButton = () => { const hasProvider = Object.keys(data || {}).length > 0; const handleUpdate = useLockFn(async (key: string) => { - await providerUpdate(key); + await updateProxyProvider(key); await mutate("getProxies"); await mutate("getProviders"); }); diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx index b2fdf3688e..f09d50727f 100644 --- a/src/components/proxy/proxy-groups.tsx +++ b/src/components/proxy/proxy-groups.tsx @@ -1,20 +1,21 @@ -import { useRef } from "react"; -import { useLockFn } from "ahooks"; -import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; +import { useProfiles } from "@/hooks/use-profiles"; +import { useVerge } from "@/hooks/use-verge"; import { + // updateProxy, + deleteConnection, getConnections, providerHealthCheck, - updateProxy, - deleteConnection, } from "@/services/api"; -import { useProfiles } from "@/hooks/use-profiles"; -import { useVerge } from "@/hooks/use-verge"; -import { BaseEmpty } from "../base"; -import { useRenderList } from "./use-render-list"; -import { ProxyRender } from "./proxy-render"; +import { selectProxy } from "@/services/cmds"; import delayManager from "@/services/delay"; import { classNames } from "@/utils"; +import { useLockFn } from "ahooks"; +import { useRef } from "react"; +import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; +import { BaseEmpty } from "../base"; import styles from "./proxy-group.module.scss"; +import { ProxyRender } from "./proxy-render"; +import { useRenderList } from "./use-render-list"; interface Props { mode: string; @@ -36,7 +37,7 @@ export const ProxyGroups = (props: Props) => { if (group.type !== "Selector" && group.type !== "Fallback") return; const { name, now } = group; - await updateProxy(name, proxy.name); + await selectProxy(name, proxy.name); onProxies(); // 断开连接 diff --git a/src/components/proxy/use-render-list.ts b/src/components/proxy/use-render-list.ts index 233a76a465..9884353817 100644 --- a/src/components/proxy/use-render-list.ts +++ b/src/components/proxy/use-render-list.ts @@ -1,14 +1,15 @@ -import useSWR from "swr"; import { useEffect, useMemo } from "react"; -import { getProxies } from "@/services/api"; +import useSWR from "swr"; +// import { getProxies } from "@/services/api"; import { useVerge } from "@/hooks/use-verge"; +import { getProxies } from "@/services/cmds"; import { filterSort } from "./use-filter-sort"; -import { useWindowWidth } from "./use-window-width"; import { - useHeadStateNew, DEFAULT_STATE, + useHeadStateNew, type HeadState, } from "./use-head-state"; +import { useWindowWidth } from "./use-window-width"; export interface IRenderItem { // 组 | head | item | empty | item col | item bottom | empty-padding @@ -24,7 +25,11 @@ export interface IRenderItem { export const useRenderList = (mode: string) => { const { data: proxiesData, mutate: mutateProxies } = useSWR( "getProxies", - getProxies, + async () => { + const res = await getProxies(); + console.log(res); + return res; + }, { refreshInterval: 45000 }, ); diff --git a/src/hooks/use-profiles.ts b/src/hooks/use-profiles.ts index d8f7f04a73..f26f3980b6 100644 --- a/src/hooks/use-profiles.ts +++ b/src/hooks/use-profiles.ts @@ -1,11 +1,12 @@ -import useSWR, { mutate } from "swr"; import { getProfiles, patchProfile, patchProfilesConfig, } from "@/services/cmds"; -import { getProxies, updateProxy } from "@/services/api"; - +import useSWR, { mutate } from "swr"; +// import { getProxies, updateProxy } from "@/services/api"; +import { updateProxy } from "@/services/api"; +import { getProxies } from "@/services/cmds"; export const useProfiles = () => { const { data: profiles, mutate: mutateProfiles } = useSWR( "getProfiles", @@ -27,6 +28,7 @@ export const useProfiles = () => { // 根据selected的节点选择 const activateSelected = async () => { const proxiesData = await getProxies(); + console.log(proxiesData); const profileData = await getProfiles(); if (!profileData || !proxiesData) return; diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index d2567b4000..ef6c31adc2 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -82,6 +82,11 @@ export default function Layout() { } }); + listen("verge://mutate-proxies", () => { + mutate("getProxies"); + mutate("getProviders"); + }); + setTimeout(() => { appWindow.show(); appWindow.unminimize(); diff --git a/src/services/api.ts b/src/services/api.ts index cf16fd26dc..d5d7572756 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -161,6 +161,8 @@ export const getProviders = async () => { ); }; +export type IProxies = Awaited>; + // proxy providers health check export const providerHealthCheck = async (name: string) => { const instance = await getAxios(); diff --git a/src/services/cmds.ts b/src/services/cmds.ts index c17f6d62ed..6f1ac0bcdc 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -3,6 +3,7 @@ import type { ManifestVersion } from "@root/scripts/generate-latest-version"; import { invoke } from "@tauri-apps/api/tauri"; import dayjs from "dayjs"; import { t } from "i18next"; +import type { IProxies } from "./api"; export async function getClashLogs() { const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/; const newRegex = /(.+?)\s+(.+?)\s+(.+)/; @@ -243,3 +244,15 @@ export async function isPortable() { if (OS_PLATFORM !== "win32") return false; return invoke("is_portable"); } + +export async function getProxies() { + return invoke("get_proxies"); +} + +export async function selectProxy(group: string, name: string) { + return invoke("select_proxy", { group, name }); +} + +export async function updateProxyProvider(name: string) { + return invoke("update_proxy_provider", { name }); +}