mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-28 07:14:40 +08:00
refactor(window): improve WindowProvider implementation
This commit is contained in:
@@ -1,46 +1,48 @@
|
||||
import { use } from "react";
|
||||
import { use, useMemo } from "react";
|
||||
|
||||
import { WindowContext, type WindowContextType } from "@/providers/window";
|
||||
import { WindowContext } from "@/providers/window-context";
|
||||
|
||||
export const useWindow = () => {
|
||||
const controlKeys = [
|
||||
"maximized",
|
||||
"minimize",
|
||||
"toggleMaximize",
|
||||
"close",
|
||||
"toggleFullscreen",
|
||||
"currentWindow",
|
||||
] as const;
|
||||
|
||||
const decorationKeys = [
|
||||
"decorated",
|
||||
"toggleDecorations",
|
||||
"refreshDecorated",
|
||||
] as const;
|
||||
|
||||
const pickWindowValues = <K extends keyof WindowContextType>(
|
||||
context: WindowContextType,
|
||||
keys: readonly K[],
|
||||
) =>
|
||||
keys.reduce(
|
||||
(result, key) => {
|
||||
result[key] = context[key];
|
||||
return result;
|
||||
},
|
||||
{} as Pick<WindowContextType, K>,
|
||||
);
|
||||
|
||||
const useWindowContext = () => {
|
||||
const context = use(WindowContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useWindow must be used within WindowProvider");
|
||||
if (!context) {
|
||||
throw new Error("useWindowContext must be used within WindowProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useWindowControls = () => {
|
||||
const {
|
||||
maximized,
|
||||
minimize,
|
||||
toggleMaximize,
|
||||
close,
|
||||
toggleFullscreen,
|
||||
currentWindow,
|
||||
} = useWindow();
|
||||
return {
|
||||
maximized,
|
||||
minimize,
|
||||
toggleMaximize,
|
||||
close,
|
||||
toggleFullscreen,
|
||||
currentWindow,
|
||||
} satisfies Pick<
|
||||
WindowContextType,
|
||||
| "maximized"
|
||||
| "minimize"
|
||||
| "toggleMaximize"
|
||||
| "close"
|
||||
| "toggleFullscreen"
|
||||
| "currentWindow"
|
||||
>;
|
||||
const context = useWindowContext();
|
||||
return useMemo(() => pickWindowValues(context, controlKeys), [context]);
|
||||
};
|
||||
|
||||
export const useWindowDecorations = () => {
|
||||
const { decorated, toggleDecorations, refreshDecorated } = useWindow();
|
||||
return { decorated, toggleDecorations, refreshDecorated } satisfies Pick<
|
||||
WindowContextType,
|
||||
"decorated" | "toggleDecorations" | "refreshDecorated"
|
||||
>;
|
||||
const context = useWindowContext();
|
||||
return useMemo(() => pickWindowValues(context, decorationKeys), [context]);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
|
||||
import { BaseErrorBoundary } from "./components/base";
|
||||
import { router } from "./pages/_routers";
|
||||
import { AppDataProvider } from "./providers/app-data-provider";
|
||||
import { WindowProvider } from "./providers/window";
|
||||
import { WindowProvider } from "./providers/window-provider";
|
||||
import { FALLBACK_LANGUAGE, initializeLanguage } from "./services/i18n";
|
||||
import {
|
||||
preloadAppData,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
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,32 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { ChainProxyContext } from "./chain-proxy-context";
|
||||
|
||||
export const ChainProxyProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const [isChainMode, setIsChainMode] = useState(false);
|
||||
const [chainConfigData, setChainConfigData] = useState<string | null>(null);
|
||||
|
||||
const setChainMode = useCallback((isChain: boolean) => {
|
||||
setIsChainMode(isChain);
|
||||
}, []);
|
||||
|
||||
const setChainConfigDataCallback = useCallback((data: string | null) => {
|
||||
setChainConfigData(data);
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
isChainMode,
|
||||
setChainMode,
|
||||
chainConfigData,
|
||||
setChainConfigData: setChainConfigDataCallback,
|
||||
}),
|
||||
[isChainMode, setChainMode, chainConfigData, setChainConfigDataCallback],
|
||||
);
|
||||
|
||||
return <ChainProxyContext value={contextValue}>{children}</ChainProxyContext>;
|
||||
};
|
||||
3
src/providers/window-context.ts
Normal file
3
src/providers/window-context.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export const WindowContext = createContext<WindowContextType | null>(null);
|
||||
102
src/providers/window-provider.tsx
Normal file
102
src/providers/window-provider.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { PropsWithChildren, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import debounce from "@/utils/debounce";
|
||||
|
||||
import { WindowContext } from "./window-context";
|
||||
|
||||
const currentWindow = getCurrentWindow();
|
||||
const initialState: Pick<WindowContextType, "decorated" | "maximized"> = {
|
||||
decorated: null,
|
||||
maximized: null,
|
||||
};
|
||||
|
||||
export const WindowProvider = ({ children }: PropsWithChildren) => {
|
||||
const [state, setState] =
|
||||
useState<Pick<WindowContextType, "decorated" | "maximized">>(initialState);
|
||||
|
||||
useEffect(() => {
|
||||
let isUnmounted = false;
|
||||
|
||||
const syncState = async () => {
|
||||
const [decorated, maximized] = await Promise.all([
|
||||
currentWindow.isDecorated(),
|
||||
currentWindow.isMaximized(),
|
||||
]);
|
||||
|
||||
if (!isUnmounted) {
|
||||
setState({ decorated, maximized });
|
||||
}
|
||||
};
|
||||
|
||||
const syncMaximized = debounce(async () => {
|
||||
if (!isUnmounted) {
|
||||
const maximized = await currentWindow.isMaximized();
|
||||
setState((prev) => ({ ...prev, maximized }));
|
||||
}
|
||||
}, 300);
|
||||
|
||||
currentWindow.setMinimizable?.(true);
|
||||
void syncState();
|
||||
|
||||
const unlistenPromise = currentWindow.onResized(syncMaximized);
|
||||
|
||||
return () => {
|
||||
isUnmounted = true;
|
||||
unlistenPromise
|
||||
.then((unlisten) => unlisten())
|
||||
.catch((err) =>
|
||||
console.warn("[WindowProvider] Failed to clean up listeners:", err),
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const actions = useMemo(() => {
|
||||
const refreshDecorated = async () => {
|
||||
const decorated = await currentWindow.isDecorated();
|
||||
setState((prev) => ({ ...prev, decorated }));
|
||||
return decorated;
|
||||
};
|
||||
|
||||
const toggleDecorations = async () => {
|
||||
const next = !(await currentWindow.isDecorated());
|
||||
await currentWindow.setDecorations(next);
|
||||
setState((prev) => ({ ...prev, decorated: next }));
|
||||
};
|
||||
|
||||
const toggleMaximize = async () => {
|
||||
const isMaximized = await currentWindow.isMaximized();
|
||||
if (isMaximized) {
|
||||
await currentWindow.unmaximize();
|
||||
} else {
|
||||
await currentWindow.maximize();
|
||||
}
|
||||
setState((prev) => ({ ...prev, maximized: !isMaximized }));
|
||||
};
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
const isFullscreen = await currentWindow.isFullscreen();
|
||||
await currentWindow.setFullscreen(!isFullscreen);
|
||||
};
|
||||
|
||||
return {
|
||||
minimize: () => currentWindow.minimize(),
|
||||
close: () => currentWindow.close(),
|
||||
refreshDecorated,
|
||||
toggleDecorations,
|
||||
toggleMaximize,
|
||||
toggleFullscreen,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
...state,
|
||||
...actions,
|
||||
currentWindow,
|
||||
}),
|
||||
[state, actions],
|
||||
);
|
||||
|
||||
return <WindowContext value={contextValue}>{children}</WindowContext>;
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
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,
|
||||
);
|
||||
@@ -1,95 +0,0 @@
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import debounce from "@/utils/debounce";
|
||||
|
||||
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 isUnmounted = false;
|
||||
|
||||
const checkMaximized = debounce(async () => {
|
||||
if (!isUnmounted) {
|
||||
const value = await currentWindow.isMaximized();
|
||||
setMaximized(value);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const unlistenPromise = currentWindow.onResized(checkMaximized);
|
||||
|
||||
return () => {
|
||||
isUnmounted = true;
|
||||
unlistenPromise
|
||||
.then((unlisten) => unlisten())
|
||||
.catch((err) => console.warn("[WindowProvider] 清理监听器失败:", err));
|
||||
};
|
||||
}, [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(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>;
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export { WindowContext } from "./WindowContext";
|
||||
export type { WindowContextType } from "./WindowContext";
|
||||
export { WindowProvider } from "./WindowProvider";
|
||||
14
src/types/global.d.ts
vendored
14
src/types/global.d.ts
vendored
@@ -198,6 +198,20 @@ interface ILogItem {
|
||||
}
|
||||
|
||||
type LogLevel = import("tauri-plugin-mihomo-api").LogLevel;
|
||||
|
||||
type AppWindow = import("@tauri-apps/api/window").WebviewWindow;
|
||||
|
||||
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: AppWindow;
|
||||
}
|
||||
type LogFilter = "all" | "debug" | "info" | "warn" | "err";
|
||||
type LogOrder = "asc" | "desc";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user