nya displaykort och race-rader i overlayn
This commit is contained in:
177
src/app.js
177
src/app.js
@@ -440,6 +440,8 @@ const TRANSLATIONS = {
|
|||||||
"overlay.mode_results": "Resultat",
|
"overlay.mode_results": "Resultat",
|
||||||
"overlay.fastest_lap": "Snabbaste varv",
|
"overlay.fastest_lap": "Snabbaste varv",
|
||||||
"overlay.fullscreen": "Fullscreen",
|
"overlay.fullscreen": "Fullscreen",
|
||||||
|
"overlay.leaderboard_live": "Live leaderboard",
|
||||||
|
"overlay.rotating_panel": "Displaypanel",
|
||||||
"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å.",
|
||||||
@@ -898,6 +900,8 @@ const TRANSLATIONS = {
|
|||||||
"overlay.mode_results": "Results",
|
"overlay.mode_results": "Results",
|
||||||
"overlay.fastest_lap": "Fastest Lap",
|
"overlay.fastest_lap": "Fastest Lap",
|
||||||
"overlay.fullscreen": "Fullscreen",
|
"overlay.fullscreen": "Fullscreen",
|
||||||
|
"overlay.leaderboard_live": "Live leaderboard",
|
||||||
|
"overlay.rotating_panel": "Display panel",
|
||||||
"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.",
|
||||||
@@ -949,6 +953,8 @@ let baselineAppVersion = "";
|
|||||||
let selectedLeaderboardKey = null;
|
let selectedLeaderboardKey = null;
|
||||||
let selectedGridSessionId = null;
|
let selectedGridSessionId = null;
|
||||||
let overlaySyncTimer = null;
|
let overlaySyncTimer = null;
|
||||||
|
let overlayRotationTimer = null;
|
||||||
|
let overlayRotationIndex = 0;
|
||||||
let overlayEvents = [];
|
let overlayEvents = [];
|
||||||
let lastOverlayLeaderKeyBySession = {};
|
let lastOverlayLeaderKeyBySession = {};
|
||||||
let lastOverlayTop3BySession = {};
|
let lastOverlayTop3BySession = {};
|
||||||
@@ -994,6 +1000,7 @@ async function init() {
|
|||||||
startAppVersionPolling();
|
startAppVersionPolling();
|
||||||
if (overlayMode) {
|
if (overlayMode) {
|
||||||
startOverlaySync();
|
startOverlaySync();
|
||||||
|
startOverlayRotation();
|
||||||
if (state.settings.wsUrl) {
|
if (state.settings.wsUrl) {
|
||||||
connectDecoder();
|
connectDecoder();
|
||||||
}
|
}
|
||||||
@@ -1545,6 +1552,16 @@ function startOverlaySync() {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startOverlayRotation() {
|
||||||
|
clearInterval(overlayRotationTimer);
|
||||||
|
overlayRotationTimer = setInterval(() => {
|
||||||
|
overlayRotationIndex = (overlayRotationIndex + 1) % 3;
|
||||||
|
if (currentView === "overlay" && overlayViewMode === "leaderboard") {
|
||||||
|
renderView();
|
||||||
|
}
|
||||||
|
}, 8000);
|
||||||
|
}
|
||||||
|
|
||||||
function renderNav() {
|
function renderNav() {
|
||||||
if (overlayMode) {
|
if (overlayMode) {
|
||||||
dom.nav.innerHTML = "";
|
dom.nav.innerHTML = "";
|
||||||
@@ -3473,6 +3490,8 @@ function renderOverlay() {
|
|||||||
const fastestRow =
|
const fastestRow =
|
||||||
[...leaderboard].filter((row) => Number.isFinite(row.bestLapMs)).sort((left, right) => left.bestLapMs - right.bestLapMs)[0] || null;
|
[...leaderboard].filter((row) => Number.isFinite(row.bestLapMs)).sort((left, right) => left.bestLapMs - right.bestLapMs)[0] || null;
|
||||||
const modeLabel = getOverlayModeLabel(overlayViewMode);
|
const modeLabel = getOverlayModeLabel(overlayViewMode);
|
||||||
|
const rotatingPanels = buildOverlayPanels(active, recent);
|
||||||
|
const activePanel = rotatingPanels.length ? rotatingPanels[overlayRotationIndex % rotatingPanels.length] : null;
|
||||||
|
|
||||||
dom.view.innerHTML = `
|
dom.view.innerHTML = `
|
||||||
<section class="overlay-shell">
|
<section class="overlay-shell">
|
||||||
@@ -3565,6 +3584,13 @@ function renderOverlay() {
|
|||||||
: `
|
: `
|
||||||
<section class="overlay-board">
|
<section class="overlay-board">
|
||||||
<div class="overlay-table-wrap overlay-display-wrap">
|
<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">
|
<section class="overlay-stats-row">
|
||||||
<article class="overlay-stat-card">
|
<article class="overlay-stat-card">
|
||||||
<span>${t("overlay.fastest_lap")}</span>
|
<span>${t("overlay.fastest_lap")}</span>
|
||||||
@@ -3582,47 +3608,15 @@ function renderOverlay() {
|
|||||||
<small>${sessionTiming?.untimed ? t("timing.elapsed") : t("timing.remaining")}</small>
|
<small>${sessionTiming?.untimed ? t("timing.elapsed") : t("timing.remaining")}</small>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
${renderOverlayLeaderboard(leaderboard)}
|
<div class="overlay-leaderboard-card">
|
||||||
|
<div class="overlay-section-head">
|
||||||
|
<h3>${t("overlay.leaderboard_live")}</h3>
|
||||||
|
</div>
|
||||||
|
${renderOverlayLeaderboard(leaderboard)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<aside class="overlay-side">
|
<aside class="overlay-side">
|
||||||
<section class="overlay-side-card">
|
${activePanel ? renderOverlaySidePanel(activePanel) : `<section class="overlay-side-card"><p>${t("timing.no_passings")}</p></section>`}
|
||||||
<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>
|
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</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) {
|
function renderLeaderboardModal(session, row) {
|
||||||
const passings = getCompetitorPassings(session, row);
|
const passings = getCompetitorPassings(session, row);
|
||||||
return `
|
return `
|
||||||
@@ -3737,23 +3783,42 @@ function renderOverlayLeaderboard(rows) {
|
|||||||
return `<p>${t("timing.no_laps")}</p>`;
|
return `<p>${t("timing.no_laps")}</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderTable(
|
return `
|
||||||
[t("table.pos"), t("table.driver"), t("table.laps"), t("table.result"), t("table.best_lap"), t("table.ahead_gap"), t("table.own_delta")],
|
<div class="overlay-race-list">
|
||||||
rows.map((row, idx) => {
|
${rows
|
||||||
const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
|
.map((row, idx) => {
|
||||||
return `
|
const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
|
||||||
<tr>
|
return `
|
||||||
<td><span class="pos-pill ${posClass}">${idx + 1}</span></td>
|
<article class="overlay-race-row ${idx === 0 ? "overlay-race-row-leader" : ""}">
|
||||||
<td>${escapeHtml(row.driverName)}</td>
|
<div class="overlay-race-pos">
|
||||||
<td>${row.laps}</td>
|
<span class="pos-pill ${posClass}">${idx + 1}</span>
|
||||||
<td>${escapeHtml(row.resultDisplay)}</td>
|
</div>
|
||||||
<td class="best">${formatLap(row.bestLapMs)}</td>
|
<div class="overlay-race-driver">
|
||||||
<td>${escapeHtml(row.gapAhead || "-")}</td>
|
<strong>${escapeHtml(row.driverName)}</strong>
|
||||||
<td>${escapeHtml(row.lapDelta || "-")}</td>
|
<span>${escapeHtml(row.transponder || "-")}</span>
|
||||||
</tr>
|
</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) {
|
function renderRecentPassings(session) {
|
||||||
|
|||||||
103
src/styles.css
103
src/styles.css
@@ -708,6 +708,55 @@ select:focus {
|
|||||||
gap: 16px;
|
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 {
|
.overlay-stats-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
@@ -743,6 +792,10 @@ select:focus {
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overlay-rotating-card {
|
||||||
|
min-height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
.overlay-side-card h3 {
|
.overlay-side-card h3 {
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
}
|
}
|
||||||
@@ -759,6 +812,52 @@ select:focus {
|
|||||||
border-bottom: 0;
|
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 {
|
.overlay-empty {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@@ -919,6 +1018,10 @@ select:focus {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overlay-race-row {
|
||||||
|
grid-template-columns: 56px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.overlay-speaker {
|
.overlay-speaker {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user