diff --git a/src/app.js b/src/app.js index 7156984..5be9189 100644 --- a/src/app.js +++ b/src/app.js @@ -10,6 +10,9 @@ import { getSessionTypeLabel as getSessionTypeLabelLogic, getStatusLabel as getS import { renderDashboardView, renderClassesView, renderDriversView, renderCarsView } from "./core_views.js"; +import { renderGuideView, renderOverlayPageView } from "./misc_views.js"; + + import { renderTimingView, renderJudgingView } from "./timing_views.js"; @@ -103,6 +106,8 @@ const renderDashboard = () => renderDashboardView({ state, dom, t, backend, getA const renderClasses = () => renderClassesView({ state, dom, t, selectedClassEditId: () => selectedClassEditId, setSelectedClassEditId: (value) => { selectedClassEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell }); const renderDrivers = () => renderDriversView({ state, dom, t, driverBrandFilter: () => driverBrandFilter, setDriverBrandFilter: (value) => { driverBrandFilter = value; }, selectedDriverEditId: () => selectedDriverEditId, setSelectedDriverEditId: (value) => { selectedDriverEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeDriver, getClassName }); const renderCars = () => renderCarsView({ state, dom, t, carBrandFilter: () => carBrandFilter, setCarBrandFilter: (value) => { carBrandFilter = value; }, selectedCarEditId: () => selectedCarEditId, setSelectedCarEditId: (value) => { selectedCarEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeCar }); +const renderGuide = () => renderGuideView({ dom, t }); +const renderOverlay = () => renderOverlayPageView({ state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatPredictedLapDelta, getActiveSession, buildLeaderboard, ensureSessionResult, getSessionTiming, getVisiblePassings, resolveEventBranding, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, getOverlayModeLabel, getStatusLabel, getObsOverlayConfig, buildOverlayPanels, normalizeStartMode, renderPositionGrid, getEventName, getSessionTypeLabel, getStartModeLabel, renderRaceStandingsTableView, renderTeamOverlay, renderObsOverlay, renderOverlayLeaderboard, renderOverlaySidePanel, formatTeamActiveMemberLabel, getSessionGridEntries, overlayViewMode, overlayMode, publicOverlayMode, overlayEvents, overlayRotationIndex, openOverlayWindow, buildOverlayUrl }); const renderTiming = () => renderTimingView({ state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatRaceClock, renderTable, uid, getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTiming, getVisiblePassings, getEventName, getSessionTypeLabel, getStatusLabel, normalizeStartMode, getStartModeLabel, renderPositionGrid, connectDecoder, disconnectDecoder, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, ensureAudioContext, validateTrackSessionForStart, pushOverlayEvent, saveState, updateHeaderState, renderView, normalizeDriver, normalizeCar, processDecoderMessage, getSelectedLeaderboardKey: () => selectedLeaderboardKey, setSelectedLeaderboardKey: (value) => { selectedLeaderboardKey = value; }, getQuickAddDraft: () => quickAddDraft, setQuickAddDraft: (value) => { quickAddDraft = value; }, getPassingValidationLabel, isCountedPassing, getCompetitorPassings, getManualCorrectionSummary, formatTeamActiveMemberLabel, lastFinishAnnouncementSessionId: () => lastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId: (value) => { lastFinishAnnouncementSessionId = value; }, lastOverlayLeaderKeyBySession, lastOverlayTop3BySession, overlayEvents: () => overlayEvents, setOverlayEvents: (value) => { overlayEvents = value; } }); const renderJudging = () => renderJudgingView({ state, dom, t, escapeHtml, formatLap, renderTable, getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTypeLabel, getJudgeFilteredRows, getJudgeFilteredLog, getCompetitorPassings, isCountedPassing, getPassingValidationLabel, undoJudgingAdjustment, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, renderView, getSelectedJudgeKey: () => selectedJudgeKey, setSelectedJudgeKey: (value) => { selectedJudgeKey = value; }, getJudgingCompetitorFilter: () => judgingCompetitorFilter, setJudgingCompetitorFilter: (value) => { judgingCompetitorFilter = value; }, getJudgingLogFilter: () => judgingLogFilter, setJudgingLogFilter: (value) => { judgingLogFilter = value; } }); const applyCompetitorCorrection = (session, row, options = {}) => applyCompetitorCorrectionLogic(session, row, options, { ensureSessionResult, uid, t, formatLap, saveState }); @@ -5130,334 +5135,6 @@ function renderAssignmentList(eventId) { }); } -function renderGuidePanel(titleKey, itemKeys, extras = "") { - return ` -
-

${t(titleKey)}

-
- - ${extras} -
-
- `; -} - -function renderGuideOverviewCard(titleKey, blurbKey) { - return ` -
- ${t(titleKey)} -

${t(blurbKey)}

-
- `; -} - -function renderGuide() { - dom.view.innerHTML = ` -
-

${t("guide.title")}

-
-

${t("guide.intro")}

-
- ${renderGuideOverviewCard("guide.sponsor_title", "guide.card_sponsor_blurb")} - ${renderGuideOverviewCard("guide.race_title", "guide.card_race_blurb")} - ${renderGuideOverviewCard("guide.team_title", "guide.card_team_blurb")} - ${renderGuideOverviewCard("guide.windows_title", "guide.card_decoder_blurb")} -
-
-
- -
- ${renderGuidePanel("guide.sponsor_title", ["guide.sponsor_1", "guide.sponsor_2", "guide.sponsor_3", "guide.sponsor_4", "guide.sponsor_5", "guide.sponsor_6"])} - ${renderGuidePanel("guide.race_wizard_title", ["guide.race_wizard_1", "guide.race_wizard_2", "guide.race_wizard_3", "guide.race_wizard_4", "guide.race_wizard_5", "guide.race_wizard_6", "guide.race_wizard_7"])} -
- -
- ${renderGuidePanel("guide.race_title", ["guide.race_1", "guide.race_2", "guide.race_3", "guide.race_4", "guide.race_4a", "guide.race_5", "guide.race_6", "guide.race_7", "guide.race_8", "guide.race_9", "guide.race_10"])} - ${renderGuidePanel("guide.manage_steps_title", ["guide.manage_steps_1", "guide.manage_steps_2", "guide.manage_steps_3", "guide.manage_steps_4", "guide.manage_steps_5", "guide.manage_steps_6", "guide.manage_steps_7", "guide.manage_steps_8"])} -
- -
- ${renderGuidePanel("guide.race_format_title", ["guide.race_format_0", "guide.race_format_1", "guide.race_format_2", "guide.race_format_3", "guide.race_format_4", "guide.race_format_5", "guide.race_format_6", "guide.race_format_7", "guide.race_format_8", "guide.race_format_9", "guide.race_format_10", "guide.race_format_11", "guide.race_format_11a", "guide.race_format_12", "guide.race_format_13", "guide.race_format_14"])} - ${renderGuidePanel("guide.validation_title", ["guide.validation_1", "guide.validation_2", "guide.validation_3", "guide.validation_4", "guide.validation_5", "guide.validation_6", "guide.validation_7", "guide.validation_8"])} -
- -
- ${renderGuidePanel("guide.free_practice_title", ["guide.free_practice_1", "guide.free_practice_2", "guide.free_practice_3"])} - ${renderGuidePanel("guide.open_practice_title", ["guide.open_practice_1", "guide.open_practice_2", "guide.open_practice_3"])} -
- -
- ${renderGuidePanel("guide.team_title", ["guide.team_1", "guide.team_2", "guide.team_3", "guide.team_4", "guide.team_5", "guide.team_6"])} - ${renderGuidePanel("guide.qualifying_title", ["guide.qualifying_1", "guide.qualifying_2", "guide.qualifying_3", "guide.qualifying_4", "guide.qualifying_5"])} -
- -
- ${renderGuidePanel("guide.dashboard_title", ["guide.dashboard_1", "guide.dashboard_2", "guide.dashboard_3"])} - ${renderGuidePanel("guide.host_title", ["guide.host_1", "guide.host_2", "guide.host_3", "guide.host_4", "guide.host_5", "guide.host_6", "guide.host_7", "guide.host_8"])} -
- -
- ${renderGuidePanel("guide.windows_title", ["guide.windows_1", "guide.windows_2", "guide.windows_3", "guide.windows_4", "guide.windows_5"])} - ${renderGuidePanel("guide.linux_title", ["guide.linux_1", "guide.linux_2", "guide.linux_3"])} -
- -
-

${t("guide.sqlite_title")}

-
- -

${t("guide.ammc_ref")}

-
-
- `; -} - -function renderOverlay() { - const active = getActiveSession(); - const leaderboard = active ? buildLeaderboard(active).slice(0, 12) : []; - const result = active ? ensureSessionResult(active.id) : null; - const sessionTiming = active ? getSessionTiming(active) : null; - const overlayClock = sessionTiming?.followUpActive - ? formatCountdown(sessionTiming?.followUpRemainingMs ?? 0) - : sessionTiming?.untimed - ? formatElapsedClock(sessionTiming?.elapsedMs ?? 0) - : formatCountdown(sessionTiming?.remainingMs ?? 0); - const recent = active && result ? getVisiblePassings(result).slice(-8).reverse() : []; - const event = active ? state.events.find((item) => item.id === active.eventId) : null; - const branding = resolveEventBranding(event); - const practiceRows = event ? buildPracticeStandings(event) : []; - const qualifyingRows = event ? buildQualifyingStandings(event) : []; - const finalRows = event ? buildFinalStandings(event) : []; - 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, { t }); - const overlayStatusLabel = sessionTiming?.followUpActive ? t("timing.follow_up_active") : active ? getStatusLabel(active.status) : ""; - const obsConfig = overlayViewMode === "obs" ? getObsOverlayConfig() : null; - const rotatingPanels = buildOverlayPanels(active, recent, { t, overlayEvents, normalizeStartMode, renderPositionGrid }); - const activePanel = rotatingPanels.length ? rotatingPanels[overlayRotationIndex % rotatingPanels.length] : null; - - const denseOverlay = overlayViewMode === "leaderboard" || overlayViewMode === "tv"; - - dom.view.innerHTML = ` - ${ - overlayMode - ? "" - : ` -
-

${t("overlay.title")}

-
-
- - - - - - - -
-
-
- ` - } -
- ${ - active - ? ` - ${ - overlayViewMode === "obs" - ? "" - : `
-
- ${branding.logoDataUrl ? `` : ""} -
-
-

${escapeHtml(getEventName(active.eventId))}

- ${escapeHtml(getSessionTypeLabel(active.type))} - ${overlayViewMode !== "tv" ? `${escapeHtml(getStartModeLabel(active.startMode))}` : ""} - ${escapeHtml(modeLabel)} -
-

${escapeHtml(active.name)}

-

${escapeHtml(branding.brandName || "JMK RB RaceController")}

-
-
-
- - ${overlayViewMode === "obs" && obsConfig && !obsConfig.showClock ? "" : `
${overlayClock}
`} -
${escapeHtml(overlayStatusLabel)}
-
-
` - } - - ${ - overlayViewMode === "speaker" - ? ` -
-
-
P1
-

${escapeHtml(topRow?.displayName || topRow?.driverName || t("common.unknown_driver"))}

-

${t("table.result")}: ${escapeHtml(topRow?.resultDisplay || "-")}

-

${t("table.best_lap")}: ${formatLap(topRow?.bestLapMs)}

-
-
-
-

${t("overlay.last_passings")}

- ${ - recent.length - ? recent - .map( - (passing) => ` -
- ${escapeHtml(passing.displayName || passing.teamName || passing.driverName || t("common.unknown_driver"))} - ${formatLap(passing.lapMs)} -
- ` - ) - .join("") - : `

${t("timing.no_passings")}

` - } -
-
-

${t("events.position_grid")}

- ${normalizeStartMode(active.startMode) === "position" ? renderPositionGrid(active) : `

${t("events.na")}

`} -
-
-

${t("overlay.event_markers")}

- ${ - overlayEvents.length - ? overlayEvents - .map( - (item) => ` -
- ${escapeHtml(item.label)} - ${new Date(item.ts).toLocaleTimeString()} -
- ` - ) - .join("") - : `

${t("timing.no_passings")}

` - } -
-
-
- ` - : overlayViewMode === "results" - ? ` -
-
-

${t("events.practice_standings")}

- ${renderRaceStandingsTableView(practiceRows, t("events.no_practice_results"))} -
-
-

${t("events.qualifying_standings")}

- ${renderRaceStandingsTableView(qualifyingRows, t("events.no_qualifying_results"))} -
-
-

${t("events.final_standings")}

- ${renderRaceStandingsTableView(finalRows, t("events.no_final_results"))} -
-
- ` - : overlayViewMode === "team" - ? renderTeamOverlay(leaderboard, result, sessionTiming, { t, escapeHtml, formatLap, formatPredictedLapDelta, formatTeamActiveMemberLabel, getVisiblePassings, renderOverlayLeaderboard }) - : overlayViewMode === "obs" - ? renderObsOverlay(active, leaderboard, result, sessionTiming, branding, { - t, - escapeHtml, - formatLap, - getVisiblePassings, - getSessionTypeLabel, - getStartModeLabel, - getStatusLabel, - getEventName, - getObsOverlayConfig, - normalizeStartMode, - renderPositionGrid, - getSessionGridEntries, - formatTeamActiveMemberLabel, - renderOverlayLeaderboard, - }) - : overlayViewMode === "tv" - ? ` -
-
-
-
- ${t("overlay.fastest_lap")} - ${formatLap(fastestRow?.bestLapMs)} -
-
${escapeHtml(fastestRow?.displayName || fastestRow?.driverName || "-")}
-
${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${getVisiblePassings(result).length || 0}
-
-
- ${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })} -
-
-
- ` - : ` -
-
-
-
- ${t("overlay.fastest_lap")} - ${formatLap(fastestRow?.bestLapMs)} -
-
${escapeHtml(fastestRow?.displayName || fastestRow?.driverName || "-")}
-
${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${result?.passings.length || 0}
-
-
- ${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })} -
-
- -
- ` - } - ` - : ` -
-

${t("overlay.title")}

-

${t("overlay.no_active")}

-
- ` - } -
- `; - - document.getElementById("overlayFullscreen")?.addEventListener("click", async () => { - const target = document.documentElement; - if (!document.fullscreenElement) { - await target.requestFullscreen?.().catch(() => {}); - return; - } - await document.exitFullscreen?.().catch(() => {}); - }); - document.getElementById("overlayLaunchLeaderboard")?.addEventListener("click", () => openOverlayWindow("leaderboard")); - document.getElementById("overlayLaunchSpeaker")?.addEventListener("click", () => openOverlayWindow("speaker")); - document.getElementById("overlayLaunchResults")?.addEventListener("click", () => openOverlayWindow("results")); - document.getElementById("overlayLaunchTv")?.addEventListener("click", () => openOverlayWindow("tv")); - document.getElementById("overlayLaunchTeam")?.addEventListener("click", () => openOverlayWindow("team")); - document.getElementById("overlayLaunchObs")?.addEventListener("click", () => openOverlayWindow("obs", { public: true })); - document.getElementById("overlayCopyObsUrl")?.addEventListener("click", async () => { - const url = buildOverlayUrl("obs", { public: true }); - if (navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(url).catch(() => {}); - return; - } - window.prompt("Copy OBS URL", url); - }); -} - function getQuickAddState(transponder) { const normalized = String(transponder || "").trim(); const driver = state.drivers.find((item) => String(item.transponder || "").trim() === normalized) || null; diff --git a/src/misc_views.js b/src/misc_views.js new file mode 100644 index 0000000..a7ee221 --- /dev/null +++ b/src/misc_views.js @@ -0,0 +1,272 @@ +function renderGuidePanel(titleKey, itemKeys, extras = "", { t }) { + return ` +
+

${t(titleKey)}

+
+ + ${extras} +
+
+ `; +} + +function renderGuideOverviewCard(titleKey, blurbKey, { t }) { + return ` +
+ ${t(titleKey)} +

${t(blurbKey)}

+
+ `; +} + +export function renderGuideView({ dom, t }) { + dom.view.innerHTML = ` +
+

${t("guide.title")}

+
+

${t("guide.intro")}

+
+ ${renderGuideOverviewCard("guide.sponsor_title", "guide.card_sponsor_blurb", { t })} + ${renderGuideOverviewCard("guide.race_title", "guide.card_race_blurb", { t })} + ${renderGuideOverviewCard("guide.team_title", "guide.card_team_blurb", { t })} + ${renderGuideOverviewCard("guide.windows_title", "guide.card_decoder_blurb", { t })} +
+
+
+ +
+ ${renderGuidePanel("guide.sponsor_title", ["guide.sponsor_1", "guide.sponsor_2", "guide.sponsor_3", "guide.sponsor_4", "guide.sponsor_5", "guide.sponsor_6"], "", { t })} + ${renderGuidePanel("guide.race_wizard_title", ["guide.race_wizard_1", "guide.race_wizard_2", "guide.race_wizard_3", "guide.race_wizard_4", "guide.race_wizard_5", "guide.race_wizard_6", "guide.race_wizard_7"], "", { t })} +
+ +
+ ${renderGuidePanel("guide.race_title", ["guide.race_1", "guide.race_2", "guide.race_3", "guide.race_4", "guide.race_4a", "guide.race_5", "guide.race_6", "guide.race_7", "guide.race_8", "guide.race_9", "guide.race_10"], "", { t })} + ${renderGuidePanel("guide.manage_steps_title", ["guide.manage_steps_1", "guide.manage_steps_2", "guide.manage_steps_3", "guide.manage_steps_4", "guide.manage_steps_5", "guide.manage_steps_6", "guide.manage_steps_7", "guide.manage_steps_8"], "", { t })} +
+ +
+ ${renderGuidePanel("guide.race_format_title", ["guide.race_format_0", "guide.race_format_1", "guide.race_format_2", "guide.race_format_3", "guide.race_format_4", "guide.race_format_5", "guide.race_format_6", "guide.race_format_7", "guide.race_format_8", "guide.race_format_9", "guide.race_format_10", "guide.race_format_11", "guide.race_format_11a", "guide.race_format_12", "guide.race_format_13", "guide.race_format_14"], "", { t })} + ${renderGuidePanel("guide.validation_title", ["guide.validation_1", "guide.validation_2", "guide.validation_3", "guide.validation_4", "guide.validation_5", "guide.validation_6", "guide.validation_7", "guide.validation_8"], "", { t })} +
+ +
+ ${renderGuidePanel("guide.free_practice_title", ["guide.free_practice_1", "guide.free_practice_2", "guide.free_practice_3"], "", { t })} + ${renderGuidePanel("guide.open_practice_title", ["guide.open_practice_1", "guide.open_practice_2", "guide.open_practice_3"], "", { t })} +
+ +
+ ${renderGuidePanel("guide.team_title", ["guide.team_1", "guide.team_2", "guide.team_3", "guide.team_4", "guide.team_5", "guide.team_6"], "", { t })} + ${renderGuidePanel("guide.qualifying_title", ["guide.qualifying_1", "guide.qualifying_2", "guide.qualifying_3", "guide.qualifying_4", "guide.qualifying_5"], "", { t })} +
+ +
+ ${renderGuidePanel("guide.dashboard_title", ["guide.dashboard_1", "guide.dashboard_2", "guide.dashboard_3"], "", { t })} + ${renderGuidePanel("guide.host_title", ["guide.host_1", "guide.host_2", "guide.host_3", "guide.host_4", "guide.host_5", "guide.host_6", "guide.host_7", "guide.host_8"], "", { t })} +
+ +
+ ${renderGuidePanel("guide.windows_title", ["guide.windows_1", "guide.windows_2", "guide.windows_3", "guide.windows_4", "guide.windows_5"], "", { t })} + ${renderGuidePanel("guide.linux_title", ["guide.linux_1", "guide.linux_2", "guide.linux_3"], "", { t })} +
+ +
+

${t("guide.sqlite_title")}

+
+ +

${t("guide.ammc_ref")}

+
+
+ `; +} + +export function renderOverlayPageView(deps) { + const { state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatPredictedLapDelta, getActiveSession, buildLeaderboard, ensureSessionResult, getSessionTiming, getVisiblePassings, resolveEventBranding, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, getOverlayModeLabel, getStatusLabel, getObsOverlayConfig, buildOverlayPanels, normalizeStartMode, renderPositionGrid, getEventName, getSessionTypeLabel, getStartModeLabel, renderRaceStandingsTableView, renderTeamOverlay, renderObsOverlay, renderOverlayLeaderboard, renderOverlaySidePanel, formatTeamActiveMemberLabel, getSessionGridEntries, overlayViewMode, overlayMode, publicOverlayMode, overlayEvents, overlayRotationIndex, openOverlayWindow, buildOverlayUrl } = deps; + + const active = getActiveSession(); + const leaderboard = active ? buildLeaderboard(active).slice(0, 12) : []; + const result = active ? ensureSessionResult(active.id) : null; + const sessionTiming = active ? getSessionTiming(active) : null; + const overlayClock = sessionTiming?.followUpActive + ? formatCountdown(sessionTiming?.followUpRemainingMs ?? 0) + : sessionTiming?.untimed + ? formatElapsedClock(sessionTiming?.elapsedMs ?? 0) + : formatCountdown(sessionTiming?.remainingMs ?? 0); + const recent = active && result ? getVisiblePassings(result).slice(-8).reverse() : []; + const event = active ? state.events.find((item) => item.id === active.eventId) : null; + const branding = resolveEventBranding(event); + const practiceRows = event ? buildPracticeStandings(event) : []; + const qualifyingRows = event ? buildQualifyingStandings(event) : []; + const finalRows = event ? buildFinalStandings(event) : []; + 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, { t }); + const overlayStatusLabel = sessionTiming?.followUpActive ? t("timing.follow_up_active") : active ? getStatusLabel(active.status) : ""; + const obsConfig = overlayViewMode === "obs" ? getObsOverlayConfig() : null; + const rotatingPanels = buildOverlayPanels(active, recent, { t, overlayEvents, normalizeStartMode, renderPositionGrid }); + const activePanel = rotatingPanels.length ? rotatingPanels[overlayRotationIndex % rotatingPanels.length] : null; + const denseOverlay = overlayViewMode === "leaderboard" || overlayViewMode === "tv"; + + dom.view.innerHTML = ` + ${overlayMode ? "" : ` +
+

${t("overlay.title")}

+
+
+ + + + + + + +
+
+
+ `} +
+ ${active ? ` + ${overlayViewMode === "obs" ? "" : `
+
+ ${branding.logoDataUrl ? `` : ""} +
+
+

${escapeHtml(getEventName(active.eventId))}

+ ${escapeHtml(getSessionTypeLabel(active.type))} + ${overlayViewMode !== "tv" ? `${escapeHtml(getStartModeLabel(active.startMode))}` : ""} + ${escapeHtml(modeLabel)} +
+

${escapeHtml(active.name)}

+

${escapeHtml(branding.brandName || "JMK RB RaceController")}

+
+
+
+ + ${overlayViewMode === "obs" && obsConfig && !obsConfig.showClock ? "" : `
${overlayClock}
`} +
${escapeHtml(overlayStatusLabel)}
+
+
`} + + ${overlayViewMode === "speaker" ? ` +
+
+
P1
+

${escapeHtml(topRow?.displayName || topRow?.driverName || t("common.unknown_driver"))}

+

${t("table.result")}: ${escapeHtml(topRow?.resultDisplay || "-")}

+

${t("table.best_lap")}: ${formatLap(topRow?.bestLapMs)}

+
+
+
+

${t("overlay.last_passings")}

+ ${recent.length ? recent.map((passing) => ` +
+ ${escapeHtml(passing.displayName || passing.teamName || passing.driverName || t("common.unknown_driver"))} + ${formatLap(passing.lapMs)} +
+ `).join("") : `

${t("timing.no_passings")}

`} +
+
+

${t("events.position_grid")}

+ ${normalizeStartMode(active.startMode) === "position" ? renderPositionGrid(active) : `

${t("events.na")}

`} +
+
+

${t("overlay.event_markers")}

+ ${overlayEvents.length ? overlayEvents.map((item) => ` +
+ ${escapeHtml(item.label)} + ${new Date(item.ts).toLocaleTimeString()} +
+ `).join("") : `

${t("timing.no_passings")}

`} +
+
+
+ ` : overlayViewMode === "results" ? ` +
+
+

${t("events.practice_standings")}

+ ${renderRaceStandingsTableView(practiceRows, t("events.no_practice_results"))} +
+
+

${t("events.qualifying_standings")}

+ ${renderRaceStandingsTableView(qualifyingRows, t("events.no_qualifying_results"))} +
+
+

${t("events.final_standings")}

+ ${renderRaceStandingsTableView(finalRows, t("events.no_final_results"))} +
+
+ ` : overlayViewMode === "team" ? renderTeamOverlay(leaderboard, result, sessionTiming, { t, escapeHtml, formatLap, formatPredictedLapDelta, formatTeamActiveMemberLabel, getVisiblePassings, renderOverlayLeaderboard }) : overlayViewMode === "obs" ? renderObsOverlay(active, leaderboard, result, sessionTiming, branding, { t, escapeHtml, formatLap, getVisiblePassings, getSessionTypeLabel, getStartModeLabel, getStatusLabel, getEventName, getObsOverlayConfig, normalizeStartMode, renderPositionGrid, getSessionGridEntries, formatTeamActiveMemberLabel, renderOverlayLeaderboard }) : overlayViewMode === "tv" ? ` +
+
+
+
+ ${t("overlay.fastest_lap")} + ${formatLap(fastestRow?.bestLapMs)} +
+
${escapeHtml(fastestRow?.displayName || fastestRow?.driverName || "-")}
+
${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${getVisiblePassings(result).length || 0}
+
+
+ ${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })} +
+
+
+ ` : ` +
+
+
+
+ ${t("overlay.fastest_lap")} + ${formatLap(fastestRow?.bestLapMs)} +
+
${escapeHtml(fastestRow?.displayName || fastestRow?.driverName || "-")}
+
${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${result?.passings.length || 0}
+
+
+ ${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })} +
+
+ +
+ `} + ` : ` +
+

${t("overlay.title")}

+

${t("overlay.no_active")}

+
+ `} +
+ `; + + document.getElementById("overlayFullscreen")?.addEventListener("click", async () => { + const target = document.documentElement; + if (!document.fullscreenElement) { + await target.requestFullscreen?.().catch(() => {}); + return; + } + await document.exitFullscreen?.().catch(() => {}); + }); + document.getElementById("overlayLaunchLeaderboard")?.addEventListener("click", () => openOverlayWindow("leaderboard")); + document.getElementById("overlayLaunchSpeaker")?.addEventListener("click", () => openOverlayWindow("speaker")); + document.getElementById("overlayLaunchResults")?.addEventListener("click", () => openOverlayWindow("results")); + document.getElementById("overlayLaunchTv")?.addEventListener("click", () => openOverlayWindow("tv")); + document.getElementById("overlayLaunchTeam")?.addEventListener("click", () => openOverlayWindow("team")); + document.getElementById("overlayLaunchObs")?.addEventListener("click", () => openOverlayWindow("obs", { public: true })); + document.getElementById("overlayCopyObsUrl")?.addEventListener("click", async () => { + const url = buildOverlayUrl("obs", { public: true }); + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(url).catch(() => {}); + return; + } + window.prompt("Copy OBS URL", url); + }); +}