Add open practice display mode

This commit is contained in:
larssand
2026-03-14 15:56:53 +01:00
parent 24cf7d9522
commit 55c166a7a7
2 changed files with 121 additions and 5 deletions

View File

@@ -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() {
<p>${t("table.start_mode")}: ${escapeHtml(getStartModeLabel(active.startMode))}${t("timing.seeding_mode")}: ${
active.seedBestLapCount > 0 ? `${active.seedBestLapCount}` : "-"
}</p>
<p>${t("timing.remaining")}: ${formatCountdown(sessionTiming?.remainingMs ?? 0)}</p>
<p>${clockLabel}: ${clockValue}</p>
<p>${t("timing.total_passings")}: ${result.passings.length}</p>
${
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() {
<p>${escapeHtml(branding.brandName)}${escapeHtml(getStartModeLabel(active.startMode))}${escapeHtml(modeLabel)}</p>
</div>
<div class="overlay-meta">
<div class="overlay-clock">${formatCountdown(sessionTiming?.remainingMs ?? 0)}</div>
<button id="overlayFullscreen" class="btn overlay-fullscreen-btn" type="button">${t("overlay.fullscreen")}</button>
<div class="overlay-clock">${overlayClock}</div>
<div class="overlay-status">${escapeHtml(getStatusLabel(active.status))}</div>
</div>
</header>
@@ -3546,7 +3564,24 @@ function renderOverlay() {
`
: `
<section class="overlay-board">
<div class="overlay-table-wrap">
<div class="overlay-table-wrap overlay-display-wrap">
<section class="overlay-stats-row">
<article class="overlay-stat-card">
<span>${t("overlay.fastest_lap")}</span>
<strong>${formatLap(fastestRow?.bestLapMs)}</strong>
<small>${escapeHtml(fastestRow?.driverName || "-")}</small>
</article>
<article class="overlay-stat-card">
<span>${t("table.laps")}</span>
<strong>${topRow?.laps || 0}</strong>
<small>${escapeHtml(topRow?.driverName || "-")}</small>
</article>
<article class="overlay-stat-card">
<span>${t("timing.total_passings")}</span>
<strong>${result?.passings.length || 0}</strong>
<small>${sessionTiming?.untimed ? t("timing.elapsed") : t("timing.remaining")}</small>
</article>
</section>
${renderOverlayLeaderboard(leaderboard)}
</div>
<aside class="overlay-side">
@@ -3602,6 +3637,15 @@ function renderOverlay() {
}
</section>
`;
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) {

View File

@@ -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;
}