mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
🐛 fix: restore window position safely
🐛 fix: restore window position safely
This commit is contained in:
@@ -42,6 +42,13 @@ export interface BrowserWindowOpts extends BrowserWindowConstructorOptions {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface WindowState {
|
||||
height?: number;
|
||||
width?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
}
|
||||
|
||||
export default class Browser {
|
||||
private app: App;
|
||||
private _browserWindow?: BrowserWindow;
|
||||
@@ -152,6 +159,46 @@ export default class Browser {
|
||||
this._browserWindow.setTitleBarOverlay(config.titleBarOverlay);
|
||||
}
|
||||
|
||||
private clampNumber(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
private resolveWindowState(
|
||||
savedState: WindowState | undefined,
|
||||
fallbackState: { height?: number; width?: number },
|
||||
): WindowState {
|
||||
const width = savedState?.width ?? fallbackState.width;
|
||||
const height = savedState?.height ?? fallbackState.height;
|
||||
const resolvedState: WindowState = { height, width };
|
||||
|
||||
const hasPosition = Number.isFinite(savedState?.x) && Number.isFinite(savedState?.y);
|
||||
if (!hasPosition) return resolvedState;
|
||||
|
||||
const x = savedState?.x as number;
|
||||
const y = savedState?.y as number;
|
||||
|
||||
const targetDisplay = screen.getDisplayMatching({
|
||||
height: height ?? 0,
|
||||
width: width ?? 0,
|
||||
x,
|
||||
y,
|
||||
});
|
||||
|
||||
const workArea = targetDisplay?.workArea ?? screen.getPrimaryDisplay().workArea;
|
||||
const resolvedWidth = typeof width === 'number' ? Math.min(width, workArea.width) : width;
|
||||
const resolvedHeight = typeof height === 'number' ? Math.min(height, workArea.height) : height;
|
||||
|
||||
const maxX = workArea.x + Math.max(0, workArea.width - (resolvedWidth ?? 0));
|
||||
const maxY = workArea.y + Math.max(0, workArea.height - (resolvedHeight ?? 0));
|
||||
|
||||
return {
|
||||
height: resolvedHeight,
|
||||
width: resolvedWidth,
|
||||
x: this.clampNumber(x, workArea.x, maxX),
|
||||
y: this.clampNumber(y, workArea.y, maxY),
|
||||
};
|
||||
}
|
||||
|
||||
private cleanupThemeListener(): void {
|
||||
if (this.themeListenerSetup) {
|
||||
// Note: nativeTheme listeners are global, consider using a centralized theme manager
|
||||
@@ -323,12 +370,17 @@ export default class Browser {
|
||||
|
||||
// Load window state
|
||||
const savedState = this.app.storeManager.get(this.windowStateKey as any) as
|
||||
| { height?: number; width?: number }
|
||||
| undefined; // Keep type for now, but only use w/h
|
||||
| WindowState
|
||||
| undefined;
|
||||
logger.info(`Creating new BrowserWindow instance: ${this.identifier}`);
|
||||
logger.debug(`[${this.identifier}] Options for new window: ${JSON.stringify(this.options)}`);
|
||||
logger.debug(
|
||||
`[${this.identifier}] Saved window state (only size used): ${JSON.stringify(savedState)}`,
|
||||
`[${this.identifier}] Saved window state: ${JSON.stringify(savedState)}`,
|
||||
);
|
||||
|
||||
const resolvedState = this.resolveWindowState(savedState, { height, width });
|
||||
logger.debug(
|
||||
`[${this.identifier}] Resolved window state: ${JSON.stringify(resolvedState)}`,
|
||||
);
|
||||
|
||||
const browserWindow = new BrowserWindow({
|
||||
@@ -337,7 +389,7 @@ export default class Browser {
|
||||
backgroundColor: '#00000000',
|
||||
darkTheme: this.isDarkMode,
|
||||
frame: false,
|
||||
height: savedState?.height || height,
|
||||
height: resolvedState.height,
|
||||
show: false,
|
||||
title,
|
||||
vibrancy: 'sidebar',
|
||||
@@ -348,7 +400,9 @@ export default class Browser {
|
||||
preload: join(preloadDir, 'index.js'),
|
||||
sandbox: false,
|
||||
},
|
||||
width: savedState?.width || width,
|
||||
width: resolvedState.width,
|
||||
x: resolvedState.x,
|
||||
y: resolvedState.y,
|
||||
...this.getPlatformThemeConfig(),
|
||||
});
|
||||
|
||||
@@ -405,12 +459,17 @@ export default class Browser {
|
||||
logger.debug(`[${this.identifier}] App is quitting, allowing window to close naturally.`);
|
||||
// Save state before quitting
|
||||
try {
|
||||
const { width, height } = browserWindow.getBounds(); // Get only width and height
|
||||
const sizeState = { height, width };
|
||||
const bounds = browserWindow.getBounds();
|
||||
const sizeState = {
|
||||
height: bounds.height,
|
||||
width: bounds.width,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
};
|
||||
logger.debug(
|
||||
`[${this.identifier}] Saving window size on quit: ${JSON.stringify(sizeState)}`,
|
||||
`[${this.identifier}] Saving window state on quit: ${JSON.stringify(sizeState)}`,
|
||||
);
|
||||
this.app.storeManager.set(this.windowStateKey as any, sizeState); // Save only size
|
||||
this.app.storeManager.set(this.windowStateKey as any, sizeState);
|
||||
} catch (error) {
|
||||
logger.error(`[${this.identifier}] Failed to save window state on quit:`, error);
|
||||
}
|
||||
@@ -437,15 +496,20 @@ export default class Browser {
|
||||
} else {
|
||||
// Window is actually closing (not keepAlive)
|
||||
logger.debug(
|
||||
`[${this.identifier}] keepAlive is false, allowing window to close. Saving size...`, // Updated log message
|
||||
`[${this.identifier}] keepAlive is false, allowing window to close. Saving state...`,
|
||||
);
|
||||
try {
|
||||
const { width, height } = browserWindow.getBounds(); // Get only width and height
|
||||
const sizeState = { height, width };
|
||||
const bounds = browserWindow.getBounds();
|
||||
const sizeState = {
|
||||
height: bounds.height,
|
||||
width: bounds.width,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
};
|
||||
logger.debug(
|
||||
`[${this.identifier}] Saving window size on close: ${JSON.stringify(sizeState)}`,
|
||||
`[${this.identifier}] Saving window state on close: ${JSON.stringify(sizeState)}`,
|
||||
);
|
||||
this.app.storeManager.set(this.windowStateKey as any, sizeState); // Save only size
|
||||
this.app.storeManager.set(this.windowStateKey as any, sizeState);
|
||||
} catch (error) {
|
||||
logger.error(`[${this.identifier}] Failed to save window state on close:`, error);
|
||||
}
|
||||
|
||||
@@ -56,9 +56,15 @@ const { mockBrowserWindow, mockNativeTheme, mockIpcMain, mockScreen, MockBrowser
|
||||
themeSource: 'system',
|
||||
},
|
||||
mockScreen: {
|
||||
getDisplayMatching: vi.fn().mockReturnValue({
|
||||
workArea: { height: 1080, width: 1920, x: 0, y: 0 },
|
||||
}),
|
||||
getDisplayNearestPoint: vi.fn().mockReturnValue({
|
||||
workArea: { height: 1080, width: 1920, x: 0, y: 0 },
|
||||
}),
|
||||
getPrimaryDisplay: vi.fn().mockReturnValue({
|
||||
workArea: { height: 1080, width: 1920, x: 0, y: 0 },
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -240,6 +246,47 @@ describe('Browser', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should restore window position from store and clamp within display', () => {
|
||||
mockStoreManagerGet.mockImplementation((key: string) => {
|
||||
if (key === 'windowSize_test-window') {
|
||||
return { height: 700, width: 900, x: 1800, y: 900 };
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
new Browser(defaultOptions, mockApp);
|
||||
|
||||
expect(MockBrowserWindow).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
height: 700,
|
||||
width: 900,
|
||||
x: 1020,
|
||||
y: 380,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should clamp saved size when it exceeds current display bounds', () => {
|
||||
mockScreen.getDisplayMatching.mockReturnValueOnce({
|
||||
workArea: { height: 800, width: 1200, x: 0, y: 0 },
|
||||
});
|
||||
mockStoreManagerGet.mockImplementation((key: string) => {
|
||||
if (key === 'windowSize_test-window') {
|
||||
return { height: 1200, width: 2000, x: 0, y: 0 };
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
new Browser(defaultOptions, mockApp);
|
||||
|
||||
expect(MockBrowserWindow).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
height: 800,
|
||||
width: 1200,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use default size when no saved state', () => {
|
||||
mockStoreManagerGet.mockReturnValue(undefined);
|
||||
|
||||
@@ -541,6 +588,8 @@ describe('Browser', () => {
|
||||
expect(mockStoreManagerSet).toHaveBeenCalledWith('windowSize_test-window', {
|
||||
height: 600,
|
||||
width: 800,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -572,6 +621,8 @@ describe('Browser', () => {
|
||||
expect(mockStoreManagerSet).toHaveBeenCalledWith('windowSize_test-window', {
|
||||
height: 600,
|
||||
width: 800,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user