add replay session

This commit is contained in:
larssand
2026-09-04 15:58:24 +02:00
parent c60b9e0057
commit 142c45be04
10 changed files with 409 additions and 4 deletions

View File

@@ -1,3 +1,89 @@
import {
advanceSessionReplay,
buildSessionReplaySnapshot,
loadSessionReplay,
pauseSessionReplay,
playSessionReplay,
resetSessionReplay,
setSessionReplaySpeed,
} from "./session_replay.js";
function stopSessionReplayTimer(runtime) {
if (runtime?.timer) clearInterval(runtime.timer);
if (runtime) runtime.timer = null;
}
function renderSessionReplayContent(runtime, { t, escapeHtml, formatLap, formatElapsedClock }) {
const snapshot = buildSessionReplaySnapshot(runtime);
const rows = snapshot.leaderboard.slice(0, 12);
return `
<div class="session-replay-progress">
<progress max="100" value="${Math.round(snapshot.progress * 100)}"></progress>
<strong>${formatElapsedClock(snapshot.elapsedMs)} / ${formatElapsedClock(snapshot.durationMs)}</strong>
<span class="pill">${escapeHtml(t(`timing.replay_status_${snapshot.status}`))}</span>
</div>
${snapshot.latestPassing
? `<p class="session-replay-latest"><span>${t("timing.replay_latest")}</span><strong>${escapeHtml(snapshot.latestPassing.driverName)}</strong><small>${formatLap(snapshot.latestPassing.lapMs)}</small></p>`
: `<p class="hint">${t("timing.replay_no_visible_passings")}</p>`}
${rows.length
? `<div class="table-wrap"><table><thead><tr><th>${t("table.pos")}</th><th>${t("table.driver")}</th><th>${t("table.laps")}</th><th>${t("table.last_lap")}</th><th>${t("table.best_lap")}</th></tr></thead><tbody>
${rows.map((row, index) => `<tr><td><span class="pos-pill">${index + 1}</span></td><td><strong>${escapeHtml(row.driverName)}</strong><small class="table-subnote">${escapeHtml(row.carName || row.transponder)}</small></td><td>${row.laps}</td><td>${formatLap(row.lastLapMs)}</td><td class="best">${formatLap(row.bestLapMs)}</td></tr>`).join("")}
</tbody></table></div>`
: ""}
`;
}
function updateSessionReplayPanel(runtime, deps) {
const content = document.querySelector("[data-session-replay-content]");
if (content) content.innerHTML = renderSessionReplayContent(runtime, deps);
}
function startSessionReplayTimer(runtime, deps) {
stopSessionReplayTimer(runtime);
runtime.timer = setInterval(() => {
advanceSessionReplay(runtime);
updateSessionReplayPanel(runtime, deps);
if (runtime.status !== "running") stopSessionReplayTimer(runtime);
}, 100);
}
function renderSessionReplayPanel(runtime, deps) {
const { state, t, escapeHtml, getEventName, formatLap, formatElapsedClock } = deps;
if (runtime.status === "running") advanceSessionReplay(runtime);
const replayable = state.sessions.filter((session) => state.resultsBySession?.[session.id]?.passings?.length);
const selectedId = runtime.sessionId || replayable[0]?.id || "";
return `
<section class="panel mt-16 session-replay-panel">
<div class="panel-header">
<div><h3>${t("timing.session_replay")}</h3><p class="hint">${t("timing.session_replay_hint")}</p></div>
<span class="pill">${t("timing.replay_isolated")}</span>
</div>
<div class="panel-body">
<div class="session-replay-controls">
<select id="sessionReplaySelect">
<option value="">${t("timing.replay_select")}</option>
${replayable.map((session) => `<option value="${escapeHtml(session.id)}" ${session.id === selectedId ? "selected" : ""}>${escapeHtml(getEventName(session.eventId))} · ${escapeHtml(session.name)} · ${state.resultsBySession[session.id].passings.length}</option>`).join("")}
</select>
<button id="sessionReplayLoad" class="btn" type="button" ${selectedId ? "" : "disabled"}>${t("timing.replay_load")}</button>
<button id="sessionReplayPlay" class="btn btn-primary" type="button" ${runtime.passings.length ? "" : "disabled"}>${t("timing.replay_play")}</button>
<button id="sessionReplayPause" class="btn" type="button" ${runtime.status === "running" ? "" : "disabled"}>${t("timing.replay_pause")}</button>
<button id="sessionReplayReset" class="btn" type="button" ${runtime.passings.length ? "" : "disabled"}>${t("timing.replay_reset")}</button>
<label>${t("timing.replay_speed")}
<select id="sessionReplaySpeed">
${[1, 2, 5, 10].map((speed) => `<option value="${speed}" ${runtime.speed === speed ? "selected" : ""}>${speed}x</option>`).join("")}
</select>
</label>
</div>
<div data-session-replay-content>
${runtime.passings.length
? renderSessionReplayContent(runtime, { t, escapeHtml, formatLap, formatElapsedClock })
: `<p class="hint">${t(replayable.length ? "timing.replay_load_hint" : "timing.replay_empty")}</p>`}
</div>
</div>
</section>
`;
}
function renderSpeakerToggle(settingKey, labelKey, { state, t }) {
return `
<label class="check-card">
@@ -267,7 +353,7 @@ export function renderTimingView(deps) {
normalizeDriver, normalizeCar, processDecoderMessage, getSelectedLeaderboardKey, setSelectedLeaderboardKey,
getQuickAddDraft, setQuickAddDraft, getPassingValidationLabel, isCountedPassing, getCompetitorPassings,
getManualCorrectionSummary, formatTeamActiveMemberLabel, formatRaceClock, renderSessionChannelAnalysis,
setLastFinishAnnouncementSessionId, lastOverlayLeaderKeyBySession, lastOverlayTop3BySession, overlayEvents, setOverlayEvents,
setLastFinishAnnouncementSessionId, lastOverlayLeaderKeyBySession, lastOverlayTop3BySession, overlayEvents, setOverlayEvents, sessionReplay,
} = deps;
const helperDeps = {
@@ -393,6 +479,8 @@ export function renderTimingView(deps) {
${active ? renderQuickAddPanel(active, helperDeps) : ""}
${renderSessionReplayPanel(sessionReplay, { state, t, escapeHtml, getEventName, formatLap, formatElapsedClock })}
<section class="panel mt-16">
<div class="panel-header"><h3>${t("timing.speaker_panel")}</h3></div>
<div class="panel-body">
@@ -462,6 +550,36 @@ export function renderTimingView(deps) {
document.getElementById("timingDisconnect")?.addEventListener("click", disconnectDecoder);
const replayRenderDeps = { t, escapeHtml, formatLap, formatElapsedClock };
document.getElementById("sessionReplayLoad")?.addEventListener("click", () => {
const sessionId = String(document.getElementById("sessionReplaySelect")?.value || "");
const sourceSession = state.sessions.find((session) => session.id === sessionId);
if (!sourceSession) return;
stopSessionReplayTimer(sessionReplay);
loadSessionReplay(sessionReplay, sourceSession, state.resultsBySession[sourceSession.id]);
renderView();
});
document.getElementById("sessionReplayPlay")?.addEventListener("click", () => {
if (!playSessionReplay(sessionReplay)) return;
renderView();
startSessionReplayTimer(sessionReplay, replayRenderDeps);
});
document.getElementById("sessionReplayPause")?.addEventListener("click", () => {
pauseSessionReplay(sessionReplay);
stopSessionReplayTimer(sessionReplay);
renderView();
});
document.getElementById("sessionReplayReset")?.addEventListener("click", () => {
stopSessionReplayTimer(sessionReplay);
resetSessionReplay(sessionReplay);
renderView();
});
document.getElementById("sessionReplaySpeed")?.addEventListener("change", (event) => {
setSessionReplaySpeed(sessionReplay, Number(event.currentTarget?.value || 1));
updateSessionReplayPanel(sessionReplay, replayRenderDeps);
});
leaderboard.forEach((row) => {
document.getElementById(`leaderboard-detail-${row.key}`)?.addEventListener("click", () => {
setSelectedLeaderboardKey(row.key);