revert: use-app-data (#6088)

* Revert "refactor(app-data): split monolithic context into focused SWR hooks (#5576)"

This reverts commit 8e8182f707.

# Conflicts:
#	src/components/home/clash-info-card.tsx
#	src/components/home/clash-mode-card.tsx
#	src/components/home/current-proxy-card.tsx
#	src/components/home/home-profile-card.tsx
#	src/components/proxy/provider-button.tsx
#	src/components/proxy/proxy-chain.tsx
#	src/components/proxy/proxy-groups.tsx
#	src/components/proxy/use-render-list.ts
#	src/components/rule/provider-button.tsx
#	src/components/setting/mods/sysproxy-viewer.tsx
#	src/hooks/use-clash-data.ts
#	src/hooks/use-current-proxy.ts
#	src/hooks/use-shared-swr-poller.ts
#	src/hooks/use-system-proxy-state.ts
#	src/pages/rules.tsx

* docs: Changelog.md
This commit is contained in:
Sline
2026-01-16 18:32:31 +08:00
committed by GitHub
parent a5f494bda2
commit fd98caccd2
19 changed files with 361 additions and 448 deletions

View File

@@ -34,6 +34,7 @@
- 修复 Linux 下无法安装 TUN 服务
- 修复可能的端口被占用误报
- 修复设置允许外部控制来源不能立即生效
- 修复前端性能回归问题
<details>
<summary><strong> ✨ 新增功能 </strong></summary>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Record<string, boolean>>({});
// 检查是否有提供者
@@ -176,8 +175,8 @@ export const ProviderButton = () => {
<List sx={{ py: 0, minHeight: 250 }}>
{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];

View File

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

View File

@@ -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 [];

View File

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

View File

@@ -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<typeof useRuleProvidersData>;
type RulesHook = ReturnType<typeof useRulesData>;
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<Record<string, boolean>>({});
// 检查是否有提供者
@@ -178,8 +163,8 @@ export const ProviderButton = ({
<List sx={{ py: 0, minHeight: 250 }}>
{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];

View File

@@ -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<DialogRef>((props, ref) => {
const { verge, patchVerge, mutateVerge } = useVerge();
const [hostOptions, setHostOptions] = useState<string[]>([]);
type SysProxy = Awaited<ReturnType<typeof getSystemProxy>>;
const [sysproxy, setSysproxy] = useState<SysProxy>();
type AutoProxy = Awaited<ReturnType<typeof getAutotemProxy>>;
const [autoproxy, setAutoproxy] = useState<AutoProxy>();
@@ -148,8 +148,12 @@ export const SysproxyViewer = forwardRef<DialogRef>((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,<local>";
};
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<DialogRef>((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<DialogRef>((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();
},

View File

@@ -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<ReturnType<typeof getBaseConfig>>;
type SystemProxy = Awaited<ReturnType<typeof getSystemProxy>>;
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]);
};

View File

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

View File

@@ -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<string, SharedPollerEntry>();
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]);
};

View File

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

View File

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

View File

@@ -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<VirtuosoHandle>(null);
const [showScrollTop, setShowScrollTop] = useState(false);
@@ -57,11 +56,7 @@ const RulesPage = () => {
}}
header={
<Box display="flex" alignItems="center" gap={1}>
<ProviderButton
ruleProviders={ruleProviders}
refreshRuleProviders={refreshRuleProviders}
refreshRules={refreshRules}
/>
<ProviderButton />
</Box>
}
>

View File

@@ -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<string, ProxyProvider>;
ruleProviders: Record<string, RuleProvider>;
systemProxyAddress: string;
refreshProxy: () => Promise<any>;
refreshClashConfig: () => Promise<any>;
refreshRules: () => Promise<any>;
refreshSysproxy: () => Promise<any>;
refreshProxyProviders: () => Promise<any>;
refreshRuleProviders: () => Promise<any>;
refreshAll: () => Promise<any>;
}
export interface ConnectionWithSpeed extends IConnectionsItem {
curUpload: number;
curDownload: number;
}
export interface ConnectionSpeedData {
id: string;
upload: number;
download: number;
timestamp: number;
}
export const AppDataContext = createContext<AppDataContextType | null>(null);
export const useAppData = () => {
const context = use(AppDataContext);
if (!context) {
throw new Error("useAppData必须在AppDataProvider内使用");
}
return context;
};

View File

@@ -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 <AppDataContext value={value}>{children}</AppDataContext>;
};