refactor: frontend (#5068)

* refactor: setting components

* refactor: frontend

* fix: settings router
This commit is contained in:
Sline
2025-10-15 18:57:44 +08:00
committed by GitHub
parent a591ee1efc
commit 0b4403b67b
34 changed files with 1072 additions and 861 deletions

View File

@@ -38,6 +38,51 @@ export const AppDataProvider = ({
},
);
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
"getClashConfig",
getBaseConfig,
{
refreshInterval: 60000, // 60秒刷新间隔减少频繁请求
revalidateOnFocus: false,
suspense: false,
errorRetryCount: 3,
},
);
// 提供者数据
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
"getProxyProviders",
calcuProxyProviders,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 3000,
suspense: false,
errorRetryCount: 3,
},
);
const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR(
"getRuleProviders",
getRuleProviders,
{
revalidateOnFocus: false,
suspense: false,
errorRetryCount: 3,
},
);
// 低频率更新数据
const { data: rulesData, mutate: refreshRules } = useSWR(
"getRules",
getRules,
{
revalidateOnFocus: false,
suspense: false,
errorRetryCount: 3,
},
);
// 监听profile和clash配置变更事件
useEffect(() => {
let lastProfileId: string | null = null;
@@ -47,7 +92,6 @@ export const AppDataProvider = ({
let isUnmounted = false;
const scheduledTimeouts = new Set<ReturnType<typeof setTimeout>>();
const cleanupFns: Array<() => void> = [];
const fallbackWindowListeners: Array<[string, EventListener]> = [];
const registerCleanup = (fn: () => void) => {
if (isUnmounted) {
@@ -57,6 +101,12 @@ export const AppDataProvider = ({
}
};
const addWindowListener = (eventName: string, handler: EventListener) => {
// eslint-disable-next-line @eslint-react/web-api/no-leaked-event-listener -- cleanup is returned by this helper
window.addEventListener(eventName, handler);
return () => window.removeEventListener(eventName, handler);
};
const scheduleTimeout = (
callback: () => void | Promise<void>,
delay: number,
@@ -70,40 +120,11 @@ export const AppDataProvider = ({
return timeoutId;
};
const clearScheduledTimeout = (
timeoutId: ReturnType<typeof setTimeout>,
) => {
if (scheduledTimeouts.has(timeoutId)) {
clearTimeout(timeoutId);
scheduledTimeouts.delete(timeoutId);
}
};
const clearAllTimeouts = () => {
scheduledTimeouts.forEach((timeoutId) => clearTimeout(timeoutId));
scheduledTimeouts.clear();
};
const withTimeout = async <T,>(
promise: Promise<T>,
timeoutMs: number,
label: string,
): Promise<T> => {
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = scheduleTimeout(() => reject(new Error(label)), timeoutMs);
});
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
if (timeoutId !== null) {
clearScheduledTimeout(timeoutId);
}
}
};
const handleProfileChanged = (event: { payload: string }) => {
const newProfileId = event.payload;
const now = Date.now();
@@ -204,8 +225,7 @@ export const AppDataProvider = ({
];
fallbackHandlers.forEach(([eventName, handler]) => {
window.addEventListener(eventName, handler);
fallbackWindowListeners.push([eventName, handler]);
registerCleanup(addWindowListener(eventName, handler));
});
}
};
@@ -215,57 +235,9 @@ export const AppDataProvider = ({
return () => {
isUnmounted = true;
clearAllTimeouts();
fallbackWindowListeners.splice(0).forEach(([eventName, handler]) => {
window.removeEventListener(eventName, handler);
});
cleanupFns.splice(0).forEach((fn) => fn());
};
}, [refreshProxy]);
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
"getClashConfig",
getBaseConfig,
{
refreshInterval: 60000, // 60秒刷新间隔减少频繁请求
revalidateOnFocus: false,
suspense: false,
errorRetryCount: 3,
},
);
// 提供者数据
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
"getProxyProviders",
calcuProxyProviders,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 3000,
suspense: false,
errorRetryCount: 3,
},
);
const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR(
"getRuleProviders",
getRuleProviders,
{
revalidateOnFocus: false,
suspense: false,
errorRetryCount: 3,
},
);
// 低频率更新数据
const { data: rulesData, mutate: refreshRules } = useSWR(
"getRules",
getRules,
{
revalidateOnFocus: false,
suspense: false,
errorRetryCount: 3,
},
);
}, [refreshProxy, refreshRules, refreshRuleProviders]);
const { data: sysproxy, mutate: refreshSysproxy } = useSWR(
"getSystemProxy",

View File

@@ -0,0 +1,20 @@
import { createContext, use } from "react";
export interface ChainProxyContextType {
isChainMode: boolean;
setChainMode: (isChain: boolean) => void;
chainConfigData: string | null;
setChainConfigData: (data: string | null) => void;
}
export const ChainProxyContext = createContext<ChainProxyContextType | null>(
null,
);
export const useChainProxy = () => {
const context = use(ChainProxyContext);
if (!context) {
throw new Error("useChainProxy must be used within a ChainProxyProvider");
}
return context;
};

View File

@@ -1,13 +1,6 @@
import React, { createContext, useCallback, use, useState } from "react";
import React, { useCallback, useMemo, useState } from "react";
interface ChainProxyContextType {
isChainMode: boolean;
setChainMode: (isChain: boolean) => void;
chainConfigData: string | null;
setChainConfigData: (data: string | null) => void;
}
const ChainProxyContext = createContext<ChainProxyContextType | null>(null);
import { ChainProxyContext } from "./chain-proxy-context";
export const ChainProxyProvider = ({
children,
@@ -25,24 +18,15 @@ export const ChainProxyProvider = ({
setChainConfigData(data);
}, []);
return (
<ChainProxyContext
value={{
isChainMode,
setChainMode,
chainConfigData,
setChainConfigData: setChainConfigDataCallback,
}}
>
{children}
</ChainProxyContext>
const contextValue = useMemo(
() => ({
isChainMode,
setChainMode,
chainConfigData,
setChainConfigData: setChainConfigDataCallback,
}),
[isChainMode, setChainMode, chainConfigData, setChainConfigDataCallback],
);
};
export const useChainProxy = () => {
const context = use(ChainProxyContext);
if (!context) {
throw new Error("useChainProxy must be used within a ChainProxyProvider");
}
return context;
return <ChainProxyContext value={contextValue}>{children}</ChainProxyContext>;
};

View File

@@ -0,0 +1,18 @@
import { getCurrentWindow } from "@tauri-apps/api/window";
import { createContext } from "react";
export interface WindowContextType {
decorated: boolean | null;
maximized: boolean | null;
toggleDecorations: () => Promise<void>;
refreshDecorated: () => Promise<boolean>;
minimize: () => void;
close: () => void;
toggleMaximize: () => Promise<void>;
toggleFullscreen: () => Promise<void>;
currentWindow: ReturnType<typeof getCurrentWindow>;
}
export const WindowContext = createContext<WindowContextType | undefined>(
undefined,
);

View File

@@ -0,0 +1,91 @@
import { getCurrentWindow } from "@tauri-apps/api/window";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { WindowContext } from "./WindowContext";
export const WindowProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const currentWindow = useMemo(() => getCurrentWindow(), []);
const [decorated, setDecorated] = useState<boolean | null>(null);
const [maximized, setMaximized] = useState<boolean | null>(null);
const close = useCallback(() => currentWindow.close(), [currentWindow]);
const minimize = useCallback(() => currentWindow.minimize(), [currentWindow]);
useEffect(() => {
let active = true;
const updateMaximized = async () => {
const value = await currentWindow.isMaximized();
if (!active) return;
setMaximized((prev) => (prev === value ? prev : value));
};
updateMaximized();
const unlistenPromise = currentWindow.onResized(updateMaximized);
return () => {
active = false;
unlistenPromise.then((unlisten) => unlisten());
};
}, [currentWindow]);
const toggleMaximize = useCallback(async () => {
if (await currentWindow.isMaximized()) {
await currentWindow.unmaximize();
setMaximized(false);
} else {
await currentWindow.maximize();
setMaximized(true);
}
}, [currentWindow]);
const toggleFullscreen = useCallback(async () => {
await currentWindow.setFullscreen(!(await currentWindow.isFullscreen()));
}, [currentWindow]);
const refreshDecorated = useCallback(async () => {
const val = await currentWindow.isDecorated();
setDecorated((prev) => (prev === val ? prev : val));
return val;
}, [currentWindow]);
const toggleDecorations = useCallback(async () => {
const currentVal = await currentWindow.isDecorated();
await currentWindow.setDecorations(!currentVal);
setDecorated(!currentVal);
}, [currentWindow]);
useEffect(() => {
refreshDecorated();
currentWindow.setMinimizable?.(true);
}, [currentWindow, refreshDecorated]);
const contextValue = useMemo(
() => ({
decorated,
maximized,
toggleDecorations,
refreshDecorated,
minimize,
close,
toggleMaximize,
toggleFullscreen,
currentWindow,
}),
[
decorated,
maximized,
toggleDecorations,
refreshDecorated,
minimize,
close,
toggleMaximize,
toggleFullscreen,
currentWindow,
],
);
return <WindowContext value={contextValue}>{children}</WindowContext>;
};

View File

@@ -0,0 +1,2 @@
export * from "./WindowContext";
export * from "./WindowProvider";