返回 DeepSeek-Reasonix
windowState.ts
根目录 / desktop / frontend / src / lib / windowState.ts
1 // useWindowStatePersistence polls the Wails runtime for window geometry and
2 // persists it via SaveWindowState so the next launch restores the same size and
3 // position. No-op in browser dev (no window.runtime).
4 //
5 // The frontend is the sole source of geometry: resize (debounced), a 5s poll,
6 // and beforeunload. Go never reads WindowGetSize/Position/IsMaximised during
7 // beforeClose or shutdown (those paths can panic on Windows when DPI reports
8 // 0). The Go shutdown hook only re-persists the last frontend-reported state.
9
10 import { useEffect } from "react";
11 import { app } from "./bridge";
12
13 export interface WindowStateSnapshot {
14 width: number;
15 height: number;
16 x: number;
17 y: number;
18 maximised: boolean;
19 }
20
21 interface WindowStateRuntime {
22 WindowGetSize(): Promise<{ w: number; h: number }>;
23 WindowGetPosition(): Promise<{ x: number; y: number }>;
24 WindowIsMaximised(): Promise<boolean>;
25 }
26
27 // Serialize capture + persistence as one operation. Resize, polling, and
28 // beforeunload can all request a save while an earlier Wails call is still in
29 // flight; a queue prevents an older completion from overwriting a newer
30 // observation. A queued request captures geometry only when it reaches the
31 // front, so it observes the latest native state instead of replaying a stale
32 // snapshot.
33 export function createWindowStateSaver(
34 runtime: WindowStateRuntime,
35 persist: (state: WindowStateSnapshot) => Promise<void>,
36 ): () => Promise<void> {
37 let lastState = "";
38 let queue = Promise.resolve();
39
40 return () => {
41 queue = queue.then(async () => {
42 try {
43 const size = await runtime.WindowGetSize();
44 const pos = await runtime.WindowGetPosition();
45 const maximised = await runtime.WindowIsMaximised();
46 const state = { width: size.w, height: size.h, x: pos.x, y: pos.y, maximised };
47 const json = JSON.stringify(state);
48 if (json === lastState) return;
49 await persist(state);
50 lastState = json;
51 } catch {
52 /* runtime not ready yet — a later request will retry */
53 }
54 });
55 return queue;
56 };
57 }
58
59 export function useWindowStatePersistence() {
60 useEffect(() => {
61 const runtime = typeof window !== "undefined" ? window.runtime : undefined;
62 if (!runtime?.WindowGetSize || !runtime.WindowGetPosition || !runtime.WindowIsMaximised) return;
63 const { WindowGetSize, WindowGetPosition, WindowIsMaximised } = runtime;
64
65 let timer: ReturnType<typeof setInterval>;
66 const save = createWindowStateSaver(
67 { WindowGetSize, WindowGetPosition, WindowIsMaximised },
68 (state) => app.SaveWindowState(state),
69 );
70
71 // Debounced save on resize (500ms after the last resize event).
72 let debounce: ReturnType<typeof setTimeout>;
73 const onResize = () => {
74 clearTimeout(debounce);
75 debounce = setTimeout(save, 500);
76 };
77 window.addEventListener("resize", onResize);
78
79 // Periodic poll every 5s for moves/maximise that don't trigger resize.
80 timer = setInterval(save, 5000);
81
82 // Best-effort save before the page unloads. Go re-persists the last
83 // accepted report during shutdown without querying the native window.
84 const onBeforeUnload = () => { void save(); };
85 window.addEventListener("beforeunload", onBeforeUnload);
86
87 return () => {
88 clearInterval(timer);
89 clearTimeout(debounce);
90 window.removeEventListener("resize", onResize);
91 window.removeEventListener("beforeunload", onBeforeUnload);
92 };
93 }, []);
94 }
95
96 export function useViewportHeightVar() {
97 useEffect(() => {
98 if (typeof window === "undefined" || typeof document === "undefined") return;
99
100 let frame = 0;
101 const root = document.documentElement;
102 const setHeight = () => {
103 frame = 0;
104 const height = Math.round(window.visualViewport?.height ?? window.innerHeight);
105 if (height > 0) root.style.setProperty("--app-viewport-height", `${height}px`);
106 };
107 const schedule = () => {
108 if (frame) window.cancelAnimationFrame(frame);
109 frame = window.requestAnimationFrame(setHeight);
110 };
111
112 schedule();
113 window.addEventListener("resize", schedule);
114 window.addEventListener("orientationchange", schedule);
115 document.addEventListener("fullscreenchange", schedule);
116 window.visualViewport?.addEventListener("resize", schedule);
117
118 return () => {
119 if (frame) window.cancelAnimationFrame(frame);
120 window.removeEventListener("resize", schedule);
121 window.removeEventListener("orientationchange", schedule);
122 document.removeEventListener("fullscreenchange", schedule);
123 window.visualViewport?.removeEventListener("resize", schedule);
124 };
125 }, []);
126 }
127
127 lines TYPESCRIPT