nya displaykort och race-rader i overlayn

This commit is contained in:
larssand
2026-03-14 16:19:45 +01:00
parent 55c166a7a7
commit 0d71cd5a18
2 changed files with 224 additions and 56 deletions

View File

@@ -440,6 +440,8 @@ const TRANSLATIONS = {
"overlay.mode_results": "Resultat",
"overlay.fastest_lap": "Snabbaste varv",
"overlay.fullscreen": "Fullscreen",
"overlay.leaderboard_live": "Live leaderboard",
"overlay.rotating_panel": "Displaypanel",
"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å.",
@@ -898,6 +900,8 @@ const TRANSLATIONS = {
"overlay.mode_results": "Results",
"overlay.fastest_lap": "Fastest Lap",
"overlay.fullscreen": "Fullscreen",
"overlay.leaderboard_live": "Live leaderboard",
"overlay.rotating_panel": "Display panel",
"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.",
@@ -949,6 +953,8 @@ let baselineAppVersion = "";
let selectedLeaderboardKey = null;
let selectedGridSessionId = null;
let overlaySyncTimer = null;
let overlayRotationTimer = null;
let overlayRotationIndex = 0;
let overlayEvents = [];
let lastOverlayLeaderKeyBySession = {};
let lastOverlayTop3BySession = {};
@@ -994,6 +1000,7 @@ async function init() {
startAppVersionPolling();
if (overlayMode) {
startOverlaySync();
startOverlayRotation();
if (state.settings.wsUrl) {
connectDecoder();
}
@@ -1545,6 +1552,16 @@ function startOverlaySync() {
}, 2000);
}
function startOverlayRotation() {
clearInterval(overlayRotationTimer);
overlayRotationTimer = setInterval(() => {
overlayRotationIndex = (overlayRotationIndex + 1) % 3;
if (currentView === "overlay" && overlayViewMode === "leaderboard") {
renderView();
}
}, 8000);
}
function renderNav() {
if (overlayMode) {
dom.nav.innerHTML = "";
@@ -3473,6 +3490,8 @@ function renderOverlay() {
const fastestRow =
[...leaderboard].filter((row) => Number.isFinite(row.bestLapMs)).sort((left, right) => left.bestLapMs - right.bestLapMs)[0] || null;
const modeLabel = getOverlayModeLabel(overlayViewMode);
const rotatingPanels = buildOverlayPanels(active, recent);
const activePanel = rotatingPanels.length ? rotatingPanels[overlayRotationIndex % rotatingPanels.length] : null;
dom.view.innerHTML = `
<section class="overlay-shell">
@@ -3565,6 +3584,13 @@ function renderOverlay() {
: `
<section class="overlay-board">
<div class="overlay-table-wrap overlay-display-wrap">
<section class="overlay-fastest-banner">
<div>
<span>${t("overlay.fastest_lap")}</span>
<strong>${formatLap(fastestRow?.bestLapMs)}</strong>
</div>
<div class="overlay-fastest-driver">${escapeHtml(fastestRow?.driverName || "-")}</div>
</section>
<section class="overlay-stats-row">
<article class="overlay-stat-card">
<span>${t("overlay.fastest_lap")}</span>
@@ -3582,47 +3608,15 @@ function renderOverlay() {
<small>${sessionTiming?.untimed ? t("timing.elapsed") : t("timing.remaining")}</small>
</article>
</section>
${renderOverlayLeaderboard(leaderboard)}
<div class="overlay-leaderboard-card">
<div class="overlay-section-head">
<h3>${t("overlay.leaderboard_live")}</h3>
</div>
${renderOverlayLeaderboard(leaderboard)}
</div>
</div>
<aside class="overlay-side">
<section class="overlay-side-card">
<h3>${t("events.position_grid")}</h3>
${normalizeStartMode(active.startMode) === "position" ? renderPositionGrid(active) : `<p>${t("events.na")}</p>`}
</section>
<section class="overlay-side-card">
<h3>${t("overlay.last_passings")}</h3>
${
recent.length
? recent
.map(
(passing) => `
<div class="overlay-passing">
<strong>${escapeHtml(passing.driverName || t("common.unknown_driver"))}</strong>
<span>${new Date(passing.timestamp).toLocaleTimeString()}</span>
</div>
`
)
.join("")
: `<p>${t("timing.no_passings")}</p>`
}
</section>
<section class="overlay-side-card">
<h3>${t("overlay.event_markers")}</h3>
${
overlayEvents.length
? overlayEvents
.map(
(item) => `
<div class="overlay-passing">
<strong>${escapeHtml(item.label)}</strong>
<span>${new Date(item.ts).toLocaleTimeString()}</span>
</div>
`
)
.join("")
: `<p>${t("timing.no_passings")}</p>`
}
</section>
${activePanel ? renderOverlaySidePanel(activePanel) : `<section class="overlay-side-card"><p>${t("timing.no_passings")}</p></section>`}
</aside>
</section>
`
@@ -3648,6 +3642,58 @@ function renderOverlay() {
});
}
function buildOverlayPanels(active, recent) {
return [
{
title: t("overlay.last_passings"),
content: recent.length
? recent
.map(
(passing) => `
<div class="overlay-passing">
<strong>${escapeHtml(passing.driverName || passing.transponder || t("common.unknown_driver"))}</strong>
<span>${new Date(passing.timestamp).toLocaleTimeString()}</span>
</div>
`
)
.join("")
: `<p>${t("timing.no_passings")}</p>`,
},
{
title: t("overlay.event_markers"),
content: overlayEvents.length
? overlayEvents
.map(
(item) => `
<div class="overlay-passing">
<strong>${escapeHtml(item.label)}</strong>
<span>${new Date(item.ts).toLocaleTimeString()}</span>
</div>
`
)
.join("")
: `<p>${t("timing.no_passings")}</p>`,
},
{
title: t("events.position_grid"),
content:
active && normalizeStartMode(active.startMode) === "position" ? renderPositionGrid(active) : `<p>${t("events.na")}</p>`,
},
];
}
function renderOverlaySidePanel(panel) {
return `
<section class="overlay-side-card overlay-rotating-card">
<div class="overlay-section-head">
<h3>${escapeHtml(panel.title)}</h3>
<span class="pill">${t("overlay.rotating_panel")}</span>
</div>
${panel.content}
</section>
`;
}
function renderLeaderboardModal(session, row) {
const passings = getCompetitorPassings(session, row);
return `
@@ -3737,23 +3783,42 @@ function renderOverlayLeaderboard(rows) {
return `<p>${t("timing.no_laps")}</p>`;
}
return renderTable(
[t("table.pos"), t("table.driver"), t("table.laps"), t("table.result"), t("table.best_lap"), t("table.ahead_gap"), t("table.own_delta")],
rows.map((row, idx) => {
const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
return `
<tr>
<td><span class="pos-pill ${posClass}">${idx + 1}</span></td>
<td>${escapeHtml(row.driverName)}</td>
<td>${row.laps}</td>
<td>${escapeHtml(row.resultDisplay)}</td>
<td class="best">${formatLap(row.bestLapMs)}</td>
<td>${escapeHtml(row.gapAhead || "-")}</td>
<td>${escapeHtml(row.lapDelta || "-")}</td>
</tr>
`;
})
);
return `
<div class="overlay-race-list">
${rows
.map((row, idx) => {
const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
return `
<article class="overlay-race-row ${idx === 0 ? "overlay-race-row-leader" : ""}">
<div class="overlay-race-pos">
<span class="pos-pill ${posClass}">${idx + 1}</span>
</div>
<div class="overlay-race-driver">
<strong>${escapeHtml(row.driverName)}</strong>
<span>${escapeHtml(row.transponder || "-")}</span>
</div>
<div class="overlay-race-metric">
<label>${t("table.result")}</label>
<strong>${escapeHtml(row.resultDisplay)}</strong>
</div>
<div class="overlay-race-metric">
<label>${t("table.ahead_gap")}</label>
<strong>${escapeHtml(row.gapAhead || "-")}</strong>
</div>
<div class="overlay-race-metric">
<label>${t("table.own_delta")}</label>
<strong>${escapeHtml(row.lapDelta || "-")}</strong>
</div>
<div class="overlay-race-best">
<label>${t("table.best_lap")}</label>
<strong>${formatLap(row.bestLapMs)}</strong>
</div>
</article>
`;
})
.join("")}
</div>
`;
}
function renderRecentPassings(session) {

View File

@@ -708,6 +708,55 @@ select:focus {
gap: 16px;
}
.overlay-fastest-banner,
.overlay-leaderboard-card {
border: 1px solid var(--line);
border-radius: 18px;
background: rgba(7, 12, 20, 0.9);
box-shadow: var(--shadow);
}
.overlay-fastest-banner {
display: flex;
justify-content: space-between;
align-items: end;
gap: 16px;
padding: 18px 20px;
background:
linear-gradient(135deg, rgba(225, 6, 0, 0.18), rgba(225, 6, 0, 0.04)),
rgba(7, 12, 20, 0.92);
}
.overlay-fastest-banner span,
.overlay-fastest-driver {
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.overlay-fastest-banner strong {
display: block;
margin-top: 6px;
font-family: Orbitron, sans-serif;
font-size: clamp(2rem, 4vw, 3.2rem);
}
.overlay-leaderboard-card {
padding: 14px;
}
.overlay-section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
}
.overlay-section-head h3 {
margin: 0;
}
.overlay-stats-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -743,6 +792,10 @@ select:focus {
padding: 14px;
}
.overlay-rotating-card {
min-height: 320px;
}
.overlay-side-card h3 {
margin: 0 0 10px;
}
@@ -759,6 +812,52 @@ select:focus {
border-bottom: 0;
}
.overlay-race-list {
display: grid;
gap: 10px;
}
.overlay-race-row {
display: grid;
grid-template-columns: 72px minmax(220px, 1.4fr) repeat(3, minmax(140px, 0.8fr)) minmax(150px, 0.9fr);
gap: 12px;
align-items: center;
padding: 14px 16px;
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 16px;
background: rgba(255, 255, 255, 0.03);
}
.overlay-race-row-leader {
border-color: rgba(225, 6, 0, 0.45);
background: linear-gradient(135deg, rgba(225, 6, 0, 0.12), rgba(255, 255, 255, 0.03));
}
.overlay-race-driver strong,
.overlay-race-metric strong,
.overlay-race-best strong {
display: block;
}
.overlay-race-driver strong {
font-size: clamp(1.2rem, 2vw, 1.7rem);
}
.overlay-race-driver span,
.overlay-race-metric label,
.overlay-race-best label {
color: var(--muted);
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.overlay-race-metric strong,
.overlay-race-best strong {
font-family: Orbitron, sans-serif;
font-size: clamp(1rem, 1.6vw, 1.35rem);
}
.overlay-empty {
display: grid;
place-items: center;
@@ -919,6 +1018,10 @@ select:focus {
grid-template-columns: 1fr;
}
.overlay-race-row {
grid-template-columns: 56px 1fr;
}
.overlay-speaker {
grid-template-columns: 1fr;
}