refactor: frontend (#5068)
* refactor: setting components * refactor: frontend * fix: settings router
This commit is contained in:
@@ -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",
|
||||
|
||||
20
src/providers/chain-proxy-context.ts
Normal file
20
src/providers/chain-proxy-context.ts
Normal 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;
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
18
src/providers/window/WindowContext.ts
Normal file
18
src/providers/window/WindowContext.ts
Normal 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,
|
||||
);
|
||||
91
src/providers/window/WindowProvider.tsx
Normal file
91
src/providers/window/WindowProvider.tsx
Normal 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>;
|
||||
};
|
||||
2
src/providers/window/index.ts
Normal file
2
src/providers/window/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./WindowContext";
|
||||
export * from "./WindowProvider";
|
||||
Reference in New Issue
Block a user