mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 00:35:38 +08:00
refactor: add debounce to optimize config updates and provider refresh handling
This commit is contained in:
@@ -10,7 +10,6 @@ use crate::{
|
|||||||
/// 获取配置文件列表
|
/// 获取配置文件列表
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_profiles() -> CmdResult<IProfiles> {
|
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||||
let _ = Tray::global().update_menu();
|
|
||||||
Ok(Config::profiles().data().clone())
|
Ok(Config::profiles().data().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,11 +153,25 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
match CoreManager::global().update_config().await {
|
match CoreManager::global().update_config().await {
|
||||||
Ok((true, _)) => {
|
Ok((true, _)) => {
|
||||||
logging!(info, Type::Cmd, true, "配置更新成功");
|
logging!(info, Type::Cmd, true, "配置更新成功");
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
let _ = Tray::global().update_tooltip();
|
|
||||||
Config::profiles().apply();
|
Config::profiles().apply();
|
||||||
wrap_err!(Config::profiles().data().save_file())?;
|
handle::Handle::refresh_clash();
|
||||||
|
|
||||||
|
crate::process::AsyncHandler::spawn(|| async move {
|
||||||
|
if let Err(e) = Tray::global().update_tooltip() {
|
||||||
|
log::warn!(target: "app", "异步更新托盘提示失败: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = Tray::global().update_menu() {
|
||||||
|
log::warn!(target: "app", "异步更新托盘菜单失败: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置文件
|
||||||
|
if let Err(e) = Config::profiles().data().save_file() {
|
||||||
|
log::warn!(target: "app", "异步保存配置文件失败: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 立即通知前端配置变更
|
||||||
if let Some(current) = ¤t_value {
|
if let Some(current) = ¤t_value {
|
||||||
logging!(info, Type::Cmd, true, "向前端发送配置变更事件: {}", current);
|
logging!(info, Type::Cmd, true, "向前端发送配置变更事件: {}", current);
|
||||||
handle::Handle::notify_profile_changed(current.clone());
|
handle::Handle::notify_profile_changed(current.clone());
|
||||||
@@ -185,7 +198,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
// 静默恢复,不触发验证
|
// 静默恢复,不触发验证
|
||||||
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||||
Config::profiles().apply();
|
Config::profiles().apply();
|
||||||
wrap_err!(Config::profiles().data().save_file())?;
|
|
||||||
|
crate::process::AsyncHandler::spawn(|| async move {
|
||||||
|
if let Err(e) = Config::profiles().data().save_file() {
|
||||||
|
log::warn!(target: "app", "异步保存恢复配置文件失败: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
logging!(info, Type::Cmd, true, "成功恢复到之前的配置");
|
logging!(info, Type::Cmd, true, "成功恢复到之前的配置");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,95 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::module::mihomo::MihomoManager;
|
use crate::{core::handle, module::mihomo::MihomoManager};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
static LAST_REFRESH_TIME: Lazy<Mutex<Option<Instant>>> = Lazy::new(|| Mutex::new(None));
|
||||||
|
static LAST_EVENT_TIME: Lazy<Mutex<Option<Instant>>> = Lazy::new(|| Mutex::new(None));
|
||||||
|
static IS_REFRESHING: AtomicBool = AtomicBool::new(false);
|
||||||
|
const REFRESH_INTERVAL: Duration = Duration::from_secs(3);
|
||||||
|
const EVENT_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||||
let mannager = MihomoManager::global();
|
let manager = MihomoManager::global();
|
||||||
|
|
||||||
mannager
|
manager
|
||||||
.refresh_proxies()
|
.refresh_proxies()
|
||||||
.await
|
.await
|
||||||
.map(|_| mannager.get_proxies())
|
.map(|_| manager.get_proxies())
|
||||||
.or_else(|_| Ok(mannager.get_proxies()))
|
.or_else(|_| Ok(manager.get_proxies()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||||
let mannager = MihomoManager::global();
|
let manager = MihomoManager::global();
|
||||||
|
let cached_data = manager.get_providers_proxies();
|
||||||
|
|
||||||
mannager
|
let safe_data = if cached_data.is_null() {
|
||||||
.refresh_providers_proxies()
|
serde_json::json!({
|
||||||
.await
|
"providers": {}
|
||||||
.map(|_| mannager.get_providers_proxies())
|
})
|
||||||
.or_else(|_| Ok(mannager.get_providers_proxies()))
|
} else {
|
||||||
|
cached_data
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查是否需要刷新
|
||||||
|
let should_refresh = {
|
||||||
|
let last_refresh = LAST_REFRESH_TIME.lock();
|
||||||
|
match *last_refresh {
|
||||||
|
Some(last_time) => last_time.elapsed() > REFRESH_INTERVAL,
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_refresh && !IS_REFRESHING.load(Ordering::Acquire) {
|
||||||
|
IS_REFRESHING.store(true, Ordering::Release);
|
||||||
|
|
||||||
|
crate::process::AsyncHandler::spawn(|| async move {
|
||||||
|
let manager = MihomoManager::global();
|
||||||
|
match manager.refresh_providers_proxies().await {
|
||||||
|
Ok(_) => {
|
||||||
|
log::debug!(target: "app", "providers_proxies后台刷新成功");
|
||||||
|
|
||||||
|
let should_send_event = {
|
||||||
|
let mut last_event = LAST_EVENT_TIME.lock();
|
||||||
|
match *last_event {
|
||||||
|
Some(last_time) => {
|
||||||
|
if last_time.elapsed() > EVENT_INTERVAL {
|
||||||
|
*last_event = Some(Instant::now());
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
*last_event = Some(Instant::now());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if should_send_event {
|
||||||
|
handle::Handle::refresh_providers_proxies();
|
||||||
|
log::debug!(target: "app", "已发送providers_proxies刷新事件");
|
||||||
|
} else {
|
||||||
|
log::debug!(target: "app", "跳过providers_proxies事件发送(频率限制)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!(target: "app", "providers_proxies后台刷新失败: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut last_refresh = LAST_REFRESH_TIME.lock();
|
||||||
|
*last_refresh = Some(Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
IS_REFRESHING.store(false, Ordering::Release);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(safe_data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use crate::{logging, utils::logging::Type};
|
|||||||
enum FrontendEvent {
|
enum FrontendEvent {
|
||||||
RefreshClash,
|
RefreshClash,
|
||||||
RefreshVerge,
|
RefreshVerge,
|
||||||
|
RefreshProvidersProxies,
|
||||||
NoticeMessage { status: String, message: String },
|
NoticeMessage { status: String, message: String },
|
||||||
ProfileChanged { current_profile_id: String },
|
ProfileChanged { current_profile_id: String },
|
||||||
TimerUpdated { profile_index: String },
|
TimerUpdated { profile_index: String },
|
||||||
@@ -121,6 +122,9 @@ impl NotificationSystem {
|
|||||||
FrontendEvent::RefreshVerge => {
|
FrontendEvent::RefreshVerge => {
|
||||||
("verge://refresh-verge-config", Ok(serde_json::json!("yes")))
|
("verge://refresh-verge-config", Ok(serde_json::json!("yes")))
|
||||||
}
|
}
|
||||||
|
FrontendEvent::RefreshProvidersProxies => {
|
||||||
|
("verge://refresh-providers-proxies", Ok(serde_json::json!("yes")))
|
||||||
|
}
|
||||||
FrontendEvent::NoticeMessage { status, message } => {
|
FrontendEvent::NoticeMessage { status, message } => {
|
||||||
match serde_json::to_value((status, message)) {
|
match serde_json::to_value((status, message)) {
|
||||||
Ok(p) => ("verge://notice-message", Ok(p)),
|
Ok(p) => ("verge://notice-message", Ok(p)),
|
||||||
@@ -309,6 +313,18 @@ impl Handle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn refresh_providers_proxies() {
|
||||||
|
let handle = Self::global();
|
||||||
|
if handle.is_exiting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_opt = handle.notification_system.read();
|
||||||
|
if let Some(system) = system_opt.as_ref() {
|
||||||
|
system.send_event(FrontendEvent::RefreshProvidersProxies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn notify_profile_changed(profile_id: String) {
|
pub fn notify_profile_changed(profile_id: String) {
|
||||||
let handle = Self::global();
|
let handle = Self::global();
|
||||||
if handle.is_exiting() {
|
if handle.is_exiting() {
|
||||||
|
|||||||
@@ -18,15 +18,16 @@ use crate::{
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub use speed_rate::{SpeedRate, Traffic};
|
pub use speed_rate::{SpeedRate, Traffic};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
use tauri::{
|
use tauri::{
|
||||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||||
tray::{MouseButton, MouseButtonState, TrayIconEvent},
|
tray::{MouseButton, MouseButtonState, TrayIconEvent},
|
||||||
@@ -46,10 +47,15 @@ pub struct Tray {
|
|||||||
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
||||||
is_subscribed: Arc<RwLock<bool>>,
|
is_subscribed: Arc<RwLock<bool>>,
|
||||||
pub rate_cache: Arc<Mutex<Option<Rate>>>,
|
pub rate_cache: Arc<Mutex<Option<Rate>>>,
|
||||||
|
last_menu_update: Mutex<Option<Instant>>,
|
||||||
|
menu_updating: AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
pub struct Tray {}
|
pub struct Tray {
|
||||||
|
last_menu_update: Mutex<Option<Instant>>,
|
||||||
|
menu_updating: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
impl TrayState {
|
impl TrayState {
|
||||||
pub fn get_common_tray_icon() -> (bool, Vec<u8>) {
|
pub fn get_common_tray_icon() -> (bool, Vec<u8>) {
|
||||||
@@ -164,10 +170,15 @@ impl Tray {
|
|||||||
shutdown_tx: Arc::new(RwLock::new(None)),
|
shutdown_tx: Arc::new(RwLock::new(None)),
|
||||||
is_subscribed: Arc::new(RwLock::new(false)),
|
is_subscribed: Arc::new(RwLock::new(false)),
|
||||||
rate_cache: Arc::new(Mutex::new(None)),
|
rate_cache: Arc::new(Mutex::new(None)),
|
||||||
|
last_menu_update: Mutex::new(None),
|
||||||
|
menu_updating: AtomicBool::new(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
return TRAY.get_or_init(|| Tray {});
|
return TRAY.get_or_init(|| Tray {
|
||||||
|
last_menu_update: Mutex::new(None),
|
||||||
|
menu_updating: AtomicBool::new(false),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&self) -> Result<()> {
|
pub fn init(&self) -> Result<()> {
|
||||||
@@ -192,8 +203,28 @@ impl Tray {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新托盘菜单
|
/// 更新托盘菜单(带频率限制)
|
||||||
pub fn update_menu(&self) -> Result<()> {
|
pub fn update_menu(&self) -> Result<()> {
|
||||||
|
// 检查是否正在更新或距离上次更新太近
|
||||||
|
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(500);
|
||||||
|
|
||||||
|
// 检查是否已有更新任务在执行
|
||||||
|
if self.menu_updating.load(Ordering::Acquire) {
|
||||||
|
log::debug!(target: "app", "托盘菜单正在更新中,跳过本次更新");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查更新频率
|
||||||
|
{
|
||||||
|
let last_update = self.last_menu_update.lock();
|
||||||
|
if let Some(last_time) = *last_update {
|
||||||
|
if last_time.elapsed() < MIN_UPDATE_INTERVAL {
|
||||||
|
log::debug!(target: "app", "托盘菜单更新频率过高,跳过本次更新");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let app_handle = match handle::Handle::global().app_handle() {
|
let app_handle = match handle::Handle::global().app_handle() {
|
||||||
Some(handle) => handle,
|
Some(handle) => handle,
|
||||||
None => {
|
None => {
|
||||||
@@ -202,6 +233,20 @@ impl Tray {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 设置更新状态
|
||||||
|
self.menu_updating.store(true, Ordering::Release);
|
||||||
|
|
||||||
|
let result = self.update_menu_internal(&app_handle);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut last_update = self.last_menu_update.lock();
|
||||||
|
*last_update = Some(Instant::now());
|
||||||
|
}
|
||||||
|
self.menu_updating.store(false, Ordering::Release);
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
|
||||||
let verge = Config::verge().latest().clone();
|
let verge = Config::verge().latest().clone();
|
||||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||||
@@ -230,6 +275,7 @@ impl Tray {
|
|||||||
profile_uid_and_name,
|
profile_uid_and_name,
|
||||||
is_lightweight_mode,
|
is_lightweight_mode,
|
||||||
)?));
|
)?));
|
||||||
|
log::debug!(target: "app", "托盘菜单更新成功");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ const Layout = () => {
|
|||||||
|
|
||||||
// 设置监听器
|
// 设置监听器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let providersDebounceTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
const listeners = [
|
const listeners = [
|
||||||
addListener("verge://refresh-clash-config", async () => {
|
addListener("verge://refresh-clash-config", async () => {
|
||||||
await getAxios(true);
|
await getAxios(true);
|
||||||
@@ -185,6 +187,18 @@ const Layout = () => {
|
|||||||
mutate("getAutotemProxy");
|
mutate("getAutotemProxy");
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
addListener("verge://refresh-providers-proxies", () => {
|
||||||
|
if (providersDebounceTimer) {
|
||||||
|
clearTimeout(providersDebounceTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
providersDebounceTimer = setTimeout(() => {
|
||||||
|
console.log('[Layout] Debounced refresh-providers-proxies event');
|
||||||
|
mutate("getProxyProviders");
|
||||||
|
providersDebounceTimer = undefined;
|
||||||
|
}, 500);
|
||||||
|
}),
|
||||||
|
|
||||||
addListener("verge://notice-message", ({ payload }) =>
|
addListener("verge://notice-message", ({ payload }) =>
|
||||||
handleNotice(payload as [string, string]),
|
handleNotice(payload as [string, string]),
|
||||||
),
|
),
|
||||||
@@ -206,6 +220,10 @@ const Layout = () => {
|
|||||||
const cleanupWindow = setupWindowListeners();
|
const cleanupWindow = setupWindowListeners();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (providersDebounceTimer) {
|
||||||
|
clearTimeout(providersDebounceTimer);
|
||||||
|
}
|
||||||
|
|
||||||
listeners.forEach((listener) => {
|
listeners.forEach((listener) => {
|
||||||
if (typeof listener.then === "function") {
|
if (typeof listener.then === "function") {
|
||||||
listener.then((unlisten) => unlisten());
|
listener.then((unlisten) => unlisten());
|
||||||
|
|||||||
@@ -297,17 +297,28 @@ const ProfilePage = () => {
|
|||||||
// 监听后端配置变更
|
// 监听后端配置变更
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unlistenPromise: Promise<() => void> | undefined;
|
let unlistenPromise: Promise<() => void> | undefined;
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
const setupListener = async () => {
|
const setupListener = async () => {
|
||||||
unlistenPromise = listen<string>('profile-changed', (event) => {
|
unlistenPromise = listen<string>('profile-changed', (event) => {
|
||||||
console.log('Profile changed event received:', event.payload);
|
console.log('Profile changed event received:', event.payload);
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
mutateProfiles();
|
mutateProfiles();
|
||||||
|
timeoutId = undefined;
|
||||||
|
}, 300);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
setupListener();
|
setupListener();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
unlistenPromise?.then(unlisten => unlisten());
|
unlistenPromise?.then(unlisten => unlisten());
|
||||||
};
|
};
|
||||||
}, [mutateProfiles, t]);
|
}, [mutateProfiles, t]);
|
||||||
|
|||||||
@@ -198,6 +198,11 @@ export const getProxyProviders = async () => {
|
|||||||
const response = await invoke<{
|
const response = await invoke<{
|
||||||
providers: Record<string, IProxyProviderItem>;
|
providers: Record<string, IProxyProviderItem>;
|
||||||
}>("get_providers_proxies");
|
}>("get_providers_proxies");
|
||||||
|
if (!response || !response.providers) {
|
||||||
|
console.warn("getProxyProviders: Invalid response structure, returning empty object");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
const providers = response.providers as Record<string, IProxyProviderItem>;
|
const providers = response.providers as Record<string, IProxyProviderItem>;
|
||||||
|
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
|
|||||||
Reference in New Issue
Block a user