Add open practice display mode
This commit is contained in:
84
src/app.js
84
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() {
|
||||
<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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user