refactor(window): improve WindowProvider implementation

This commit is contained in:
Slinetrac
2025-12-09 14:33:09 +08:00
parent bae65a523a
commit d8e386e394
10 changed files with 156 additions and 203 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
import { createContext } from "react";
export const WindowContext = createContext<WindowContextType | null>(null);

View 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>;
};

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
export { WindowContext } from "./WindowContext";
export type { WindowContextType } from "./WindowContext";
export { WindowProvider } from "./WindowProvider";

14
src/types/global.d.ts vendored
View File

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