From 55c166a7a7e3f6fa810164200b5c67a8f943e17a Mon Sep 17 00:00:00 2001 From: larssand Date: Sat, 14 Mar 2026 15:56:53 +0100 Subject: [PATCH] Add open practice display mode --- src/app.js | 84 +++++++++++++++++++++++++++++++++++++++++++++++--- src/styles.css | 42 +++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 5 deletions(-) diff --git a/src/app.js b/src/app.js index 23dad75..55a1a7d 100644 --- a/src/app.js +++ b/src/app.js @@ -242,6 +242,7 @@ const TRANSLATIONS = { "timing.total_passings": "Totala passeringar", "timing.started": "Startad", "timing.remaining": "Nedräkning", + "timing.elapsed": "Körtid", "timing.race_finished": "Race is finished", "timing.no_active": "Ingen aktiv session vald.", "timing.leaderboard": "Live leaderboard", @@ -437,6 +438,8 @@ const TRANSLATIONS = { "overlay.mode_leaderboard": "Leaderboard", "overlay.mode_speaker": "Speaker", "overlay.mode_results": "Resultat", + "overlay.fastest_lap": "Snabbaste varv", + "overlay.fullscreen": "Fullscreen", "overlay.event_markers": "Eventmarkörer", "guide.host_title": "Hur Managed AMMC körs", "guide.host_1": "1. AMMC körs alltid på samma maskin som `npm start` eller `node server.js` körs på.", @@ -697,6 +700,7 @@ const TRANSLATIONS = { "timing.total_passings": "Total passings", "timing.started": "Started", "timing.remaining": "Countdown", + "timing.elapsed": "Elapsed", "timing.race_finished": "Race is finished", "timing.no_active": "No active session selected.", "timing.leaderboard": "Live Leaderboard", @@ -892,6 +896,8 @@ const TRANSLATIONS = { "overlay.mode_leaderboard": "Leaderboard", "overlay.mode_speaker": "Speaker", "overlay.mode_results": "Results", + "overlay.fastest_lap": "Fastest Lap", + "overlay.fullscreen": "Fullscreen", "overlay.event_markers": "Event markers", "guide.host_title": "How Managed AMMC Runs", "guide.host_1": "1. AMMC always runs on the same machine where `npm start` or `node server.js` is running.", @@ -1829,6 +1835,10 @@ function handleSessionTimerTick() { return { changed: false }; } + if (isUntimedSession(active)) { + return { changed: false }; + } + const timing = getSessionTiming(active); if (timing.remainingMs > 0) { return { changed: false }; @@ -3074,6 +3084,8 @@ function renderTiming() { const result = active ? ensureSessionResult(active.id) : null; const leaderboard = active ? buildLeaderboard(active) : []; const sessionTiming = active ? getSessionTiming(active) : null; + const clockLabel = active && sessionTiming?.untimed ? t("timing.elapsed") : t("timing.remaining"); + const clockValue = sessionTiming?.untimed ? formatElapsedClock(sessionTiming?.elapsedMs ?? 0) : formatCountdown(sessionTiming?.remainingMs ?? 0); const showFinishedBanner = Boolean(active && active.status === "finished" && active.finishedByTimer); const selectedRow = leaderboard.find((row) => row.key === selectedLeaderboardKey) || null; if (selectedLeaderboardKey && !selectedRow) { @@ -3127,7 +3139,7 @@ function renderTiming() {

${t("table.start_mode")}: ${escapeHtml(getStartModeLabel(active.startMode))} • ${t("timing.seeding_mode")}: ${ active.seedBestLapCount > 0 ? `${active.seedBestLapCount}` : "-" }

-

${t("timing.remaining")}: ${formatCountdown(sessionTiming?.remainingMs ?? 0)}

+

${clockLabel}: ${clockValue}

${t("timing.total_passings")}: ${result.passings.length}

${ active.type === "free_practice" @@ -3448,6 +3460,9 @@ function renderOverlay() { const leaderboard = active ? buildLeaderboard(active).slice(0, 12) : []; const result = active ? ensureSessionResult(active.id) : null; const sessionTiming = active ? getSessionTiming(active) : null; + const overlayClock = sessionTiming?.untimed + ? formatElapsedClock(sessionTiming?.elapsedMs ?? 0) + : formatCountdown(sessionTiming?.remainingMs ?? 0); const recent = active && result ? result.passings.slice(-8).reverse() : []; const event = active ? state.events.find((item) => item.id === active.eventId) : null; const branding = resolveEventBranding(event); @@ -3455,6 +3470,8 @@ function renderOverlay() { const qualifyingRows = event ? buildQualifyingStandings(event) : []; const finalRows = event ? buildFinalStandings(event) : []; const topRow = leaderboard[0] || null; + const fastestRow = + [...leaderboard].filter((row) => Number.isFinite(row.bestLapMs)).sort((left, right) => left.bestLapMs - right.bestLapMs)[0] || null; const modeLabel = getOverlayModeLabel(overlayViewMode); dom.view.innerHTML = ` @@ -3470,7 +3487,8 @@ function renderOverlay() {

${escapeHtml(branding.brandName)} • ${escapeHtml(getStartModeLabel(active.startMode))} • ${escapeHtml(modeLabel)}

-
${formatCountdown(sessionTiming?.remainingMs ?? 0)}
+ +
${overlayClock}
${escapeHtml(getStatusLabel(active.status))}
@@ -3546,7 +3564,24 @@ function renderOverlay() { ` : `
-
+
+
+
+ ${t("overlay.fastest_lap")} + ${formatLap(fastestRow?.bestLapMs)} + ${escapeHtml(fastestRow?.driverName || "-")} +
+
+ ${t("table.laps")} + ${topRow?.laps || 0} + ${escapeHtml(topRow?.driverName || "-")} +
+
+ ${t("timing.total_passings")} + ${result?.passings.length || 0} + ${sessionTiming?.untimed ? t("timing.elapsed") : t("timing.remaining")} +
+
${renderOverlayLeaderboard(leaderboard)}
`; + + document.getElementById("overlayFullscreen")?.addEventListener("click", async () => { + const target = document.documentElement; + if (!document.fullscreenElement) { + await target.requestFullscreen?.().catch(() => {}); + return; + } + await document.exitFullscreen?.().catch(() => {}); + }); } function renderLeaderboardModal(session, row) { @@ -4069,11 +4113,18 @@ function getEventName(eventId) { return state.events.find((x) => x.id === eventId)?.name || t("common.unknown_event"); } +function isUntimedSession(session) { + return String(session?.type || "").toLowerCase() === "open_practice"; +} + function getActiveSession() { return state.sessions.find((s) => s.id === state.activeSessionId) || null; } function getSessionTargetMs(session) { + if (isUntimedSession(session)) { + return null; + } return Math.max(1, Number(session?.durationMin || 0)) * 60 * 1000; } @@ -4084,7 +4135,8 @@ function getSessionTiming(session, nowTs = Date.now()) { return { targetMs, elapsedMs, - remainingMs: Math.max(0, targetMs - elapsedMs), + remainingMs: targetMs === null ? null : Math.max(0, targetMs - elapsedMs), + untimed: targetMs === null, }; } @@ -4613,6 +4665,20 @@ function formatCountdown(ms) { return `${m}:${s}`; } +function formatElapsedClock(ms) { + const total = Math.max(0, Math.floor(ms / 1000)); + const h = Math.floor(total / 3600) + .toString() + .padStart(1, "0"); + const m = Math.floor((total % 3600) / 60) + .toString() + .padStart(2, "0"); + const s = Math.floor(total % 60) + .toString() + .padStart(2, "0"); + return `${h}:${m}:${s}`; +} + function formatRaceClock(ms) { const total = Math.max(0, Math.floor(ms)); const m = Math.floor(total / 60000) @@ -5711,10 +5777,18 @@ function buildOverlayUrl(mode = "leaderboard") { } function openOverlayWindow(mode = "leaderboard") { - const overlayWindow = window.open(buildOverlayUrl(mode), "_blank", "noopener,noreferrer,width=1600,height=900"); + const width = Math.max(1280, window.screen?.availWidth || 1600); + const height = Math.max(720, window.screen?.availHeight || 900); + const overlayWindow = window.open( + buildOverlayUrl(mode), + "_blank", + `noopener,noreferrer,popup=yes,left=0,top=0,width=${width},height=${height}` + ); if (!overlayWindow) { alert(t("error.print_blocked")); + return; } + overlayWindow.focus(); } function applyBumpsForRace(event) { diff --git a/src/styles.css b/src/styles.css index 0fb36ad..782812c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -630,6 +630,9 @@ select:focus { .overlay-meta { text-align: right; + display: grid; + justify-items: end; + gap: 10px; } .overlay-clock { @@ -638,6 +641,10 @@ select:focus { font-weight: 800; } +.overlay-fullscreen-btn { + align-self: start; +} + .overlay-status { color: var(--muted); text-transform: uppercase; @@ -696,6 +703,37 @@ select:focus { padding: 8px 12px 12px; } +.overlay-display-wrap { + display: grid; + gap: 16px; +} + +.overlay-stats-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.overlay-stat-card { + border: 1px solid var(--line); + border-radius: 16px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02)); + padding: 14px 16px; +} + +.overlay-stat-card span, +.overlay-stat-card small { + display: block; + color: var(--muted); +} + +.overlay-stat-card strong { + display: block; + margin: 8px 0 4px; + font-family: Orbitron, sans-serif; + font-size: clamp(1.4rem, 2vw, 2.1rem); +} + .overlay-side { display: grid; gap: 16px; @@ -877,6 +915,10 @@ select:focus { grid-template-columns: 1fr; } + .overlay-stats-row { + grid-template-columns: 1fr; + } + .overlay-speaker { grid-template-columns: 1fr; }