Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 74 additions & 2 deletions apps/overlay/electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
});
Expand Down
25 changes: 21 additions & 4 deletions apps/overlay/src/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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');
Expand Down