diff --git a/src/hooks/use-window.ts b/src/hooks/use-window.ts index ea2632b43..94dcb4656 100644 --- a/src/hooks/use-window.ts +++ b/src/hooks/use-window.ts @@ -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 = ( + context: WindowContextType, + keys: readonly K[], +) => + keys.reduce( + (result, key) => { + result[key] = context[key]; + return result; + }, + {} as Pick, + ); + +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]); }; diff --git a/src/main.tsx b/src/main.tsx index c2b9d42ca..accb409b8 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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, diff --git a/src/providers/chain-proxy-context.ts b/src/providers/chain-proxy-context.ts deleted file mode 100644 index 279eb7535..000000000 --- a/src/providers/chain-proxy-context.ts +++ /dev/null @@ -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( - null, -); - -export const useChainProxy = () => { - const context = use(ChainProxyContext); - if (!context) { - throw new Error("useChainProxy must be used within a ChainProxyProvider"); - } - return context; -}; diff --git a/src/providers/chain-proxy-provider.tsx b/src/providers/chain-proxy-provider.tsx deleted file mode 100644 index c9ba32e53..000000000 --- a/src/providers/chain-proxy-provider.tsx +++ /dev/null @@ -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(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 {children}; -}; diff --git a/src/providers/window-context.ts b/src/providers/window-context.ts new file mode 100644 index 000000000..e5236403b --- /dev/null +++ b/src/providers/window-context.ts @@ -0,0 +1,3 @@ +import { createContext } from "react"; + +export const WindowContext = createContext(null); diff --git a/src/providers/window-provider.tsx b/src/providers/window-provider.tsx new file mode 100644 index 000000000..3cc930e77 --- /dev/null +++ b/src/providers/window-provider.tsx @@ -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 = { + decorated: null, + maximized: null, +}; + +export const WindowProvider = ({ children }: PropsWithChildren) => { + const [state, setState] = + useState>(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 {children}; +}; diff --git a/src/providers/window/WindowContext.ts b/src/providers/window/WindowContext.ts deleted file mode 100644 index e5f7380e6..000000000 --- a/src/providers/window/WindowContext.ts +++ /dev/null @@ -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; - refreshDecorated: () => Promise; - minimize: () => void; - close: () => void; - toggleMaximize: () => Promise; - toggleFullscreen: () => Promise; - currentWindow: ReturnType; -} - -export const WindowContext = createContext( - undefined, -); diff --git a/src/providers/window/WindowProvider.tsx b/src/providers/window/WindowProvider.tsx deleted file mode 100644 index b43d22daf..000000000 --- a/src/providers/window/WindowProvider.tsx +++ /dev/null @@ -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(null); - const [maximized, setMaximized] = useState(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 {children}; -}; diff --git a/src/providers/window/index.ts b/src/providers/window/index.ts deleted file mode 100644 index 609d65409..000000000 --- a/src/providers/window/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { WindowContext } from "./WindowContext"; -export type { WindowContextType } from "./WindowContext"; -export { WindowProvider } from "./WindowProvider"; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index fd1c5ddc7..9879bfba5 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -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; + refreshDecorated: () => Promise; + minimize: () => void; + close: () => void; + toggleMaximize: () => Promise; + toggleFullscreen: () => Promise; + currentWindow: AppWindow; +} type LogFilter = "all" | "debug" | "info" | "warn" | "err"; type LogOrder = "asc" | "desc";