ab825488b8
- W2/UX19: windowState.ts persists the window's normal bounds + maximized flag to <userData>/window-state.json (no new dep); createWindow restores from it with a screen.getAllDisplays() visibility guard so it never reopens off a disconnected monitor. Debounced save on resize/move + save on close (which may hide to tray). - W10: notify() flashFrame(true)s the taskbar when a completion/failure lands while the window is unfocused/minimized (Windows clears the flash on focus). - W11: already implemented (badge.ts overlay dot + setOverlayIcon) — ticked. typecheck + 268 tests + eslint + prettier green. (Window restore + flash want a live quit/relaunch confirm.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import { app, screen, type BrowserWindow, type Rectangle } from 'electron'
|
|
import { join } from 'path'
|
|
import { readFileSync, writeFileSync } from 'fs'
|
|
import { logger } from './logger'
|
|
|
|
/**
|
|
* Persist and restore the main window's size / position / maximized state across
|
|
* launches (W2 / UX19) — Windows apps are expected to reopen where you left them.
|
|
* Stored as one small JSON object in `<userData>/window-state.json`; a disconnected
|
|
* monitor is handled by a visibility check so the window can never restore off-screen.
|
|
*/
|
|
|
|
interface WindowState {
|
|
bounds: Rectangle
|
|
maximized: boolean
|
|
}
|
|
|
|
// Matches the createWindow() defaults; used on first run and when a saved position
|
|
// is no longer on any connected display.
|
|
const DEFAULT_SIZE = { width: 920, height: 700 }
|
|
|
|
function stateFile(): string {
|
|
return join(app.getPath('userData'), 'window-state.json')
|
|
}
|
|
|
|
function readState(): WindowState | null {
|
|
try {
|
|
const s = JSON.parse(readFileSync(stateFile(), 'utf8')) as Partial<WindowState>
|
|
if (s?.bounds && typeof s.bounds.width === 'number' && typeof s.bounds.height === 'number') {
|
|
return { bounds: s.bounds as Rectangle, maximized: !!s.maximized }
|
|
}
|
|
} catch {
|
|
/* first run or corrupt — fall back to defaults */
|
|
}
|
|
return null
|
|
}
|
|
|
|
/** True when `b` overlaps the work area of some connected display (i.e. is reachable). */
|
|
function isOnSomeDisplay(b: Rectangle): boolean {
|
|
return screen.getAllDisplays().some((d) => {
|
|
const wa = d.workArea
|
|
return (
|
|
b.x < wa.x + wa.width &&
|
|
b.x + b.width > wa.x &&
|
|
b.y < wa.y + wa.height &&
|
|
b.y + b.height > wa.y
|
|
)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* The bounds + maximized flag to open the window with. Returns just a size (Electron
|
|
* then centers it) when there's no usable saved position — first run, or the saved
|
|
* monitor is gone.
|
|
*/
|
|
export function initialWindowState(): {
|
|
width: number
|
|
height: number
|
|
x?: number
|
|
y?: number
|
|
maximized: boolean
|
|
} {
|
|
const saved = readState()
|
|
if (saved && isOnSomeDisplay(saved.bounds)) {
|
|
return { ...saved.bounds, maximized: saved.maximized }
|
|
}
|
|
return { ...DEFAULT_SIZE, maximized: saved?.maximized ?? false }
|
|
}
|
|
|
|
/**
|
|
* Persist the window's current *normal* bounds (un-maximized) + maximized flag.
|
|
* Best-effort; a write failure is logged, never thrown. Callers should debounce.
|
|
*/
|
|
export function saveWindowState(win: BrowserWindow): void {
|
|
if (win.isDestroyed() || win.isMinimized()) return
|
|
try {
|
|
const state: WindowState = { bounds: win.getNormalBounds(), maximized: win.isMaximized() }
|
|
writeFileSync(stateFile(), JSON.stringify(state))
|
|
} catch (e) {
|
|
logger.warn('failed to save window state', e)
|
|
}
|
|
}
|