mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-15 11:14:31 +08:00
🐛 fix(window): 修复窗口尺寸与最大化状态未记忆
- 启动时恢复上次普通窗口尺寸与最大化状态 - 在标题栏最大化和退出前采样最新原生窗口状态 - 最大化前保留普通窗口恢复边界 - 补充启动恢复与退出持久化回归测试
This commit is contained in:
@@ -13,6 +13,41 @@ const appCss = readFileSync(
|
||||
|
||||
describe('settings center tool entries', () => {
|
||||
|
||||
it('captures native window bounds before maximising and before the final quit flush', () => {
|
||||
const startupRestoreStart = appSource.indexOf('const restoreWindowState = async');
|
||||
const startupRestoreEnd = appSource.indexOf('if (useStore.persist.hasHydrated())', startupRestoreStart);
|
||||
const startupRestoreSource = appSource.slice(startupRestoreStart, startupRestoreEnd);
|
||||
const restoreNormalBoundsBeforeMaximise = startupRestoreSource.indexOf('applyRestoredWindowBounds(bounds);');
|
||||
const startupMaximiseCall = startupRestoreSource.indexOf('applyStartupWindowChrome(1);');
|
||||
|
||||
expect(startupRestoreStart).toBeGreaterThanOrEqual(0);
|
||||
expect(startupRestoreEnd).toBeGreaterThan(startupRestoreStart);
|
||||
expect(restoreNormalBoundsBeforeMaximise).toBeGreaterThanOrEqual(0);
|
||||
expect(startupMaximiseCall).toBeGreaterThan(restoreNormalBoundsBeforeMaximise);
|
||||
|
||||
const titleBarToggleStart = appSource.indexOf('const handleTitleBarWindowToggle = async');
|
||||
const titleBarToggleEnd = appSource.indexOf('const handleTitleBarDoubleClick =', titleBarToggleStart);
|
||||
const titleBarToggleSource = appSource.slice(titleBarToggleStart, titleBarToggleEnd);
|
||||
const captureBeforeMaximise = titleBarToggleSource.indexOf('await captureMainWindowStateRef.current();');
|
||||
const maximiseCall = titleBarToggleSource.indexOf('WindowMaximise();', captureBeforeMaximise);
|
||||
|
||||
expect(titleBarToggleStart).toBeGreaterThanOrEqual(0);
|
||||
expect(titleBarToggleEnd).toBeGreaterThan(titleBarToggleStart);
|
||||
expect(captureBeforeMaximise).toBeGreaterThanOrEqual(0);
|
||||
expect(maximiseCall).toBeGreaterThan(captureBeforeMaximise);
|
||||
|
||||
const confirmedActionStart = appSource.indexOf('const runConfirmedAction = async');
|
||||
const confirmedActionEnd = appSource.indexOf('if (confirmedAction)', confirmedActionStart);
|
||||
const confirmedActionSource = appSource.slice(confirmedActionStart, confirmedActionEnd);
|
||||
const captureOnQuit = confirmedActionSource.indexOf('captureWindowState:');
|
||||
const flushOnQuit = confirmedActionSource.indexOf('flushAppState:');
|
||||
|
||||
expect(confirmedActionStart).toBeGreaterThanOrEqual(0);
|
||||
expect(confirmedActionEnd).toBeGreaterThan(confirmedActionStart);
|
||||
expect(captureOnQuit).toBeGreaterThanOrEqual(0);
|
||||
expect(flushOnQuit).toBeGreaterThan(captureOnQuit);
|
||||
});
|
||||
|
||||
it('keeps the resize minimise probe independent from DPR debounce and clears it on unmount', () => {
|
||||
const scaleEffectStart = appSource.indexOf('let minimisedCheckTimer: number | null = null;');
|
||||
const dprScheduleStart = appSource.indexOf('const scheduleDevicePixelRatioCheck = (trigger: WindowsScaleCheckTrigger) => {', scaleEffectStart);
|
||||
|
||||
@@ -228,6 +228,7 @@ import {
|
||||
collectApplicationQuitUnsavedSQLTargets,
|
||||
saveApplicationQuitUnsavedSQLTargets,
|
||||
} from './utils/sqlEditorApplicationQuit';
|
||||
import { prepareApplicationQuitPersistence } from './utils/applicationQuitPersistence';
|
||||
import { flushQueryTabDraftSnapshots } from './utils/sqlFileTabDrafts';
|
||||
import {
|
||||
APP_APPLICATION_QUIT_MODAL_Z_INDEX,
|
||||
@@ -1155,6 +1156,7 @@ function App() {
|
||||
const windowDiagSequenceRef = React.useRef(0);
|
||||
const windowDiagLastSignatureRef = React.useRef('');
|
||||
const windowDiagLastAtRef = React.useRef(0);
|
||||
const captureMainWindowStateRef = React.useRef<() => Promise<void>>(async () => undefined);
|
||||
const connectionWorkbenchState = getConnectionWorkbenchState(isStoreHydrated, hasLoadedSecureConfig);
|
||||
const securityUpdateStatusMeta = useMemo(
|
||||
() => getSecurityUpdateStatusMeta(securityUpdateStatus, t),
|
||||
@@ -1584,24 +1586,12 @@ function App() {
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
const restoreNormalWindowBounds = async (bounds: {
|
||||
const applyRestoredWindowBounds = (bounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}) => {
|
||||
try {
|
||||
if (await WindowIsFullscreen()) {
|
||||
WindowUnfullscreen();
|
||||
await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs));
|
||||
}
|
||||
if (await WindowIsMaximised()) {
|
||||
WindowUnmaximise();
|
||||
await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to restore normal window chrome', e);
|
||||
}
|
||||
const state = useStore.getState();
|
||||
const viewport = readCurrentVisibleViewport();
|
||||
const nextBounds = resolveVisibleStartupWindowBounds(bounds, viewport);
|
||||
@@ -1622,7 +1612,28 @@ function App() {
|
||||
});
|
||||
WindowSetPosition(setPosition.x, setPosition.y);
|
||||
state.setWindowBounds(nextBounds);
|
||||
state.setWindowState('normal');
|
||||
};
|
||||
|
||||
const restoreNormalWindowBounds = async (bounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}) => {
|
||||
try {
|
||||
if (await WindowIsFullscreen()) {
|
||||
WindowUnfullscreen();
|
||||
await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs));
|
||||
}
|
||||
if (await WindowIsMaximised()) {
|
||||
WindowUnmaximise();
|
||||
await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to restore normal window chrome', e);
|
||||
}
|
||||
applyRestoredWindowBounds(bounds);
|
||||
useStore.getState().setWindowState('normal');
|
||||
};
|
||||
|
||||
const restoreWindowState = async () => {
|
||||
@@ -1637,19 +1648,28 @@ function App() {
|
||||
restoredOnce = true;
|
||||
|
||||
const state = useStore.getState();
|
||||
const bounds = state.windowBounds;
|
||||
const restoreMode = resolveStartupWindowRestoreMode(
|
||||
state.startupFullscreen,
|
||||
state.windowState,
|
||||
);
|
||||
if (restoreMode !== 'normal') {
|
||||
if (bounds && bounds.width >= 400 && bounds.height >= 300) {
|
||||
try {
|
||||
// Seed the OS restore rectangle before maximising so a later
|
||||
// unmaximise returns to the user's last normal window bounds.
|
||||
applyRestoredWindowBounds(bounds);
|
||||
} catch (e) {
|
||||
console.warn('Failed to prepare remembered normal window bounds', e);
|
||||
}
|
||||
}
|
||||
markStartupWindowRestorePending(startupRestoreGraceMs);
|
||||
applyStartupWindowChrome(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// The disabled preference is strict: restore a normal window even if
|
||||
// an older build persisted an automatic maximised/fullscreen state.
|
||||
// Without a remembered maximised state, restore the last normal bounds.
|
||||
markStartupWindowRestorePending(startupRestoreGraceMs);
|
||||
const bounds = state.windowBounds;
|
||||
const viewport = readCurrentVisibleViewport();
|
||||
try {
|
||||
if (!bounds || bounds.width < 400 || bounds.height < 300) {
|
||||
@@ -1752,6 +1772,7 @@ function App() {
|
||||
// 静默忽略
|
||||
}
|
||||
};
|
||||
captureMainWindowStateRef.current = saveWindowState;
|
||||
|
||||
const scheduleWindowStateSave = (delayMs = 120) => {
|
||||
if (cancelled || !hydrated) {
|
||||
@@ -1885,6 +1906,9 @@ function App() {
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (captureMainWindowStateRef.current === saveWindowState) {
|
||||
captureMainWindowStateRef.current = async () => undefined;
|
||||
}
|
||||
if (eventSaveTimer !== null) {
|
||||
window.clearTimeout(eventSaveTimer);
|
||||
}
|
||||
@@ -2792,8 +2816,11 @@ function App() {
|
||||
const runConfirmedAction = async (): Promise<boolean> => {
|
||||
let accepted = false;
|
||||
try {
|
||||
flushQueryTabDraftSnapshots();
|
||||
await flushAppStatePersistence();
|
||||
await prepareApplicationQuitPersistence({
|
||||
captureWindowState: () => captureMainWindowStateRef.current(),
|
||||
flushDrafts: flushQueryTabDraftSnapshots,
|
||||
flushAppState: flushAppStatePersistence,
|
||||
});
|
||||
if (confirmedAction) {
|
||||
accepted = await confirmedAction();
|
||||
} else {
|
||||
@@ -4421,9 +4448,17 @@ function App() {
|
||||
if (isMaximised) {
|
||||
WindowUnmaximise();
|
||||
} else {
|
||||
// Preserve the latest normal bounds before the native maximise transition
|
||||
// makes WindowGetSize report the maximised surface.
|
||||
await captureMainWindowStateRef.current();
|
||||
WindowMaximise();
|
||||
}
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 96));
|
||||
await waitForWindowCondition({
|
||||
read: async () => (await WindowIsMaximised()) !== isMaximised,
|
||||
wait: (delayMs) => new Promise((resolve) => window.setTimeout(resolve, delayMs)),
|
||||
maxChecks: 16,
|
||||
intervalMs: 40,
|
||||
});
|
||||
await syncWindowStateFromRuntime();
|
||||
void emitWindowDiagnostic('action:titlebar-toggle:after-set-maximise-state');
|
||||
} catch (_) {
|
||||
|
||||
30
frontend/src/utils/applicationQuitPersistence.test.ts
Normal file
30
frontend/src/utils/applicationQuitPersistence.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { prepareApplicationQuitPersistence } from './applicationQuitPersistence';
|
||||
|
||||
describe('prepareApplicationQuitPersistence', () => {
|
||||
it('captures the latest native window before flushing persisted app state', async () => {
|
||||
const calls: string[] = [];
|
||||
const captureWindowState = vi.fn(async () => {
|
||||
calls.push('capture-window');
|
||||
});
|
||||
const flushDrafts = vi.fn(() => {
|
||||
calls.push('flush-drafts');
|
||||
});
|
||||
const flushAppState = vi.fn(async () => {
|
||||
calls.push('flush-app-state');
|
||||
});
|
||||
|
||||
await prepareApplicationQuitPersistence({
|
||||
captureWindowState,
|
||||
flushDrafts,
|
||||
flushAppState,
|
||||
});
|
||||
|
||||
expect(calls).toEqual([
|
||||
'capture-window',
|
||||
'flush-drafts',
|
||||
'flush-app-state',
|
||||
]);
|
||||
});
|
||||
});
|
||||
15
frontend/src/utils/applicationQuitPersistence.ts
Normal file
15
frontend/src/utils/applicationQuitPersistence.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type ApplicationQuitPersistenceTasks = {
|
||||
captureWindowState: () => Promise<void>;
|
||||
flushDrafts: () => void;
|
||||
flushAppState: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const prepareApplicationQuitPersistence = async ({
|
||||
captureWindowState,
|
||||
flushDrafts,
|
||||
flushAppState,
|
||||
}: ApplicationQuitPersistenceTasks): Promise<void> => {
|
||||
await captureWindowState();
|
||||
flushDrafts();
|
||||
await flushAppState();
|
||||
};
|
||||
@@ -99,12 +99,16 @@ describe('windowStartupLayout', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps startup normal when the explicit fullscreen preference is disabled', () => {
|
||||
expect(resolveStartupWindowRestoreMode(false)).toBe('normal');
|
||||
it('restores the last normal window when startup maximise is disabled', () => {
|
||||
expect(resolveStartupWindowRestoreMode(false, 'normal')).toBe('normal');
|
||||
});
|
||||
|
||||
it('restores a user-maximised window when startup maximise is disabled', () => {
|
||||
expect(resolveStartupWindowRestoreMode(false, 'maximized')).toBe('maximised');
|
||||
});
|
||||
|
||||
it('maximises the startup window on every desktop platform when enabled', () => {
|
||||
expect(resolveStartupWindowRestoreMode(true)).toBe('maximised');
|
||||
expect(resolveStartupWindowRestoreMode(true, 'normal')).toBe('maximised');
|
||||
});
|
||||
|
||||
it('does not accept a stale Windows WebView surface as a settled maximised window', () => {
|
||||
|
||||
@@ -17,6 +17,7 @@ const MIN_STARTUP_WIDTH = 900;
|
||||
const MIN_STARTUP_HEIGHT = 600;
|
||||
|
||||
export type StartupWindowRestoreMode = 'normal' | 'maximised';
|
||||
export type PersistedMainWindowState = 'normal' | 'maximized' | 'fullscreen';
|
||||
|
||||
export type StartupWindowSurfaceSnapshot = {
|
||||
surfaceWidth: number;
|
||||
@@ -31,13 +32,15 @@ export type StartupMaximisedWindowSnapshot = StartupWindowSurfaceSnapshot & {
|
||||
|
||||
const MIN_MAXIMISED_SURFACE_COVERAGE = 0.95;
|
||||
|
||||
/**
|
||||
* The explicit startup preference is authoritative. A disabled preference must
|
||||
* not be overridden by a previously maximised window or a size heuristic.
|
||||
*/
|
||||
/** Force maximise when requested; otherwise restore the last observed window state. */
|
||||
export const resolveStartupWindowRestoreMode = (
|
||||
startupMaximised: boolean,
|
||||
): StartupWindowRestoreMode => startupMaximised ? 'maximised' : 'normal';
|
||||
persistedWindowState: PersistedMainWindowState = 'normal',
|
||||
): StartupWindowRestoreMode => (
|
||||
startupMaximised || persistedWindowState === 'maximized' || persistedWindowState === 'fullscreen'
|
||||
? 'maximised'
|
||||
: 'normal'
|
||||
);
|
||||
|
||||
/**
|
||||
* Determine whether the native maximised state has also reached the WebView surface.
|
||||
|
||||
Reference in New Issue
Block a user