diff --git a/apps/overlay/electron/main.js b/apps/overlay/electron/main.js index 5e06c1d..66eb6be 100644 --- a/apps/overlay/electron/main.js +++ b/apps/overlay/electron/main.js @@ -16,6 +16,52 @@ if (require('./squirrel-startup')) { let mainWindow = null; let tray = null; let clickThrough = false; + +// ── Persisted window size (issue #69) ── +// The controller body window should reopen at the size it was last left, so +// the user can line it up once for OBS and just hit "start recording" next +// time. We store width/height in a small JSON file under userData, keyed per +// mode (single vs. multi) so the two layouts don't clobber each other. The +// default for the single-controller window is 720×540 — a clean capture size. +const DEFAULT_WINDOW_SIZE = { width: 720, height: 540 }; +const MULTI_WINDOW_SIZE = { width: 1100, height: 520 }; +let windowStatePath = null; // resolved once app is ready (needs userData path) +let saveWindowSizeTimer = null; + +function getWindowStatePath() { + if (!windowStatePath) { + windowStatePath = path.join(app.getPath('userData'), 'window-state.json'); + } + return windowStatePath; +} + +function readWindowState() { + try { + return JSON.parse(fs.readFileSync(getWindowStatePath(), 'utf8')) || {}; + } catch (e) { + return {}; + } +} + +function writeWindowState(state) { + try { + fs.writeFileSync(getWindowStatePath(), JSON.stringify(state, null, 2), 'utf8'); + } catch (e) { + // Non-fatal — size persistence is a convenience, not correctness. + console.log('[window-state] save failed:', e.message); + } +} + +// Sanity-check a saved size before trusting it (corrupt file, off-screen tiny +// values, etc.). Falls back to the mode's default for anything out of range. +function sanitizeSize(saved, fallback) { + const w = saved && Number(saved.width); + const h = saved && Number(saved.height); + return { + width: Number.isFinite(w) && w >= 200 && w <= 8000 ? Math.round(w) : fallback.width, + height: Number.isFinite(h) && h >= 200 && h <= 8000 ? Math.round(h) : fallback.height, + }; +} // Detached HUD windows — at most one open per kind. Tracked so the main // renderer can forward profile/state events to them via IPC without // re-opening or duplicating. @@ -92,9 +138,16 @@ function openInventoryWindow() { function createWindow() { const useMulti = process.env.OVERLAY_MULTI === '1' || process.argv.includes('--multi'); + + // Restore the last-used size for this mode (issue #69), falling back to the + // mode's default (single = 720×540). + const stateKey = useMulti ? 'multi' : 'main'; + const defaultSize = useMulti ? MULTI_WINDOW_SIZE : DEFAULT_WINDOW_SIZE; + const { width, height } = sanitizeSize(readWindowState()[stateKey], defaultSize); + mainWindow = new BrowserWindow({ - width: useMulti ? 1100 : 600, - height: useMulti ? 520 : 400, + width, + height, transparent: true, frame: false, alwaysOnTop: true, @@ -122,6 +175,25 @@ function createWindow() { // DevTools: Cmd+Shift+I to open manually (auto-open triggers Autofill errors) + // Persist the window size as the user resizes (issue #69). Debounced so a + // drag-resize doesn't hammer the disk; also saved on close as a backstop. + const persistSize = () => { + if (!mainWindow || mainWindow.isDestroyed()) return; + if (mainWindow.isMinimized() || mainWindow.isFullScreen()) return; + const [w, h] = mainWindow.getSize(); + const state = readWindowState(); + state[stateKey] = { width: w, height: h }; + writeWindowState(state); + }; + mainWindow.on('resize', () => { + if (saveWindowSizeTimer) clearTimeout(saveWindowSizeTimer); + saveWindowSizeTimer = setTimeout(persistSize, 400); + }); + mainWindow.on('close', () => { + if (saveWindowSizeTimer) { clearTimeout(saveWindowSizeTimer); saveWindowSizeTimer = null; } + persistSize(); + }); + mainWindow.on('closed', () => { mainWindow = null; }); diff --git a/apps/overlay/src/js/app.js b/apps/overlay/src/js/app.js index 34825f1..0275304 100644 --- a/apps/overlay/src/js/app.js +++ b/apps/overlay/src/js/app.js @@ -572,6 +572,11 @@ async function init() { overlay.setVisible(false); modelReady = false; } + // Re-apply the saved camera preset (issue #70) now that the 3D overlay + // exists — the module-level selectCameraPreset() ran before `overlay` was + // created, so it only updated the button highlight, not the actual view. + overlay.setCameraPreset(selectedCameraPreset); + // Apply HUD labels for whatever the current profile resolved to, even // when no gamepad was detected at startup — the user might enable the // HUD before plugging in, and the labels should match the model's @@ -2095,12 +2100,22 @@ function applyGreenScreen() { greenScreenToggle.addEventListener('change', applyGreenScreen); greenScreenColorInput.addEventListener('input', applyGreenScreen); -// Camera presets — one selected at a time, used as calibration view -let selectedCameraPreset = 'player'; +// Camera presets — one selected at a time, used as calibration view. +// The last-selected preset is persisted (issue #70) so reopening the overlay +// restores the user's preferred view (e.g. Top) instead of always resetting +// to Player. +const CAMERA_PRESETS = ['front', 'back', 'left', 'right', 'player', 'top']; +const CAMERA_PRESET_KEY = 'overlay:cameraPreset'; +function loadSavedCameraPreset() { + const saved = localStorage.getItem(CAMERA_PRESET_KEY); + return CAMERA_PRESETS.includes(saved) ? saved : 'player'; +} +let selectedCameraPreset = loadSavedCameraPreset(); const cameraPresetBtns = document.querySelectorAll('.camera-presets button'); function selectCameraPreset(preset) { selectedCameraPreset = preset; + try { localStorage.setItem(CAMERA_PRESET_KEY, preset); } catch (e) { /* ignore */ } if (overlay) overlay.setCameraPreset(preset); cameraPresetBtns.forEach(b => { b.classList.toggle('selected', b.dataset.preset === preset); @@ -2111,8 +2126,10 @@ cameraPresetBtns.forEach((btn) => { btn.addEventListener('click', () => selectCameraPreset(btn.dataset.preset)); }); -// Set default selection (overlay not ready yet, just highlights the button) -selectCameraPreset('player'); +// Apply the saved (or default) selection — overlay may not be ready yet, in +// which case this just highlights the button; init() re-applies it to the 3D +// view once the overlay exists. +selectCameraPreset(selectedCameraPreset); // ── Window display toggles (cosmetic only — never affects functionality) ── const showTitleCheck = document.getElementById('show-title');