diff --git a/ui/src/routes/test/+page.svelte b/ui/src/routes/test/+page.svelte index c02affe..1039ced 100644 --- a/ui/src/routes/test/+page.svelte +++ b/ui/src/routes/test/+page.svelte @@ -58,6 +58,7 @@ type FrameState = { res: ResolutionPreset; + displayViewportId: string; sessionId: string; src: string | null; muted: boolean; @@ -100,14 +101,17 @@ const enabled = allResolutions.filter((r) => r.enabled); const byId = new Map(prev.map((f) => [f.res.id, f])); const persisted = loadPersistedFrameSessionIds(); + const persistedDisplay = loadPersistedDisplayViewportIds(); frames = enabled.map((res) => { const existing = byId.get(res.id); if (existing) { existing.res = res; + existing.displayViewportId ||= persistedDisplay[res.id] ?? res.id; return existing; } return { res, + displayViewportId: persistedDisplay[res.id] ?? res.id, sessionId: persisted[res.id] ?? defaultSessionIdFor(res.id), src: null, muted: true, @@ -431,6 +435,11 @@ const lgsBase = `${location.origin}`; const lgsHostPort = location.host; const SESSION_STORAGE_PREFIX = 'stake-dev-tool:test-sessions:'; + const SIDEBAR_COLLAPSED_STORAGE_KEY = 'stake-dev-tool:test-sidebar-collapsed'; + const VIEWPORT_STORAGE_PREFIX = 'stake-dev-tool:test-display-viewports:'; + const FULLSCREEN_VIEWPORT_ID = '__fullscreen'; + + const activeFrame = $derived(frames[0] ?? null); function frameSessionStorageKey(): string | null { if (!gameSlug || !gameUrl) return null; @@ -458,7 +467,90 @@ localStorage.setItem(key, JSON.stringify(sessionIds)); } + function displayViewportStorageKey(): string | null { + if (!gameSlug || !gameUrl) return null; + return `${VIEWPORT_STORAGE_PREFIX}${gameSlug}:${gameUrl}`; + } + + function loadPersistedDisplayViewportIds(): Record { + const key = displayViewportStorageKey(); + if (!key) return {}; + try { + return JSON.parse(localStorage.getItem(key) ?? '{}') as Record; + } catch { + return {}; + } + } + + function persistDisplayViewportIds() { + const key = displayViewportStorageKey(); + if (!key) return; + try { + const displayIds = Object.fromEntries(frames.map((f) => [f.res.id, f.displayViewportId])); + localStorage.setItem(key, JSON.stringify(displayIds)); + } catch { + // Storage is only a convenience for the test view. + } + } + + function normalizeFrameDisplayViewport(frame: FrameState) { + if (frame.displayViewportId === FULLSCREEN_VIEWPORT_ID) return; + if (!allResolutions.some((r) => r.id === frame.displayViewportId)) { + frame.displayViewportId = frame.res.id; + } + } + + function normalizeAllDisplayViewports() { + frames.forEach(normalizeFrameDisplayViewport); + persistDisplayViewportIds(); + } + + function selectFrameViewport(frame: FrameState, id: string) { + frame.displayViewportId = id; + normalizeFrameDisplayViewport(frame); + persistDisplayViewportIds(); + } + + function displayViewportResolution(frame: FrameState): ResolutionPreset { + return allResolutions.find((r) => r.id === frame.displayViewportId) ?? frame.res; + } + + function isFrameFullscreen(frame: FrameState): boolean { + return frame.displayViewportId === FULLSCREEN_VIEWPORT_ID; + } + + function viewportWidthStyle(frame: FrameState): string { + const res = displayViewportResolution(frame); + return isFrameFullscreen(frame) ? 'width: 100%;' : `width: ${res.width}px;`; + } + + function viewportBoxStyle(frame: FrameState): string { + const res = displayViewportResolution(frame); + return isFrameFullscreen(frame) + ? 'width: 100%;' + : `width: ${res.width}px; height: ${res.height}px;`; + } + + function loadPersistedSidebarCollapsed() { + try { + sidebarCollapsed = localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY) === '1'; + } catch { + sidebarCollapsed = false; + } + } + + function setSidebarCollapsed(next: boolean) { + sidebarCollapsed = next; + try { + localStorage.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, next ? '1' : '0'); + } catch { + // Keep the UI responsive even when browser storage is unavailable. + } + } + onMount(async () => { + loadPersistedSidebarCollapsed(); + const params = page.url.searchParams; gameUrl = params.get('gameUrl') ?? ''; gameSlug = params.get('gameSlug') ?? ''; @@ -470,6 +562,7 @@ const s = await settingsHttp.get(); allResolutions = s.resolutions; rebuildFramesFromResolutions(); + normalizeAllDisplayViewports(); const f = await forcedEventHttp.get(); forcedEventBanner = f.forced; await reloadSavedRounds(); @@ -501,6 +594,7 @@ const s = await settingsHttp.toggle(id, enabled); allResolutions = s.resolutions; rebuildFramesFromResolutions(frames); + normalizeAllDisplayViewports(); const newlyEnabled = frames.filter((f) => f.src === null); for (const f of newlyEnabled) { await reloadFrame(f); @@ -524,6 +618,7 @@ const s = await settingsHttp.addCustom(label, newCustomWidth, newCustomHeight); allResolutions = s.resolutions; rebuildFramesFromResolutions(frames); + normalizeAllDisplayViewports(); newCustomLabel = ''; const last = frames[frames.length - 1]; if (last && last.src === null) await reloadFrame(last); @@ -542,6 +637,7 @@ const s = await settingsHttp.deleteCustom(id); allResolutions = s.resolutions; rebuildFramesFromResolutions(frames); + normalizeAllDisplayViewports(); } catch (e) { toast.error(e instanceof Error ? e.message : String(e)); } finally { @@ -588,15 +684,18 @@ } async function reloadAll() { + if (frames.length === 0) { + toast.error('Enable at least one resolution before reloading.'); + return; + } busy = true; try { - // Clear all iframes first, then load one at a time to avoid WebGL races. frames.forEach((f) => (f.src = null)); for (const f of frames) { await reloadFrame(f); await new Promise((r) => setTimeout(r, 800)); } - toast.success(`Reloaded ${frames.length} frames · balance=${balance} ${currency}`); + toast.success(`Reloaded ${frames.length} frames - balance=${balance} ${currency}`); } catch (e) { toast.error(e instanceof Error ? e.message : String(e)); } finally { @@ -708,7 +807,7 @@ variant="outline" size="icon" class="h-9 w-9 flex-shrink-0 rounded-full border-border/80 bg-background/80 shadow-sm hover:bg-muted" - onclick={() => (sidebarCollapsed = !sidebarCollapsed)} + onclick={() => setSidebarCollapsed(!sidebarCollapsed)} aria-label={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'} aria-expanded={!sidebarCollapsed} > @@ -1111,8 +1210,8 @@ @@ -1238,11 +1337,34 @@ {#each frames as frame (frame.res.id)}
-
-
- {frame.res.label} +
+
+ + + + {isFrameFullscreen(frame) ? 'Fullscreen' : displayViewportResolution(frame).label} + - {frame.res.width}×{frame.res.height} + {#if isFrameFullscreen(frame)} + fit browser + {:else} + {displayViewportResolution(frame).width}x{displayViewportResolution(frame).height} + {/if}
@@ -1285,7 +1407,7 @@ {/snippet} - Reload this frame + Reload this viewport @@ -1310,9 +1432,10 @@
+
{#if frame.history[0]} {@const last = frame.history[0]} @@ -1335,8 +1458,8 @@
{#if frame.src}