mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-11 02:33:51 +08:00
Update Layout
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
</ul>`
|
||||
}
|
||||
|
||||
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 = `<p class="route-logs-message route-logs-error">${escapeHtml(data.error || "Could not read logs.")}</p>`
|
||||
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`
|
||||
<div class="screen-recordings-wrapper dashcam-routes-wrapper">
|
||||
<section class="screen-recordings-widget dashcam-library">
|
||||
<header class="dashcam-library-header">
|
||||
<div><p class="dashcam-library-eyebrow">Local recordings</p><h1>Dashcam Routes</h1></div>
|
||||
<button class="dashcam-refresh-button" type="button" @click="${refresh}" disabled="${() => state.loading || false}"><i class="bi bi-arrow-clockwise"></i> Refresh</button>
|
||||
</header>
|
||||
${() => {
|
||||
const stats = computeRouteStats(state.routes)
|
||||
return html`
|
||||
<header class="dashcam-library-header">
|
||||
<div class="dashcam-header-info">
|
||||
<p class="dashcam-library-eyebrow">Local recordings</p>
|
||||
<h1>Dashcam Routes</h1>
|
||||
<div class="dashcam-stats-bar">
|
||||
<span class="dashcam-stat-chip"><i class="bi bi-car-front-fill"></i> <strong>${stats.count}</strong> ${stats.count === 1 ? "drive" : "drives"}</span>
|
||||
<span class="dashcam-stat-chip"><i class="bi bi-stopwatch-fill"></i> <strong>${stats.formattedDuration}</strong> total</span>
|
||||
${stats.preservedCount > 0 ? html`<span class="dashcam-stat-chip stat-chip-preserved"><i class="bi bi-heart-fill"></i> <strong>${stats.preservedCount}</strong> preserved</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashcam-header-controls">
|
||||
<button class="dashcam-refresh-button" type="button" @click="${refresh}" disabled="${() => state.loading || false}" title="Refresh routes">
|
||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</header>`
|
||||
}}
|
||||
|
||||
<div class="dashcam-toolbar">
|
||||
<label class="dashcam-search">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="search" placeholder="Search names, dates, or route IDs" aria-label="Search routes" value="${() => state.searchQuery}" @input="${event => { state.searchQuery = event.target.value }}">
|
||||
</label>
|
||||
<div class="dashcam-search-box">
|
||||
<i class="bi bi-search dashcam-search-icon"></i>
|
||||
<input type="search" placeholder="Search route names, dates, or hash IDs..." aria-label="Search routes" value="${() => state.searchQuery}" @input="${event => { state.searchQuery = event.target.value }}">
|
||||
${() => state.searchQuery ? html`
|
||||
<button class="dashcam-search-clear" type="button" title="Clear search" @click="${() => { state.searchQuery = "" }}"><i class="bi bi-x"></i></button>
|
||||
` : ""}
|
||||
</div>
|
||||
<div class="dashcam-filter-group">
|
||||
<button class="dashcam-filter-pill ${() => !state.showPreservedOnly ? "active" : ""}" type="button" @click="${() => { state.showPreservedOnly = false }}">
|
||||
All Drives
|
||||
</button>
|
||||
<button class="dashcam-filter-pill ${() => state.showPreservedOnly ? "active" : ""}" type="button" @click="${() => { state.showPreservedOnly = true }}">
|
||||
<i class="bi bi-heart-fill"></i> Preserved
|
||||
</button>
|
||||
</div>
|
||||
<label class="dashcam-sort">
|
||||
<span>Sort</span>
|
||||
<select @change="${event => { state.sortOrder = event.target.value }}">
|
||||
<option value="newest" selected="${() => state.sortOrder === "newest" || false}">Newest</option>
|
||||
<option value="oldest" selected="${() => state.sortOrder === "oldest" || false}">Oldest</option>
|
||||
<option value="newest" selected="${() => state.sortOrder === "newest" || false}">Newest first</option>
|
||||
<option value="oldest" selected="${() => state.sortOrder === "oldest" || false}">Oldest first</option>
|
||||
<option value="longest" selected="${() => state.sortOrder === "longest" || false}">Longest duration</option>
|
||||
<option value="shortest" selected="${() => state.sortOrder === "shortest" || false}">Shortest duration</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="dashcam-preserved-filter" type="button" aria-pressed="${() => String(state.showPreservedOnly)}" @click="${() => { state.showPreservedOnly = !state.showPreservedOnly }}">
|
||||
<i class="bi bi-heart-fill"></i> ${() => state.showPreservedOnly ? "Preserved only" : "All routes"}
|
||||
</button>
|
||||
<div class="dashcam-view-toggle" aria-label="Layout view mode">
|
||||
<button class="dashcam-view-btn ${() => state.viewMode === "list" ? "active" : ""}" type="button" title="List View" @click="${() => { state.viewMode = "list" }}">
|
||||
<i class="bi bi-list-ul"></i>
|
||||
</button>
|
||||
<button class="dashcam-view-btn ${() => state.viewMode === "grid" ? "active" : ""}" type="button" title="Grid View" @click="${() => { state.viewMode = "grid" }}">
|
||||
<i class="bi bi-grid-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${() => {
|
||||
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`
|
||||
<div class="dashcam-results-summary" aria-live="polite">
|
||||
<span>${view.matching.length} matching route${view.matching.length === 1 ? "" : "s"}</span>
|
||||
${state.loading ? html`<span>Loading ${state.progress} of ${state.total}</span>` : html`<span>${state.routes.length} total</span>`}
|
||||
<span>${view.matching.length} matching drive${view.matching.length === 1 ? "" : "s"}</span>
|
||||
${state.loading ? html`<span>Loading ${state.progress} of ${state.total}</span>` : html`<span>${state.routes.length} total local</span>`}
|
||||
</div>
|
||||
${state.error ? html`<p class="screen-recordings-message dashcam-error">${state.error}</p>` : ""}
|
||||
${state.isDeletingAll ? html`<p class="screen-recordings-message">Deleting routes…</p>` : ""}
|
||||
@@ -493,25 +541,93 @@ export function RouteRecordings() {
|
||||
<div class="dashcam-date-groups">
|
||||
${groups.map(group => html`
|
||||
<section class="dashcam-date-group">
|
||||
<h2>${group.label}</h2>
|
||||
<div class="screen-recordings-grid dashcam-routes-grid">
|
||||
${group.routes.map(route => html`
|
||||
<article class="recording-card dashcam-route-card" @click="${() => { state.selectedRoute = route }}">
|
||||
<button class="preserved-icon" type="button" aria-label="${route.is_preserved ? "Remove preservation" : "Preserve route"}" title="${route.is_preserved ? "Preserved" : "Not preserved"}" @click="${event => togglePreserved(route, event)}">
|
||||
${() => html`<i class="bi ${route.is_preserved ? "bi-heart-fill" : "bi-heart"}"></i>`}
|
||||
</button>
|
||||
<div class="recording-preview-container dashcam-preview">
|
||||
<span class="dashcam-preview-fallback"><i class="bi bi-camera-video"></i><small>Preview unavailable</small></span>
|
||||
<img src="${route.png}" class="recording-preview" loading="lazy" alt="" @error="${thumbnailFailed}">
|
||||
</div>
|
||||
<div class="dashcam-card-body">
|
||||
<h3>${route.displayName}</h3>
|
||||
${route.isCustomName ? html`<p class="dashcam-card-date">${route.displayDate}</p>` : ""}
|
||||
<p class="dashcam-card-details"><span>${formatApproxDuration(route.approxDurationSeconds)}</span><span>${route.segmentCount} segment${route.segmentCount === 1 ? "" : "s"}</span></p>
|
||||
<p class="dashcam-preserve-status ${route.is_preserved ? "preserved" : ""}"><i class="bi ${route.is_preserved ? "bi-heart-fill" : "bi-heart"}"></i> ${route.is_preserved ? "Preserved" : "Not preserved"}</p>
|
||||
</div>
|
||||
</article>`)}
|
||||
<div class="dashcam-date-group-header">
|
||||
<h2>${group.label}</h2>
|
||||
<span class="dashcam-date-group-count">${group.routes.length} ${group.routes.length === 1 ? "drive" : "drives"}</span>
|
||||
</div>
|
||||
${isGrid ? html`
|
||||
<div class="screen-recordings-grid dashcam-routes-grid">
|
||||
${group.routes.map(route => html`
|
||||
<article class="recording-card dashcam-route-card ${route.is_preserved ? "is-preserved" : ""}" @click="${() => { state.selectedRoute = route }}">
|
||||
<div class="dashcam-card-top-bar">
|
||||
<button class="btn-route-action btn-preserve ${route.is_preserved ? "active" : ""}" type="button" aria-label="${route.is_preserved ? "Remove preservation" : "Preserve route"}" title="${route.is_preserved ? "Preserved (click to unpreserve)" : "Click to preserve"}" @click="${event => togglePreserved(route, event)}">
|
||||
<i class="bi ${route.is_preserved ? "bi-heart-fill" : "bi-heart"}"></i>
|
||||
</button>
|
||||
<span class="meta-pill id-pill" title="Route ID: ${route.name}"><i class="bi bi-hash"></i>${route.name.split("--").slice(1).join("--") || route.name}</span>
|
||||
</div>
|
||||
<div class="recording-preview-container dashcam-preview">
|
||||
<span class="dashcam-preview-fallback"><i class="bi bi-camera-video"></i><small>Preview unavailable</small></span>
|
||||
<img src="${route.png}" class="recording-preview" loading="lazy" alt="" @error="${thumbnailFailed}">
|
||||
<span class="dashcam-grid-play-overlay"><i class="bi bi-play-fill"></i></span>
|
||||
</div>
|
||||
<div class="dashcam-card-body">
|
||||
<div class="dashcam-route-title-row">
|
||||
<h3 title="${route.displayName}">${route.displayName}</h3>
|
||||
${route.isCustomName ? html`<span class="dashcam-custom-badge" title="Custom name"><i class="bi bi-tag-fill"></i></span>` : ""}
|
||||
</div>
|
||||
${route.isCustomName ? html`<p class="dashcam-card-date">${route.displayDate}</p>` : ""}
|
||||
<div class="dashcam-route-meta-pills">
|
||||
<span class="meta-pill duration-pill"><i class="bi bi-clock"></i> ${formatApproxDuration(route.approxDurationSeconds)}</span>
|
||||
<span class="meta-pill segments-pill"><i class="bi bi-collection-play"></i> ${route.segmentCount} seg</span>
|
||||
</div>
|
||||
<div class="dashcam-card-actions" @click="${event => event.stopPropagation()}">
|
||||
<button class="btn-route-action btn-play" type="button" title="Play route" @click="${() => { state.selectedRoute = route }}">
|
||||
<i class="bi bi-play-fill"></i> Play
|
||||
</button>
|
||||
<button class="btn-route-action btn-icon" type="button" title="View logs" @click="${event => openLogsFromRow(route, event)}">
|
||||
<i class="bi bi-file-earmark-arrow-down"></i>
|
||||
</button>
|
||||
<button class="btn-route-action btn-icon" type="button" title="Rename" @click="${() => renameRoute(route)}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn-route-action btn-icon btn-danger-action" type="button" title="Delete" @click="${() => deleteRoute(route)}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>`)}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="dashcam-routes-list">
|
||||
${group.routes.map(route => html`
|
||||
<article class="dashcam-route-row ${route.is_preserved ? "is-preserved" : ""}" @click="${() => { state.selectedRoute = route }}">
|
||||
<div class="dashcam-mini-preview">
|
||||
<span class="dashcam-mini-fallback"><i class="bi bi-camera-video"></i></span>
|
||||
<img src="${route.png}" class="dashcam-mini-img" loading="lazy" alt="" @error="${thumbnailFailed}">
|
||||
<span class="dashcam-mini-play-overlay"><i class="bi bi-play-fill"></i></span>
|
||||
</div>
|
||||
<div class="dashcam-route-info">
|
||||
<div class="dashcam-route-title-row">
|
||||
<h3 class="dashcam-route-title" title="${route.displayName}">${route.displayName}</h3>
|
||||
${route.isCustomName ? html`<span class="dashcam-custom-badge" title="Custom name"><i class="bi bi-tag-fill"></i> Custom</span>` : ""}
|
||||
</div>
|
||||
${route.isCustomName ? html`<p class="dashcam-route-subdate"><i class="bi bi-calendar3"></i> ${route.displayDate}</p>` : ""}
|
||||
<div class="dashcam-route-meta-pills">
|
||||
<span class="meta-pill duration-pill" title="Estimated duration"><i class="bi bi-clock"></i> ${formatApproxDuration(route.approxDurationSeconds)}</span>
|
||||
<span class="meta-pill segments-pill" title="Segments recorded"><i class="bi bi-collection-play"></i> ${route.segmentCount} segment${route.segmentCount === 1 ? "" : "s"}</span>
|
||||
${route.is_preserved ? html`<span class="meta-pill preserved-pill" title="Preserved from deletion"><i class="bi bi-heart-fill"></i> Preserved</span>` : ""}
|
||||
<span class="meta-pill id-pill" title="Route ID: ${route.name}"><i class="bi bi-hash"></i>${route.name.split("--").slice(1).join("--") || route.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashcam-route-actions" @click="${event => event.stopPropagation()}">
|
||||
<button class="btn-route-action btn-play" type="button" title="Play recording" @click="${() => { state.selectedRoute = route }}">
|
||||
<i class="bi bi-play-fill"></i> <span>Play</span>
|
||||
</button>
|
||||
<button class="btn-route-action btn-preserve ${route.is_preserved ? "active" : ""}" type="button" aria-label="${route.is_preserved ? "Remove preservation" : "Preserve route"}" title="${route.is_preserved ? "Preserved (click to unpreserve)" : "Click to preserve"}" @click="${event => togglePreserved(route, event)}">
|
||||
<i class="bi ${route.is_preserved ? "bi-heart-fill" : "bi-heart"}"></i>
|
||||
</button>
|
||||
<button class="btn-route-action btn-icon" type="button" title="View & download logs" @click="${event => openLogsFromRow(route, event)}">
|
||||
<i class="bi bi-file-earmark-arrow-down"></i>
|
||||
</button>
|
||||
<button class="btn-route-action btn-icon" type="button" title="Rename route" @click="${() => renameRoute(route)}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn-route-action btn-icon btn-danger-action" type="button" title="Delete route" @click="${() => deleteRoute(route)}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</article>`)}
|
||||
</div>`}
|
||||
</section>`)}
|
||||
</div>
|
||||
${view.truncated ? html`<p class="screen-recordings-message">Showing the first ${MAX_RENDERED_ROUTES} of ${view.matching.length} matching routes.</p>` : ""}`
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user