Extract guide and overlay views into module
This commit is contained in:
333
src/app.js
333
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 `
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>${t(titleKey)}</h3></div>
|
||||
<div class="panel-body">
|
||||
<ul class="guide-list">
|
||||
${itemKeys.map((key) => `<li>${t(key)}</li>`).join("")}
|
||||
</ul>
|
||||
${extras}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGuideOverviewCard(titleKey, blurbKey) {
|
||||
return `
|
||||
<article class="guide-overview-card">
|
||||
<strong>${t(titleKey)}</strong>
|
||||
<p>${t(blurbKey)}</p>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGuide() {
|
||||
dom.view.innerHTML = `
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>${t("guide.title")}</h3></div>
|
||||
<div class="panel-body">
|
||||
<p>${t("guide.intro")}</p>
|
||||
<div class="guide-overview-grid mt-16">
|
||||
${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")}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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"])}
|
||||
</div>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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"])}
|
||||
</div>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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"])}
|
||||
</div>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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"])}
|
||||
</div>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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"])}
|
||||
</div>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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"])}
|
||||
</div>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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"])}
|
||||
</div>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<div class="panel-header"><h3>${t("guide.sqlite_title")}</h3></div>
|
||||
<div class="panel-body">
|
||||
<ul class="guide-list">
|
||||
<li>${t("guide.sqlite_1")}</li>
|
||||
<li>${t("guide.sqlite_2")}</li>
|
||||
<li>${t("guide.sqlite_3")}</li>
|
||||
<li>${t("guide.sqlite_4")}</li>
|
||||
<li>${t("guide.sqlite_5")}</li>
|
||||
</ul>
|
||||
<p><a href="https://www.ammconverter.eu/docs/intro/quick-start/" target="_blank" rel="noreferrer">${t("guide.ammc_ref")}</a></p>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
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
|
||||
? ""
|
||||
: `
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>${t("overlay.title")}</h3></div>
|
||||
<div class="panel-body">
|
||||
<div class="actions">
|
||||
<button id="overlayLaunchLeaderboard" class="btn" type="button">${t("timing.open_overlay")}</button>
|
||||
<button id="overlayLaunchSpeaker" class="btn" type="button">${t("timing.open_speaker_overlay")}</button>
|
||||
<button id="overlayLaunchResults" class="btn" type="button">${t("timing.open_results_overlay")}</button>
|
||||
<button id="overlayLaunchTv" class="btn" type="button">${t("timing.open_tv_overlay")}</button>
|
||||
<button id="overlayLaunchTeam" class="btn" type="button">${t("timing.open_team_overlay")}</button>
|
||||
<button id="overlayLaunchObs" class="btn" type="button">${t("timing.open_obs_overlay")}</button>
|
||||
<button id="overlayCopyObsUrl" class="btn" type="button">${t("overlay.obs_copy_url")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
<section class="overlay-shell ${overlayViewMode === "tv" ? "overlay-shell-tv" : ""} ${overlayViewMode === "obs" ? "overlay-shell-obs" : ""} ${overlayViewMode === "obs" && obsConfig ? `overlay-shell-obs-theme-${obsConfig.theme}` : ""} ${overlayViewMode === "obs" && obsConfig ? `overlay-shell-obs-layout-${obsConfig.layout}` : ""} ${denseOverlay ? "overlay-shell-dense" : ""} ${publicOverlayMode ? "overlay-shell-public" : ""}">
|
||||
${
|
||||
active
|
||||
? `
|
||||
${
|
||||
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"
|
||||
? `
|
||||
<section class="overlay-speaker">
|
||||
<div class="overlay-speaker-main">
|
||||
<div class="overlay-speaker-label">P1</div>
|
||||
<h2>${escapeHtml(topRow?.displayName || topRow?.driverName || t("common.unknown_driver"))}</h2>
|
||||
<p>${t("table.result")}: ${escapeHtml(topRow?.resultDisplay || "-")}</p>
|
||||
<p>${t("table.best_lap")}: ${formatLap(topRow?.bestLapMs)}</p>
|
||||
</div>
|
||||
<div class="overlay-speaker-side">
|
||||
<section class="overlay-side-card">
|
||||
<h3>${t("overlay.last_passings")}</h3>
|
||||
${
|
||||
recent.length
|
||||
? recent
|
||||
.map(
|
||||
(passing) => `
|
||||
<div class="overlay-passing">
|
||||
<strong>${escapeHtml(passing.displayName || passing.teamName || passing.driverName || t("common.unknown_driver"))}</strong>
|
||||
<span>${formatLap(passing.lapMs)}</span>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("")
|
||||
: `<p>${t("timing.no_passings")}</p>`
|
||||
}
|
||||
</section>
|
||||
<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.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>
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
: overlayViewMode === "results"
|
||||
? `
|
||||
<section class="overlay-results">
|
||||
<div class="overlay-side-card">
|
||||
<h3>${t("events.practice_standings")}</h3>
|
||||
${renderRaceStandingsTableView(practiceRows, t("events.no_practice_results"))}
|
||||
</div>
|
||||
<div class="overlay-side-card">
|
||||
<h3>${t("events.qualifying_standings")}</h3>
|
||||
${renderRaceStandingsTableView(qualifyingRows, t("events.no_qualifying_results"))}
|
||||
</div>
|
||||
<div class="overlay-side-card">
|
||||
<h3>${t("events.final_standings")}</h3>
|
||||
${renderRaceStandingsTableView(finalRows, t("events.no_final_results"))}
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
: 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"
|
||||
? `
|
||||
<section class="overlay-board overlay-board-tv">
|
||||
<div class="overlay-table-wrap overlay-display-wrap overlay-display-wrap-dense">
|
||||
<section class="overlay-fastest-banner overlay-fastest-banner-dense">
|
||||
<div class="overlay-fastest-banner-copy">
|
||||
<span>${t("overlay.fastest_lap")}</span>
|
||||
<strong>${formatLap(fastestRow?.bestLapMs)}</strong>
|
||||
</div>
|
||||
<div class="overlay-fastest-driver">${escapeHtml(fastestRow?.displayName || fastestRow?.driverName || "-")}</div>
|
||||
<div class="overlay-fastest-meta">${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${getVisiblePassings(result).length || 0}</div>
|
||||
</section>
|
||||
<div class="overlay-leaderboard-card overlay-leaderboard-card-tv overlay-leaderboard-card-dense">
|
||||
${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
: `
|
||||
<section class="overlay-board">
|
||||
<div class="overlay-table-wrap overlay-display-wrap overlay-display-wrap-dense">
|
||||
<section class="overlay-fastest-banner overlay-fastest-banner-dense">
|
||||
<div class="overlay-fastest-banner-copy">
|
||||
<span>${t("overlay.fastest_lap")}</span>
|
||||
<strong>${formatLap(fastestRow?.bestLapMs)}</strong>
|
||||
</div>
|
||||
<div class="overlay-fastest-driver">${escapeHtml(fastestRow?.displayName || fastestRow?.driverName || "-")}</div>
|
||||
<div class="overlay-fastest-meta">${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${result?.passings.length || 0}</div>
|
||||
</section>
|
||||
<div class="overlay-leaderboard-card overlay-leaderboard-card-dense">
|
||||
${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })}
|
||||
</div>
|
||||
</div>
|
||||
<aside class="overlay-side">
|
||||
${activePanel ? renderOverlaySidePanel(activePanel, { t, escapeHtml }) : `<section class="overlay-side-card"><p>${t("timing.no_passings")}</p></section>`}
|
||||
</aside>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
`
|
||||
: `
|
||||
<section class="overlay-empty">
|
||||
<h1>${t("overlay.title")}</h1>
|
||||
<p>${t("overlay.no_active")}</p>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
|
||||
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;
|
||||
|
||||
272
src/misc_views.js
Normal file
272
src/misc_views.js
Normal file
@@ -0,0 +1,272 @@
|
||||
function renderGuidePanel(titleKey, itemKeys, extras = "", { t }) {
|
||||
return `
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>${t(titleKey)}</h3></div>
|
||||
<div class="panel-body">
|
||||
<ul class="guide-list">
|
||||
${itemKeys.map((key) => `<li>${t(key)}</li>`).join("")}
|
||||
</ul>
|
||||
${extras}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGuideOverviewCard(titleKey, blurbKey, { t }) {
|
||||
return `
|
||||
<article class="guide-overview-card">
|
||||
<strong>${t(titleKey)}</strong>
|
||||
<p>${t(blurbKey)}</p>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderGuideView({ dom, t }) {
|
||||
dom.view.innerHTML = `
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>${t("guide.title")}</h3></div>
|
||||
<div class="panel-body">
|
||||
<p>${t("guide.intro")}</p>
|
||||
<div class="guide-overview-grid mt-16">
|
||||
${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 })}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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 })}
|
||||
</div>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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 })}
|
||||
</div>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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 })}
|
||||
</div>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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 })}
|
||||
</div>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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 })}
|
||||
</div>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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 })}
|
||||
</div>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${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 })}
|
||||
</div>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<div class="panel-header"><h3>${t("guide.sqlite_title")}</h3></div>
|
||||
<div class="panel-body">
|
||||
<ul class="guide-list">
|
||||
<li>${t("guide.sqlite_1")}</li>
|
||||
<li>${t("guide.sqlite_2")}</li>
|
||||
<li>${t("guide.sqlite_3")}</li>
|
||||
<li>${t("guide.sqlite_4")}</li>
|
||||
<li>${t("guide.sqlite_5")}</li>
|
||||
</ul>
|
||||
<p><a href="https://www.ammconverter.eu/docs/intro/quick-start/" target="_blank" rel="noreferrer">${t("guide.ammc_ref")}</a></p>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
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 ? "" : `
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>${t("overlay.title")}</h3></div>
|
||||
<div class="panel-body">
|
||||
<div class="actions">
|
||||
<button id="overlayLaunchLeaderboard" class="btn" type="button">${t("timing.open_overlay")}</button>
|
||||
<button id="overlayLaunchSpeaker" class="btn" type="button">${t("timing.open_speaker_overlay")}</button>
|
||||
<button id="overlayLaunchResults" class="btn" type="button">${t("timing.open_results_overlay")}</button>
|
||||
<button id="overlayLaunchTv" class="btn" type="button">${t("timing.open_tv_overlay")}</button>
|
||||
<button id="overlayLaunchTeam" class="btn" type="button">${t("timing.open_team_overlay")}</button>
|
||||
<button id="overlayLaunchObs" class="btn" type="button">${t("timing.open_obs_overlay")}</button>
|
||||
<button id="overlayCopyObsUrl" class="btn" type="button">${t("overlay.obs_copy_url")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`}
|
||||
<section class="overlay-shell ${overlayViewMode === "tv" ? "overlay-shell-tv" : ""} ${overlayViewMode === "obs" ? "overlay-shell-obs" : ""} ${overlayViewMode === "obs" && obsConfig ? `overlay-shell-obs-theme-${obsConfig.theme}` : ""} ${overlayViewMode === "obs" && obsConfig ? `overlay-shell-obs-layout-${obsConfig.layout}` : ""} ${denseOverlay ? "overlay-shell-dense" : ""} ${publicOverlayMode ? "overlay-shell-public" : ""}">
|
||||
${active ? `
|
||||
${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" ? `
|
||||
<section class="overlay-speaker">
|
||||
<div class="overlay-speaker-main">
|
||||
<div class="overlay-speaker-label">P1</div>
|
||||
<h2>${escapeHtml(topRow?.displayName || topRow?.driverName || t("common.unknown_driver"))}</h2>
|
||||
<p>${t("table.result")}: ${escapeHtml(topRow?.resultDisplay || "-")}</p>
|
||||
<p>${t("table.best_lap")}: ${formatLap(topRow?.bestLapMs)}</p>
|
||||
</div>
|
||||
<div class="overlay-speaker-side">
|
||||
<section class="overlay-side-card">
|
||||
<h3>${t("overlay.last_passings")}</h3>
|
||||
${recent.length ? recent.map((passing) => `
|
||||
<div class="overlay-passing">
|
||||
<strong>${escapeHtml(passing.displayName || passing.teamName || passing.driverName || t("common.unknown_driver"))}</strong>
|
||||
<span>${formatLap(passing.lapMs)}</span>
|
||||
</div>
|
||||
`).join("") : `<p>${t("timing.no_passings")}</p>`}
|
||||
</section>
|
||||
<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.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>
|
||||
</div>
|
||||
</section>
|
||||
` : overlayViewMode === "results" ? `
|
||||
<section class="overlay-results">
|
||||
<div class="overlay-side-card">
|
||||
<h3>${t("events.practice_standings")}</h3>
|
||||
${renderRaceStandingsTableView(practiceRows, t("events.no_practice_results"))}
|
||||
</div>
|
||||
<div class="overlay-side-card">
|
||||
<h3>${t("events.qualifying_standings")}</h3>
|
||||
${renderRaceStandingsTableView(qualifyingRows, t("events.no_qualifying_results"))}
|
||||
</div>
|
||||
<div class="overlay-side-card">
|
||||
<h3>${t("events.final_standings")}</h3>
|
||||
${renderRaceStandingsTableView(finalRows, t("events.no_final_results"))}
|
||||
</div>
|
||||
</section>
|
||||
` : 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" ? `
|
||||
<section class="overlay-board overlay-board-tv">
|
||||
<div class="overlay-table-wrap overlay-display-wrap overlay-display-wrap-dense">
|
||||
<section class="overlay-fastest-banner overlay-fastest-banner-dense">
|
||||
<div class="overlay-fastest-banner-copy">
|
||||
<span>${t("overlay.fastest_lap")}</span>
|
||||
<strong>${formatLap(fastestRow?.bestLapMs)}</strong>
|
||||
</div>
|
||||
<div class="overlay-fastest-driver">${escapeHtml(fastestRow?.displayName || fastestRow?.driverName || "-")}</div>
|
||||
<div class="overlay-fastest-meta">${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${getVisiblePassings(result).length || 0}</div>
|
||||
</section>
|
||||
<div class="overlay-leaderboard-card overlay-leaderboard-card-tv overlay-leaderboard-card-dense">
|
||||
${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
` : `
|
||||
<section class="overlay-board">
|
||||
<div class="overlay-table-wrap overlay-display-wrap overlay-display-wrap-dense">
|
||||
<section class="overlay-fastest-banner overlay-fastest-banner-dense">
|
||||
<div class="overlay-fastest-banner-copy">
|
||||
<span>${t("overlay.fastest_lap")}</span>
|
||||
<strong>${formatLap(fastestRow?.bestLapMs)}</strong>
|
||||
</div>
|
||||
<div class="overlay-fastest-driver">${escapeHtml(fastestRow?.displayName || fastestRow?.driverName || "-")}</div>
|
||||
<div class="overlay-fastest-meta">${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${result?.passings.length || 0}</div>
|
||||
</section>
|
||||
<div class="overlay-leaderboard-card overlay-leaderboard-card-dense">
|
||||
${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })}
|
||||
</div>
|
||||
</div>
|
||||
<aside class="overlay-side">
|
||||
${activePanel ? renderOverlaySidePanel(activePanel, { t, escapeHtml }) : `<section class="overlay-side-card"><p>${t("timing.no_passings")}</p></section>`}
|
||||
</aside>
|
||||
</section>
|
||||
`}
|
||||
` : `
|
||||
<section class="overlay-empty">
|
||||
<h1>${t("overlay.title")}</h1>
|
||||
<p>${t("overlay.no_active")}</p>
|
||||
</section>
|
||||
`}
|
||||
</section>
|
||||
`;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user