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.total_passings": "Totala passeringar",
|
||||||
"timing.started": "Startad",
|
"timing.started": "Startad",
|
||||||
"timing.remaining": "Nedräkning",
|
"timing.remaining": "Nedräkning",
|
||||||
|
"timing.elapsed": "Körtid",
|
||||||
"timing.race_finished": "Race is finished",
|
"timing.race_finished": "Race is finished",
|
||||||
"timing.no_active": "Ingen aktiv session vald.",
|
"timing.no_active": "Ingen aktiv session vald.",
|
||||||
"timing.leaderboard": "Live leaderboard",
|
"timing.leaderboard": "Live leaderboard",
|
||||||
@@ -437,6 +438,8 @@ const TRANSLATIONS = {
|
|||||||
"overlay.mode_leaderboard": "Leaderboard",
|
"overlay.mode_leaderboard": "Leaderboard",
|
||||||
"overlay.mode_speaker": "Speaker",
|
"overlay.mode_speaker": "Speaker",
|
||||||
"overlay.mode_results": "Resultat",
|
"overlay.mode_results": "Resultat",
|
||||||
|
"overlay.fastest_lap": "Snabbaste varv",
|
||||||
|
"overlay.fullscreen": "Fullscreen",
|
||||||
"overlay.event_markers": "Eventmarkörer",
|
"overlay.event_markers": "Eventmarkörer",
|
||||||
"guide.host_title": "Hur Managed AMMC körs",
|
"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å.",
|
"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.total_passings": "Total passings",
|
||||||
"timing.started": "Started",
|
"timing.started": "Started",
|
||||||
"timing.remaining": "Countdown",
|
"timing.remaining": "Countdown",
|
||||||
|
"timing.elapsed": "Elapsed",
|
||||||
"timing.race_finished": "Race is finished",
|
"timing.race_finished": "Race is finished",
|
||||||
"timing.no_active": "No active session selected.",
|
"timing.no_active": "No active session selected.",
|
||||||
"timing.leaderboard": "Live Leaderboard",
|
"timing.leaderboard": "Live Leaderboard",
|
||||||
@@ -892,6 +896,8 @@ const TRANSLATIONS = {
|
|||||||
"overlay.mode_leaderboard": "Leaderboard",
|
"overlay.mode_leaderboard": "Leaderboard",
|
||||||
"overlay.mode_speaker": "Speaker",
|
"overlay.mode_speaker": "Speaker",
|
||||||
"overlay.mode_results": "Results",
|
"overlay.mode_results": "Results",
|
||||||
|
"overlay.fastest_lap": "Fastest Lap",
|
||||||
|
"overlay.fullscreen": "Fullscreen",
|
||||||
"overlay.event_markers": "Event markers",
|
"overlay.event_markers": "Event markers",
|
||||||
"guide.host_title": "How Managed AMMC Runs",
|
"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.",
|
"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 };
|
return { changed: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isUntimedSession(active)) {
|
||||||
|
return { changed: false };
|
||||||
|
}
|
||||||
|
|
||||||
const timing = getSessionTiming(active);
|
const timing = getSessionTiming(active);
|
||||||
if (timing.remainingMs > 0) {
|
if (timing.remainingMs > 0) {
|
||||||
return { changed: false };
|
return { changed: false };
|
||||||
@@ -3074,6 +3084,8 @@ function renderTiming() {
|
|||||||
const result = active ? ensureSessionResult(active.id) : null;
|
const result = active ? ensureSessionResult(active.id) : null;
|
||||||
const leaderboard = active ? buildLeaderboard(active) : [];
|
const leaderboard = active ? buildLeaderboard(active) : [];
|
||||||
const sessionTiming = active ? getSessionTiming(active) : null;
|
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 showFinishedBanner = Boolean(active && active.status === "finished" && active.finishedByTimer);
|
||||||
const selectedRow = leaderboard.find((row) => row.key === selectedLeaderboardKey) || null;
|
const selectedRow = leaderboard.find((row) => row.key === selectedLeaderboardKey) || null;
|
||||||
if (selectedLeaderboardKey && !selectedRow) {
|
if (selectedLeaderboardKey && !selectedRow) {
|
||||||
@@ -3127,7 +3139,7 @@ function renderTiming() {
|
|||||||
<p>${t("table.start_mode")}: ${escapeHtml(getStartModeLabel(active.startMode))} • ${t("timing.seeding_mode")}: ${
|
<p>${t("table.start_mode")}: ${escapeHtml(getStartModeLabel(active.startMode))} • ${t("timing.seeding_mode")}: ${
|
||||||
active.seedBestLapCount > 0 ? `${active.seedBestLapCount}` : "-"
|
active.seedBestLapCount > 0 ? `${active.seedBestLapCount}` : "-"
|
||||||
}</p>
|
}</p>
|
||||||
<p>${t("timing.remaining")}: ${formatCountdown(sessionTiming?.remainingMs ?? 0)}</p>
|
<p>${clockLabel}: ${clockValue}</p>
|
||||||
<p>${t("timing.total_passings")}: ${result.passings.length}</p>
|
<p>${t("timing.total_passings")}: ${result.passings.length}</p>
|
||||||
${
|
${
|
||||||
active.type === "free_practice"
|
active.type === "free_practice"
|
||||||
@@ -3448,6 +3460,9 @@ function renderOverlay() {
|
|||||||
const leaderboard = active ? buildLeaderboard(active).slice(0, 12) : [];
|
const leaderboard = active ? buildLeaderboard(active).slice(0, 12) : [];
|
||||||
const result = active ? ensureSessionResult(active.id) : null;
|
const result = active ? ensureSessionResult(active.id) : null;
|
||||||
const sessionTiming = active ? getSessionTiming(active) : 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 recent = active && result ? result.passings.slice(-8).reverse() : [];
|
||||||
const event = active ? state.events.find((item) => item.id === active.eventId) : null;
|
const event = active ? state.events.find((item) => item.id === active.eventId) : null;
|
||||||
const branding = resolveEventBranding(event);
|
const branding = resolveEventBranding(event);
|
||||||
@@ -3455,6 +3470,8 @@ function renderOverlay() {
|
|||||||
const qualifyingRows = event ? buildQualifyingStandings(event) : [];
|
const qualifyingRows = event ? buildQualifyingStandings(event) : [];
|
||||||
const finalRows = event ? buildFinalStandings(event) : [];
|
const finalRows = event ? buildFinalStandings(event) : [];
|
||||||
const topRow = leaderboard[0] || null;
|
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);
|
const modeLabel = getOverlayModeLabel(overlayViewMode);
|
||||||
|
|
||||||
dom.view.innerHTML = `
|
dom.view.innerHTML = `
|
||||||
@@ -3470,7 +3487,8 @@ function renderOverlay() {
|
|||||||
<p>${escapeHtml(branding.brandName)} • ${escapeHtml(getStartModeLabel(active.startMode))} • ${escapeHtml(modeLabel)}</p>
|
<p>${escapeHtml(branding.brandName)} • ${escapeHtml(getStartModeLabel(active.startMode))} • ${escapeHtml(modeLabel)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="overlay-meta">
|
<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 class="overlay-status">${escapeHtml(getStatusLabel(active.status))}</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -3546,7 +3564,24 @@ function renderOverlay() {
|
|||||||
`
|
`
|
||||||
: `
|
: `
|
||||||
<section class="overlay-board">
|
<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)}
|
${renderOverlayLeaderboard(leaderboard)}
|
||||||
</div>
|
</div>
|
||||||
<aside class="overlay-side">
|
<aside class="overlay-side">
|
||||||
@@ -3602,6 +3637,15 @@ function renderOverlay() {
|
|||||||
}
|
}
|
||||||
</section>
|
</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) {
|
function renderLeaderboardModal(session, row) {
|
||||||
@@ -4069,11 +4113,18 @@ function getEventName(eventId) {
|
|||||||
return state.events.find((x) => x.id === eventId)?.name || t("common.unknown_event");
|
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() {
|
function getActiveSession() {
|
||||||
return state.sessions.find((s) => s.id === state.activeSessionId) || null;
|
return state.sessions.find((s) => s.id === state.activeSessionId) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSessionTargetMs(session) {
|
function getSessionTargetMs(session) {
|
||||||
|
if (isUntimedSession(session)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return Math.max(1, Number(session?.durationMin || 0)) * 60 * 1000;
|
return Math.max(1, Number(session?.durationMin || 0)) * 60 * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4084,7 +4135,8 @@ function getSessionTiming(session, nowTs = Date.now()) {
|
|||||||
return {
|
return {
|
||||||
targetMs,
|
targetMs,
|
||||||
elapsedMs,
|
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}`;
|
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) {
|
function formatRaceClock(ms) {
|
||||||
const total = Math.max(0, Math.floor(ms));
|
const total = Math.max(0, Math.floor(ms));
|
||||||
const m = Math.floor(total / 60000)
|
const m = Math.floor(total / 60000)
|
||||||
@@ -5711,10 +5777,18 @@ function buildOverlayUrl(mode = "leaderboard") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openOverlayWindow(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) {
|
if (!overlayWindow) {
|
||||||
alert(t("error.print_blocked"));
|
alert(t("error.print_blocked"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
overlayWindow.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyBumpsForRace(event) {
|
function applyBumpsForRace(event) {
|
||||||
|
|||||||
@@ -630,6 +630,9 @@ select:focus {
|
|||||||
|
|
||||||
.overlay-meta {
|
.overlay-meta {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
display: grid;
|
||||||
|
justify-items: end;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-clock {
|
.overlay-clock {
|
||||||
@@ -638,6 +641,10 @@ select:focus {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overlay-fullscreen-btn {
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
.overlay-status {
|
.overlay-status {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -696,6 +703,37 @@ select:focus {
|
|||||||
padding: 8px 12px 12px;
|
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 {
|
.overlay-side {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
@@ -877,6 +915,10 @@ select:focus {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overlay-stats-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.overlay-speaker {
|
.overlay-speaker {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user