fix: disable tun mode menu on tray when tun mode is unavailable (#4975)
* fix: check if service installed when toggle tun mode on tray * chore: cargo fmt * fix: auto disable tun mode * docs: update UPDATELOG.md * fix: init Tun mode status * chore: update * feat: disable tun mode tray menu when tun mode is unavailable * fix: restart core when uninstall service is canceled * chore: remove check notification when toggle tun mode * chore: fix updatelog --------- Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
- 修复 Linux WebKit 网络进程的崩溃
|
- 修复 Linux WebKit 网络进程的崩溃
|
||||||
- 修复无法导入订阅
|
- 修复无法导入订阅
|
||||||
- 修复实际导入成功但显示导入失败的问题
|
- 修复实际导入成功但显示导入失败的问题
|
||||||
|
- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题
|
||||||
- 修复删除订阅时未能实际删除相关文件
|
- 修复删除订阅时未能实际删除相关文件
|
||||||
- 修复 macOS 连接界面显示异常
|
- 修复 macOS 连接界面显示异常
|
||||||
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
|
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
- 允许在 `界面设置` 修改 `悬浮跳转导航延迟`
|
- 允许在 `界面设置` 修改 `悬浮跳转导航延迟`
|
||||||
- 添加热键绑定错误的提示信息
|
- 添加热键绑定错误的提示信息
|
||||||
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题
|
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题
|
||||||
|
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
cmd,
|
||||||
config::{PrfItem, profiles_append_item_safe},
|
config::{PrfItem, profiles_append_item_safe},
|
||||||
constants::{files, timing},
|
constants::{files, timing},
|
||||||
core::{CoreManager, handle, validate::CoreConfigValidator},
|
core::{CoreManager, handle, service, tray, validate::CoreConfigValidator},
|
||||||
enhance, logging,
|
enhance, logging, logging_error,
|
||||||
utils::{Draft, dirs, help, logging::Type},
|
utils::{Draft, dirs, help, logging::Type},
|
||||||
};
|
};
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
@@ -55,6 +56,20 @@ impl Config {
|
|||||||
pub async fn init_config() -> Result<()> {
|
pub async fn init_config() -> Result<()> {
|
||||||
Self::ensure_default_profile_items().await?;
|
Self::ensure_default_profile_items().await?;
|
||||||
|
|
||||||
|
// init Tun mode
|
||||||
|
if !cmd::system::is_admin().unwrap_or_default()
|
||||||
|
&& service::is_service_available().await.is_err()
|
||||||
|
{
|
||||||
|
let verge = Config::verge().await;
|
||||||
|
verge.draft_mut().enable_tun_mode = Some(false);
|
||||||
|
verge.apply();
|
||||||
|
let _ = tray::Tray::global().update_tray_display().await;
|
||||||
|
|
||||||
|
// 分离数据获取和异步调用避免Send问题
|
||||||
|
let verge_data = Config::verge().await.latest_ref().clone();
|
||||||
|
logging_error!(Type::Core, verge_data.save_file().await);
|
||||||
|
}
|
||||||
|
|
||||||
let validation_result = Self::generate_and_validate().await?;
|
let validation_result = Self::generate_and_validate().await?;
|
||||||
|
|
||||||
if let Some((msg_type, msg_content)) = validation_result {
|
if let Some((msg_type, msg_content)) = validation_result {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use anyhow::{Result, bail};
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
|
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
|
||||||
use tauri::{AppHandle, Manager};
|
|
||||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||||
|
|
||||||
/// Enum representing all available hotkey functions
|
/// Enum representing all available hotkey functions
|
||||||
@@ -105,66 +104,53 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the function associated with a hotkey function enum
|
/// Execute the function associated with a hotkey function enum
|
||||||
fn execute_function(function: HotkeyFunction, app_handle: &AppHandle) {
|
fn execute_function(function: HotkeyFunction) {
|
||||||
let app_handle = app_handle.clone();
|
|
||||||
match function {
|
match function {
|
||||||
HotkeyFunction::OpenOrCloseDashboard => {
|
HotkeyFunction::OpenOrCloseDashboard => {
|
||||||
AsyncHandler::spawn(async move || {
|
AsyncHandler::spawn(async move || {
|
||||||
crate::feat::open_or_close_dashboard().await;
|
crate::feat::open_or_close_dashboard().await;
|
||||||
notify_event(app_handle, NotificationEvent::DashboardToggled).await;
|
notify_event(NotificationEvent::DashboardToggled).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
HotkeyFunction::ClashModeRule => {
|
HotkeyFunction::ClashModeRule => {
|
||||||
AsyncHandler::spawn(async move || {
|
AsyncHandler::spawn(async move || {
|
||||||
feat::change_clash_mode("rule".into()).await;
|
feat::change_clash_mode("rule".into()).await;
|
||||||
notify_event(
|
notify_event(NotificationEvent::ClashModeChanged { mode: "Rule" }).await;
|
||||||
app_handle,
|
|
||||||
NotificationEvent::ClashModeChanged { mode: "Rule" },
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
HotkeyFunction::ClashModeGlobal => {
|
HotkeyFunction::ClashModeGlobal => {
|
||||||
AsyncHandler::spawn(async move || {
|
AsyncHandler::spawn(async move || {
|
||||||
feat::change_clash_mode("global".into()).await;
|
feat::change_clash_mode("global".into()).await;
|
||||||
notify_event(
|
notify_event(NotificationEvent::ClashModeChanged { mode: "Global" }).await;
|
||||||
app_handle,
|
|
||||||
NotificationEvent::ClashModeChanged { mode: "Global" },
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
HotkeyFunction::ClashModeDirect => {
|
HotkeyFunction::ClashModeDirect => {
|
||||||
AsyncHandler::spawn(async move || {
|
AsyncHandler::spawn(async move || {
|
||||||
feat::change_clash_mode("direct".into()).await;
|
feat::change_clash_mode("direct".into()).await;
|
||||||
notify_event(
|
notify_event(NotificationEvent::ClashModeChanged { mode: "Direct" }).await;
|
||||||
app_handle,
|
|
||||||
NotificationEvent::ClashModeChanged { mode: "Direct" },
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
HotkeyFunction::ToggleSystemProxy => {
|
HotkeyFunction::ToggleSystemProxy => {
|
||||||
AsyncHandler::spawn(async move || {
|
AsyncHandler::spawn(async move || {
|
||||||
feat::toggle_system_proxy().await;
|
feat::toggle_system_proxy().await;
|
||||||
notify_event(app_handle, NotificationEvent::SystemProxyToggled).await;
|
notify_event(NotificationEvent::SystemProxyToggled).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
HotkeyFunction::ToggleTunMode => {
|
HotkeyFunction::ToggleTunMode => {
|
||||||
AsyncHandler::spawn(async move || {
|
AsyncHandler::spawn(async move || {
|
||||||
feat::toggle_tun_mode(None).await;
|
feat::toggle_tun_mode(None).await;
|
||||||
notify_event(app_handle, NotificationEvent::TunModeToggled).await;
|
notify_event(NotificationEvent::TunModeToggled).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
HotkeyFunction::EntryLightweightMode => {
|
HotkeyFunction::EntryLightweightMode => {
|
||||||
AsyncHandler::spawn(async move || {
|
AsyncHandler::spawn(async move || {
|
||||||
entry_lightweight_mode().await;
|
entry_lightweight_mode().await;
|
||||||
notify_event(app_handle, NotificationEvent::LightweightModeEntered).await;
|
notify_event(NotificationEvent::LightweightModeEntered).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
HotkeyFunction::Quit => {
|
HotkeyFunction::Quit => {
|
||||||
AsyncHandler::spawn(async move || {
|
AsyncHandler::spawn(async move || {
|
||||||
notify_event(app_handle, NotificationEvent::AppQuit).await;
|
notify_event(NotificationEvent::AppQuit).await;
|
||||||
feat::quit().await;
|
feat::quit().await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -172,7 +158,7 @@ impl Hotkey {
|
|||||||
HotkeyFunction::Hide => {
|
HotkeyFunction::Hide => {
|
||||||
AsyncHandler::spawn(async move || {
|
AsyncHandler::spawn(async move || {
|
||||||
feat::hide().await;
|
feat::hide().await;
|
||||||
notify_event(app_handle, NotificationEvent::AppHidden).await;
|
notify_event(NotificationEvent::AppHidden).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,14 +210,12 @@ impl Hotkey {
|
|||||||
|
|
||||||
let is_quit = matches!(function, HotkeyFunction::Quit);
|
let is_quit = matches!(function, HotkeyFunction::Quit);
|
||||||
|
|
||||||
manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| {
|
manager.on_shortcut(hotkey, move |_app_handle, hotkey_event, event| {
|
||||||
let hotkey_event_owned = *hotkey_event;
|
let hotkey_event_owned = *hotkey_event;
|
||||||
let event_owned = event;
|
let event_owned = event;
|
||||||
let function_owned = function;
|
let function_owned = function;
|
||||||
let is_quit_owned = is_quit;
|
let is_quit_owned = is_quit;
|
||||||
|
|
||||||
let app_handle_cloned = app_handle.clone();
|
|
||||||
|
|
||||||
AsyncHandler::spawn(move || async move {
|
AsyncHandler::spawn(move || async move {
|
||||||
if event_owned.state == ShortcutState::Pressed {
|
if event_owned.state == ShortcutState::Pressed {
|
||||||
logging!(
|
logging!(
|
||||||
@@ -242,11 +226,11 @@ impl Hotkey {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if hotkey_event_owned.key == Code::KeyQ && is_quit_owned {
|
if hotkey_event_owned.key == Code::KeyQ && is_quit_owned {
|
||||||
if let Some(window) = app_handle_cloned.get_webview_window("main")
|
if let Some(window) = handle::Handle::get_window()
|
||||||
&& window.is_focused().unwrap_or(false)
|
&& window.is_focused().unwrap_or(false)
|
||||||
{
|
{
|
||||||
logging!(debug, Type::Hotkey, "Executing quit function");
|
logging!(debug, Type::Hotkey, "Executing quit function");
|
||||||
Self::execute_function(function_owned, &app_handle_cloned);
|
Self::execute_function(function_owned);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logging!(debug, Type::Hotkey, "Executing function directly");
|
logging!(debug, Type::Hotkey, "Executing function directly");
|
||||||
@@ -258,14 +242,14 @@ impl Hotkey {
|
|||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
|
|
||||||
if is_enable_global_hotkey {
|
if is_enable_global_hotkey {
|
||||||
Self::execute_function(function_owned, &app_handle_cloned);
|
Self::execute_function(function_owned);
|
||||||
} else {
|
} else {
|
||||||
use crate::utils::window_manager::WindowManager;
|
use crate::utils::window_manager::WindowManager;
|
||||||
let is_visible = WindowManager::is_main_window_visible();
|
let is_visible = WindowManager::is_main_window_visible();
|
||||||
let is_focused = WindowManager::is_main_window_focused();
|
let is_focused = WindowManager::is_main_window_focused();
|
||||||
|
|
||||||
if is_focused && is_visible {
|
if is_focused && is_visible {
|
||||||
Self::execute_function(function_owned, &app_handle_cloned);
|
Self::execute_function(function_owned);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
|
core::tray,
|
||||||
logging, logging_error,
|
logging, logging_error,
|
||||||
utils::{dirs, init::service_writer_config, logging::Type},
|
utils::{dirs, init::service_writer_config, logging::Type},
|
||||||
};
|
};
|
||||||
@@ -531,6 +532,7 @@ impl ServiceManager {
|
|||||||
return Err(anyhow::anyhow!("服务不可用: {}", reason));
|
return Err(anyhow::anyhow!("服务不可用: {}", reason));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let _ = tray::Tray::global().update_tray_display().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use tauri_plugin_mihomo::models::Proxies;
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub mod speed_rate;
|
pub mod speed_rate;
|
||||||
use crate::config::PrfSelected;
|
use crate::config::PrfSelected;
|
||||||
|
use crate::core::service;
|
||||||
use crate::module::lightweight;
|
use crate::module::lightweight;
|
||||||
use crate::process::AsyncHandler;
|
use crate::process::AsyncHandler;
|
||||||
use crate::utils::window_manager::WindowManager;
|
use crate::utils::window_manager::WindowManager;
|
||||||
@@ -297,6 +298,9 @@ impl Tray {
|
|||||||
let verge = Config::verge().await.latest_ref().clone();
|
let verge = Config::verge().await.latest_ref().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);
|
||||||
|
let tun_mode_available = cmd::system::is_admin().unwrap_or_default()
|
||||||
|
|| service::is_service_available().await.is_ok();
|
||||||
|
println!("tun_mode_available: {}", tun_mode_available);
|
||||||
let mode = {
|
let mode = {
|
||||||
Config::clash()
|
Config::clash()
|
||||||
.await
|
.await
|
||||||
@@ -322,6 +326,7 @@ impl Tray {
|
|||||||
Some(mode.as_str()),
|
Some(mode.as_str()),
|
||||||
*system_proxy,
|
*system_proxy,
|
||||||
*tun_mode,
|
*tun_mode,
|
||||||
|
tun_mode_available,
|
||||||
profile_uid_and_name,
|
profile_uid_and_name,
|
||||||
is_lightweight_mode,
|
is_lightweight_mode,
|
||||||
)
|
)
|
||||||
@@ -837,6 +842,7 @@ async fn create_tray_menu(
|
|||||||
mode: Option<&str>,
|
mode: Option<&str>,
|
||||||
system_proxy_enabled: bool,
|
system_proxy_enabled: bool,
|
||||||
tun_mode_enabled: bool,
|
tun_mode_enabled: bool,
|
||||||
|
tun_mode_available: bool,
|
||||||
profile_uid_and_name: Vec<(String, String)>,
|
profile_uid_and_name: Vec<(String, String)>,
|
||||||
is_lightweight_mode: bool,
|
is_lightweight_mode: bool,
|
||||||
) -> Result<tauri::menu::Menu<Wry>> {
|
) -> Result<tauri::menu::Menu<Wry>> {
|
||||||
@@ -980,7 +986,7 @@ async fn create_tray_menu(
|
|||||||
app_handle,
|
app_handle,
|
||||||
MenuIds::TUN_MODE,
|
MenuIds::TUN_MODE,
|
||||||
&texts.tun_mode,
|
&texts.tun_mode,
|
||||||
true,
|
tun_mode_available,
|
||||||
tun_mode_enabled,
|
tun_mode_enabled,
|
||||||
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
|
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::utils::i18n::t;
|
use crate::{core::handle, utils::i18n::t};
|
||||||
|
|
||||||
use tauri::AppHandle;
|
|
||||||
use tauri_plugin_notification::NotificationExt;
|
use tauri_plugin_notification::NotificationExt;
|
||||||
|
|
||||||
pub enum NotificationEvent<'a> {
|
pub enum NotificationEvent<'a> {
|
||||||
@@ -16,8 +15,10 @@ pub enum NotificationEvent<'a> {
|
|||||||
AppHidden,
|
AppHidden,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify(app: &AppHandle, title: &str, body: &str) {
|
fn notify(title: &str, body: &str) {
|
||||||
app.notification()
|
let app_handle = handle::Handle::app_handle();
|
||||||
|
app_handle
|
||||||
|
.notification()
|
||||||
.builder()
|
.builder()
|
||||||
.title(title)
|
.title(title)
|
||||||
.body(body)
|
.body(body)
|
||||||
@@ -25,49 +26,44 @@ fn notify(app: &AppHandle, title: &str, body: &str) {
|
|||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn notify_event<'a>(app: AppHandle, event: NotificationEvent<'a>) {
|
pub async fn notify_event<'a>(event: NotificationEvent<'a>) {
|
||||||
match event {
|
match event {
|
||||||
NotificationEvent::DashboardToggled => {
|
NotificationEvent::DashboardToggled => {
|
||||||
notify(
|
notify(
|
||||||
&app,
|
|
||||||
&t("DashboardToggledTitle").await,
|
&t("DashboardToggledTitle").await,
|
||||||
&t("DashboardToggledBody").await,
|
&t("DashboardToggledBody").await,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
NotificationEvent::ClashModeChanged { mode } => {
|
NotificationEvent::ClashModeChanged { mode } => {
|
||||||
notify(
|
notify(
|
||||||
&app,
|
|
||||||
&t("ClashModeChangedTitle").await,
|
&t("ClashModeChangedTitle").await,
|
||||||
&t_with_args("ClashModeChangedBody", mode).await,
|
&t_with_args("ClashModeChangedBody", mode).await,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
NotificationEvent::SystemProxyToggled => {
|
NotificationEvent::SystemProxyToggled => {
|
||||||
notify(
|
notify(
|
||||||
&app,
|
|
||||||
&t("SystemProxyToggledTitle").await,
|
&t("SystemProxyToggledTitle").await,
|
||||||
&t("SystemProxyToggledBody").await,
|
&t("SystemProxyToggledBody").await,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
NotificationEvent::TunModeToggled => {
|
NotificationEvent::TunModeToggled => {
|
||||||
notify(
|
notify(
|
||||||
&app,
|
|
||||||
&t("TunModeToggledTitle").await,
|
&t("TunModeToggledTitle").await,
|
||||||
&t("TunModeToggledBody").await,
|
&t("TunModeToggledBody").await,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
NotificationEvent::LightweightModeEntered => {
|
NotificationEvent::LightweightModeEntered => {
|
||||||
notify(
|
notify(
|
||||||
&app,
|
|
||||||
&t("LightweightModeEnteredTitle").await,
|
&t("LightweightModeEnteredTitle").await,
|
||||||
&t("LightweightModeEnteredBody").await,
|
&t("LightweightModeEnteredBody").await,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
NotificationEvent::AppQuit => {
|
NotificationEvent::AppQuit => {
|
||||||
notify(&app, &t("AppQuitTitle").await, &t("AppQuitBody").await);
|
notify(&t("AppQuitTitle").await, &t("AppQuitBody").await);
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
NotificationEvent::AppHidden => {
|
NotificationEvent::AppHidden => {
|
||||||
notify(&app, &t("AppHiddenTitle").await, &t("AppHiddenBody").await);
|
notify(&t("AppHiddenTitle").await, &t("AppHiddenBody").await);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,13 +117,8 @@ const ProxyControlSwitches = ({
|
|||||||
const { uninstallServiceAndRestartCore } = useServiceUninstaller();
|
const { uninstallServiceAndRestartCore } = useServiceUninstaller();
|
||||||
const { actualState: systemProxyActualState, toggleSystemProxy } =
|
const { actualState: systemProxyActualState, toggleSystemProxy } =
|
||||||
useSystemProxyState();
|
useSystemProxyState();
|
||||||
const {
|
const { isServiceOk, isTunModeAvailable, mutateSystemState } =
|
||||||
isServiceMode,
|
useSystemState();
|
||||||
isTunModeAvailable,
|
|
||||||
mutateRunningMode,
|
|
||||||
mutateServiceOk,
|
|
||||||
mutateTunModeAvailable,
|
|
||||||
} = useSystemState();
|
|
||||||
|
|
||||||
const sysproxyRef = useRef<DialogRef>(null);
|
const sysproxyRef = useRef<DialogRef>(null);
|
||||||
const tunRef = useRef<DialogRef>(null);
|
const tunRef = useRef<DialogRef>(null);
|
||||||
@@ -148,9 +143,7 @@ const ProxyControlSwitches = ({
|
|||||||
const onInstallService = useLockFn(async () => {
|
const onInstallService = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
await installServiceAndRestartCore();
|
await installServiceAndRestartCore();
|
||||||
await mutateRunningMode();
|
await mutateSystemState();
|
||||||
await mutateServiceOk();
|
|
||||||
await mutateTunModeAvailable();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showNotice("error", (err as Error).message || String(err));
|
showNotice("error", (err as Error).message || String(err));
|
||||||
}
|
}
|
||||||
@@ -158,11 +151,11 @@ const ProxyControlSwitches = ({
|
|||||||
|
|
||||||
const onUninstallService = useLockFn(async () => {
|
const onUninstallService = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
await handleTunToggle(false);
|
if (verge?.enable_tun_mode) {
|
||||||
|
await handleTunToggle(false);
|
||||||
|
}
|
||||||
await uninstallServiceAndRestartCore();
|
await uninstallServiceAndRestartCore();
|
||||||
await mutateRunningMode();
|
await mutateSystemState();
|
||||||
await mutateServiceOk();
|
|
||||||
await mutateTunModeAvailable();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showNotice("error", (err as Error).message || String(err));
|
showNotice("error", (err as Error).message || String(err));
|
||||||
}
|
}
|
||||||
@@ -198,22 +191,22 @@ const ProxyControlSwitches = ({
|
|||||||
extraIcons={
|
extraIcons={
|
||||||
<>
|
<>
|
||||||
{!isTunModeAvailable && (
|
{!isTunModeAvailable && (
|
||||||
<TooltipIcon
|
<>
|
||||||
title={t("TUN requires Service Mode or Admin Mode")}
|
<TooltipIcon
|
||||||
icon={WarningRounded}
|
title={t("TUN requires Service Mode or Admin Mode")}
|
||||||
sx={{ color: "warning.main", ml: 1 }}
|
icon={WarningRounded}
|
||||||
/>
|
sx={{ color: "warning.main", ml: 1 }}
|
||||||
|
/>
|
||||||
|
<TooltipIcon
|
||||||
|
title={t("Install Service")}
|
||||||
|
icon={BuildRounded}
|
||||||
|
color="primary"
|
||||||
|
onClick={onInstallService}
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{!isTunModeAvailable && (
|
{isServiceOk && (
|
||||||
<TooltipIcon
|
|
||||||
title={t("Install Service")}
|
|
||||||
icon={BuildRounded}
|
|
||||||
color="primary"
|
|
||||||
onClick={onInstallService}
|
|
||||||
sx={{ ml: 1 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isServiceMode && (
|
|
||||||
<TooltipIcon
|
<TooltipIcon
|
||||||
title={t("Uninstall Service")}
|
title={t("Uninstall Service")}
|
||||||
icon={DeleteForeverRounded}
|
icon={DeleteForeverRounded}
|
||||||
|
|||||||
@@ -1,73 +1,99 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
import { getRunningMode, isAdmin, isServiceAvailable } from "@/services/cmds";
|
import { getRunningMode, isAdmin, isServiceAvailable } from "@/services/cmds";
|
||||||
|
import { showNotice } from "@/services/noticeService";
|
||||||
|
|
||||||
|
import { useVerge } from "./use-verge";
|
||||||
|
|
||||||
|
export interface SystemState {
|
||||||
|
runningMode: "Sidecar" | "Service";
|
||||||
|
isAdminMode: boolean;
|
||||||
|
isServiceOk: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSystemState = {
|
||||||
|
runningMode: "Sidecar",
|
||||||
|
isAdminMode: false,
|
||||||
|
isServiceOk: false,
|
||||||
|
} as SystemState;
|
||||||
|
|
||||||
|
let disablingTunMode = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义 hook 用于获取系统运行状态
|
* 自定义 hook 用于获取系统运行状态
|
||||||
* 包括运行模式、管理员状态、系统服务是否可用
|
* 包括运行模式、管理员状态、系统服务是否可用
|
||||||
*/
|
*/
|
||||||
export function useSystemState() {
|
export function useSystemState() {
|
||||||
// 获取运行模式
|
const { t } = useTranslation();
|
||||||
const {
|
const { verge, patchVerge } = useVerge();
|
||||||
data: runningMode = "Sidecar",
|
|
||||||
mutate: mutateRunningMode,
|
|
||||||
isLoading: runningModeLoading,
|
|
||||||
} = useSWR("getRunningMode", getRunningMode, {
|
|
||||||
suspense: false,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
});
|
|
||||||
const isSidecarMode = runningMode === "Sidecar";
|
|
||||||
const isServiceMode = runningMode === "Service";
|
|
||||||
|
|
||||||
// 获取管理员状态
|
const {
|
||||||
const { data: isAdminMode = false, isLoading: isAdminLoading } = useSWR(
|
data: systemState,
|
||||||
"isAdmin",
|
mutate: mutateSystemState,
|
||||||
isAdmin,
|
isLoading,
|
||||||
|
} = useSWR(
|
||||||
|
"getSystemState",
|
||||||
|
async () => {
|
||||||
|
const [runningMode, isAdminMode, isServiceOk] = await Promise.all([
|
||||||
|
getRunningMode(),
|
||||||
|
isAdmin(),
|
||||||
|
isServiceAvailable(),
|
||||||
|
]);
|
||||||
|
return { runningMode, isAdminMode, isServiceOk } as SystemState;
|
||||||
|
},
|
||||||
{
|
{
|
||||||
suspense: false,
|
suspense: true,
|
||||||
revalidateOnFocus: false,
|
refreshInterval: 30000,
|
||||||
|
fallback: defaultSystemState,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const isSidecarMode = systemState.runningMode === "Sidecar";
|
||||||
data: isServiceOk = false,
|
const isServiceMode = systemState.runningMode === "Service";
|
||||||
mutate: mutateServiceOk,
|
const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk;
|
||||||
isLoading: isServiceLoading,
|
|
||||||
} = useSWR(isServiceMode ? "isServiceAvailable" : null, isServiceAvailable, {
|
|
||||||
suspense: false,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
onSuccess: (data) => {
|
|
||||||
console.log("[useSystemState] 服务状态更新:", data);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error("[useSystemState] 服务状态检查失败:", error);
|
|
||||||
},
|
|
||||||
// isPaused: () => !isServiceMode, // 仅在非 Service 模式下暂停请求
|
|
||||||
});
|
|
||||||
|
|
||||||
const isLoading =
|
const enable_tun_mode = verge?.enable_tun_mode;
|
||||||
runningModeLoading || isAdminLoading || (isServiceMode && isServiceLoading);
|
useEffect(() => {
|
||||||
|
if (enable_tun_mode === undefined) return;
|
||||||
|
|
||||||
const { data: isTunModeAvailable = false, mutate: mutateTunModeAvailable } =
|
if (
|
||||||
useSWR(
|
!disablingTunMode &&
|
||||||
["isTunModeAvailable", isAdminMode, isServiceOk],
|
enable_tun_mode &&
|
||||||
() => isAdminMode || isServiceOk,
|
!isTunModeAvailable &&
|
||||||
{
|
!isLoading
|
||||||
suspense: false,
|
) {
|
||||||
revalidateOnFocus: false,
|
disablingTunMode = true;
|
||||||
},
|
patchVerge({ enable_tun_mode: false })
|
||||||
);
|
.then(() => {
|
||||||
|
showNotice(
|
||||||
|
"info",
|
||||||
|
t("TUN Mode automatically disabled due to service unavailable"),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("[useVerge] 自动关闭TUN模式失败:", err);
|
||||||
|
showNotice("error", t("Failed to disable TUN Mode automatically"));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
const tid = setTimeout(() => {
|
||||||
|
// 避免 verge 数据更新不及时导致重复执行关闭 Tun 模式
|
||||||
|
disablingTunMode = false;
|
||||||
|
clearTimeout(tid);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [enable_tun_mode, isTunModeAvailable, patchVerge, isLoading, t]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
runningMode,
|
runningMode: systemState.runningMode,
|
||||||
isAdminMode,
|
isAdminMode: systemState.isAdminMode,
|
||||||
|
isServiceOk: systemState.isServiceOk,
|
||||||
isSidecarMode,
|
isSidecarMode,
|
||||||
isServiceMode,
|
isServiceMode,
|
||||||
isServiceOk,
|
|
||||||
isTunModeAvailable,
|
isTunModeAvailable,
|
||||||
mutateRunningMode,
|
mutateSystemState,
|
||||||
mutateServiceOk,
|
|
||||||
mutateTunModeAvailable,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
import { useSystemState } from "@/hooks/use-system-state";
|
|
||||||
import { getVergeConfig, patchVergeConfig } from "@/services/cmds";
|
import { getVergeConfig, patchVergeConfig } from "@/services/cmds";
|
||||||
import { showNotice } from "@/services/noticeService";
|
|
||||||
|
|
||||||
export const useVerge = () => {
|
export const useVerge = () => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const { isTunModeAvailable, isServiceMode, isLoading } = useSystemState();
|
|
||||||
const disablingRef = useRef(false);
|
|
||||||
|
|
||||||
const { data: verge, mutate: mutateVerge } = useSWR(
|
const { data: verge, mutate: mutateVerge } = useSWR(
|
||||||
"getVergeConfig",
|
"getVergeConfig",
|
||||||
async () => {
|
async () => {
|
||||||
@@ -24,53 +16,6 @@ export const useVerge = () => {
|
|||||||
mutateVerge();
|
mutateVerge();
|
||||||
};
|
};
|
||||||
|
|
||||||
const { enable_tun_mode } = verge ?? {};
|
|
||||||
|
|
||||||
const mutateVergeRef = useRef(mutateVerge);
|
|
||||||
const tRef = useRef(t);
|
|
||||||
const enableTunRef = useRef(enable_tun_mode);
|
|
||||||
const isLoadingRef = useRef(isLoading);
|
|
||||||
const isServiceModeRef = useRef(isServiceMode);
|
|
||||||
|
|
||||||
mutateVergeRef.current = mutateVerge;
|
|
||||||
tRef.current = t;
|
|
||||||
enableTunRef.current = enable_tun_mode;
|
|
||||||
isLoadingRef.current = isLoading;
|
|
||||||
isServiceModeRef.current = isServiceMode;
|
|
||||||
|
|
||||||
const doDisable = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
if (isServiceModeRef.current === true) return;
|
|
||||||
await patchVergeConfig({ enable_tun_mode: false });
|
|
||||||
await mutateVergeRef.current?.();
|
|
||||||
showNotice(
|
|
||||||
"info",
|
|
||||||
tRef.current(
|
|
||||||
"TUN Mode automatically disabled due to service unavailable",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[useVerge] 自动关闭TUN模式失败:", err);
|
|
||||||
showNotice(
|
|
||||||
"error",
|
|
||||||
tRef.current("Failed to disable TUN Mode automatically"),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
disablingRef.current = false;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isTunModeAvailable === true) return;
|
|
||||||
if (isLoadingRef.current === true) return;
|
|
||||||
if (enableTunRef.current !== true) return;
|
|
||||||
if (isServiceModeRef.current === true) return;
|
|
||||||
if (disablingRef.current) return;
|
|
||||||
|
|
||||||
disablingRef.current = true;
|
|
||||||
void doDisable();
|
|
||||||
}, [isTunModeAvailable, doDisable]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
verge,
|
verge,
|
||||||
mutateVerge,
|
mutateVerge,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const executeWithErrorHandling = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useServiceInstaller = () => {
|
export const useServiceInstaller = () => {
|
||||||
const { mutateRunningMode, mutateServiceOk } = useSystemState();
|
const { mutateSystemState } = useSystemState();
|
||||||
|
|
||||||
const installServiceAndRestartCore = useCallback(async () => {
|
const installServiceAndRestartCore = useCallback(async () => {
|
||||||
await executeWithErrorHandling(
|
await executeWithErrorHandling(
|
||||||
@@ -34,9 +34,13 @@ export const useServiceInstaller = () => {
|
|||||||
"Service Installed Successfully",
|
"Service Installed Successfully",
|
||||||
);
|
);
|
||||||
|
|
||||||
await executeWithErrorHandling(() => restartCore(), "Restarting Core...");
|
await executeWithErrorHandling(
|
||||||
await mutateRunningMode();
|
() => restartCore(),
|
||||||
await mutateServiceOk();
|
"Restarting Core...",
|
||||||
}, [mutateRunningMode, mutateServiceOk]);
|
"Clash Core Restarted",
|
||||||
|
);
|
||||||
|
|
||||||
|
await mutateSystemState();
|
||||||
|
}, [mutateSystemState]);
|
||||||
return { installServiceAndRestartCore };
|
return { installServiceAndRestartCore };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,21 +25,26 @@ const executeWithErrorHandling = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useServiceUninstaller = () => {
|
export const useServiceUninstaller = () => {
|
||||||
const { mutateRunningMode, mutateServiceOk } = useSystemState();
|
const { mutateSystemState } = useSystemState();
|
||||||
|
|
||||||
const uninstallServiceAndRestartCore = useCallback(async () => {
|
const uninstallServiceAndRestartCore = useCallback(async () => {
|
||||||
await executeWithErrorHandling(() => stopCore(), "Stopping Core...");
|
try {
|
||||||
|
await executeWithErrorHandling(() => stopCore(), "Stopping Core...");
|
||||||
await executeWithErrorHandling(
|
await executeWithErrorHandling(
|
||||||
() => uninstallService(),
|
() => uninstallService(),
|
||||||
"Uninstalling Service...",
|
"Uninstalling Service...",
|
||||||
"Service Uninstalled Successfully",
|
"Service Uninstalled Successfully",
|
||||||
);
|
);
|
||||||
|
} catch (ignore) {
|
||||||
await executeWithErrorHandling(() => restartCore(), "Restarting Core...");
|
} finally {
|
||||||
await mutateRunningMode();
|
await executeWithErrorHandling(
|
||||||
await mutateServiceOk();
|
() => restartCore(),
|
||||||
}, [mutateRunningMode, mutateServiceOk]);
|
"Restarting Core...",
|
||||||
|
"Clash Core Restarted",
|
||||||
|
);
|
||||||
|
await mutateSystemState();
|
||||||
|
}
|
||||||
|
}, [mutateSystemState]);
|
||||||
|
|
||||||
return { uninstallServiceAndRestartCore };
|
return { uninstallServiceAndRestartCore };
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user