From 8e8182f70793f44e76ca3bb71eb98d6d60701e13 Mon Sep 17 00:00:00 2001 From: Sline Date: Mon, 24 Nov 2025 16:18:31 +0800 Subject: [PATCH] refactor(app-data): split monolithic context into focused SWR hooks (#5576) * refactor(app-data): split monolithic context into focused SWR hooks * refactor(swr): unify polling and consolidate proxy/config/provider data flow --- src/components/home/clash-info-card.tsx | 17 +- src/components/home/clash-mode-card.tsx | 4 +- src/components/home/current-proxy-card.tsx | 6 +- src/components/home/home-profile-card.tsx | 4 +- src/components/proxy/provider-button.tsx | 9 +- src/components/proxy/proxy-chain.tsx | 4 +- src/components/proxy/proxy-groups.tsx | 4 +- src/components/proxy/use-render-list.ts | 24 +- src/components/rule/provider-button.tsx | 22 +- .../setting/mods/sysproxy-viewer.tsx | 27 ++- src/hooks/app-data.ts | 206 ++++++++++++++++++ src/hooks/use-current-proxy.ts | 24 +- src/hooks/use-shared-swr-poller.ts | 121 ++++++++++ src/hooks/use-system-proxy-state.ts | 4 +- src/pages/rules.tsx | 11 +- src/providers/app-data-context.ts | 51 ----- src/providers/app-data-provider.tsx | 189 ++-------------- 17 files changed, 432 insertions(+), 295 deletions(-) create mode 100644 src/hooks/app-data.ts create mode 100644 src/hooks/use-shared-swr-poller.ts delete mode 100644 src/providers/app-data-context.ts diff --git a/src/components/home/clash-info-card.tsx b/src/components/home/clash-info-card.tsx index ed62732f5..bb89c7d31 100644 --- a/src/components/home/clash-info-card.tsx +++ b/src/components/home/clash-info-card.tsx @@ -3,8 +3,14 @@ import { Divider, Stack, Typography } from "@mui/material"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { + useAppUptime, + useClashConfig, + useRulesData, + useSystemProxyAddress, + useSystemProxyData, +} from "@/hooks/app-data"; import { useClash } from "@/hooks/use-clash"; -import { useAppData } from "@/providers/app-data-context"; import { EnhancedCard } from "./enhanced-card"; @@ -19,7 +25,14 @@ const formatUptime = (uptimeMs: number) => { export const ClashInfoCard = () => { const { t } = useTranslation(); const { version: clashVersion } = useClash(); - const { clashConfig, rules, uptime, systemProxyAddress } = useAppData(); + const { clashConfig } = useClashConfig(); + const { sysproxy } = useSystemProxyData(); + const { rules } = useRulesData(); + const { uptime } = useAppUptime(); + const systemProxyAddress = useSystemProxyAddress({ + clashConfig, + sysproxy, + }); // 使用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 ea48128ca..2624ce2dd 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/app-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 } = useAppData(); + const { clashConfig, refreshClashConfig } = useClashConfig(); // 支持的模式列表 const modeList = CLASH_MODES; diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index c29db668b..94acc2a46 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -34,10 +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/app-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"; // 本地存储的键名 @@ -100,7 +100,9 @@ export const CurrentProxyCard = () => { const { t } = useTranslation(); const navigate = useNavigate(); const theme = useTheme(); - const { proxies, clashConfig, refreshProxy, rules } = useAppData(); + const { proxies, refreshProxy } = useProxiesData(); + const { clashConfig } = useClashConfig(); + const { rules } = useRulesData(); 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 be033f28f..8e40287f4 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 { useAppData } from "@/providers/app-data-context"; +import { useRefreshAll } from "@/hooks/app-data"; import { openWebUrl, updateProfile } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; import parseTraffic from "@/utils/parse-traffic"; @@ -281,7 +281,7 @@ export const HomeProfileCard = ({ }: HomeProfileCardProps) => { const { t } = useTranslation(); const navigate = useNavigate(); - const { refreshAll } = useAppData(); + const refreshAll = useRefreshAll(); // 更新当前订阅 const [updating, setUpdating] = useState(false); diff --git a/src/components/proxy/provider-button.tsx b/src/components/proxy/provider-button.tsx index 435516386..68217775a 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 { useAppData } from "@/providers/app-data-context"; +import { useProxiesData, useProxyProvidersData } from "@/hooks/app-data"; import { showNotice } from "@/services/noticeService"; import parseTraffic from "@/utils/parse-traffic"; @@ -48,7 +48,8 @@ const parseExpire = (expire?: number) => { export const ProviderButton = () => { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData(); + const { proxyProviders, refreshProxyProviders } = useProxyProvidersData(); + const { refreshProxy } = useProxiesData(); const [updating, setUpdating] = useState>({}); // 检查是否有提供者 @@ -175,8 +176,8 @@ export const ProviderButton = () => { {Object.entries(proxyProviders || {}) .sort() - .map(([key, item]) => { - const provider = item; + .map(([key, provider]) => { + if (!provider) return null; 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 99bf10435..b07757d2d 100644 --- a/src/components/proxy/proxy-chain.tsx +++ b/src/components/proxy/proxy-chain.tsx @@ -39,7 +39,7 @@ import { selectNodeForGroup, } from "tauri-plugin-mihomo-api"; -import { useAppData } from "@/providers/app-data-context"; +import { useProxiesData } from "@/hooks/app-data"; import { calcuProxies, updateProxyChainConfigInRuntime } from "@/services/cmds"; interface ProxyChainItem { @@ -199,7 +199,7 @@ export const ProxyChain = ({ }: ProxyChainProps) => { const theme = useTheme(); const { t } = useTranslation(); - const { proxies } = useAppData(); + const { proxies } = useProxiesData(); const [isConnecting, setIsConnecting] = useState(false); const markUnsavedChanges = useCallback(() => { onMarkUnsavedChanges?.(); diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx index 84e771c40..d85ba397c 100644 --- a/src/components/proxy/proxy-groups.tsx +++ b/src/components/proxy/proxy-groups.tsx @@ -15,9 +15,9 @@ import { useTranslation } from "react-i18next"; import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api"; +import { useProxiesData } from "@/hooks/app-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"; @@ -61,7 +61,7 @@ export const ProxyGroups = (props: Props) => { }>({ open: false, message: "" }); const { verge } = useVerge(); - const { proxies: proxiesData } = useAppData(); + const { proxies: proxiesData } = useProxiesData(); const groups = proxiesData?.groups; const availableGroups = useMemo(() => groups ?? [], [groups]); diff --git a/src/components/proxy/use-render-list.ts b/src/components/proxy/use-render-list.ts index 7a5949ae3..2ebe89ada 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/app-data"; import { useVerge } from "@/hooks/use-verge"; -import { useAppData } from "@/providers/app-data-context"; import { getRuntimeConfig } from "@/services/cmds"; import delayManager from "@/services/delay"; @@ -33,24 +33,8 @@ interface IProxyItem { } // 代理组类型 -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; +type ProxyGroup = IProxyGroupItem & { + now?: string; }; export interface IRenderItem { @@ -99,7 +83,7 @@ export const useRenderList = ( selectedGroup?: string | null, ) => { // 使用全局数据提供者 - const { proxies: proxiesData, refreshProxy } = useAppData(); + const { proxies: proxiesData, refreshProxy } = useProxiesData(); 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 367fa3d6f..875be01b3 100644 --- a/src/components/rule/provider-button.tsx +++ b/src/components/rule/provider-button.tsx @@ -21,7 +21,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { updateRuleProvider } from "tauri-plugin-mihomo-api"; -import { useAppData } from "@/providers/app-data-context"; +import type { useRuleProvidersData, useRulesData } from "@/hooks/app-data"; import { showNotice } from "@/services/noticeService"; // 辅助组件 - 类型框 @@ -37,10 +37,22 @@ const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({ lineHeight: 1.25, })); -export const ProviderButton = () => { +type RuleProvidersHook = ReturnType; +type RulesHook = ReturnType; + +interface ProviderButtonProps { + ruleProviders: RuleProvidersHook["ruleProviders"]; + refreshRuleProviders: RuleProvidersHook["refreshRuleProviders"]; + refreshRules: RulesHook["refreshRules"]; +} + +export const ProviderButton = ({ + ruleProviders, + refreshRuleProviders, + refreshRules, +}: ProviderButtonProps) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const { ruleProviders, refreshRules, refreshRuleProviders } = useAppData(); const [updating, setUpdating] = useState>({}); // 检查是否有提供者 @@ -163,8 +175,8 @@ export const ProviderButton = () => { {Object.entries(ruleProviders || {}) .sort() - .map(([key, item]) => { - const provider = item; + .map(([key, provider]) => { + if (!provider) return null; 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 4d70e82a1..078261564 100644 --- a/src/components/setting/mods/sysproxy-viewer.tsx +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -20,15 +20,18 @@ import { useState, } from "react"; import { useTranslation } from "react-i18next"; -import useSWR, { mutate } from "swr"; -import { getBaseConfig } from "tauri-plugin-mihomo-api"; +import { mutate } from "swr"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { BaseFieldset } from "@/components/base/base-fieldset"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { EditorViewer } from "@/components/profile/editor-viewer"; +import { + useClashConfig, + useSystemProxyAddress, + useSystemProxyData, +} from "@/hooks/app-data"; import { useVerge } from "@/hooks/use-verge"; -import { useAppData } from "@/providers/app-data-context"; import { getAutotemProxy, getNetworkInterfacesInfo, @@ -92,9 +95,6 @@ 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(); @@ -129,12 +129,8 @@ 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 { data: clashConfig } = useSWR("getClashConfig", getBaseConfig, { - revalidateOnFocus: false, - revalidateIfStale: true, - dedupingInterval: 1000, - errorRetryInterval: 5000, - }); + const { clashConfig } = useClashConfig(); + const { sysproxy, refreshSysproxy } = useSystemProxyData(); const prevMixedPortRef = useRef(clashConfig?.mixedPort); @@ -168,7 +164,10 @@ export const SysproxyViewer = forwardRef((props, ref) => { updateProxy(); }, [clashConfig?.mixedPort, value.pac]); - const { systemProxyAddress } = useAppData(); + const systemProxyAddress = useSystemProxyAddress({ + clashConfig, + sysproxy, + }); // 为当前状态计算系统代理地址 const getSystemProxyAddress = useMemo(() => { @@ -209,7 +208,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { pac_content: pac_file_content ?? DEFAULT_PAC, proxy_host: proxy_host ?? "127.0.0.1", }); - getSystemProxy().then((p) => setSysproxy(p)); + void refreshSysproxy(); getAutotemProxy().then((p) => setAutoproxy(p)); fetchNetworkInterfaces(); }, diff --git a/src/hooks/app-data.ts b/src/hooks/app-data.ts new file mode 100644 index 000000000..cfd113058 --- /dev/null +++ b/src/hooks/app-data.ts @@ -0,0 +1,206 @@ +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 index 7d3523269..e1d1ab1ba 100644 --- a/src/hooks/use-current-proxy.ts +++ b/src/hooks/use-current-proxy.ts @@ -1,17 +1,11 @@ import { useMemo } from "react"; -import { useAppData } from "@/providers/app-data-context"; - -// 定义代理组类型 -interface ProxyGroup { - name: string; - now: string; -} +import { useClashConfig, useProxiesData } from "@/hooks/app-data"; // 获取当前代理节点信息的自定义Hook export const useCurrentProxy = () => { - // 从AppDataProvider获取数据 - const { proxies, clashConfig, refreshProxy } = useAppData(); + const { proxies, refreshProxy } = useProxiesData(); + const { clashConfig } = useClashConfig(); // 获取当前模式 const currentMode = clashConfig?.mode?.toLowerCase() || "rule"; @@ -20,11 +14,15 @@ export const useCurrentProxy = () => { const currentProxyInfo = useMemo(() => { if (!proxies) return { currentProxy: null, primaryGroupName: null }; - const { global, groups, records } = proxies; + const globalGroup = proxies.global as IProxyGroupItem | undefined; + const groups: IProxyGroupItem[] = Array.isArray(proxies.groups) + ? (proxies.groups as IProxyGroupItem[]) + : []; + const records = (proxies.records || {}) as Record; // 默认信息 let primaryGroupName = "GLOBAL"; - let currentName = global?.now; + let currentName = globalGroup?.now; // 在规则模式下,寻找主要代理组(通常是第一个或者名字包含特定关键词的组) if (currentMode === "rule" && groups.length > 0) { @@ -37,11 +35,11 @@ export const useCurrentProxy = () => { "自动选择", ]; const primaryGroup = - groups.find((group: ProxyGroup) => + groups.find((group) => primaryKeywords.some((keyword) => group.name.toLowerCase().includes(keyword.toLowerCase()), ), - ) || groups.filter((g: ProxyGroup) => g.name !== "GLOBAL")[0]; + ) || groups.filter((g) => g.name !== "GLOBAL")[0]; if (primaryGroup) { primaryGroupName = primaryGroup.name; diff --git a/src/hooks/use-shared-swr-poller.ts b/src/hooks/use-shared-swr-poller.ts new file mode 100644 index 000000000..d5399ff37 --- /dev/null +++ b/src/hooks/use-shared-swr-poller.ts @@ -0,0 +1,121 @@ +import { useEffect } from "react"; +import type { Key } from "swr"; + +type SharedPollerEntry = { + subscribers: number; + timer: number | null; + interval: number; + callback: (() => void) | null; + 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; + 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, + 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 a04e12377..ef25274b1 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/app-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 } = useAppData(); + const { sysproxy } = useSystemProxyData(); const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy, { revalidateOnFocus: true, revalidateOnReconnect: true, diff --git a/src/pages/rules.tsx b/src/pages/rules.tsx index 66e7a5692..266db15d7 100644 --- a/src/pages/rules.tsx +++ b/src/pages/rules.tsx @@ -8,12 +8,13 @@ import { BaseSearchBox } from "@/components/base/base-search-box"; 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/app-data"; import { useVisibility } from "@/hooks/use-visibility"; -import { useAppData } from "@/providers/app-data-context"; const RulesPage = () => { const { t } = useTranslation(); - const { rules = [], refreshRules, refreshRuleProviders } = useAppData(); + const { rules = [], refreshRules } = useRulesData(); + const { ruleProviders, refreshRuleProviders } = useRuleProvidersData(); const [match, setMatch] = useState(() => (_: string) => true); const virtuosoRef = useRef(null); const [showScrollTop, setShowScrollTop] = useState(false); @@ -57,7 +58,11 @@ const RulesPage = () => { }} header={ - + } > diff --git a/src/providers/app-data-context.ts b/src/providers/app-data-context.ts deleted file mode 100644 index 7b7244aba..000000000 --- a/src/providers/app-data-context.ts +++ /dev/null @@ -1,51 +0,0 @@ -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 b7898ddc1..7a7c80e46 100644 --- a/src/providers/app-data-provider.tsx +++ b/src/providers/app-data-provider.tsx @@ -1,63 +1,25 @@ 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 { PropsWithChildren, useCallback, useEffect } from "react"; +import { useSWRConfig } from "swr"; -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"; +// 负责监听全局事件并驱动 SWR 刷新,避免包裹全局 context 带来的额外渲染 +export const AppDataProvider = ({ children }: PropsWithChildren) => { + useAppDataEventBridge(); + return <>{children}; +}; -import { AppDataContext, AppDataContextType } from "./app-data-context"; +const useAppDataEventBridge = () => { + const { mutate } = useSWRConfig(); -// 全局数据提供者组件 -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 refreshProxy = useCallback(() => mutate("getProxies"), [mutate]); + const refreshClashConfig = useCallback( + () => mutate("getClashConfig"), + [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, + const refreshRules = useCallback(() => mutate("getRules"), [mutate]); + const refreshRuleProviders = useCallback( + () => mutate("getRuleProviders"), + [mutate], ); useEffect(() => { @@ -220,125 +182,10 @@ export const AppDataProvider = ({ 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}; };