From 8316c75c7836af254257d6be338d99c5c4c5923c Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Fri, 12 Dec 2025 16:42:22 +0800 Subject: [PATCH] Revert "refactor(window): improve WindowProvider implementation" May break frontend input on macOS This reverts commit d8e386e3 --- Changelog.md | 1 - src/hooks/use-window.ts | 70 ++++++++------- src/main.tsx | 2 +- src/providers/chain-proxy-context.ts | 20 +++++ src/providers/chain-proxy-provider.tsx | 32 +++++++ src/providers/window-context.ts | 3 - src/providers/window-provider.tsx | 109 ------------------------ src/providers/window/WindowContext.ts | 18 ++++ src/providers/window/WindowProvider.tsx | 95 +++++++++++++++++++++ src/providers/window/index.ts | 3 + src/types/global.d.ts | 14 --- 11 files changed, 203 insertions(+), 164 deletions(-) create mode 100644 src/providers/chain-proxy-context.ts create mode 100644 src/providers/chain-proxy-provider.tsx delete mode 100644 src/providers/window-context.ts delete mode 100644 src/providers/window-provider.tsx create mode 100644 src/providers/window/WindowContext.ts create mode 100644 src/providers/window/WindowProvider.tsx create mode 100644 src/providers/window/index.ts diff --git a/Changelog.md b/Changelog.md index 0a639f2ab..b6f32fdfa 100644 --- a/Changelog.md +++ b/Changelog.md @@ -26,7 +26,6 @@ - 修复 macOS 在安装和卸载服务时提示与操作不匹配 - 修复菜单排序模式拖拽异常 - 修复托盘菜单代理组前的异常勾选状态 -- 修复自定义标题栏按钮在最小化/最大化/关闭后 hover 状态残留
✨ 新增功能 diff --git a/src/hooks/use-window.ts b/src/hooks/use-window.ts index 94dcb4656..ea2632b43 100644 --- a/src/hooks/use-window.ts +++ b/src/hooks/use-window.ts @@ -1,48 +1,46 @@ -import { use, useMemo } from "react"; +import { use } from "react"; -import { WindowContext } from "@/providers/window-context"; +import { WindowContext, type WindowContextType } from "@/providers/window"; -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 = () => { +export const useWindow = () => { const context = use(WindowContext); - if (!context) { - throw new Error("useWindowContext must be used within WindowProvider"); + if (context === undefined) { + throw new Error("useWindow must be used within WindowProvider"); } return context; }; export const useWindowControls = () => { - const context = useWindowContext(); - return useMemo(() => pickWindowValues(context, controlKeys), [context]); + const { + maximized, + minimize, + toggleMaximize, + close, + toggleFullscreen, + currentWindow, + } = useWindow(); + return { + maximized, + minimize, + toggleMaximize, + close, + toggleFullscreen, + currentWindow, + } satisfies Pick< + WindowContextType, + | "maximized" + | "minimize" + | "toggleMaximize" + | "close" + | "toggleFullscreen" + | "currentWindow" + >; }; export const useWindowDecorations = () => { - const context = useWindowContext(); - return useMemo(() => pickWindowValues(context, decorationKeys), [context]); + const { decorated, toggleDecorations, refreshDecorated } = useWindow(); + return { decorated, toggleDecorations, refreshDecorated } satisfies Pick< + WindowContextType, + "decorated" | "toggleDecorations" | "refreshDecorated" + >; }; diff --git a/src/main.tsx b/src/main.tsx index accb409b8..c2b9d42ca 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-provider"; +import { WindowProvider } from "./providers/window"; 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 new file mode 100644 index 000000000..279eb7535 --- /dev/null +++ b/src/providers/chain-proxy-context.ts @@ -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( + 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 new file mode 100644 index 000000000..c9ba32e53 --- /dev/null +++ b/src/providers/chain-proxy-provider.tsx @@ -0,0 +1,32 @@ +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 deleted file mode 100644 index e5236403b..000000000 --- a/src/providers/window-context.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from "react"; - -export const WindowContext = createContext(null); diff --git a/src/providers/window-provider.tsx b/src/providers/window-provider.tsx deleted file mode 100644 index 8bc8aaa12..000000000 --- a/src/providers/window-provider.tsx +++ /dev/null @@ -1,109 +0,0 @@ -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: async () => { - // Delay one frame so the UI can clear :hover before the window hides. - await new Promise((resolve) => setTimeout(resolve, 10)); - await currentWindow.minimize(); - }, - close: async () => { - await new Promise((resolve) => setTimeout(resolve, 20)); - await 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 new file mode 100644 index 000000000..e5f7380e6 --- /dev/null +++ b/src/providers/window/WindowContext.ts @@ -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; + 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 new file mode 100644 index 000000000..b43d22daf --- /dev/null +++ b/src/providers/window/WindowProvider.tsx @@ -0,0 +1,95 @@ +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 new file mode 100644 index 000000000..609d65409 --- /dev/null +++ b/src/providers/window/index.ts @@ -0,0 +1,3 @@ +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 9879bfba5..fd1c5ddc7 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -198,20 +198,6 @@ 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";