From e3bfb66ded91e946c743a0925d5ac9877117ca32 Mon Sep 17 00:00:00 2001 From: dirwin31 <83434411+dirwin31@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:43:35 -0700 Subject: [PATCH] Update Layout --- .../components/recordings/dashcam_routes.css | 637 +++++++++++++++--- .../components/recordings/dashcam_routes.js | 192 ++++-- .../recordings/dashcam_routes_helpers.js | 48 ++ 3 files changed, 759 insertions(+), 118 deletions(-) diff --git a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css index 4286d1e6b8..3d3186b021 100644 --- a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css +++ b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css @@ -445,7 +445,15 @@ .dashcam-library-header { justify-content: space-between; + align-items: flex-start; gap: var(--gap-md); + flex-wrap: wrap; +} + +.dashcam-header-info { + display: flex; + flex-direction: column; + gap: 0.25rem; } .dashcam-library-header h1, @@ -471,89 +479,230 @@ text-transform: uppercase; } -.dashcam-refresh-button, -.dashcam-preserved-filter, -.dashcam-sort, -.dashcam-search { +/* Stats Chips in Header */ +.dashcam-stats-bar { + display: flex; + flex-wrap: wrap; + gap: var(--gap-sm); + margin-top: 0.4rem; +} + +.dashcam-stat-chip { + align-items: center; + background: var(--input-bg); + border: var(--border-width-thin) solid var(--sidebar-border-color); + border-radius: var(--border-radius-xl); + color: var(--text-muted); + display: inline-flex; + font-size: var(--font-size-xs); + gap: 0.35rem; + padding: 0.3rem 0.75rem; +} + +.dashcam-stat-chip strong { + color: var(--text-color); + font-weight: var(--font-weight-demi-bold); +} + +.dashcam-stat-chip i { + color: var(--main-fg); + font-size: 0.85rem; +} + +.dashcam-stat-chip.stat-chip-preserved i { + color: #ef6078; +} + +.dashcam-header-controls { + display: flex; + align-items: center; + gap: var(--gap-sm); +} + +.dashcam-refresh-button { background: var(--input-bg); border: var(--border-width-thin) solid var(--sidebar-border-color); border-radius: var(--border-radius-md); color: var(--text-color); -} - -.dashcam-refresh-button, -.dashcam-preserved-filter { cursor: pointer; - font-size: var(--font-size-base); + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-size: var(--font-size-sm); font-weight: var(--font-weight-demi-bold); - padding: 0.7rem 1rem; + padding: 0.6rem 0.95rem; + transition: background-color var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast); } -.dashcam-refresh-button:disabled, -.dashcam-player-actions button:disabled { +.dashcam-refresh-button:hover { + background: var(--sidebar-active-bg); + border-color: var(--main-fg); + transform: translateY(-1px); +} + +.dashcam-refresh-button:disabled { cursor: not-allowed; opacity: var(--disabled-opacity); } +/* Modern Toolbar */ .dashcam-toolbar { flex-wrap: wrap; gap: var(--gap-sm); margin-top: var(--margin-lg); } -.dashcam-search { +.dashcam-search-box { align-items: center; + background: var(--input-bg); + border: var(--border-width-thin) solid var(--sidebar-border-color); + border-radius: var(--border-radius-md); display: flex; - flex: 1 1 22rem; + flex: 1 1 18rem; gap: var(--gap-sm); - padding: 0 0.9rem; + padding: 0 0.8rem; + position: relative; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); } -.dashcam-search input, -.dashcam-sort select { +.dashcam-search-box:focus-within { + border-color: var(--main-fg); + box-shadow: 0 0 0 2px rgba(139, 108, 197, 0.2); +} + +.dashcam-search-icon { + color: var(--text-muted); + font-size: 0.95rem; +} + +.dashcam-search-box input { background: transparent; border: 0; color: var(--text-color); font: inherit; - outline: 0; -} - -.dashcam-search input { + font-size: var(--font-size-sm); min-width: 0; - padding: 0.75rem 0; + outline: 0; + padding: 0.65rem 0; width: 100%; } +.dashcam-search-clear { + background: transparent; + border: 0; + color: var(--text-muted); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0.2rem; + border-radius: 50%; + font-size: 1.1rem; + line-height: 1; + transition: color var(--transition-fast), background-color var(--transition-fast); +} + +.dashcam-search-clear:hover { + color: var(--text-color); + background: var(--sidebar-active-bg); +} + +.dashcam-filter-group { + display: inline-flex; + background: var(--input-bg); + border: var(--border-width-thin) solid var(--sidebar-border-color); + border-radius: var(--border-radius-md); + padding: 2px; + gap: 2px; +} + +.dashcam-filter-pill { + background: transparent; + border: 0; + border-radius: calc(var(--border-radius-md) - 2px); + color: var(--text-muted); + cursor: pointer; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-demi-bold); + padding: 0.5rem 0.85rem; + transition: background-color var(--transition-fast), color var(--transition-fast); +} + +.dashcam-filter-pill.active { + background: var(--sidebar-active-bg); + color: var(--text-color); + box-shadow: inset 0 0 0 1px var(--main-fg); +} + +.dashcam-filter-pill .bi-heart-fill { + color: #ef6078; + margin-right: 0.25rem; +} + .dashcam-sort { + align-items: center; + background: var(--input-bg); + border: var(--border-width-thin) solid var(--sidebar-border-color); + border-radius: var(--border-radius-md); + display: flex; gap: var(--gap-sm); - padding: 0.65rem 0.85rem; + padding: 0.5rem 0.85rem; } .dashcam-sort span { color: var(--text-muted); - font-size: var(--font-size-sm); + font-size: var(--font-size-xs); + text-transform: uppercase; + letter-spacing: 0.05em; } .dashcam-sort select { + background: transparent; + border: 0; + color: var(--text-color); cursor: pointer; + font: inherit; + font-size: var(--font-size-sm); + outline: 0; } -.dashcam-preserved-filter[aria-pressed="true"] { - background: rgba(230, 78, 102, 0.16); - border-color: rgba(230, 78, 102, 0.65); +.dashcam-view-toggle { + display: inline-flex; + background: var(--input-bg); + border: var(--border-width-thin) solid var(--sidebar-border-color); + border-radius: var(--border-radius-md); + padding: 2px; + gap: 2px; } -.dashcam-preserved-filter .bi-heart-fill { - color: #ef6078; - margin-right: 0.35rem; +.dashcam-view-btn { + background: transparent; + border: 0; + border-radius: calc(var(--border-radius-md) - 2px); + color: var(--text-muted); + cursor: pointer; + font-size: 1.1rem; + padding: 0.4rem 0.65rem; + display: flex; + align-items: center; + justify-content: center; + transition: background-color var(--transition-fast), color var(--transition-fast); +} + +.dashcam-view-btn.active { + background: var(--sidebar-active-bg); + color: var(--main-fg); + box-shadow: inset 0 0 0 1px var(--main-fg); } .dashcam-results-summary { color: var(--text-muted); - font-size: var(--font-size-sm); + font-size: var(--font-size-xs); justify-content: space-between; margin-top: var(--margin-base); min-height: 1.5rem; + text-transform: uppercase; + letter-spacing: 0.04em; } .dashcam-date-groups, @@ -565,51 +714,332 @@ margin-top: var(--margin-lg); } -.dashcam-date-group > h2 { +.dashcam-date-group-header { + display: flex; + align-items: center; + justify-content: space-between; border-bottom: var(--border-width-thin) solid var(--sidebar-border-color); - color: var(--text-color); - font-size: var(--font-size-lg); - margin: 0; - padding-bottom: var(--padding-sm); + padding-bottom: var(--padding-xs); + margin-bottom: var(--margin-sm); } +.dashcam-date-group-header h2 { + color: var(--text-color); + font-size: var(--font-size-base); + font-weight: var(--font-weight-bold); + margin: 0; +} + +.dashcam-date-group-count { + color: var(--text-muted); + font-size: var(--font-size-xs); +} + +/* ============================================================ + Modernized List View (Data & Telemetry Focused) + ============================================================ */ +.dashcam-routes-list { + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.dashcam-route-row { + align-items: center; + background: var(--card-bg); + border: var(--border-width-thin) solid var(--sidebar-border-color); + border-radius: var(--border-radius-md); + box-shadow: var(--shadow-xs); + cursor: pointer; + display: flex; + gap: var(--gap-md); + padding: 0.75rem 1rem; + position: relative; + transition: border-color var(--transition-fast), background-color var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast); +} + +.dashcam-route-row:hover { + background: rgba(18, 18, 36, 0.95); + border-color: rgba(139, 108, 197, 0.4); + box-shadow: var(--shadow-sm); + transform: translateY(-1px); +} + +.dashcam-route-row.is-preserved { + border-left: 3px solid #ef6078; +} + +/* Compact Mini Preview (Non-dominant) */ +.dashcam-mini-preview { + aspect-ratio: 16 / 9; + background: var(--input-bg); + border: var(--border-width-thin) solid var(--sidebar-border-color); + border-radius: var(--border-radius-sm); + flex-shrink: 0; + height: 52px; + overflow: hidden; + position: relative; + width: 92px; +} + +.dashcam-mini-fallback { + align-items: center; + color: var(--text-muted); + display: flex; + inset: 0; + justify-content: center; + position: absolute; + font-size: 1.25rem; +} + +.dashcam-mini-preview:not(.thumbnail-failed) .dashcam-mini-fallback { + visibility: hidden; +} + +.dashcam-mini-img { + background: var(--sidebar-bg); + height: 100%; + object-fit: cover; + position: absolute; + width: 100%; + z-index: 1; +} + +.dashcam-mini-play-overlay { + align-items: center; + background: rgba(10, 10, 22, 0.6); + color: white; + display: flex; + font-size: 1.2rem; + inset: 0; + justify-content: center; + opacity: 0; + position: absolute; + transition: opacity var(--transition-fast); + z-index: 2; +} + +.dashcam-route-row:hover .dashcam-mini-play-overlay { + opacity: 1; +} + +/* Route Info & Telemetry */ +.dashcam-route-info { + display: flex; + flex: 1; + flex-direction: column; + gap: 0.25rem; + min-width: 0; +} + +.dashcam-route-title-row { + align-items: center; + display: flex; + gap: 0.5rem; + min-width: 0; +} + +.dashcam-route-title { + color: var(--text-color); + font-size: var(--font-size-base); + font-weight: var(--font-weight-demi-bold); + line-height: 1.3; + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dashcam-custom-badge { + align-items: center; + background: rgba(139, 108, 197, 0.15); + border: 1px solid rgba(139, 108, 197, 0.35); + border-radius: var(--border-radius-sm); + color: var(--main-fg); + display: inline-flex; + font-size: 0.7rem; + font-weight: var(--font-weight-bold); + gap: 0.25rem; + padding: 0.1rem 0.4rem; + text-transform: uppercase; + flex-shrink: 0; +} + +.dashcam-route-subdate { + color: var(--text-muted); + font-size: var(--font-size-xs); + margin: 0; + display: flex; + align-items: center; + gap: 0.35rem; +} + +.dashcam-route-meta-pills { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin-top: 0.15rem; +} + +.meta-pill { + align-items: center; + background: var(--input-bg); + border: var(--border-width-thin) solid var(--sidebar-border-color); + border-radius: var(--border-radius-sm); + color: var(--text-muted); + display: inline-flex; + font-size: var(--font-size-xs); + gap: 0.3rem; + padding: 0.15rem 0.5rem; + white-space: nowrap; +} + +.meta-pill.duration-pill { + color: var(--text-color); +} + +.meta-pill.duration-pill i { + color: var(--main-fg); +} + +.meta-pill.segments-pill i { + color: var(--text-muted); +} + +.meta-pill.preserved-pill { + background: rgba(239, 96, 120, 0.12); + border-color: rgba(239, 96, 120, 0.35); + color: #ef6078; +} + +.meta-pill.id-pill { + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.02em; + opacity: 0.85; +} + +/* Route Action Buttons */ +.dashcam-route-actions { + align-items: center; + display: flex; + flex-shrink: 0; + gap: 0.4rem; +} + +.btn-route-action { + align-items: center; + background: var(--input-bg); + border: var(--border-width-thin) solid var(--sidebar-border-color); + border-radius: var(--border-radius-md); + color: var(--text-color); + cursor: pointer; + display: inline-flex; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-demi-bold); + gap: 0.35rem; + justify-content: center; + padding: 0.45rem 0.75rem; + transition: background-color var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast); +} + +.btn-route-action:hover { + background: var(--sidebar-active-bg); + border-color: var(--main-fg); + transform: translateY(-1px); +} + +.btn-route-action.btn-play { + background: var(--main-fg); + border-color: var(--main-fg); + color: var(--text-on-primary); +} + +.btn-route-action.btn-play:hover { + filter: brightness(1.12); +} + +.btn-route-action.btn-preserve { + padding: 0.45rem 0.6rem; + color: var(--text-muted); +} + +.btn-route-action.btn-preserve.active { + background: rgba(239, 96, 120, 0.15); + border-color: rgba(239, 96, 120, 0.45); + color: #ef6078; +} + +.btn-route-action.btn-preserve:hover { + color: #ef6078; +} + +.btn-route-action.btn-icon { + color: var(--text-muted); + padding: 0.45rem 0.6rem; +} + +.btn-route-action.btn-icon:hover { + color: var(--text-color); +} + +.btn-route-action.btn-danger-action:hover { + background: rgba(224, 85, 119, 0.15); + border-color: var(--danger-bg); + color: var(--danger-bg); +} + +/* ============================================================ + Compact Grid View Mode + ============================================================ */ .dashcam-library .screen-recordings-grid.dashcam-routes-grid { - gap: clamp(0.8rem, 1.5vw, 1.25rem); - grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); - margin-top: var(--margin-base); + gap: clamp(0.8rem, 1.5vw, 1.1rem); + grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); + margin-top: var(--margin-sm); } .dashcam-library .recording-card.dashcam-route-card { + background: var(--card-bg); border: var(--border-width-thin) solid var(--sidebar-border-color); - border-radius: var(--border-radius-lg); - box-shadow: var(--shadow-sm); + border-radius: var(--border-radius-md); + box-shadow: var(--shadow-xs); + display: flex; + flex-direction: column; overflow: hidden; position: relative; text-align: left; + transition: border-color var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast); } .dashcam-library .recording-card.dashcam-route-card:hover { - box-shadow: var(--shadow-md); + border-color: rgba(139, 108, 197, 0.45); + box-shadow: var(--shadow-sm); transform: translateY(-2px); } -.dashcam-route-card .preserved-icon { +.dashcam-card-top-bar { align-items: center; - background: rgba(10, 12, 18, 0.7); - border: 0; - border-radius: 50%; - color: white; + background: var(--secondary-bg); + border-bottom: var(--border-width-thin) solid var(--sidebar-border-color); display: flex; - height: 2.6rem; - justify-content: center; - right: 0.7rem; - top: 0.7rem; - width: 2.6rem; + justify-content: space-between; + padding: 0.35rem 0.6rem; +} + +.dashcam-card-top-bar .btn-preserve { + background: transparent; + border: 0; + padding: 0.15rem 0.3rem; } .dashcam-route-card .dashcam-preview { + aspect-ratio: 16 / 9; background: linear-gradient(135deg, var(--sidebar-bg), var(--input-bg)); + max-height: 120px; overflow: hidden; + position: relative; + width: 100%; } .dashcam-preview-fallback { @@ -624,7 +1054,7 @@ } .dashcam-preview-fallback i { - font-size: 2rem; + font-size: 1.6rem; } .dashcam-preview:not(.thumbnail-failed) .dashcam-preview-fallback { @@ -633,16 +1063,42 @@ .dashcam-preview img { background: var(--sidebar-bg); + height: 100%; + object-fit: cover; + width: 100%; z-index: 1; } +.dashcam-grid-play-overlay { + align-items: center; + background: rgba(10, 10, 22, 0.55); + color: white; + display: flex; + font-size: 1.8rem; + inset: 0; + justify-content: center; + opacity: 0; + position: absolute; + transition: opacity var(--transition-fast); + z-index: 2; +} + +.dashcam-route-card:hover .dashcam-grid-play-overlay { + opacity: 1; +} + .dashcam-card-body { - padding: var(--padding-base); + display: flex; + flex: 1; + flex-direction: column; + gap: 0.4rem; + padding: 0.75rem; } .dashcam-card-body h3 { color: var(--text-color); - font-size: var(--font-size-lg); + font-size: var(--font-size-base); + font-weight: var(--font-weight-demi-bold); line-height: 1.3; margin: 0; overflow: hidden; @@ -650,34 +1106,26 @@ white-space: nowrap; } -.dashcam-card-date, -.dashcam-card-details, -.dashcam-preserve-status { +.dashcam-card-date { color: var(--text-muted); - font-size: var(--font-size-sm); - margin: var(--margin-xs) 0 0; -} - -.dashcam-card-details { - display: flex; - gap: var(--gap-sm); -} - -.dashcam-card-details span + span::before { - content: "·"; - margin-right: var(--gap-sm); -} - -.dashcam-preserve-status { font-size: var(--font-size-xs); - font-weight: var(--font-weight-demi-bold); - text-transform: uppercase; + margin: 0; } -.dashcam-preserve-status.preserved { - color: #ef6078; +.dashcam-card-actions { + align-items: center; + border-top: var(--border-width-thin) solid var(--sidebar-border-color); + display: flex; + gap: 0.3rem; + margin-top: auto; + padding-top: 0.5rem; } +.dashcam-card-actions .btn-play { + flex: 1; +} + +/* Empty & Loading States */ .dashcam-loading, .dashcam-empty-state { align-items: center; @@ -840,6 +1288,7 @@ margin-left: auto; } +/* Responsive Adaptations */ @media only screen and (max-width: 768px) { .screen-recordings-wrapper.dashcam-routes-wrapper { padding: 0 var(--padding-sm) var(--padding-lg); @@ -856,9 +1305,19 @@ flex-direction: column; } - .dashcam-refresh-button, - .dashcam-preserved-filter, - .dashcam-sort { + .dashcam-toolbar { + flex-direction: column; + align-items: stretch; + } + + .dashcam-search-box { + flex: 1 1 auto; + width: 100%; + } + + .dashcam-filter-group, + .dashcam-sort, + .dashcam-view-toggle { justify-content: center; } @@ -867,6 +1326,24 @@ flex: 1; } + .dashcam-route-row { + flex-direction: column; + align-items: stretch; + gap: var(--gap-sm); + } + + .dashcam-mini-preview { + width: 100%; + height: auto; + max-height: 140px; + } + + .dashcam-route-actions { + border-top: var(--border-width-thin) solid var(--sidebar-border-color); + padding-top: var(--padding-xs); + justify-content: space-between; + } + .dashcam-library .screen-recordings-grid.dashcam-routes-grid { grid-template-columns: 1fr; } diff --git a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js index d60554c6ac..652ef44599 100644 --- a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js +++ b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js @@ -4,6 +4,7 @@ import { Modal } from "/assets/components/modal.js" import { buildRouteView, cameraVideoUrl, + computeRouteStats, formatApproxDuration, getSegmentStatus, groupRoutesByDate, @@ -19,6 +20,7 @@ const state = reactive({ searchQuery: "", sortOrder: "newest", showPreservedOnly: false, + viewMode: "list", progress: 0, total: 0, showDeleteAllModal: false, @@ -29,6 +31,7 @@ let routesAbortController = null let routesRequestToken = 0 let seenRouteNames = new Set() let overlay = null +const routeLogsCache = new Map() function routeLabel(route) { return route.displayName || route.displayDate || route.name @@ -218,7 +221,7 @@ function openLogsDialog(route, logsButton, getCachedLogs, setCachedLogs) { const closeLogsDialog = () => { document.removeEventListener("keydown", handleKeydown) closeDialog(logsDialog) - logsButton.focus() + logsButton?.focus() } const handleKeydown = event => { if (event.key === "Escape") closeLogsDialog() } closeButton.onclick = closeLogsDialog @@ -240,7 +243,7 @@ function openLogsDialog(route, logsButton, getCachedLogs, setCachedLogs) { ` } - const cachedLogs = getCachedLogs() + const cachedLogs = getCachedLogs?.() if (cachedLogs) { renderLogs(cachedLogs) return @@ -253,7 +256,7 @@ function openLogsDialog(route, logsButton, getCachedLogs, setCachedLogs) { if (content.isConnected) content.innerHTML = `

${escapeHtml(data.error || "Could not read logs.")}

` return } - setCachedLogs(data) + setCachedLogs?.(data) if (content.isConnected) renderLogs(data) }) .catch(error => { @@ -261,6 +264,16 @@ function openLogsDialog(route, logsButton, getCachedLogs, setCachedLogs) { }) } +function openLogsFromRow(route, event) { + const button = event.currentTarget + openLogsDialog( + route, + button, + () => routeLogsCache.get(route.name) || null, + value => { routeLogsCache.set(route.name, value) }, + ) +} + async function openOverlay(route) { if (overlay) return overlay = document.createElement("div") @@ -369,7 +382,6 @@ async function openOverlay(route) { setPlayerMessage("Switching camera…") video.src = cameraVideoUrl(segments[current], selectedCamera) video.load() - // Switching cameras deliberately leaves current and the status strip unchanged. }) } @@ -406,7 +418,7 @@ function closeOverlay() { } async function togglePreserved(route, event) { - event.stopPropagation() + event?.stopPropagation?.() const isPreserved = !route.is_preserved try { const response = await fetch(`/api/routes/${route.name}/preserve`, { method: isPreserved ? "POST" : "DELETE" }) @@ -456,35 +468,71 @@ export function RouteRecordings() { return html`
-
-

Local recordings

Dashcam Routes

- -
+ ${() => { + const stats = computeRouteStats(state.routes) + return html` +
+
+

Local recordings

+

Dashcam Routes

+
+ ${stats.count} ${stats.count === 1 ? "drive" : "drives"} + ${stats.formattedDuration} total + ${stats.preservedCount > 0 ? html` ${stats.preservedCount} preserved` : ""} +
+
+
+ +
+
` + }}
- + +
+ + +
- +
+ + +
${() => { const view = buildRouteView(state.routes, { preservedOnly: state.showPreservedOnly, searchQuery: state.searchQuery, sortOrder: state.sortOrder }) const groups = groupRoutesByDate(view.visible) + const isGrid = state.viewMode === "grid" + return html`
- ${view.matching.length} matching route${view.matching.length === 1 ? "" : "s"} - ${state.loading ? html`Loading ${state.progress} of ${state.total}` : html`${state.routes.length} total`} + ${view.matching.length} matching drive${view.matching.length === 1 ? "" : "s"} + ${state.loading ? html`Loading ${state.progress} of ${state.total}` : html`${state.routes.length} total local`}
${state.error ? html`

${state.error}

` : ""} ${state.isDeletingAll ? html`

Deleting routes…

` : ""} @@ -493,25 +541,93 @@ export function RouteRecordings() {
${groups.map(group => html`
-

${group.label}

-
- ${group.routes.map(route => html` -
- -
- Preview unavailable - -
-
-

${route.displayName}

- ${route.isCustomName ? html`

${route.displayDate}

` : ""} -

${formatApproxDuration(route.approxDurationSeconds)}${route.segmentCount} segment${route.segmentCount === 1 ? "" : "s"}

-

${route.is_preserved ? "Preserved" : "Not preserved"}

-
-
`)} +
+

${group.label}

+ ${group.routes.length} ${group.routes.length === 1 ? "drive" : "drives"}
+ ${isGrid ? html` +
+ ${group.routes.map(route => html` +
+
+ + ${route.name.split("--").slice(1).join("--") || route.name} +
+
+ Preview unavailable + + +
+
+
+

${route.displayName}

+ ${route.isCustomName ? html`` : ""} +
+ ${route.isCustomName ? html`

${route.displayDate}

` : ""} +
+ ${formatApproxDuration(route.approxDurationSeconds)} + ${route.segmentCount} seg +
+
+ + + + +
+
+
`)} +
+ ` : html` +
+ ${group.routes.map(route => html` +
+
+ + + +
+
+
+

${route.displayName}

+ ${route.isCustomName ? html` Custom` : ""} +
+ ${route.isCustomName ? html`

${route.displayDate}

` : ""} +
+ ${formatApproxDuration(route.approxDurationSeconds)} + ${route.segmentCount} segment${route.segmentCount === 1 ? "" : "s"} + ${route.is_preserved ? html` Preserved` : ""} + ${route.name.split("--").slice(1).join("--") || route.name} +
+
+
+ + + + + +
+
`)} +
`}
`)}
${view.truncated ? html`

Showing the first ${MAX_RENDERED_ROUTES} of ${view.matching.length} matching routes.

` : ""}` diff --git a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes_helpers.js b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes_helpers.js index 41372c48a5..eb251b11e6 100644 --- a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes_helpers.js +++ b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes_helpers.js @@ -34,7 +34,55 @@ export function normalizeRoute(route, locale) { } } +export function formatTotalDuration(seconds) { + const totalMinutes = Math.max(0, Math.round(Number(seconds) / 60) || 0) + if (totalMinutes < 1) return "0 min" + if (totalMinutes < 60) return `${totalMinutes} min` + const hours = Math.floor(totalMinutes / 60) + const remaining = totalMinutes % 60 + return remaining > 0 ? `${hours}h ${remaining}m` : `${hours}h` +} + +export function computeRouteStats(routes = []) { + const list = Array.isArray(routes) ? routes : [] + let totalDurationSeconds = 0 + let preservedCount = 0 + let totalSegments = 0 + + for (const route of list) { + if (route) { + if (Number.isFinite(route.approxDurationSeconds)) { + totalDurationSeconds += Math.max(0, route.approxDurationSeconds) + } + if (route.is_preserved) { + preservedCount += 1 + } + if (Number.isFinite(route.segmentCount)) { + totalSegments += Math.max(0, route.segmentCount) + } + } + } + + return { + count: list.length, + totalDurationSeconds, + formattedDuration: formatTotalDuration(totalDurationSeconds), + preservedCount, + totalSegments, + } +} + export function sortRoutes(routes, sortOrder = "newest") { + if (sortOrder === "longest" || sortOrder === "shortest") { + const direction = sortOrder === "longest" ? -1 : 1 + return [...routes].sort((left, right) => { + const leftDur = Number.isFinite(left?.approxDurationSeconds) ? left.approxDurationSeconds : -1 + const rightDur = Number.isFinite(right?.approxDurationSeconds) ? right.approxDurationSeconds : -1 + if (leftDur !== rightDur) return (leftDur - rightDur) * direction + return String(left?.name || "").localeCompare(String(right?.name || "")) + }) + } + const direction = sortOrder === "oldest" ? 1 : -1 return [...routes].sort((left, right) => { const leftTime = left?._startedAtMs