diff --git a/src/components/home/clash-mode-card.tsx b/src/components/home/clash-mode-card.tsx index 0ecaa1c7e..bbd942949 100644 --- a/src/components/home/clash-mode-card.tsx +++ b/src/components/home/clash-mode-card.tsx @@ -16,6 +16,7 @@ import { useCoreDataStatus, } from '@/providers/app-data-context' import { patchClashMode } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' import { setCacheData } from '@/services/query-client' import type { TranslationKey } from '@/types/generated/i18n-keys' @@ -90,7 +91,10 @@ export const ClashModeCard = () => { setOptimisticMode(mode) try { - await patchClashMode(mode) + await mutate(() => patchClashMode(mode), { + id: 'patch-clash-mode', + errorNotice: false, + }) } catch (error) { setOptimisticMode(null) showNotice.error(error) diff --git a/src/components/proxy/provider-button.tsx b/src/components/proxy/provider-button.tsx index f622f8a43..8c86ae8c2 100644 --- a/src/components/proxy/provider-button.tsx +++ b/src/components/proxy/provider-button.tsx @@ -24,6 +24,7 @@ import { updateProxyProvider } from 'tauri-plugin-mihomo-api' import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context' import { syncRuntimeProviders } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' import parseTraffic from '@/utils/parse-traffic' @@ -57,10 +58,16 @@ export const ProviderButton = () => { try { setUpdating((prev) => ({ ...prev, [name]: true })) - await updateProxyProvider(name) + await mutate(() => updateProxyProvider(name), { + id: `update-proxy-provider-${name}`, + errorNotice: false, + }) await refreshProxy() - void syncRuntimeProviders() + void mutate(() => syncRuntimeProviders(), { + id: 'sync-runtime-providers', + errorNotice: false, + }) showNotice.success( 'proxies.feedback.notifications.provider.updateSuccess', @@ -97,7 +104,10 @@ export const ProviderButton = () => { for (const name of allProviders) { try { - await updateProxyProvider(name) + await mutate(() => updateProxyProvider(name), { + id: `update-proxy-provider-${name}`, + errorNotice: false, + }) setUpdating((prev) => ({ ...prev, [name]: false })) } catch (err) { console.error(`更新 ${name} 失败`, err) @@ -105,7 +115,10 @@ export const ProviderButton = () => { } await refreshProxy() - void syncRuntimeProviders() + void mutate(() => syncRuntimeProviders(), { + id: 'sync-runtime-providers', + errorNotice: false, + }) showNotice.success('proxies.feedback.notifications.provider.allUpdated') } catch (err) { diff --git a/src/components/proxy/proxy-chain.tsx b/src/components/proxy/proxy-chain.tsx index 878453bd1..d29d9ffc5 100644 --- a/src/components/proxy/proxy-chain.tsx +++ b/src/components/proxy/proxy-chain.tsx @@ -45,6 +45,7 @@ import { useRuntimeConfig } from '@/hooks/use-clash' import { useRecordSelection } from '@/hooks/use-record-selection' import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context' import { updateProxyChainConfigInRuntime } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { selectGlobalChainNodes, selectRuleChainMembers, @@ -444,7 +445,10 @@ export const ProxyChain = ({ if (isConnected) { setIsConnecting(true) try { - await updateProxyChainConfigInRuntime(null) + await mutate(() => updateProxyChainConfigInRuntime(null), { + id: 'update-proxy-chain-runtime', + errorNotice: false, + }) const targetGroup = mode === 'global' @@ -453,12 +457,22 @@ export const ProxyChain = ({ if (targetGroup) { try { - await selectNodeForGroup(targetGroup, 'DIRECT') + await mutate(() => selectNodeForGroup(targetGroup, 'DIRECT'), { + id: 'select-proxy-node', + errorNotice: false, + }) recordSelection(targetGroup, 'DIRECT') } catch { if (currentProxyChain.length >= 1) { try { - await selectNodeForGroup(targetGroup, currentProxyChain[0].name) + await mutate( + () => + selectNodeForGroup(targetGroup, currentProxyChain[0].name), + { + id: 'select-proxy-node', + errorNotice: false, + }, + ) recordSelection(targetGroup, currentProxyChain[0].name) } catch { // ignore @@ -471,7 +485,10 @@ export const ProxyChain = ({ localStorage.removeItem('proxy-chain-exit-node') localStorage.removeItem('proxy-chain-items') - await closeAllConnections() + await mutate(() => closeAllConnections(), { + id: 'close-all-connections', + errorNotice: false, + }) await refreshProxy() onUpdateChain([]) @@ -502,7 +519,10 @@ export const ProxyChain = ({ // 第一步:保存链式代理配置 const chainProxies = currentProxyChain.map((node) => node.name) debugLog('Saving chain config:', chainProxies) - await updateProxyChainConfigInRuntime(chainProxies) + await mutate(() => updateProxyChainConfigInRuntime(chainProxies), { + id: 'update-proxy-chain-runtime', + errorNotice: false, + }) debugLog('Chain configuration saved successfully') // 第二步:连接到代理链的最后一个节点 @@ -516,7 +536,13 @@ export const ProxyChain = ({ const targetGroup = mode === 'global' ? 'GLOBAL' : selectedGroup - await selectNodeForGroup(targetGroup || 'GLOBAL', lastNode.name) + await mutate( + () => selectNodeForGroup(targetGroup || 'GLOBAL', lastNode.name), + { + id: 'select-proxy-node', + errorNotice: false, + }, + ) // The chain moves the group like any other selection, so the profile has to learn about // it: what the profile holds is what gets re-applied the next time the core starts. recordSelection(targetGroup || 'GLOBAL', lastNode.name) @@ -593,7 +619,10 @@ export const ProxyChain = ({ { - updateProxyChainConfigInRuntime(null) + mutate(() => updateProxyChainConfigInRuntime(null), { + id: 'update-proxy-chain-runtime', + errorNotice: false, + }) localStorage.removeItem('proxy-chain-group') localStorage.removeItem('proxy-chain-exit-node') localStorage.removeItem('proxy-chain-items') diff --git a/src/components/proxy/proxy-groups-chain.tsx b/src/components/proxy/proxy-groups-chain.tsx index f60c0447d..a52435091 100644 --- a/src/components/proxy/proxy-groups-chain.tsx +++ b/src/components/proxy/proxy-groups-chain.tsx @@ -24,6 +24,7 @@ import { useTranslation } from 'react-i18next' import { useProxiesData } from '@/providers/app-data-context' import { updateProxyChainConfigInRuntime } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { isInteractableMember, type ProxyGroupView, @@ -404,7 +405,10 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) { handleGroupMenuClose() if (mode === 'rule') { - updateProxyChainConfigInRuntime(null) + mutate(() => updateProxyChainConfigInRuntime(null), { + id: 'update-proxy-chain-runtime', + errorNotice: false, + }) localStorage.removeItem('proxy-chain-group') localStorage.removeItem('proxy-chain-exit-node') localStorage.removeItem('proxy-chain-items') diff --git a/src/components/rule/provider-button.tsx b/src/components/rule/provider-button.tsx index ad11cc464..c394e6569 100644 --- a/src/components/rule/provider-button.tsx +++ b/src/components/rule/provider-button.tsx @@ -23,6 +23,7 @@ import { updateRuleProvider } from 'tauri-plugin-mihomo-api' import { useAppRefreshers, useRulesData } from '@/providers/app-data-context' import { syncRuntimeProviders } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({ @@ -50,11 +51,17 @@ export const ProviderButton = () => { try { setUpdating((prev) => ({ ...prev, [name]: true })) - await updateRuleProvider(name) + await mutate(() => updateRuleProvider(name), { + id: `update-rule-provider-${name}`, + errorNotice: false, + }) await refreshRules() await refreshRuleProviders() - void syncRuntimeProviders() + void mutate(() => syncRuntimeProviders(), { + id: 'sync-runtime-providers', + errorNotice: false, + }) showNotice.success( 'rules.feedback.notifications.provider.updateSuccess', @@ -91,7 +98,10 @@ export const ProviderButton = () => { for (const name of allProviders) { try { - await updateRuleProvider(name) + await mutate(() => updateRuleProvider(name), { + id: `update-rule-provider-${name}`, + errorNotice: false, + }) setUpdating((prev) => ({ ...prev, [name]: false })) } catch (err) { console.error(`更新 ${name} 失败`, err) @@ -100,7 +110,10 @@ export const ProviderButton = () => { await refreshRules() await refreshRuleProviders() - void syncRuntimeProviders() + void mutate(() => syncRuntimeProviders(), { + id: 'sync-runtime-providers', + errorNotice: false, + }) showNotice.success('rules.feedback.notifications.provider.allUpdated') } catch (err) { diff --git a/src/components/setting/mods/lite-mode-viewer.tsx b/src/components/setting/mods/lite-mode-viewer.tsx index 02b38660c..90301668c 100644 --- a/src/components/setting/mods/lite-mode-viewer.tsx +++ b/src/components/setting/mods/lite-mode-viewer.tsx @@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next' import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base' import { useVerge } from '@/hooks/use-verge' import { entry_lightweight_mode } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' export function LiteModeViewer({ ref }: { ref?: Ref }) { @@ -72,7 +73,12 @@ export function LiteModeViewer({ ref }: { ref?: Ref }) { color: 'primary.main', '&:hover': { textDecoration: 'underline' }, }} - onClick={async () => await entry_lightweight_mode()} + onClick={async () => + await mutate(() => entry_lightweight_mode(), { + id: 'entry-lightweight-mode', + errorNotice: false, + }) + } > {t('shared.actions.enable')} diff --git a/src/hooks/use-proxy-selection.ts b/src/hooks/use-proxy-selection.ts index fe9d98400..8300c3bf2 100644 --- a/src/hooks/use-proxy-selection.ts +++ b/src/hooks/use-proxy-selection.ts @@ -12,6 +12,7 @@ import { } from '@/hooks/use-record-selection' import { useVerge } from '@/hooks/use-verge' import { syncTrayProxySelection } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { debugLog } from '@/utils/debug' // 缓存连接清理 @@ -58,7 +59,10 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => { // 切换节点 const syncTraySelection = useCallback(() => { - syncTrayProxySelection().catch((error) => { + mutate(() => syncTrayProxySelection(), { + id: 'sync-tray-selection', + errorNotice: false, + }).catch((error) => { console.error('[ProxySelection] 托盘状态同步失败:', error) }) }, []) @@ -75,9 +79,15 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => { try { if (isFixedProxy) { - await unfixedProxy(groupName) + await mutate(() => unfixedProxy(groupName), { + id: 'select-proxy-node', + errorNotice: false, + }) } else { - await selectNodeForGroup(groupName, proxyName) + await mutate(() => selectNodeForGroup(groupName, proxyName), { + id: 'select-proxy-node', + errorNotice: false, + }) } onSuccess?.() syncTraySelection() diff --git a/src/hooks/use-record-selection.ts b/src/hooks/use-record-selection.ts index bb2ed36f2..6e0af590c 100644 --- a/src/hooks/use-record-selection.ts +++ b/src/hooks/use-record-selection.ts @@ -1,6 +1,7 @@ import { useCallback } from 'react' import { forgetSelectedNode, recordSelectedNode } from '@/services/cmds' +import { mutate } from '@/services/mutate' /** * Persists every group selection because core-local state is not durable across all run modes. @@ -9,7 +10,10 @@ import { forgetSelectedNode, recordSelectedNode } from '@/services/cmds' export const useRecordSelection = () => { return useCallback(async (groupName: string, proxyName: string) => { try { - await recordSelectedNode(groupName, proxyName) + await mutate(() => recordSelectedNode(groupName, proxyName), { + id: 'record-selected-node', + errorNotice: false, + }) } catch (error) { console.error('[Selection] 保存代理选择失败:', error) } @@ -19,7 +23,10 @@ export const useRecordSelection = () => { export const useForgetSelection = () => { return useCallback(async (groupName: string) => { try { - await forgetSelectedNode(groupName) + await mutate(() => forgetSelectedNode(groupName), { + id: 'forget-selected-node', + errorNotice: false, + }) } catch (error) { console.error('[Selection] 清除代理选择失败:', error) } diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index 650ee3630..9a0158226 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -41,6 +41,7 @@ import { useConnectionData } from '@/hooks/use-connection-data' import { useConnectionSetting } from '@/hooks/use-connection-setting' import { useTrafficData } from '@/hooks/use-traffic-data' import { useVisibility } from '@/hooks/use-visibility' +import { mutate } from '@/services/mutate' import parseTraffic from '@/utils/parse-traffic' type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[] @@ -152,7 +153,12 @@ const ConnectionsPage = () => { [connectionsType, filterConn], ) - const onCloseAll = useLockFn(closeAllConnections) + const onCloseAll = useLockFn(() => + mutate(() => closeAllConnections(), { + id: 'close-all-connections', + errorNotice: false, + }), + ) const handleSearch = useCallback( (match: (content: string) => boolean, state: SearchState) => { diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 32e1aafa7..3937d4e57 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -35,6 +35,7 @@ import { ProxyTunCard } from '@/components/home/proxy-tun-card' import { useProfiles } from '@/hooks/use-profiles' import { useVerge } from '@/hooks/use-verge' import { entry_lightweight_mode } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' import { openExternalUrl } from '@/utils/open-external-url' @@ -327,7 +328,12 @@ const HomePage = () => { await entry_lightweight_mode()} + onClick={async () => + await mutate(() => entry_lightweight_mode(), { + id: 'entry-lightweight-mode', + errorNotice: false, + }) + } size="small" color="inherit" > diff --git a/src/pages/proxies.tsx b/src/pages/proxies.tsx index 89093d8f5..ac70a1c61 100644 --- a/src/pages/proxies.tsx +++ b/src/pages/proxies.tsx @@ -16,6 +16,7 @@ import { patchClashMode, updateProxyChainConfigInRuntime, } from '@/services/cmds' +import { mutate } from '@/services/mutate' import { showNotice } from '@/services/notice-service' import { debugLog } from '@/utils/debug' @@ -57,7 +58,10 @@ const ProxyPage = () => { const onChangeMode = useLockFn(async (mode: Mode) => { try { // patchClashMode 在后端 PATCH 失败时会 reject,需提示用户而非静默失败 - await patchClashMode(mode) + await mutate(() => patchClashMode(mode), { + id: 'patch-clash-mode', + errorNotice: false, + }) refreshClashConfig() } catch (error) { showNotice.error(error) @@ -75,7 +79,10 @@ const ProxyPage = () => { // 退出链式代理模式时,清除链式代理配置 try { debugLog('Exiting chain mode, clearing chain configuration') - await updateProxyChainConfigInRuntime(null) + await mutate(() => updateProxyChainConfigInRuntime(null), { + id: 'update-proxy-chain-runtime', + errorNotice: false, + }) debugLog('Chain configuration cleared successfully') } catch (error) { console.error('Failed to clear chain configuration:', error)