modda layoten för obs overlay
This commit is contained in:
187
src/app.js
187
src/app.js
@@ -6472,26 +6472,30 @@ function renderOverlay() {
|
||||
${
|
||||
active
|
||||
? `
|
||||
<header class="overlay-header">
|
||||
<div class="overlay-header-main">
|
||||
${branding.logoDataUrl ? `<img class="overlay-logo" src="${escapeHtml(branding.logoDataUrl)}" alt="logo" />` : ""}
|
||||
<div class="overlay-header-copy">
|
||||
<div class="overlay-kicker-row">
|
||||
<p class="overlay-kicker">${escapeHtml(getEventName(active.eventId))}</p>
|
||||
<span class="pill">${escapeHtml(getSessionTypeLabel(active.type))}</span>
|
||||
${overlayViewMode !== "tv" ? `<span class="pill">${escapeHtml(getStartModeLabel(active.startMode))}</span>` : ""}
|
||||
<span class="pill">${escapeHtml(modeLabel)}</span>
|
||||
</div>
|
||||
<h1>${escapeHtml(active.name)}</h1>
|
||||
<p class="overlay-header-sub">${escapeHtml(branding.brandName || "JMK RB RaceController")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay-meta">
|
||||
<button id="overlayFullscreen" class="btn overlay-fullscreen-btn" type="button">${t("overlay.fullscreen")}</button>
|
||||
${overlayViewMode === "obs" && obsConfig && !obsConfig.showClock ? "" : `<div class="overlay-clock">${overlayClock}</div>`}
|
||||
<div class="overlay-status">${escapeHtml(overlayStatusLabel)}</div>
|
||||
</div>
|
||||
</header>
|
||||
${
|
||||
overlayViewMode === "obs"
|
||||
? ""
|
||||
: `<header class="overlay-header">
|
||||
<div class="overlay-header-main">
|
||||
${branding.logoDataUrl ? `<img class="overlay-logo" src="${escapeHtml(branding.logoDataUrl)}" alt="logo" />` : ""}
|
||||
<div class="overlay-header-copy">
|
||||
<div class="overlay-kicker-row">
|
||||
<p class="overlay-kicker">${escapeHtml(getEventName(active.eventId))}</p>
|
||||
<span class="pill">${escapeHtml(getSessionTypeLabel(active.type))}</span>
|
||||
${overlayViewMode !== "tv" ? `<span class="pill">${escapeHtml(getStartModeLabel(active.startMode))}</span>` : ""}
|
||||
<span class="pill">${escapeHtml(modeLabel)}</span>
|
||||
</div>
|
||||
<h1>${escapeHtml(active.name)}</h1>
|
||||
<p class="overlay-header-sub">${escapeHtml(branding.brandName || "JMK RB RaceController")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay-meta">
|
||||
<button id="overlayFullscreen" class="btn overlay-fullscreen-btn" type="button">${t("overlay.fullscreen")}</button>
|
||||
${overlayViewMode === "obs" && obsConfig && !obsConfig.showClock ? "" : `<div class="overlay-clock">${overlayClock}</div>`}
|
||||
<div class="overlay-status">${escapeHtml(overlayStatusLabel)}</div>
|
||||
</div>
|
||||
</header>`
|
||||
}
|
||||
|
||||
${
|
||||
overlayViewMode === "speaker"
|
||||
@@ -6646,6 +6650,8 @@ function renderObsOverlay(active, leaderboard, result, sessionTiming, branding)
|
||||
const readyPositionGrid = active && active.status === "ready" && normalizeStartMode(active.startMode) === "position";
|
||||
const showStartGrid = obsConfig.showGrid && readyPositionGrid;
|
||||
const leadRow = compactRows[0] || null;
|
||||
const fastestRow =
|
||||
[...compactRows].filter((row) => Number.isFinite(row.bestLapMs)).sort((left, right) => left.bestLapMs - right.bestLapMs)[0] || null;
|
||||
const visiblePassings = getVisiblePassings(result);
|
||||
const elapsedOrRemaining = sessionTiming?.untimed
|
||||
? formatElapsedClock(sessionTiming?.elapsedMs ?? 0)
|
||||
@@ -6716,104 +6722,67 @@ function renderObsOverlay(active, leaderboard, result, sessionTiming, branding)
|
||||
`;
|
||||
}
|
||||
|
||||
const towerRowsHtml = showStartGrid
|
||||
? getSessionGridEntries(active)
|
||||
.slice(0, obsConfig.rows)
|
||||
.map((entry, idx) => {
|
||||
const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
|
||||
return `
|
||||
<article class="overlay-obs-tower-row ${idx === 0 ? "overlay-obs-row-leader" : ""}">
|
||||
<span class="pos-pill ${posClass}">${entry.slot}</span>
|
||||
<div class="overlay-obs-tower-driver">
|
||||
<strong>${escapeHtml(entry.name)}</strong>
|
||||
<span>${escapeHtml(entry.meta || "-")}</span>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("")
|
||||
: compactRows
|
||||
.map((row, idx) => {
|
||||
const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
|
||||
const trail = [
|
||||
obsConfig.showLaps ? `<span>${row.laps ?? 0}L</span>` : "",
|
||||
obsConfig.showResult ? `<span>${escapeHtml(row.resultDisplay)}</span>` : "",
|
||||
obsConfig.showBest ? `<span>${formatLap(row.bestLapMs)}</span>` : "",
|
||||
obsConfig.showGap ? `<span>${escapeHtml(row.gapDisplay || row.gapAhead || "-")}</span>` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("");
|
||||
return `
|
||||
<article class="overlay-obs-tower-row ${idx === 0 ? "overlay-obs-row-leader" : ""}">
|
||||
<span class="pos-pill ${posClass}">${idx + 1}</span>
|
||||
<div class="overlay-obs-tower-driver">
|
||||
<strong>${escapeHtml(row.displayName || row.driverName)}</strong>
|
||||
<span>${escapeHtml(row.teamId ? formatTeamActiveMemberLabel(row) : row.subLabel || row.transponder || "-")}</span>
|
||||
</div>
|
||||
${trail ? `<div class="overlay-obs-tower-trail">${trail}</div>` : ""}
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<section class="overlay-obs-layout">
|
||||
<div class="overlay-obs-main">
|
||||
<div class="overlay-obs-brandline">
|
||||
<section class="overlay-obs-tower">
|
||||
<div class="overlay-obs-tower-head">
|
||||
<div class="overlay-obs-tower-brand">
|
||||
${branding.logoDataUrl ? `<img class="overlay-logo overlay-obs-logo" src="${escapeHtml(branding.logoDataUrl)}" alt="logo" />` : ""}
|
||||
<div>
|
||||
<p class="overlay-kicker">${escapeHtml(getEventName(active.eventId))}</p>
|
||||
<h2>${escapeHtml(active.name)}</h2>
|
||||
<div class="overlay-obs-meta">
|
||||
<span>${escapeHtml(getSessionTypeLabel(active.type))}</span>
|
||||
<span>${escapeHtml(getStartModeLabel(active.startMode))}</span>
|
||||
<span>${t("table.laps")}: ${leadRow?.laps || 0}</span>
|
||||
<span>${t("timing.total_passings")}: ${visiblePassings.length || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${obsConfig.showClock ? `
|
||||
<div class="overlay-obs-clockblock">
|
||||
<strong>${elapsedOrRemaining}</strong>
|
||||
<span>${escapeHtml(sessionTiming?.followUpActive ? t("timing.follow_up_active") : getStatusLabel(active.status))}</span>
|
||||
</div>
|
||||
` : ""}
|
||||
${obsConfig.showClock ? `<div class="overlay-obs-tower-clock">${elapsedOrRemaining}</div>` : ""}
|
||||
</div>
|
||||
<div class="overlay-obs-content">
|
||||
<section class="overlay-obs-feature">
|
||||
${showStartGrid
|
||||
? `
|
||||
<div class="overlay-section-head">
|
||||
<h3>${t("events.start_grid")}</h3>
|
||||
<span class="pill">${escapeHtml(getStartModeLabel(active.startMode))}</span>
|
||||
</div>
|
||||
${renderPositionGrid(active)}
|
||||
`
|
||||
: obsConfig.showFastest
|
||||
? `
|
||||
<div class="overlay-section-head">
|
||||
<h3>${t("overlay.fastest_lap")}</h3>
|
||||
<span class="pill">${t("overlay.mode_obs")}</span>
|
||||
</div>
|
||||
<div class="overlay-fastest-banner overlay-fastest-banner-dense overlay-fastest-banner-obs">
|
||||
<div class="overlay-fastest-banner-copy">
|
||||
<span>${t("overlay.fastest_lap")}</span>
|
||||
<strong>${formatLap(leadRow?.bestLapMs)}</strong>
|
||||
</div>
|
||||
<div class="overlay-fastest-driver">${escapeHtml(leadRow?.displayName || leadRow?.driverName || "-")}</div>
|
||||
<div class="overlay-fastest-meta">${escapeHtml(branding.brandName || "JMK RB RaceController")}</div>
|
||||
</div>
|
||||
<div class="overlay-obs-gridhint">
|
||||
<strong>${t("events.position_grid")}</strong>
|
||||
<span>${t("overlay.leaderboard_live")}</span>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="overlay-section-head">
|
||||
<h3>${escapeHtml(branding.brandName || "JMK RB RaceController")}</h3>
|
||||
<span class="pill">${t("overlay.mode_obs")}</span>
|
||||
</div>
|
||||
<div class="overlay-obs-gridhint">
|
||||
<strong>${escapeHtml(getEventName(active.eventId))}</strong>
|
||||
<span>${escapeHtml(active.name)}</span>
|
||||
</div>
|
||||
`}
|
||||
</section>
|
||||
<section class="overlay-obs-standings">
|
||||
${compactRows.length ? compactRows.map((row, idx) => {
|
||||
const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
|
||||
return `
|
||||
<article class="overlay-obs-row ${idx === 0 ? "overlay-obs-row-leader" : ""}">
|
||||
<span class="pos-pill ${posClass}">${idx + 1}</span>
|
||||
<div class="overlay-obs-driver">
|
||||
<strong>${escapeHtml(row.displayName || row.driverName)}</strong>
|
||||
<span>${escapeHtml(row.teamId ? formatTeamActiveMemberLabel(row) : row.subLabel || row.transponder || "-")}</span>
|
||||
</div>
|
||||
${obsConfig.showLaps ? `
|
||||
<div class="overlay-obs-metric">
|
||||
<label>${t("table.laps")}</label>
|
||||
<strong>${row.laps ?? 0}</strong>
|
||||
</div>` : ""}
|
||||
${obsConfig.showResult ? `
|
||||
<div class="overlay-obs-metric">
|
||||
<label>${t("table.result")}</label>
|
||||
<strong>${escapeHtml(row.resultDisplay)}</strong>
|
||||
</div>` : ""}
|
||||
${obsConfig.showBest ? `
|
||||
<div class="overlay-obs-metric">
|
||||
<label>${t("table.best_lap")}</label>
|
||||
<strong>${formatLap(row.bestLapMs)}</strong>
|
||||
</div>` : ""}
|
||||
${obsConfig.showGap ? `
|
||||
<div class="overlay-obs-metric">
|
||||
<label>${t("table.gap")}</label>
|
||||
<strong>${escapeHtml(row.gapDisplay || row.gapAhead || "-")}</strong>
|
||||
</div>` : ""}
|
||||
</article>
|
||||
`;
|
||||
}).join("") : `<p>${t("timing.no_laps")}</p>`}
|
||||
</section>
|
||||
<div class="overlay-obs-tower-meta">
|
||||
<span>${escapeHtml(getSessionTypeLabel(active.type))}</span>
|
||||
<span>${escapeHtml(getStartModeLabel(active.startMode))}</span>
|
||||
${showStartGrid ? `<span>${t("events.start_grid")}</span>` : ""}
|
||||
${!showStartGrid && obsConfig.showFastest ? `<span>${t("overlay.fastest_lap")}: ${formatLap(fastestRow?.bestLapMs)}</span>` : ""}
|
||||
</div>
|
||||
<section class="overlay-obs-tower-standings">
|
||||
${towerRowsHtml || `<p>${t("timing.no_laps")}</p>`}
|
||||
</section>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
119
src/styles.css
119
src/styles.css
@@ -2162,3 +2162,122 @@ select:focus {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.overlay-shell-obs-layout-leaderboard {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.overlay-shell-obs-layout-leaderboard .overlay-obs-layout,
|
||||
.overlay-shell-obs-layout-leaderboard .overlay-obs-tower {
|
||||
max-width: 430px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.overlay-obs-tower {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.overlay-obs-tower-head,
|
||||
.overlay-obs-tower-meta,
|
||||
.overlay-obs-tower-standings {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--panel) 90%, transparent);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.overlay-obs-tower-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.overlay-obs-tower-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.overlay-obs-tower-brand h2 {
|
||||
margin: 0;
|
||||
font-family: Orbitron, sans-serif;
|
||||
font-size: clamp(0.88rem, 1.1vw, 1.05rem);
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.overlay-obs-tower-clock {
|
||||
font-family: Orbitron, sans-serif;
|
||||
font-size: clamp(0.98rem, 1.2vw, 1.15rem);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.overlay-obs-tower-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 8px;
|
||||
padding: 6px 10px;
|
||||
color: var(--muted);
|
||||
font-size: 0.58rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.overlay-obs-tower-standings {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.overlay-obs-tower-row {
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(0, 1fr) auto;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 5px 6px;
|
||||
border-radius: 8px;
|
||||
background: var(--surface-card);
|
||||
border: 1px solid color-mix(in srgb, var(--line) 84%, white 16%);
|
||||
}
|
||||
|
||||
.overlay-obs-tower-driver {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.overlay-obs-tower-driver strong {
|
||||
display: block;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.05;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.overlay-obs-tower-driver span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.52rem;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.overlay-obs-tower-trail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
font-family: Orbitron, sans-serif;
|
||||
font-size: 0.66rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.overlay-obs-tower-trail span {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user