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:
oomeow
2025-10-31 17:31:40 +08:00
committed by GitHub
parent 5187712a71
commit 8c734a5a35
11 changed files with 175 additions and 197 deletions

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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);
} }
} }
} }

View File

@@ -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(())
} }
} }

View File

@@ -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()),
)?; )?;

View File

@@ -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);
} }
} }
} }

View File

@@ -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}

View File

@@ -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,
}; };
} }

View File

@@ -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,

View File

@@ -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 };
}; };

View File

@@ -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 };
}; };