Files
dostalgia/frontend/src/pages/Play.svelte
T
David Alvarez 3ae6f43ccb
Build & Deploy / build-and-deploy (push) Successful in 50s
feat: responsive mobile UI across all pages
Global:
- Viewport meta already present; added -webkit-text-size-adjust: 100%
- Container padding reduces to 12px on mobile (was 24px)
- Smaller heading sizes (h1: 1.5rem, h2: 1.25rem)
- Buttons get min-height 40px for touch targets
- Inputs forced to 16px font-size to prevent iOS zoom
- Thinner scrollbar on mobile

Header:
- Logo text shrinks to 1rem on mobile, icon to 28px
- Header padding reduces to 12px

Library:
- Toolbar stacks vertically on mobile, full-width inputs
- Grid forced to 2 columns on narrow screens (480px), reduced gap

GameCard:
- Smaller info text, padding, badge on 480px screens

GameDetail:
- Cover section capped at 200px width on mobile
- Media thumbnails shrink to 160x90
- Scrape header stacks vertically with full-width buttons
- Edit rows stack vertically on mobile

Play:
- DOS container caps at 50vh on mobile, smaller border-radius
- Overlay text/icon sizes reduced, more compact layout
2026-05-26 17:22:15 +02:00

244 lines
6.1 KiB
Svelte

<script>
import { getGame, bundleUrl } from "../lib/api.js";
import { push } from "../lib/router.js";
let { id } = $props();
let game = $state(null);
let loading = $state(true);
let error = $state("");
let booting = $state(false);
let running = $state(false);
let started = false;
let dosContainer;
let dosCI = null;
async function load() {
loading = true;
try {
game = await getGame(id);
} catch (e) {
error = "Game not found";
}
loading = false;
}
async function startEmulator() {
if (started || !game?.ready || !dosContainer) return;
started = true;
booting = true;
// Load js-dos CSS
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "https://v8.js-dos.com/latest/js-dos.css";
document.head.appendChild(link);
// Load js-dos script from CDN
if (!window.Dos) {
try {
await new Promise((resolve, reject) => {
const s = document.createElement("script");
s.src = "https://v8.js-dos.com/latest/js-dos.js";
s.onload = resolve;
s.onerror = reject;
document.head.appendChild(s);
});
} catch {
error = "Failed to load emulator. Check your internet connection.";
booting = false;
return;
}
}
// Start the DOS emulator
try {
const url = bundleUrl(game);
if (!url || !window.Dos) throw new Error("Failed to resolve game bundle");
dosCI = await window.Dos(dosContainer, { url });
running = true;
} catch (e) {
error = e.message;
}
booting = false;
}
function stopEmulator() {
// Force full page reload which terminates all workers, AudioContexts
// and WASM threads. Hash is preserved so the SPA routes correctly.
window.location.hash = `#/game/${game.id}`;
window.location.reload();
}
$effect(() => { load(); });
// Auto-start when both game data and the container exist
$effect(() => {
if (game?.ready && dosContainer && !started) {
startEmulator();
}
});
// Cleanup emulator on component unmount (browser back, nav away)
$effect(() => {
return () => {
if (dosCI) {
try { dosCI.exit(); } catch (_) {}
dosCI = null;
}
if (dosContainer) {
dosContainer.innerHTML = "";
}
};
});
</script>
<div class="play-page">
<!-- Top bar: shown when game is loaded -->
<div class="play-header" class:active={game}>
{#if game}
<button class="link-button" onclick={stopEmulator}> {game.title}</button>
{#if running}
<button class="btn stop-btn" onclick={stopEmulator}> Stop</button>
{/if}
{/if}
</div>
<!-- Error state -->
{#if error && !booting}
<div class="error-box">{error}</div>
{/if}
<!-- Loading / booting overlay (covers the canvas while loading) -->
{#if !running}
<div class="overlay">
{#if loading}
<div class="overlay-icon"></div>
<p>Loading game data...</p>
{:else if booting}
<div class="overlay-icon">⚙️</div>
<h2>{game?.title || ""}</h2>
<p>Starting emulator...</p>
<p class="save-hint">💾 Game saves are stored automatically</p>
{:else if error}
<div class="overlay-icon">⚠️</div>
<h2>Failed to start</h2>
<p>{error}</p>
{:else if !game?.ready}
<div class="overlay-icon">🕹️</div>
<h2>Not ready</h2>
<p>This game hasn't been processed yet.</p>
{/if}
</div>
{/if}
<!-- dos-container is always in the DOM so bind:this works on first render -->
<div bind:this={dosContainer} class="dos-container"></div>
</div>
<style>
.play-page { padding: 16px 0; position: relative; }
@media (max-width: 640px) { .play-page { padding: 8px 0; } }
.play-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
opacity: 0;
transition: opacity 0.2s;
}
.play-header.active { opacity: 1; }
.link-button {
background: none;
border: none;
color: var(--text-dim);
font-size: 0.9rem;
cursor: pointer;
padding: 0;
font-family: var(--font-sans);
}
.link-button:hover { color: var(--phosphor); }
.stop-btn {
border-color: #cc3333;
color: #ff4444;
}
.stop-btn:hover {
background: #331111;
box-shadow: 0 0 12px #331111;
}
/* Emulator canvas — always mounted */
.dos-container {
width: 100%;
aspect-ratio: 4/3;
max-height: 80vh;
background: #000;
border-radius: var(--radius);
overflow: hidden;
border: 1px solid var(--border);
}
@media (max-width: 640px) {
.dos-container { aspect-ratio: 4/3; max-height: 50vh; border-radius: var(--radius-sm); }
}
.dos-container :global(canvas) {
width: 100% !important;
height: 100% !important;
}
/* Overlay shown while loading / booting, covers the empty container */
.overlay {
position: absolute;
inset: 0;
top: 50px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
background: var(--bg);
z-index: 10;
border-radius: var(--radius);
border: 1px dashed var(--border);
margin-top: 4px;
padding: 20px;
}
@media (max-width: 640px) {
.overlay { top: 44px; border-radius: var(--radius-sm); padding: 16px; }
.overlay-icon { font-size: 2rem; }
.overlay h2 { font-size: 1rem; }
.overlay p { font-size: 0.85rem; }
}
.overlay-icon { font-size: 3rem; margin-bottom: 12px; }
.overlay h2 {
color: var(--phosphor);
font-family: var(--font-mono);
margin-bottom: 4px;
}
.overlay p { color: var(--text-dim); }
.save-hint {
display: inline-block;
margin-top: 16px;
padding: 6px 16px;
border-radius: 12px;
background: var(--phosphor-burn);
color: var(--phosphor-dim);
font-size: 0.85rem;
}
.error-box {
position: absolute;
inset: 0;
top: 50px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 4px;
background: #331111;
border: 1px solid #cc3333;
color: #ff6666;
border-radius: var(--radius);
z-index: 10;
}
</style>