mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-12 05:32:09 +08:00
Revert "refactor: profile switch (#5197)"
This reverts commit c2dcd86722.
This commit is contained in:
@@ -6,15 +6,8 @@ import {
|
||||
RuleProvider,
|
||||
} from "tauri-plugin-mihomo-api";
|
||||
|
||||
import { ProxiesView, type ProfileSwitchStatus } from "@/services/cmds";
|
||||
|
||||
export interface AppDataContextType {
|
||||
proxies: ProxiesView | null;
|
||||
proxyHydration: "none" | "snapshot" | "live";
|
||||
proxyTargetProfileId: string | null;
|
||||
proxyDisplayProfileId: string | null;
|
||||
isProxyRefreshPending: boolean;
|
||||
switchStatus: ProfileSwitchStatus | null;
|
||||
proxies: any;
|
||||
clashConfig: BaseConfig;
|
||||
rules: Rule[];
|
||||
sysproxy: any;
|
||||
|
||||
+138
-581
@@ -1,6 +1,6 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import useSWR, { mutate as globalMutate } from "swr";
|
||||
import React, { useCallback, useEffect, useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import {
|
||||
getBaseConfig,
|
||||
getRuleProviders,
|
||||
@@ -9,53 +9,31 @@ import {
|
||||
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import {
|
||||
calcuProxies,
|
||||
calcuProxyProviders,
|
||||
getAppUptime,
|
||||
getProfileSwitchStatus,
|
||||
getProfileSwitchEvents,
|
||||
getProfiles as fetchProfilesConfig,
|
||||
getRunningMode,
|
||||
readProfileFile,
|
||||
getSystemProxy,
|
||||
type ProxiesView,
|
||||
type ProfileSwitchStatus,
|
||||
type SwitchResultStatus,
|
||||
} from "@/services/cmds";
|
||||
import { SWR_DEFAULTS, SWR_SLOW_POLL } from "@/services/config";
|
||||
import { useProfileStore } from "@/stores/profile-store";
|
||||
import {
|
||||
applyLiveProxyPayload,
|
||||
fetchLiveProxies,
|
||||
type ProxiesUpdatedPayload,
|
||||
useProxyStore,
|
||||
} from "@/stores/proxy-store";
|
||||
import { createProxySnapshotFromProfile } from "@/utils/proxy-snapshot";
|
||||
import { SWR_DEFAULTS, SWR_REALTIME, SWR_SLOW_POLL } from "@/services/config";
|
||||
|
||||
import { AppDataContext, AppDataContextType } from "./app-data-context";
|
||||
|
||||
// Global app data provider
|
||||
// 全局数据提供者组件
|
||||
export const AppDataProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { verge } = useVerge();
|
||||
const applyProfileSwitchResult = useProfileStore(
|
||||
(state) => state.applySwitchResult,
|
||||
);
|
||||
const commitProfileSnapshot = useProfileStore(
|
||||
(state) => state.commitHydrated,
|
||||
);
|
||||
const setSwitchEventSeq = useProfileStore((state) => state.setLastEventSeq);
|
||||
const proxyView = useProxyStore((state) => state.data);
|
||||
const proxyHydration = useProxyStore((state) => state.hydration);
|
||||
const proxyProfileId = useProxyStore((state) => state.lastProfileId);
|
||||
const pendingProxyProfileId = useProxyStore(
|
||||
(state) => state.pendingProfileId,
|
||||
);
|
||||
const setProxySnapshot = useProxyStore((state) => state.setSnapshot);
|
||||
const clearPendingProxyProfile = useProxyStore(
|
||||
(state) => state.clearPendingProfile,
|
||||
|
||||
const { data: proxiesData, mutate: refreshProxy } = useSWR(
|
||||
"getProxies",
|
||||
calcuProxies,
|
||||
{
|
||||
...SWR_REALTIME,
|
||||
onError: (err) => console.warn("[DataProvider] Proxy fetch failed:", err),
|
||||
},
|
||||
);
|
||||
|
||||
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
|
||||
@@ -82,259 +60,25 @@ export const AppDataProvider = ({
|
||||
SWR_DEFAULTS,
|
||||
);
|
||||
|
||||
const { data: switchStatus, mutate: mutateSwitchStatus } =
|
||||
useSWR<ProfileSwitchStatus>(
|
||||
"getProfileSwitchStatus",
|
||||
getProfileSwitchStatus,
|
||||
{
|
||||
refreshInterval: (status) =>
|
||||
status && (status.isSwitching || (status.queue?.length ?? 0) > 0)
|
||||
? 400
|
||||
: 4000,
|
||||
dedupingInterval: 200,
|
||||
},
|
||||
);
|
||||
|
||||
const isUnmountedRef = useRef(false);
|
||||
// Keep track of pending timers so we can cancel them on unmount and avoid stray updates.
|
||||
const scheduledTimeoutsRef = useRef<Set<number>>(new Set());
|
||||
// Shared metadata to dedupe switch events coming from both polling and subscriptions.
|
||||
const switchMetaRef = useRef<{
|
||||
pendingProfileId: string | null;
|
||||
lastResultTaskId: number | null;
|
||||
}>({
|
||||
pendingProfileId: null,
|
||||
lastResultTaskId: null,
|
||||
});
|
||||
const switchEventSeqRef = useRef(0);
|
||||
const profileChangeMetaRef = useRef<{
|
||||
lastProfileId: string | null;
|
||||
lastEventTs: number;
|
||||
}>({
|
||||
lastProfileId: null,
|
||||
lastEventTs: 0,
|
||||
});
|
||||
const lastClashRefreshAtRef = useRef(0);
|
||||
const PROFILE_EVENT_DEDUP_MS = 400;
|
||||
const CLASH_REFRESH_DEDUP_MS = 300;
|
||||
|
||||
// Thin wrapper around setTimeout that no-ops once the provider unmounts.
|
||||
const scheduleTimeout = useCallback(
|
||||
(callback: () => void | Promise<void>, delay: number) => {
|
||||
if (isUnmountedRef.current) return -1;
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
scheduledTimeoutsRef.current.delete(timeoutId);
|
||||
if (!isUnmountedRef.current) {
|
||||
void callback();
|
||||
}
|
||||
}, delay);
|
||||
|
||||
scheduledTimeoutsRef.current.add(timeoutId);
|
||||
return timeoutId;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const clearAllTimeouts = useCallback(() => {
|
||||
scheduledTimeoutsRef.current.forEach((timeoutId) =>
|
||||
clearTimeout(timeoutId),
|
||||
);
|
||||
scheduledTimeoutsRef.current.clear();
|
||||
}, []);
|
||||
|
||||
// Delay live proxy refreshes slightly so we don't hammer Mihomo while a switch is still applying.
|
||||
const queueProxyRefresh = useCallback(
|
||||
(reason: string, delay = 1500) => {
|
||||
scheduleTimeout(() => {
|
||||
fetchLiveProxies().catch((error) =>
|
||||
console.warn(
|
||||
`[DataProvider] Proxy refresh failed (${reason}, fallback):`,
|
||||
error,
|
||||
),
|
||||
);
|
||||
}, delay);
|
||||
},
|
||||
[scheduleTimeout],
|
||||
);
|
||||
// Prime the proxy store with the static selections from the profile YAML before live data arrives.
|
||||
const seedProxySnapshot = useCallback(
|
||||
async (profileId: string) => {
|
||||
if (!profileId) return;
|
||||
|
||||
try {
|
||||
const yamlContent = await readProfileFile(profileId);
|
||||
const snapshot = createProxySnapshotFromProfile(yamlContent);
|
||||
if (!snapshot) return;
|
||||
|
||||
setProxySnapshot(snapshot, profileId);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[DataProvider] Failed to seed proxy snapshot from profile:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
},
|
||||
[setProxySnapshot],
|
||||
);
|
||||
|
||||
const handleSwitchResult = useCallback(
|
||||
(result: SwitchResultStatus) => {
|
||||
// Ignore duplicate notifications for the same switch execution.
|
||||
const meta = switchMetaRef.current;
|
||||
if (result.taskId === meta.lastResultTaskId) {
|
||||
return;
|
||||
}
|
||||
meta.lastResultTaskId = result.taskId;
|
||||
|
||||
// Optimistically update the SWR cache so the UI shows the new profile immediately.
|
||||
void globalMutate(
|
||||
"getProfiles",
|
||||
(current?: IProfilesConfig | null) => {
|
||||
if (!current || !result.success) {
|
||||
return current;
|
||||
}
|
||||
if (current.current === result.profileId) {
|
||||
return current;
|
||||
}
|
||||
return {
|
||||
...current,
|
||||
current: result.profileId,
|
||||
};
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
applyProfileSwitchResult(result);
|
||||
if (!result.success) {
|
||||
clearPendingProxyProfile();
|
||||
}
|
||||
|
||||
if (result.success && result.cancelled !== true) {
|
||||
// Once the backend settles, refresh all dependent data in the background.
|
||||
scheduleTimeout(() => {
|
||||
void Promise.allSettled([
|
||||
fetchProfilesConfig().then((data) => {
|
||||
commitProfileSnapshot(data);
|
||||
globalMutate("getProfiles", data, false);
|
||||
}),
|
||||
fetchLiveProxies(),
|
||||
refreshProxyProviders(),
|
||||
refreshRules(),
|
||||
refreshRuleProviders(),
|
||||
]).catch((error) => {
|
||||
console.warn(
|
||||
"[DataProvider] Background refresh after profile switch failed:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
void mutateSwitchStatus((current) => {
|
||||
if (!current) {
|
||||
return current;
|
||||
}
|
||||
const filteredQueue = current.queue.filter(
|
||||
(task) => task.taskId !== result.taskId,
|
||||
);
|
||||
const active =
|
||||
current.active && current.active.taskId === result.taskId
|
||||
? null
|
||||
: current.active;
|
||||
const isSwitching = filteredQueue.length > 0;
|
||||
return {
|
||||
...current,
|
||||
active,
|
||||
queue: filteredQueue,
|
||||
isSwitching,
|
||||
lastResult: result,
|
||||
};
|
||||
}, false);
|
||||
},
|
||||
[
|
||||
scheduleTimeout,
|
||||
refreshProxyProviders,
|
||||
refreshRules,
|
||||
refreshRuleProviders,
|
||||
mutateSwitchStatus,
|
||||
applyProfileSwitchResult,
|
||||
commitProfileSnapshot,
|
||||
clearPendingProxyProfile,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
isUnmountedRef.current = false;
|
||||
return () => {
|
||||
isUnmountedRef.current = true;
|
||||
clearAllTimeouts();
|
||||
};
|
||||
}, [clearAllTimeouts]);
|
||||
let lastProfileId: string | null = null;
|
||||
let lastUpdateTime = 0;
|
||||
const refreshThrottle = 800;
|
||||
|
||||
useEffect(() => {
|
||||
if (!switchStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = switchMetaRef.current;
|
||||
const nextTarget =
|
||||
switchStatus.active?.profileId ??
|
||||
(switchStatus.queue.length > 0 ? switchStatus.queue[0].profileId : null);
|
||||
|
||||
if (nextTarget && nextTarget !== meta.pendingProfileId) {
|
||||
meta.pendingProfileId = nextTarget;
|
||||
void seedProxySnapshot(nextTarget);
|
||||
} else if (!nextTarget) {
|
||||
meta.pendingProfileId = null;
|
||||
}
|
||||
|
||||
const lastResult = switchStatus.lastResult ?? null;
|
||||
if (lastResult) {
|
||||
handleSwitchResult(lastResult);
|
||||
}
|
||||
}, [switchStatus, seedProxySnapshot, handleSwitchResult]);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
|
||||
const pollEvents = async () => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const events = await getProfileSwitchEvents(switchEventSeqRef.current);
|
||||
if (events.length > 0) {
|
||||
switchEventSeqRef.current = events[events.length - 1].sequence;
|
||||
setSwitchEventSeq(switchEventSeqRef.current);
|
||||
events.forEach((event) => handleSwitchResult(event.result));
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[DataProvider] Failed to poll switch events:", error);
|
||||
} finally {
|
||||
if (!disposed) {
|
||||
const nextDelay =
|
||||
switchStatus &&
|
||||
(switchStatus.isSwitching || (switchStatus.queue?.length ?? 0) > 0)
|
||||
? 250
|
||||
: 1000;
|
||||
scheduleTimeout(pollEvents, nextDelay);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scheduleTimeout(pollEvents, 0);
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, [scheduleTimeout, handleSwitchResult, switchStatus, setSwitchEventSeq]);
|
||||
|
||||
useEffect(() => {
|
||||
let isUnmounted = false;
|
||||
const scheduledTimeouts = new Set<number>();
|
||||
const cleanupFns: Array<() => void> = [];
|
||||
|
||||
const registerCleanup = (fn: () => void) => {
|
||||
cleanupFns.push(fn);
|
||||
if (isUnmounted) {
|
||||
try {
|
||||
fn();
|
||||
} catch (error) {
|
||||
console.error("[DataProvider] Immediate cleanup failed:", error);
|
||||
}
|
||||
} else {
|
||||
cleanupFns.push(fn);
|
||||
}
|
||||
};
|
||||
|
||||
const addWindowListener = (eventName: string, handler: EventListener) => {
|
||||
@@ -343,319 +87,140 @@ export const AppDataProvider = ({
|
||||
return () => window.removeEventListener(eventName, handler);
|
||||
};
|
||||
|
||||
const runProfileChangedPipeline = (
|
||||
profileId: string | null,
|
||||
source: "tauri" | "window",
|
||||
const scheduleTimeout = (
|
||||
callback: () => void | Promise<void>,
|
||||
delay: number,
|
||||
) => {
|
||||
if (isUnmounted) return -1;
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
scheduledTimeouts.delete(timeoutId);
|
||||
if (!isUnmounted) {
|
||||
void callback();
|
||||
}
|
||||
}, delay);
|
||||
|
||||
scheduledTimeouts.add(timeoutId);
|
||||
return timeoutId;
|
||||
};
|
||||
|
||||
const clearAllTimeouts = () => {
|
||||
scheduledTimeouts.forEach((timeoutId) => clearTimeout(timeoutId));
|
||||
scheduledTimeouts.clear();
|
||||
};
|
||||
|
||||
const handleProfileChanged = (event: { payload: string }) => {
|
||||
const newProfileId = event.payload;
|
||||
const now = Date.now();
|
||||
const meta = profileChangeMetaRef.current;
|
||||
|
||||
if (
|
||||
meta.lastProfileId === profileId &&
|
||||
now - meta.lastEventTs < PROFILE_EVENT_DEDUP_MS
|
||||
lastProfileId === newProfileId &&
|
||||
now - lastUpdateTime < refreshThrottle
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
meta.lastProfileId = profileId;
|
||||
meta.lastEventTs = now;
|
||||
|
||||
if (profileId) {
|
||||
void seedProxySnapshot(profileId);
|
||||
}
|
||||
|
||||
queueProxyRefresh(`profile-changed-${source}`, 500);
|
||||
lastProfileId = newProfileId;
|
||||
lastUpdateTime = now;
|
||||
|
||||
scheduleTimeout(() => {
|
||||
void fetchProfilesConfig()
|
||||
.then((data) => {
|
||||
commitProfileSnapshot(data);
|
||||
globalMutate("getProfiles", data, false);
|
||||
})
|
||||
.catch((error) =>
|
||||
console.warn(
|
||||
"[AppDataProvider] Failed to refresh profiles after profile change:",
|
||||
error,
|
||||
),
|
||||
);
|
||||
void refreshProxyProviders().catch((error) =>
|
||||
console.warn(
|
||||
"[AppDataProvider] Proxy providers refresh failed after profile change:",
|
||||
error,
|
||||
),
|
||||
refreshRules().catch((error) =>
|
||||
console.warn("[DataProvider] Rules refresh failed:", error),
|
||||
);
|
||||
void refreshRules().catch((error) =>
|
||||
console.warn(
|
||||
"[AppDataProvider] Rules refresh failed after profile change:",
|
||||
error,
|
||||
),
|
||||
);
|
||||
void refreshRuleProviders().catch((error) =>
|
||||
console.warn(
|
||||
"[AppDataProvider] Rule providers refresh failed after profile change:",
|
||||
error,
|
||||
),
|
||||
refreshRuleProviders().catch((error) =>
|
||||
console.warn("[DataProvider] Rule providers refresh failed:", error),
|
||||
);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const handleProfileChanged = (event: { payload: string }) => {
|
||||
runProfileChangedPipeline(event.payload ?? null, "tauri");
|
||||
};
|
||||
|
||||
const runRefreshClashPipeline = (source: "tauri" | "window") => {
|
||||
const handleRefreshClash = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastClashRefreshAtRef.current < CLASH_REFRESH_DEDUP_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastClashRefreshAtRef.current = now;
|
||||
if (now - lastUpdateTime <= refreshThrottle) return;
|
||||
|
||||
lastUpdateTime = now;
|
||||
scheduleTimeout(() => {
|
||||
void refreshClashConfig().catch((error) =>
|
||||
console.warn(
|
||||
"[AppDataProvider] Clash config refresh failed after backend update:",
|
||||
error,
|
||||
),
|
||||
refreshProxy().catch((error) =>
|
||||
console.error("[DataProvider] Proxy refresh failed:", error),
|
||||
);
|
||||
void refreshRules().catch((error) =>
|
||||
console.warn(
|
||||
"[AppDataProvider] Rules refresh failed after backend update:",
|
||||
error,
|
||||
),
|
||||
);
|
||||
void refreshRuleProviders().catch((error) =>
|
||||
console.warn(
|
||||
"[AppDataProvider] Rule providers refresh failed after backend update:",
|
||||
error,
|
||||
),
|
||||
);
|
||||
void refreshProxyProviders().catch((error) =>
|
||||
console.warn(
|
||||
"[AppDataProvider] Proxy providers refresh failed after backend update:",
|
||||
error,
|
||||
),
|
||||
);
|
||||
}, 0);
|
||||
|
||||
queueProxyRefresh(`refresh-clash-config-${source}`, 400);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const handleProfileUpdateCompleted = (_: { payload: { uid: string } }) => {
|
||||
queueProxyRefresh("profile-update-completed", 3000);
|
||||
if (!isUnmountedRef.current) {
|
||||
scheduleTimeout(() => {
|
||||
void refreshProxyProviders().catch((error) =>
|
||||
console.warn(
|
||||
"[DataProvider] Proxy providers refresh failed after profile update completed:",
|
||||
error,
|
||||
),
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
const handleRefreshProxy = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastUpdateTime <= refreshThrottle) return;
|
||||
|
||||
const isProxiesPayload = (
|
||||
value: unknown,
|
||||
): value is ProxiesUpdatedPayload => {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
const candidate = value as Partial<ProxiesUpdatedPayload>;
|
||||
return candidate.proxies !== undefined && candidate.proxies !== null;
|
||||
};
|
||||
|
||||
const handleProxiesUpdatedPayload = (
|
||||
rawPayload: unknown,
|
||||
source: "tauri" | "window",
|
||||
) => {
|
||||
if (!isProxiesPayload(rawPayload)) {
|
||||
console.warn(
|
||||
`[AppDataProvider] Ignored ${source} proxies-updated payload`,
|
||||
rawPayload,
|
||||
lastUpdateTime = now;
|
||||
scheduleTimeout(() => {
|
||||
refreshProxy().catch((error) =>
|
||||
console.warn("[DataProvider] Proxy refresh failed:", error),
|
||||
);
|
||||
queueProxyRefresh(`proxies-updated-${source}-invalid`, 500);
|
||||
return;
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const initializeListeners = async () => {
|
||||
try {
|
||||
const unlistenProfile = await listen<string>(
|
||||
"profile-changed",
|
||||
handleProfileChanged,
|
||||
);
|
||||
registerCleanup(unlistenProfile);
|
||||
} catch (error) {
|
||||
console.error("[AppDataProvider] 监听 Profile 事件失败:", error);
|
||||
}
|
||||
|
||||
try {
|
||||
applyLiveProxyPayload(rawPayload);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[AppDataProvider] Failed to apply ${source} proxies-updated payload`,
|
||||
error,
|
||||
const unlistenClash = await listen(
|
||||
"verge://refresh-clash-config",
|
||||
handleRefreshClash,
|
||||
);
|
||||
queueProxyRefresh(`proxies-updated-${source}-apply-failed`, 500);
|
||||
const unlistenProxy = await listen(
|
||||
"verge://refresh-proxy-config",
|
||||
handleRefreshProxy,
|
||||
);
|
||||
|
||||
registerCleanup(() => {
|
||||
unlistenClash();
|
||||
unlistenProxy();
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("[AppDataProvider] 设置 Tauri 事件监听器失败:", error);
|
||||
|
||||
const fallbackHandlers: Array<[string, EventListener]> = [
|
||||
["verge://refresh-clash-config", handleRefreshClash],
|
||||
["verge://refresh-proxy-config", handleRefreshProxy],
|
||||
];
|
||||
|
||||
fallbackHandlers.forEach(([eventName, handler]) => {
|
||||
registerCleanup(addWindowListener(eventName, handler));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
listen<{ uid: string }>(
|
||||
"profile-update-completed",
|
||||
handleProfileUpdateCompleted,
|
||||
)
|
||||
.then(registerCleanup)
|
||||
.catch((error) =>
|
||||
console.error(
|
||||
"[AppDataProvider] failed to attach profile update listeners:",
|
||||
error,
|
||||
),
|
||||
);
|
||||
|
||||
listen<string>("profile-changed", handleProfileChanged)
|
||||
.then(registerCleanup)
|
||||
.catch((error) =>
|
||||
console.error(
|
||||
"[AppDataProvider] failed to attach profile-changed listener:",
|
||||
error,
|
||||
),
|
||||
);
|
||||
|
||||
listen<ProxiesUpdatedPayload>("proxies-updated", (event) => {
|
||||
handleProxiesUpdatedPayload(event.payload, "tauri");
|
||||
})
|
||||
.then(registerCleanup)
|
||||
.catch((error) =>
|
||||
console.error(
|
||||
"[AppDataProvider] failed to attach proxies-updated listener:",
|
||||
error,
|
||||
),
|
||||
);
|
||||
|
||||
listen("verge://refresh-clash-config", () => {
|
||||
runRefreshClashPipeline("tauri");
|
||||
})
|
||||
.then(registerCleanup)
|
||||
.catch((error) =>
|
||||
console.error(
|
||||
"[AppDataProvider] failed to attach refresh-clash-config listener:",
|
||||
error,
|
||||
),
|
||||
);
|
||||
|
||||
listen("verge://refresh-proxy-config", () => {
|
||||
queueProxyRefresh("refresh-proxy-config-tauri", 500);
|
||||
})
|
||||
.then(registerCleanup)
|
||||
.catch((error) =>
|
||||
console.error(
|
||||
"[AppDataProvider] failed to attach refresh-proxy-config listener:",
|
||||
error,
|
||||
),
|
||||
);
|
||||
|
||||
const fallbackHandlers: Array<[string, EventListener]> = [
|
||||
[
|
||||
"profile-update-completed",
|
||||
((event: Event) => {
|
||||
const payload = (event as CustomEvent<{ uid: string }>).detail ?? {
|
||||
uid: "",
|
||||
};
|
||||
handleProfileUpdateCompleted({ payload });
|
||||
}) as EventListener,
|
||||
],
|
||||
[
|
||||
"profile-changed",
|
||||
((event: Event) => {
|
||||
const payload = (event as CustomEvent<string | null>).detail ?? null;
|
||||
runProfileChangedPipeline(payload, "window");
|
||||
}) as EventListener,
|
||||
],
|
||||
[
|
||||
"proxies-updated",
|
||||
((event: Event) => {
|
||||
const payload = (event as CustomEvent<ProxiesUpdatedPayload>).detail;
|
||||
handleProxiesUpdatedPayload(payload, "window");
|
||||
}) as EventListener,
|
||||
],
|
||||
[
|
||||
"verge://refresh-clash-config",
|
||||
(() => {
|
||||
runRefreshClashPipeline("window");
|
||||
}) as EventListener,
|
||||
],
|
||||
[
|
||||
"verge://refresh-proxy-config",
|
||||
(() => {
|
||||
queueProxyRefresh("refresh-proxy-config-window", 500);
|
||||
}) as EventListener,
|
||||
],
|
||||
];
|
||||
|
||||
fallbackHandlers.forEach(([eventName, handler]) => {
|
||||
registerCleanup(addWindowListener(eventName, handler));
|
||||
});
|
||||
void initializeListeners();
|
||||
|
||||
return () => {
|
||||
cleanupFns.forEach((fn) => {
|
||||
isUnmounted = true;
|
||||
clearAllTimeouts();
|
||||
|
||||
const errors: Error[] = [];
|
||||
cleanupFns.splice(0).forEach((fn) => {
|
||||
try {
|
||||
fn();
|
||||
} catch (error) {
|
||||
console.error("[AppDataProvider] cleanup error:", error);
|
||||
errors.push(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(
|
||||
`[DataProvider] ${errors.length} errors during cleanup:`,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
commitProfileSnapshot,
|
||||
queueProxyRefresh,
|
||||
refreshClashConfig,
|
||||
refreshProxyProviders,
|
||||
refreshRuleProviders,
|
||||
refreshRules,
|
||||
scheduleTimeout,
|
||||
seedProxySnapshot,
|
||||
]);
|
||||
|
||||
const switchTargetProfileId =
|
||||
switchStatus?.active?.profileId ??
|
||||
(switchStatus && switchStatus.queue.length > 0
|
||||
? switchStatus.queue[0].profileId
|
||||
: null);
|
||||
|
||||
const proxyTargetProfileId =
|
||||
switchTargetProfileId ?? pendingProxyProfileId ?? proxyProfileId ?? null;
|
||||
const displayProxyStateRef = useRef<{
|
||||
view: ProxiesView | null;
|
||||
profileId: string | null;
|
||||
}>({
|
||||
view: proxyView,
|
||||
profileId: proxyTargetProfileId,
|
||||
});
|
||||
|
||||
const currentDisplay = displayProxyStateRef.current;
|
||||
|
||||
if (!proxyView) {
|
||||
if (
|
||||
currentDisplay.view !== null ||
|
||||
currentDisplay.profileId !== proxyTargetProfileId
|
||||
) {
|
||||
displayProxyStateRef.current = {
|
||||
view: null,
|
||||
profileId: proxyTargetProfileId,
|
||||
};
|
||||
}
|
||||
} else if (proxyHydration === "live") {
|
||||
if (
|
||||
currentDisplay.view !== proxyView ||
|
||||
currentDisplay.profileId !== proxyTargetProfileId
|
||||
) {
|
||||
displayProxyStateRef.current = {
|
||||
view: proxyView,
|
||||
profileId: proxyTargetProfileId,
|
||||
};
|
||||
}
|
||||
} else if (!currentDisplay.view) {
|
||||
displayProxyStateRef.current = {
|
||||
view: proxyView,
|
||||
profileId: proxyTargetProfileId,
|
||||
};
|
||||
}
|
||||
const displayProxyState = displayProxyStateRef.current;
|
||||
const proxyDisplayProfileId = displayProxyState.profileId;
|
||||
const proxiesForRender = displayProxyState.view ?? proxyView;
|
||||
const isProxyRefreshPending =
|
||||
(switchStatus?.isSwitching ?? false) ||
|
||||
proxyHydration !== "live" ||
|
||||
proxyTargetProfileId !== proxyDisplayProfileId;
|
||||
}, [refreshProxy, refreshRules, refreshRuleProviders]);
|
||||
|
||||
const { data: sysproxy, mutate: refreshSysproxy } = useSWR(
|
||||
"getSystemProxy",
|
||||
@@ -675,10 +240,10 @@ export const AppDataProvider = ({
|
||||
errorRetryCount: 1,
|
||||
});
|
||||
|
||||
// Provide unified refresh method
|
||||
// 提供统一的刷新方法
|
||||
const refreshAll = useCallback(async () => {
|
||||
await Promise.all([
|
||||
fetchLiveProxies(),
|
||||
refreshProxy(),
|
||||
refreshClashConfig(),
|
||||
refreshRules(),
|
||||
refreshSysproxy(),
|
||||
@@ -686,6 +251,7 @@ export const AppDataProvider = ({
|
||||
refreshRuleProviders(),
|
||||
]);
|
||||
}, [
|
||||
refreshProxy,
|
||||
refreshClashConfig,
|
||||
refreshRules,
|
||||
refreshSysproxy,
|
||||
@@ -693,22 +259,22 @@ export const AppDataProvider = ({
|
||||
refreshRuleProviders,
|
||||
]);
|
||||
|
||||
// Aggregate data into context value
|
||||
// 聚合所有数据
|
||||
const value = useMemo(() => {
|
||||
// Compute the system proxy address
|
||||
// 计算系统代理地址
|
||||
const calculateSystemProxyAddress = () => {
|
||||
if (!verge || !clashConfig) return "-";
|
||||
|
||||
const isPacMode = verge.proxy_auto_config ?? false;
|
||||
|
||||
if (isPacMode) {
|
||||
// PAC mode: display the desired proxy address
|
||||
// PAC模式:显示我们期望设置的代理地址
|
||||
const proxyHost = verge.proxy_host || "127.0.0.1";
|
||||
const proxyPort =
|
||||
verge.verge_mixed_port || clashConfig.mixedPort || 7897;
|
||||
return `${proxyHost}:${proxyPort}`;
|
||||
} else {
|
||||
// HTTP proxy mode: prefer system address, fallback to desired address if invalid
|
||||
// HTTP代理模式:优先使用系统地址,但如果格式不正确则使用期望地址
|
||||
const systemServer = sysproxy?.server;
|
||||
if (
|
||||
systemServer &&
|
||||
@@ -717,7 +283,7 @@ export const AppDataProvider = ({
|
||||
) {
|
||||
return systemServer;
|
||||
} else {
|
||||
// System address invalid: fallback to desired proxy address
|
||||
// 系统地址无效,返回期望的代理地址
|
||||
const proxyHost = verge.proxy_host || "127.0.0.1";
|
||||
const proxyPort =
|
||||
verge.verge_mixed_port || clashConfig.mixedPort || 7897;
|
||||
@@ -727,27 +293,22 @@ export const AppDataProvider = ({
|
||||
};
|
||||
|
||||
return {
|
||||
// Data
|
||||
proxies: proxiesForRender,
|
||||
proxyHydration,
|
||||
proxyTargetProfileId,
|
||||
proxyDisplayProfileId,
|
||||
isProxyRefreshPending,
|
||||
switchStatus: switchStatus ?? null,
|
||||
// 数据
|
||||
proxies: proxiesData,
|
||||
clashConfig,
|
||||
rules: rulesData?.rules || [],
|
||||
sysproxy,
|
||||
runningMode,
|
||||
uptime: uptimeData || 0,
|
||||
|
||||
// Provider data
|
||||
// 提供者数据
|
||||
proxyProviders: proxyProviders || {},
|
||||
ruleProviders: ruleProviders?.providers || {},
|
||||
|
||||
systemProxyAddress: calculateSystemProxyAddress(),
|
||||
|
||||
// Refresh helpers
|
||||
refreshProxy: fetchLiveProxies,
|
||||
// 刷新方法
|
||||
refreshProxy,
|
||||
refreshClashConfig,
|
||||
refreshRules,
|
||||
refreshSysproxy,
|
||||
@@ -756,12 +317,7 @@ export const AppDataProvider = ({
|
||||
refreshAll,
|
||||
} as AppDataContextType;
|
||||
}, [
|
||||
proxiesForRender,
|
||||
proxyHydration,
|
||||
proxyTargetProfileId,
|
||||
proxyDisplayProfileId,
|
||||
isProxyRefreshPending,
|
||||
switchStatus,
|
||||
proxiesData,
|
||||
clashConfig,
|
||||
rulesData,
|
||||
sysproxy,
|
||||
@@ -770,6 +326,7 @@ export const AppDataProvider = ({
|
||||
proxyProviders,
|
||||
ruleProviders,
|
||||
verge,
|
||||
refreshProxy,
|
||||
refreshClashConfig,
|
||||
refreshRules,
|
||||
refreshSysproxy,
|
||||
|
||||
Reference in New Issue
Block a user