mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-18 16:30:32 +08:00
refactor(window): improve WindowProvider implementation
This commit is contained in:
parent
bae65a523a
commit
d8e386e394
@ -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);
|
const context = use(WindowContext);
|
||||||
if (context === undefined) {
|
if (!context) {
|
||||||
throw new Error("useWindow must be used within WindowProvider");
|
throw new Error("useWindowContext must be used within WindowProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useWindowControls = () => {
|
export const useWindowControls = () => {
|
||||||
const {
|
const context = useWindowContext();
|
||||||
maximized,
|
return useMemo(() => pickWindowValues(context, controlKeys), [context]);
|
||||||
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 = () => {
|
export const useWindowDecorations = () => {
|
||||||
const { decorated, toggleDecorations, refreshDecorated } = useWindow();
|
const context = useWindowContext();
|
||||||
return { decorated, toggleDecorations, refreshDecorated } satisfies Pick<
|
return useMemo(() => pickWindowValues(context, decorationKeys), [context]);
|
||||||
WindowContextType,
|
|
||||||
"decorated" | "toggleDecorations" | "refreshDecorated"
|
|
||||||
>;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
|
|||||||
import { BaseErrorBoundary } from "./components/base";
|
import { BaseErrorBoundary } from "./components/base";
|
||||||
import { router } from "./pages/_routers";
|
import { router } from "./pages/_routers";
|
||||||
import { AppDataProvider } from "./providers/app-data-provider";
|
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 { FALLBACK_LANGUAGE, initializeLanguage } from "./services/i18n";
|
||||||
import {
|
import {
|
||||||
preloadAppData,
|
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 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 LogFilter = "all" | "debug" | "info" | "warn" | "err";
|
||||||
type LogOrder = "asc" | "desc";
|
type LogOrder = "asc" | "desc";
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user