clash-verge-rev/src/providers/app-data-provider.tsx
Slinetrac c672a6fef3
refactor: lint (#6511)
* refactor: lint

* chore: remove eslint-plugin/config-prettier
2026-03-15 07:40:11 +00:00

340 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { listen } from '@tauri-apps/api/event'
import React, { useCallback, useEffect, useMemo } from 'react'
import useSWR from 'swr'
import {
getBaseConfig,
getRuleProviders,
getRules,
} from 'tauri-plugin-mihomo-api'
import { useVerge } from '@/hooks/use-verge'
import {
calcuProxies,
calcuProxyProviders,
getAppUptime,
getRunningMode,
getSystemProxy,
} from '@/services/cmds'
import { SWR_DEFAULTS, SWR_MIHOMO } from '@/services/config'
import { AppDataContext, AppDataContextType } from './app-data-context'
// 全局数据提供者组件
export const AppDataProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const { verge } = useVerge()
const { data: proxiesData, mutate: refreshProxy } = useSWR(
'getProxies',
calcuProxies,
SWR_MIHOMO,
)
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
'getClashConfig',
getBaseConfig,
SWR_MIHOMO,
)
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
'getProxyProviders',
calcuProxyProviders,
SWR_MIHOMO,
)
const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR(
'getRuleProviders',
getRuleProviders,
SWR_MIHOMO,
)
const { data: rulesData, mutate: refreshRules } = useSWR(
'getRules',
getRules,
SWR_MIHOMO,
)
useEffect(() => {
let lastProfileId: string | null = null
let lastUpdateTime = 0
const refreshThrottle = 800
let isUnmounted = false
const scheduledTimeouts = new Set<number>()
const cleanupFns: Array<() => void> = []
const registerCleanup = (fn: () => void) => {
if (isUnmounted) {
try {
fn()
} catch (error) {
console.error('[DataProvider] Immediate cleanup failed:', error)
}
} else {
cleanupFns.push(fn)
}
}
const addWindowListener = (eventName: string, handler: EventListener) => {
// eslint-disable-next-line @eslint-react/web-api/no-leaked-event-listener
window.addEventListener(eventName, handler)
return () => window.removeEventListener(eventName, handler)
}
const scheduleTimeout = (
callback: () => void | Promise<void>,
delay: number,
) => {
if (isUnmounted) return -1
const timeoutId = window.setTimeout(() => {
scheduledTimeouts.delete(timeoutId)
if (!isUnmounted) {
void callback()
}
}, delay)
scheduledTimeouts.add(timeoutId)
return timeoutId
}
const clearAllTimeouts = () => {
scheduledTimeouts.forEach((timeoutId) => clearTimeout(timeoutId))
scheduledTimeouts.clear()
}
const handleProfileChanged = (event: { payload: string }) => {
const newProfileId = event.payload
const now = Date.now()
if (
lastProfileId === newProfileId &&
now - lastUpdateTime < refreshThrottle
) {
return
}
lastProfileId = newProfileId
lastUpdateTime = now
scheduleTimeout(() => {
refreshRules().catch((error) =>
console.warn('[DataProvider] Rules refresh failed:', error),
)
refreshRuleProviders().catch((error) =>
console.warn('[DataProvider] Rule providers refresh failed:', error),
)
}, 200)
}
const handleRefreshClash = () => {
const now = Date.now()
if (now - lastUpdateTime <= refreshThrottle) return
lastUpdateTime = now
scheduleTimeout(async () => {
await Promise.all([
refreshProxy().catch((error) =>
console.error('[DataProvider] Proxy refresh failed:', error),
),
refreshClashConfig().catch((error) =>
console.error('[DataProvider] Clash config refresh failed:', error),
),
])
}, 200)
}
const handleRefreshProxy = () => {
const now = Date.now()
if (now - lastUpdateTime <= refreshThrottle) return
lastUpdateTime = now
scheduleTimeout(() => {
refreshProxy().catch((error) =>
console.warn('[DataProvider] Proxy refresh failed:', error),
)
}, 200)
}
const initializeListeners = async () => {
try {
const unlistenProfile = await listen<string>(
'profile-changed',
handleProfileChanged,
)
registerCleanup(unlistenProfile)
} catch (error) {
console.error('[AppDataProvider] 监听 Profile 事件失败:', error)
}
try {
const unlistenClash = await listen(
'verge://refresh-clash-config',
handleRefreshClash,
)
const unlistenProxy = await listen(
'verge://refresh-proxy-config',
handleRefreshProxy,
)
registerCleanup(() => {
unlistenClash()
unlistenProxy()
})
} catch (error) {
console.warn('[AppDataProvider] 设置 Tauri 事件监听器失败:', error)
const fallbackHandlers: Array<[string, EventListener]> = [
['verge://refresh-clash-config', handleRefreshClash],
['verge://refresh-proxy-config', handleRefreshProxy],
]
fallbackHandlers.forEach(([eventName, handler]) => {
registerCleanup(addWindowListener(eventName, handler))
})
}
}
void initializeListeners()
return () => {
isUnmounted = true
clearAllTimeouts()
const errors: Error[] = []
cleanupFns.splice(0).forEach((fn) => {
try {
fn()
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)))
}
})
if (errors.length > 0) {
console.error(
`[DataProvider] ${errors.length} errors during cleanup:`,
errors,
)
}
}
}, [refreshProxy, refreshClashConfig, refreshRules, refreshRuleProviders])
const { data: sysproxy, mutate: refreshSysproxy } = useSWR(
'getSystemProxy',
getSystemProxy,
SWR_DEFAULTS,
)
const { data: runningMode } = useSWR(
'getRunningMode',
getRunningMode,
SWR_DEFAULTS,
)
const { data: uptimeData } = useSWR('appUptime', getAppUptime, {
...SWR_DEFAULTS,
refreshInterval: 3000,
errorRetryCount: 1,
})
// 提供统一的刷新方法
const refreshAll = useCallback(async () => {
await Promise.all([
refreshProxy(),
refreshClashConfig(),
refreshRules(),
refreshSysproxy(),
refreshProxyProviders(),
refreshRuleProviders(),
])
}, [
refreshProxy,
refreshClashConfig,
refreshRules,
refreshSysproxy,
refreshProxyProviders,
refreshRuleProviders,
])
// 聚合所有数据
const value = useMemo(() => {
// 计算系统代理地址
const calculateSystemProxyAddress = () => {
if (!verge || !clashConfig) return '-'
const isPacMode = verge.proxy_auto_config ?? false
if (isPacMode) {
// PAC模式显示我们期望设置的代理地址
const proxyHost = verge.proxy_host || '127.0.0.1'
const proxyPort =
verge.verge_mixed_port || clashConfig.mixedPort || 7897
return `${proxyHost}:${proxyPort}`
} else {
// HTTP代理模式优先使用系统地址但如果格式不正确则使用期望地址
const systemServer = sysproxy?.server
if (
systemServer &&
systemServer !== '-' &&
!systemServer.startsWith(':')
) {
return systemServer
} else {
// 系统地址无效,返回期望的代理地址
const proxyHost = verge.proxy_host || '127.0.0.1'
const proxyPort =
verge.verge_mixed_port || clashConfig.mixedPort || 7897
return `${proxyHost}:${proxyPort}`
}
}
}
return {
// 数据
proxies: proxiesData,
clashConfig,
rules: rulesData?.rules ?? [],
sysproxy,
runningMode,
uptime: uptimeData || 0,
// 提供者数据
proxyProviders: proxyProviders || {},
ruleProviders: ruleProviders?.providers || {},
systemProxyAddress: calculateSystemProxyAddress(),
// 刷新方法
refreshProxy,
refreshClashConfig,
refreshRules,
refreshSysproxy,
refreshProxyProviders,
refreshRuleProviders,
refreshAll,
} as AppDataContextType
}, [
proxiesData,
clashConfig,
rulesData,
sysproxy,
runningMode,
uptimeData,
proxyProviders,
ruleProviders,
verge,
refreshProxy,
refreshClashConfig,
refreshRules,
refreshSysproxy,
refreshProxyProviders,
refreshRuleProviders,
refreshAll,
])
return <AppDataContext value={value}>{children}</AppDataContext>
}