From ba760a04d60f049f636781faef00c7b85b42d50f Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Mon, 10 Aug 2026 00:46:05 -0400 Subject: [PATCH] fix(dashcam): proper mobile row reflow + import status at top with ETA - yaris.css: use .dash-row class on all rows; only td:first-child and .dash-actions-td visible on mobile (<576px); buttons never overflow - dash.js: add dash-row class + dash-actions-td wrapper div to dashcam file rows; import status moved to card footer at top with spinner + ETA - local-videos.js: add dash-row class to video rows - dash.html: import status bar (spinner + ETA + result) in device card at top; removed old footer import status from files card Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- static/js/dash.js | 95 ++++++++++++++++++++++++++++----------- static/js/local-videos.js | 1 + static/yaris.css | 42 +++++++---------- templates/dash.html | 12 +++-- 4 files changed, 94 insertions(+), 56 deletions(-) diff --git a/static/js/dash.js b/static/js/dash.js index 9ca6eb6..3493d8d 100644 --- a/static/js/dash.js +++ b/static/js/dash.js @@ -4,7 +4,11 @@ // elements (#map, #speedGraph) that don't exist on this page. const deviceStatus = document.getElementById('dash-device-status'); +const importStatusBar = document.getElementById('dash-import-status-bar'); const importStatus = document.getElementById('dash-import-status'); +const importEta = document.getElementById('dash-import-eta'); +const importResultBar = document.getElementById('dash-import-result-bar'); +const importResult = document.getElementById('dash-import-result'); const scanBtn = document.getElementById('dash-scan-btn'); const importBtn = document.getElementById('dash-import-btn'); const fileSummary = document.getElementById('dash-file-summary'); @@ -14,6 +18,36 @@ const selectAllBtn = document.getElementById('dash-select-all-btn'); const importSelectedBtn = document.getElementById('dash-import-selected-btn'); const deleteAllBtn = document.getElementById('dash-delete-all-btn'); let importInProgress = false; +let importStartTime = null; +let importTotal = 0; +let importEtaInterval = null; + +function showImportProgress(msg, total) { + importTotal = total || 0; + importStartTime = Date.now(); + importStatus.textContent = msg; + importEta.textContent = ''; + importStatusBar.hidden = false; + importResultBar.hidden = true; + // Update ETA every second based on elapsed time vs total + clearInterval(importEtaInterval); + if (importTotal > 0) { + importEtaInterval = setInterval(() => { + const elapsed = (Date.now() - importStartTime) / 1000; + // rough heuristic: ~3s per video on average + const estTotal = importTotal * 3; + const remaining = Math.max(0, Math.round(estTotal - elapsed)); + importEta.textContent = remaining > 0 ? `~${remaining}s remaining` : ''; + }, 1000); + } +} + +function showImportResult(msg) { + clearInterval(importEtaInterval); + importStatusBar.hidden = true; + importResult.textContent = msg; + importResultBar.hidden = false; +} function fetchJson(url, options) { return fetch(url, options).then(response => response.json().then(data => { @@ -73,8 +107,10 @@ function renderDashcamFiles(summary) { row.dataset.path = file.path; row.dataset.size = file.size; row.dataset.modified = file.modified_at; + row.className = 'dash-row'; const selection = document.createElement('td'); + selection.className = 'dash-col-check'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'form-check-input'; @@ -83,27 +119,38 @@ function renderDashcamFiles(summary) { selection.appendChild(checkbox); row.appendChild(selection); - row.appendChild(createCell(file.path)); - row.appendChild(createCell(YarisUI.formatBytes(file.size))); - row.appendChild(createCell(new Date(file.modified_at * 1000).toLocaleString())); + const pathCell = createCell(file.path); + pathCell.className = 'dash-col-file'; + row.appendChild(pathCell); - const actions = document.createElement('td'); - actions.className = 'data-list-actions'; + const sizeCell = createCell(YarisUI.formatBytes(file.size)); + sizeCell.className = 'dash-col-size'; + row.appendChild(sizeCell); + + const dateCell = createCell(new Date(file.modified_at * 1000).toLocaleString()); + dateCell.className = 'dash-col-date'; + row.appendChild(dateCell); + + const actionsTd = document.createElement('td'); + actionsTd.className = 'dash-actions-td'; + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'data-list-actions'; const importButton = document.createElement('button'); importButton.type = 'button'; - importButton.className = 'btn btn-sm btn-outline-primary'; + importButton.className = 'btn btn-sm btn-outline-primary btn-icon-mobile'; importButton.dataset.importPath = file.path; - importButton.innerHTML = 'Import'; - actions.appendChild(importButton); + importButton.innerHTML = ' Import'; + actionsDiv.appendChild(importButton); const deleteButton = document.createElement('button'); deleteButton.type = 'button'; - deleteButton.className = 'btn btn-sm btn-outline-danger'; + deleteButton.className = 'btn btn-sm btn-outline-danger btn-icon-mobile'; deleteButton.dataset.deletePath = file.path; deleteButton.dataset.confirm = `Permanently delete "${file.path}" from the dashcam?`; - deleteButton.innerHTML = 'Delete'; - actions.appendChild(deleteButton); - row.appendChild(actions); + deleteButton.innerHTML = ' Delete'; + actionsDiv.appendChild(deleteButton); + actionsTd.appendChild(actionsDiv); + row.appendChild(actionsTd); fileListBody.appendChild(row); }); @@ -153,25 +200,23 @@ function describeImportResult(summary) { } function importDashcamFiles(paths) { - importStatus.textContent = `Importing ${paths.length} selected video(s)...`; + showImportProgress(`Importing ${paths.length} video(s)…`, paths.length); setImportBusy(true); return fetchJson('/api/dash/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paths }), }).then(summary => { - importStatus.textContent = describeImportResult(summary); + showImportResult(describeImportResult(summary)); paths.forEach(path => { const checkbox = Array.from( fileListBody.querySelectorAll('[data-select-path]') ).find(candidate => candidate.dataset.selectPath === path); - if (checkbox) { - checkbox.checked = false; - } + if (checkbox) checkbox.checked = false; }); return summary; }).catch(err => { - importStatus.textContent = `Selected import failed: ${err.message}`; + showImportResult(`Selected import failed: ${err.message}`); return null; }).finally(() => setImportBusy(false)); } @@ -202,29 +247,27 @@ function scanForDashcam() { } function importDashcam() { - importStatus.textContent = 'Importing... this can take a while for large videos.'; + showImportProgress('Importing all videos… this can take a while.', 0); setImportBusy(true); fetch('/api/dash/import', { method: 'POST' }) .then(response => response.json()) .then(summary => { if (summary.status !== 'ok') { - importStatus.textContent = summary.message || ('Import failed: ' + summary.status); + showImportResult(summary.message || ('Import failed: ' + summary.status)); return; } - let text = `Found ${summary.found} video(s), copied ${summary.copied}, skipped ${summary.skipped} (already imported).`; + let text = `Found ${summary.found} video(s), copied ${summary.copied}, skipped ${summary.skipped}.`; if (summary.errors && summary.errors.length > 0) { text += ` Errors: ${summary.errors.join('; ')}`; } - importStatus.textContent = text; + showImportResult(text); refreshDashcamFiles(); }) .catch(err => { - importStatus.textContent = 'Error while importing: ' + err; + showImportResult('Error while importing: ' + err); }) - .finally(() => { - setImportBusy(false); - }); + .finally(() => setImportBusy(false)); } scanBtn.addEventListener('click', scanForDashcam); diff --git a/static/js/local-videos.js b/static/js/local-videos.js index da10d9f..393e1cd 100644 --- a/static/js/local-videos.js +++ b/static/js/local-videos.js @@ -95,6 +95,7 @@ files.forEach(function (file) { const row = document.createElement("tr"); + row.className = "dash-row"; row.dataset.path = file.path; // Thumbnail cell — also contains filename for mobile view diff --git a/static/yaris.css b/static/yaris.css index f29d636..a124dcd 100644 --- a/static/yaris.css +++ b/static/yaris.css @@ -275,41 +275,31 @@ body { .dash-col-file { max-width: 14rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } } -/* Mobile: reflow table rows into flex cards */ +/* Mobile: reflow .dash-row into a flex card — thumb + name on left, buttons on right */ @media (max-width: 575.98px) { - /* Hide table header entirely */ + /* Hide table header */ #local-video-list thead, #dashcam-file-list thead { display: none; } - /* Each row becomes a horizontal flex card */ - #local-video-list tbody tr, - #dashcam-file-list tbody tr { - display: flex; - align-items: center; - border-bottom: 1px solid var(--bs-border-color); - padding: .25rem .5rem; - gap: .5rem; - } + /* Row becomes flex */ + .dash-row { display: flex !important; align-items: center; gap: .5rem; + border-bottom: 1px solid var(--bs-border-color); padding: .25rem .5rem; } - /* Hide all cells except thumb and actions */ - #local-video-list tbody td, - #dashcam-file-list tbody td { display: none; } + /* Hide all cells by default */ + .dash-row > td { display: none !important; } - /* Show only thumb cell and actions cell */ - #local-video-list tbody td:first-child, - #local-video-list tbody .dash-actions-td, - #dashcam-file-list tbody td:first-child, - #dashcam-file-list tbody .dash-actions-td { display: flex; align-items: center; } + /* Show only first cell (thumb or checkbox) and actions */ + .dash-row > td:first-child, + .dash-row > .dash-actions-td { display: flex !important; align-items: center; } - /* Thumb cell takes remaining space */ - #local-video-list tbody td:first-child, - #dashcam-file-list tbody td:first-child { flex: 1; min-width: 0; } + /* First cell takes remaining space */ + .dash-row > td:first-child { flex: 1; min-width: 0; } - /* Actions cell stays at natural width, never shrinks */ - .dash-actions-td { flex-shrink: 0; white-space: nowrap; } + /* Actions never shrink */ + .dash-row > .dash-actions-td { flex-shrink: 0; } - /* Filename label below thumb */ - .dash-thumb-name { display: block !important; font-size: .7rem; } + /* Filename under thumbnail on mobile */ + .dash-thumb-name { display: block !important; font-size: .7rem; margin-top: .2rem; } /* Shrink thumbnail */ .local-video-thumbnail { width: 4.5rem; } diff --git a/templates/dash.html b/templates/dash.html index 7012525..faf876e 100644 --- a/templates/dash.html +++ b/templates/dash.html @@ -18,6 +18,14 @@ Import all + + {# ── Files on dashcam (collapsible) ─────────────────────────────────── #} @@ -59,10 +67,6 @@ "Connect the dashcam in USB storage mode, then refresh this list." ) }} -