diff --git a/Changelog.md b/Changelog.md index b649ddbeb..0041bf976 100644 --- a/Changelog.md +++ b/Changelog.md @@ -34,6 +34,7 @@ - 修复 Linux 下无法安装 TUN 服务 - 修复可能的端口被占用误报 - 修复设置允许外部控制来源不能立即生效 +- 修复前端性能回归问题
✨ 新增功能 diff --git a/src/components/home/clash-info-card.tsx b/src/components/home/clash-info-card.tsx index 7a4bff47f..ed62732f5 100644 --- a/src/components/home/clash-info-card.tsx +++ b/src/components/home/clash-info-card.tsx @@ -4,13 +4,7 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useClash } from "@/hooks/use-clash"; -import { - useAppUptime, - useClashConfig, - useRulesData, - useSystemProxyAddress, - useSystemProxyData, -} from "@/hooks/use-clash-data"; +import { useAppData } from "@/providers/app-data-context"; import { EnhancedCard } from "./enhanced-card"; @@ -25,14 +19,7 @@ const formatUptime = (uptimeMs: number) => { export const ClashInfoCard = () => { const { t } = useTranslation(); const { version: clashVersion } = useClash(); - const { clashConfig } = useClashConfig(); - const { sysproxy } = useSystemProxyData(); - const { rules } = useRulesData(); - const { uptime } = useAppUptime(); - const systemProxyAddress = useSystemProxyAddress({ - clashConfig, - sysproxy, - }); + const { clashConfig, rules, uptime, systemProxyAddress } = useAppData(); // 使用useMemo缓存格式化后的uptime,避免频繁计算 const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]); diff --git a/src/components/home/clash-mode-card.tsx b/src/components/home/clash-mode-card.tsx index e6fb20841..ea48128ca 100644 --- a/src/components/home/clash-mode-card.tsx +++ b/src/components/home/clash-mode-card.tsx @@ -9,8 +9,8 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { closeAllConnections } from "tauri-plugin-mihomo-api"; -import { useClashConfig } from "@/hooks/use-clash-data"; import { useVerge } from "@/hooks/use-verge"; +import { useAppData } from "@/providers/app-data-context"; import { patchClashMode } from "@/services/cmds"; import type { TranslationKey } from "@/types/generated/i18n-keys"; @@ -41,7 +41,7 @@ const MODE_META: Record< export const ClashModeCard = () => { const { t } = useTranslation(); const { verge } = useVerge(); - const { clashConfig, refreshClashConfig } = useClashConfig(); + const { clashConfig, refreshClashConfig } = useAppData(); // 支持的模式列表 const modeList = CLASH_MODES; diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index 628fcf906..c192f1e53 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -34,14 +34,10 @@ import { useNavigate } from "react-router"; import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api"; import { EnhancedCard } from "@/components/home/enhanced-card"; -import { - useClashConfig, - useProxiesData, - useRulesData, -} from "@/hooks/use-clash-data"; import { useProfiles } from "@/hooks/use-profiles"; import { useProxySelection } from "@/hooks/use-proxy-selection"; import { useVerge } from "@/hooks/use-verge"; +import { useAppData } from "@/providers/app-data-context"; import delayManager from "@/services/delay"; import { debugLog } from "@/utils/debug"; @@ -105,9 +101,7 @@ export const CurrentProxyCard = () => { const { t } = useTranslation(); const navigate = useNavigate(); const theme = useTheme(); - const { proxies, refreshProxy } = useProxiesData(); - const { clashConfig } = useClashConfig(); - const { rules } = useRulesData(); + const { proxies, clashConfig, refreshProxy, rules } = useAppData(); const { verge } = useVerge(); const { current: currentProfile } = useProfiles(); const autoDelayEnabled = verge?.enable_auto_delay_detection ?? false; diff --git a/src/components/home/home-profile-card.tsx b/src/components/home/home-profile-card.tsx index e92794ba4..a3ee76f7f 100644 --- a/src/components/home/home-profile-card.tsx +++ b/src/components/home/home-profile-card.tsx @@ -24,7 +24,7 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; -import { useRefreshAll } from "@/hooks/use-clash-data"; +import { useAppData } from "@/providers/app-data-context"; import { openWebUrl, updateProfile } from "@/services/cmds"; import { showNotice } from "@/services/notice-service"; import parseTraffic from "@/utils/parse-traffic"; @@ -281,7 +281,7 @@ export const HomeProfileCard = ({ }: HomeProfileCardProps) => { const { t } = useTranslation(); const navigate = useNavigate(); - const refreshAll = useRefreshAll(); + const { refreshAll } = useAppData(); // 更新当前订阅 const [updating, setUpdating] = useState(false); diff --git a/src/components/proxy/provider-button.tsx b/src/components/proxy/provider-button.tsx index 900901612..5b2ea4302 100644 --- a/src/components/proxy/provider-button.tsx +++ b/src/components/proxy/provider-button.tsx @@ -22,7 +22,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { updateProxyProvider } from "tauri-plugin-mihomo-api"; -import { useProxiesData, useProxyProvidersData } from "@/hooks/use-clash-data"; +import { useAppData } from "@/providers/app-data-context"; import { showNotice } from "@/services/notice-service"; import parseTraffic from "@/utils/parse-traffic"; @@ -48,8 +48,7 @@ const parseExpire = (expire?: number) => { export const ProviderButton = () => { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const { proxyProviders, refreshProxyProviders } = useProxyProvidersData(); - const { refreshProxy } = useProxiesData(); + const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData(); const [updating, setUpdating] = useState>({}); // 检查是否有提供者 @@ -176,8 +175,8 @@ export const ProviderButton = () => { {Object.entries(proxyProviders || {}) .sort() - .map(([key, provider]) => { - if (!provider) return null; + .map(([key, item]) => { + const provider = item; const time = dayjs(provider.updatedAt); const isUpdating = updating[key]; diff --git a/src/components/proxy/proxy-chain.tsx b/src/components/proxy/proxy-chain.tsx index acadb63b4..c26d14a3d 100644 --- a/src/components/proxy/proxy-chain.tsx +++ b/src/components/proxy/proxy-chain.tsx @@ -38,7 +38,7 @@ import { selectNodeForGroup, } from "tauri-plugin-mihomo-api"; -import { useProxiesData } from "@/hooks/use-clash-data"; +import { useAppData } from "@/providers/app-data-context"; import { updateProxyChainConfigInRuntime } from "@/services/cmds"; import { debugLog } from "@/utils/debug"; @@ -199,7 +199,7 @@ export const ProxyChain = ({ }: ProxyChainProps) => { const theme = useTheme(); const { t } = useTranslation(); - const { proxies, refreshProxy } = useProxiesData(); + const { proxies, refreshProxy } = useAppData(); const [isConnecting, setIsConnecting] = useState(false); const markUnsavedChanges = useCallback(() => { onMarkUnsavedChanges?.(); @@ -221,7 +221,7 @@ export const ProxyChain = ({ } const proxyChainGroup = proxies.groups.find( - (group) => group.name === selectedGroup, + (group: { name: string }) => group.name === selectedGroup, ); return proxyChainGroup?.now === lastNode.name; diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx index ee7636f3f..a2b8827cd 100644 --- a/src/components/proxy/proxy-groups.tsx +++ b/src/components/proxy/proxy-groups.tsx @@ -16,9 +16,9 @@ import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api"; import { BaseEmpty } from "@/components/base"; -import { useProxiesData } from "@/hooks/use-clash-data"; import { useProxySelection } from "@/hooks/use-proxy-selection"; import { useVerge } from "@/hooks/use-verge"; +import { useAppData } from "@/providers/app-data-context"; import { updateProxyChainConfigInRuntime } from "@/services/cmds"; import delayManager from "@/services/delay"; import { debugLog } from "@/utils/debug"; @@ -80,7 +80,7 @@ export const ProxyGroups = (props: Props) => { }>({ open: false, message: "" }); const { verge } = useVerge(); - const { proxies: proxiesData } = useProxiesData(); + const { proxies: proxiesData } = useAppData(); const groups = proxiesData?.groups; const availableGroups = useMemo(() => { if (!groups) return []; diff --git a/src/components/proxy/use-render-list.ts b/src/components/proxy/use-render-list.ts index a9c267f30..a1333d6d5 100644 --- a/src/components/proxy/use-render-list.ts +++ b/src/components/proxy/use-render-list.ts @@ -1,8 +1,8 @@ import { useEffect, useMemo } from "react"; import useSWR from "swr"; -import { useProxiesData } from "@/hooks/use-clash-data"; import { useVerge } from "@/hooks/use-verge"; +import { useAppData } from "@/providers/app-data-context"; import { getRuntimeConfig } from "@/services/cmds"; import delayManager from "@/services/delay"; import { debugLog } from "@/utils/debug"; @@ -34,8 +34,24 @@ interface IProxyItem { } // 代理组类型 -type ProxyGroup = IProxyGroupItem & { - now?: string; +type ProxyGroup = { + name: string; + type: string; + udp: boolean; + xudp: boolean; + tfo: boolean; + mptcp: boolean; + smux: boolean; + history: { + time: string; + delay: number; + }[]; + now: string; + all: IProxyItem[]; + hidden?: boolean; + icon?: string; + testUrl?: string; + provider?: string; }; export interface IRenderItem { @@ -84,7 +100,7 @@ export const useRenderList = ( selectedGroup?: string | null, ) => { // 使用全局数据提供者 - const { proxies: proxiesData, refreshProxy } = useProxiesData(); + const { proxies: proxiesData, refreshProxy } = useAppData(); const { verge } = useVerge(); const { width } = useWindowWidth(); const [headStates, setHeadState] = useHeadStateNew(); diff --git a/src/components/rule/provider-button.tsx b/src/components/rule/provider-button.tsx index 2db852482..829dbc28d 100644 --- a/src/components/rule/provider-button.tsx +++ b/src/components/rule/provider-button.tsx @@ -21,10 +21,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { updateRuleProvider } from "tauri-plugin-mihomo-api"; -import type { - useRuleProvidersData, - useRulesData, -} from "@/hooks/use-clash-data"; +import { useAppData } from "@/providers/app-data-context"; import { showNotice } from "@/services/notice-service"; // 辅助组件 - 类型框 @@ -40,22 +37,10 @@ const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({ lineHeight: 1.25, })); -type RuleProvidersHook = ReturnType; -type RulesHook = ReturnType; - -interface ProviderButtonProps { - ruleProviders: RuleProvidersHook["ruleProviders"]; - refreshRuleProviders: RuleProvidersHook["refreshRuleProviders"]; - refreshRules: RulesHook["refreshRules"]; -} - -export const ProviderButton = ({ - ruleProviders, - refreshRuleProviders, - refreshRules, -}: ProviderButtonProps) => { +export const ProviderButton = () => { const { t } = useTranslation(); const [open, setOpen] = useState(false); + const { ruleProviders, refreshRules, refreshRuleProviders } = useAppData(); const [updating, setUpdating] = useState>({}); // 检查是否有提供者 @@ -178,8 +163,8 @@ export const ProviderButton = ({ {Object.entries(ruleProviders || {}) .sort() - .map(([key, provider]) => { - if (!provider) return null; + .map(([key, item]) => { + const provider = item; const time = dayjs(provider.updatedAt); const isUpdating = updating[key]; diff --git a/src/components/setting/mods/sysproxy-viewer.tsx b/src/components/setting/mods/sysproxy-viewer.tsx index ebb3aa045..9635c0056 100644 --- a/src/components/setting/mods/sysproxy-viewer.tsx +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -22,7 +22,8 @@ import { useState, } from "react"; import { useTranslation } from "react-i18next"; -import { mutate } from "swr"; +import useSWR, { mutate } from "swr"; +import { getBaseConfig } from "tauri-plugin-mihomo-api"; import { BaseDialog, @@ -33,12 +34,8 @@ import { TooltipIcon, } from "@/components/base"; import { EditorViewer } from "@/components/profile/editor-viewer"; -import { - useClashConfig, - useSystemProxyAddress, - useSystemProxyData, -} from "@/hooks/use-clash-data"; import { useVerge } from "@/hooks/use-verge"; +import { useAppData } from "@/providers/app-data-context"; import { getAutotemProxy, getNetworkInterfacesInfo, @@ -110,6 +107,9 @@ export const SysproxyViewer = forwardRef((props, ref) => { const { verge, patchVerge, mutateVerge } = useVerge(); const [hostOptions, setHostOptions] = useState([]); + type SysProxy = Awaited>; + const [sysproxy, setSysproxy] = useState(); + type AutoProxy = Awaited>; const [autoproxy, setAutoproxy] = useState(); @@ -148,8 +148,12 @@ export const SysproxyViewer = forwardRef((props, ref) => { return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,"; }; - const { clashConfig } = useClashConfig(); - const { sysproxy, refreshSysproxy } = useSystemProxyData(); + const { data: clashConfig } = useSWR("getClashConfig", getBaseConfig, { + revalidateOnFocus: false, + revalidateIfStale: true, + dedupingInterval: 1000, + errorRetryInterval: 5000, + }); const prevMixedPortRef = useRef(clashConfig?.mixedPort); @@ -183,10 +187,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { updateProxy(); }, [clashConfig?.mixedPort, value.pac]); - const systemProxyAddress = useSystemProxyAddress({ - clashConfig, - sysproxy, - }); + const { systemProxyAddress } = useAppData(); // 为当前状态计算系统代理地址 const getSystemProxyAddress = useMemo(() => { @@ -236,7 +237,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { pac_content: pac_file_content ?? DEFAULT_PAC, proxy_host: proxy_host ?? "127.0.0.1", }); - void refreshSysproxy(); + getSystemProxy().then((p) => setSysproxy(p)); getAutotemProxy().then((p) => setAutoproxy(p)); fetchNetworkInterfaces(); }, diff --git a/src/hooks/use-clash-data.ts b/src/hooks/use-clash-data.ts deleted file mode 100644 index cfd113058..000000000 --- a/src/hooks/use-clash-data.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { useCallback, useMemo } from "react"; -import useSWR, { useSWRConfig } from "swr"; -import { - getBaseConfig, - getRuleProviders, - getRules, -} from "tauri-plugin-mihomo-api"; - -import { - calcuProxies, - calcuProxyProviders, - getAppUptime, - getSystemProxy, -} from "@/services/cmds"; -import { SWR_DEFAULTS, SWR_REALTIME, SWR_SLOW_POLL } from "@/services/config"; - -import { useSharedSWRPoller } from "./use-shared-swr-poller"; -import { useVerge } from "./use-verge"; - -export const useProxiesData = () => { - const { mutate: globalMutate } = useSWRConfig(); - const { data, error, isLoading } = useSWR("getProxies", calcuProxies, { - ...SWR_REALTIME, - refreshInterval: 0, - onError: (err) => console.warn("[AppData] Proxy fetch failed:", err), - }); - - const refreshProxy = useCallback( - () => globalMutate("getProxies"), - [globalMutate], - ); - const pollerRefresh = useCallback(() => { - void globalMutate("getProxies"); - }, [globalMutate]); - - useSharedSWRPoller("getProxies", SWR_REALTIME.refreshInterval, pollerRefresh); - - return { - proxies: data, - refreshProxy, - isLoading, - error, - }; -}; - -export const useClashConfig = () => { - const { mutate: globalMutate } = useSWRConfig(); - const { data, error, isLoading } = useSWR("getClashConfig", getBaseConfig, { - ...SWR_SLOW_POLL, - refreshInterval: 0, - }); - - const refreshClashConfig = useCallback( - () => globalMutate("getClashConfig"), - [globalMutate], - ); - const pollerRefresh = useCallback(() => { - void globalMutate("getClashConfig"); - }, [globalMutate]); - - useSharedSWRPoller( - "getClashConfig", - SWR_SLOW_POLL.refreshInterval, - pollerRefresh, - ); - - return { - clashConfig: data, - refreshClashConfig, - isLoading, - error, - }; -}; - -export const useProxyProvidersData = () => { - const { data, error, isLoading, mutate } = useSWR( - "getProxyProviders", - calcuProxyProviders, - SWR_DEFAULTS, - ); - - const refreshProxyProviders = useCallback(() => mutate(), [mutate]); - - return { - proxyProviders: data || {}, - refreshProxyProviders, - isLoading, - error, - }; -}; - -export const useRuleProvidersData = () => { - const { data, error, isLoading, mutate } = useSWR( - "getRuleProviders", - getRuleProviders, - SWR_DEFAULTS, - ); - - const refreshRuleProviders = useCallback(() => mutate(), [mutate]); - - return { - ruleProviders: data?.providers || {}, - refreshRuleProviders, - isLoading, - error, - }; -}; - -export const useRulesData = () => { - const { data, error, isLoading, mutate } = useSWR( - "getRules", - getRules, - SWR_DEFAULTS, - ); - - const refreshRules = useCallback(() => mutate(), [mutate]); - - return { - rules: data?.rules || [], - refreshRules, - isLoading, - error, - }; -}; - -export const useSystemProxyData = () => { - const { data, error, isLoading, mutate } = useSWR( - "getSystemProxy", - getSystemProxy, - SWR_DEFAULTS, - ); - - const refreshSysproxy = useCallback(() => mutate(), [mutate]); - - return { - sysproxy: data, - refreshSysproxy, - isLoading, - error, - }; -}; - -type ClashConfig = Awaited>; -type SystemProxy = Awaited>; - -interface SystemProxyAddressParams { - clashConfig?: ClashConfig | null; - sysproxy?: SystemProxy | null; -} - -export const useSystemProxyAddress = ({ - clashConfig, - sysproxy, -}: SystemProxyAddressParams) => { - const { verge } = useVerge(); - - return useMemo(() => { - if (!verge || !clashConfig) return "-"; - - const isPacMode = verge.proxy_auto_config ?? false; - - if (isPacMode) { - const proxyHost = verge.proxy_host || "127.0.0.1"; - const proxyPort = verge.verge_mixed_port || clashConfig.mixedPort || 7897; - return [proxyHost, proxyPort].join(":"); - } - - const systemServer = sysproxy?.server; - if (systemServer && systemServer !== "-" && !systemServer.startsWith(":")) { - return systemServer; - } - - const proxyHost = verge.proxy_host || "127.0.0.1"; - const proxyPort = verge.verge_mixed_port || clashConfig.mixedPort || 7897; - return [proxyHost, proxyPort].join(":"); - }, [clashConfig, sysproxy, verge]); -}; - -export const useAppUptime = () => { - const { data, error, isLoading } = useSWR("appUptime", getAppUptime, { - ...SWR_DEFAULTS, - refreshInterval: 3000, - errorRetryCount: 1, - }); - - return { - uptime: data || 0, - error, - isLoading, - }; -}; - -export const useRefreshAll = () => { - const { mutate } = useSWRConfig(); - - return useCallback(async () => { - await Promise.all([ - mutate("getProxies"), - mutate("getClashConfig"), - mutate("getRules"), - mutate("getSystemProxy"), - mutate("getProxyProviders"), - mutate("getRuleProviders"), - ]); - }, [mutate]); -}; diff --git a/src/hooks/use-current-proxy.ts b/src/hooks/use-current-proxy.ts new file mode 100644 index 000000000..7d3523269 --- /dev/null +++ b/src/hooks/use-current-proxy.ts @@ -0,0 +1,76 @@ +import { useMemo } from "react"; + +import { useAppData } from "@/providers/app-data-context"; + +// 定义代理组类型 +interface ProxyGroup { + name: string; + now: string; +} + +// 获取当前代理节点信息的自定义Hook +export const useCurrentProxy = () => { + // 从AppDataProvider获取数据 + const { proxies, clashConfig, refreshProxy } = useAppData(); + + // 获取当前模式 + const currentMode = clashConfig?.mode?.toLowerCase() || "rule"; + + // 获取当前代理节点信息 + const currentProxyInfo = useMemo(() => { + if (!proxies) return { currentProxy: null, primaryGroupName: null }; + + const { global, groups, records } = proxies; + + // 默认信息 + let primaryGroupName = "GLOBAL"; + let currentName = global?.now; + + // 在规则模式下,寻找主要代理组(通常是第一个或者名字包含特定关键词的组) + if (currentMode === "rule" && groups.length > 0) { + // 查找主要的代理组(优先级:包含关键词 > 第一个非GLOBAL组) + const primaryKeywords = [ + "auto", + "select", + "proxy", + "节点选择", + "自动选择", + ]; + const primaryGroup = + groups.find((group: ProxyGroup) => + primaryKeywords.some((keyword) => + group.name.toLowerCase().includes(keyword.toLowerCase()), + ), + ) || groups.filter((g: ProxyGroup) => g.name !== "GLOBAL")[0]; + + if (primaryGroup) { + primaryGroupName = primaryGroup.name; + currentName = primaryGroup.now; + } + } + + // 如果找不到当前节点,返回null + if (!currentName) return { currentProxy: null, primaryGroupName }; + + // 获取完整的节点信息 + const currentProxy = records[currentName] || { + name: currentName, + type: "Unknown", + udp: false, + xudp: false, + tfo: false, + mptcp: false, + smux: false, + history: [], + }; + + return { currentProxy, primaryGroupName }; + }, [proxies, currentMode]); + + return { + currentProxy: currentProxyInfo.currentProxy, + primaryGroupName: currentProxyInfo.primaryGroupName, + mode: currentMode, + refreshProxy, + }; +}; diff --git a/src/hooks/use-shared-swr-poller.ts b/src/hooks/use-shared-swr-poller.ts deleted file mode 100644 index 8829bde18..000000000 --- a/src/hooks/use-shared-swr-poller.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { useEffect } from "react"; -import type { Key } from "swr"; - -type SharedPollerEntry = { - subscribers: number; - timer: number | null; - interval: number; - callback: (() => void) | null; - lastFired: number; - refreshWhenHidden: boolean; - refreshWhenOffline: boolean; -}; - -const sharedPollers = new Map(); - -const isDocumentHidden = () => { - if (typeof document === "undefined") return false; - return document.visibilityState === "hidden"; -}; - -const isOffline = () => { - if (typeof navigator === "undefined") return false; - return navigator.onLine === false; -}; - -const ensureTimer = (key: string, entry: SharedPollerEntry) => { - if (typeof window === "undefined") return; - - if (entry.timer !== null) { - clearInterval(entry.timer); - } - - entry.timer = window.setInterval(() => { - if (!entry.refreshWhenHidden && isDocumentHidden()) return; - if (!entry.refreshWhenOffline && isOffline()) return; - const now = Date.now(); - if (entry.lastFired && now - entry.lastFired < entry.interval / 2) { - // Skip duplicate fire within half interval to coalesce concurrent consumers - return; - } - entry.lastFired = now; - entry.callback?.(); - }, entry.interval); -}; - -const registerSharedPoller = ( - key: string, - interval: number, - callback: () => void, - options: { refreshWhenHidden: boolean; refreshWhenOffline: boolean }, -) => { - let entry = sharedPollers.get(key); - - if (!entry) { - entry = { - subscribers: 0, - timer: null, - interval, - callback, - lastFired: 0, - refreshWhenHidden: options.refreshWhenHidden, - refreshWhenOffline: options.refreshWhenOffline, - }; - sharedPollers.set(key, entry); - } - - entry.subscribers += 1; - entry.callback = callback; - entry.interval = Math.min(entry.interval, interval); - entry.refreshWhenHidden = - entry.refreshWhenHidden || options.refreshWhenHidden; - entry.refreshWhenOffline = - entry.refreshWhenOffline || options.refreshWhenOffline; - - ensureTimer(key, entry); - - return () => { - const current = sharedPollers.get(key); - if (!current) return; - - current.subscribers -= 1; - if (current.subscribers <= 0) { - if (current.timer !== null) { - clearInterval(current.timer); - } - sharedPollers.delete(key); - } - }; -}; - -const normalizeKey = (key: Key): string | null => { - if (typeof key === "string") return key; - if (typeof key === "number" || typeof key === "boolean") return String(key); - if (Array.isArray(key)) { - try { - return JSON.stringify(key); - } catch { - return null; - } - } - return null; -}; - -export interface SharedSWRPollerOptions { - refreshWhenHidden?: boolean; - refreshWhenOffline?: boolean; -} - -export const useSharedSWRPoller = ( - key: Key, - interval?: number, - callback?: () => void, - options?: SharedSWRPollerOptions, -) => { - const refreshWhenHidden = options?.refreshWhenHidden ?? false; - const refreshWhenOffline = options?.refreshWhenOffline ?? false; - - useEffect(() => { - if (!key || !interval || interval <= 0 || !callback) return; - - const serializedKey = normalizeKey(key); - if (!serializedKey) return; - - return registerSharedPoller(serializedKey, interval, callback, { - refreshWhenHidden, - refreshWhenOffline, - }); - }, [key, interval, callback, refreshWhenHidden, refreshWhenOffline]); -}; diff --git a/src/hooks/use-system-proxy-state.ts b/src/hooks/use-system-proxy-state.ts index b6dc94752..a04e12377 100644 --- a/src/hooks/use-system-proxy-state.ts +++ b/src/hooks/use-system-proxy-state.ts @@ -2,14 +2,14 @@ import { useLockFn } from "ahooks"; import useSWR, { mutate } from "swr"; import { closeAllConnections } from "tauri-plugin-mihomo-api"; -import { useSystemProxyData } from "@/hooks/use-clash-data"; import { useVerge } from "@/hooks/use-verge"; +import { useAppData } from "@/providers/app-data-context"; import { getAutotemProxy } from "@/services/cmds"; // 系统代理状态检测统一逻辑 export const useSystemProxyState = () => { const { verge, mutateVerge, patchVerge } = useVerge(); - const { sysproxy } = useSystemProxyData(); + const { sysproxy } = useAppData(); const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy, { revalidateOnFocus: true, revalidateOnReconnect: true, diff --git a/src/hooks/use-system-state.ts b/src/hooks/use-system-state.ts index 37dc4000e..58f14d55a 100644 --- a/src/hooks/use-system-state.ts +++ b/src/hooks/use-system-state.ts @@ -4,7 +4,6 @@ import useSWR from "swr"; import { getRunningMode, isAdmin, isServiceAvailable } from "@/services/cmds"; import { showNotice } from "@/services/notice-service"; -import { useSharedSWRPoller } from "./use-shared-swr-poller"; import { useVerge } from "./use-verge"; export interface SystemState { @@ -44,20 +43,11 @@ export function useSystemState() { }, { suspense: true, - refreshInterval: 0, + refreshInterval: 30000, fallback: defaultSystemState, }, ); - useSharedSWRPoller( - "getSystemState", - 30000, - () => { - void mutateSystemState(); - }, - { refreshWhenHidden: false, refreshWhenOffline: false }, - ); - const isSidecarMode = systemState.runningMode === "Sidecar"; const isServiceMode = systemState.runningMode === "Service"; const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk; diff --git a/src/pages/rules.tsx b/src/pages/rules.tsx index 026539423..019a01d6b 100644 --- a/src/pages/rules.tsx +++ b/src/pages/rules.tsx @@ -7,13 +7,12 @@ import { BaseEmpty, BasePage, BaseSearchBox } from "@/components/base"; import { ScrollTopButton } from "@/components/layout/scroll-top-button"; import { ProviderButton } from "@/components/rule/provider-button"; import RuleItem from "@/components/rule/rule-item"; -import { useRuleProvidersData, useRulesData } from "@/hooks/use-clash-data"; import { useVisibility } from "@/hooks/use-visibility"; +import { useAppData } from "@/providers/app-data-context"; const RulesPage = () => { const { t } = useTranslation(); - const { rules = [], refreshRules } = useRulesData(); - const { ruleProviders, refreshRuleProviders } = useRuleProvidersData(); + const { rules = [], refreshRules, refreshRuleProviders } = useAppData(); const [match, setMatch] = useState(() => (_: string) => true); const virtuosoRef = useRef(null); const [showScrollTop, setShowScrollTop] = useState(false); @@ -57,11 +56,7 @@ const RulesPage = () => { }} header={ - + } > diff --git a/src/providers/app-data-context.ts b/src/providers/app-data-context.ts new file mode 100644 index 000000000..7b7244aba --- /dev/null +++ b/src/providers/app-data-context.ts @@ -0,0 +1,51 @@ +import { createContext, use } from "react"; +import { + BaseConfig, + ProxyProvider, + Rule, + RuleProvider, +} from "tauri-plugin-mihomo-api"; + +export interface AppDataContextType { + proxies: any; + clashConfig: BaseConfig; + rules: Rule[]; + sysproxy: any; + runningMode?: string; + uptime: number; + proxyProviders: Record; + ruleProviders: Record; + systemProxyAddress: string; + + refreshProxy: () => Promise; + refreshClashConfig: () => Promise; + refreshRules: () => Promise; + refreshSysproxy: () => Promise; + refreshProxyProviders: () => Promise; + refreshRuleProviders: () => Promise; + refreshAll: () => Promise; +} + +export interface ConnectionWithSpeed extends IConnectionsItem { + curUpload: number; + curDownload: number; +} + +export interface ConnectionSpeedData { + id: string; + upload: number; + download: number; + timestamp: number; +} + +export const AppDataContext = createContext(null); + +export const useAppData = () => { + const context = use(AppDataContext); + + if (!context) { + throw new Error("useAppData必须在AppDataProvider内使用"); + } + + return context; +}; diff --git a/src/providers/app-data-provider.tsx b/src/providers/app-data-provider.tsx index 7a7c80e46..b7898ddc1 100644 --- a/src/providers/app-data-provider.tsx +++ b/src/providers/app-data-provider.tsx @@ -1,25 +1,63 @@ import { listen } from "@tauri-apps/api/event"; -import { PropsWithChildren, useCallback, useEffect } from "react"; -import { useSWRConfig } from "swr"; +import React, { useCallback, useEffect, useMemo } from "react"; +import useSWR from "swr"; +import { + getBaseConfig, + getRuleProviders, + getRules, +} from "tauri-plugin-mihomo-api"; -// 负责监听全局事件并驱动 SWR 刷新,避免包裹全局 context 带来的额外渲染 -export const AppDataProvider = ({ children }: PropsWithChildren) => { - useAppDataEventBridge(); - return <>{children}; -}; +import { useVerge } from "@/hooks/use-verge"; +import { + calcuProxies, + calcuProxyProviders, + getAppUptime, + getRunningMode, + getSystemProxy, +} from "@/services/cmds"; +import { SWR_DEFAULTS, SWR_REALTIME, SWR_SLOW_POLL } from "@/services/config"; -const useAppDataEventBridge = () => { - const { mutate } = useSWRConfig(); +import { AppDataContext, AppDataContextType } from "./app-data-context"; - const refreshProxy = useCallback(() => mutate("getProxies"), [mutate]); - const refreshClashConfig = useCallback( - () => mutate("getClashConfig"), - [mutate], +// 全局数据提供者组件 +export const AppDataProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { verge } = useVerge(); + + const { data: proxiesData, mutate: refreshProxy } = useSWR( + "getProxies", + calcuProxies, + { + ...SWR_REALTIME, + onError: (err) => console.warn("[DataProvider] Proxy fetch failed:", err), + }, ); - const refreshRules = useCallback(() => mutate("getRules"), [mutate]); - const refreshRuleProviders = useCallback( - () => mutate("getRuleProviders"), - [mutate], + + const { data: clashConfig, mutate: refreshClashConfig } = useSWR( + "getClashConfig", + getBaseConfig, + SWR_SLOW_POLL, + ); + + const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR( + "getProxyProviders", + calcuProxyProviders, + SWR_DEFAULTS, + ); + + const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR( + "getRuleProviders", + getRuleProviders, + SWR_DEFAULTS, + ); + + const { data: rulesData, mutate: refreshRules } = useSWR( + "getRules", + getRules, + SWR_DEFAULTS, ); useEffect(() => { @@ -182,10 +220,125 @@ const useAppDataEventBridge = () => { if (errors.length > 0) { console.error( - "[DataProvider] " + errors.length + " errors during cleanup:", + `[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 {children}; };