333 lines
13 KiB
JavaScript
333 lines
13 KiB
JavaScript
// Dashcam page: scan for a USB dashcam, then trigger importing its videos.
|
|
// Uses plain fetch() (not socket.io / yaris.js) since this is a one-shot
|
|
// request/response flow, not a live data stream, and yaris.js assumes DOM
|
|
// 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');
|
|
const fileListBody = document.querySelector('#dashcam-file-list [data-data-list-body]');
|
|
const filesRefreshBtn = document.getElementById('dash-files-refresh-btn');
|
|
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;
|
|
// Auto-hide result after 10s
|
|
setTimeout(() => { importResultBar.hidden = true; }, 10000);
|
|
}
|
|
|
|
function fetchJson(url, options) {
|
|
return fetch(url, options).then(response => response.json().then(data => {
|
|
if (!response.ok) {
|
|
const details = data.errors && data.errors.length ? data.errors.join('; ') : null;
|
|
throw new Error(data.message || details || data.status || `HTTP ${response.status}`);
|
|
}
|
|
return data;
|
|
}));
|
|
}
|
|
|
|
function createCell(text) {
|
|
const cell = document.createElement('td');
|
|
cell.textContent = text;
|
|
return cell;
|
|
}
|
|
|
|
function selectedPaths() {
|
|
return Array.from(fileListBody.querySelectorAll('[data-select-path]:checked'))
|
|
.map(checkbox => checkbox.dataset.selectPath);
|
|
}
|
|
|
|
function updateSelectionControls() {
|
|
const checkboxes = Array.from(fileListBody.querySelectorAll('[data-select-path]'));
|
|
const selectedCount = selectedPaths().length;
|
|
importSelectedBtn.disabled = importInProgress || selectedCount === 0;
|
|
selectAllBtn.disabled = checkboxes.length === 0;
|
|
selectAllBtn.textContent = checkboxes.length > 0 && selectedCount === checkboxes.length
|
|
? 'Clear selection'
|
|
: 'Select all';
|
|
importSelectedBtn.innerHTML = selectedCount
|
|
? `<i class="fas fa-download me-1"></i>Import selected (${selectedCount})`
|
|
: '<i class="fas fa-download me-1"></i>Import selected';
|
|
}
|
|
|
|
function setImportBusy(busy) {
|
|
importInProgress = busy;
|
|
importBtn.disabled = busy;
|
|
fileListBody.querySelectorAll('[data-import-path]').forEach(button => {
|
|
button.disabled = busy;
|
|
});
|
|
updateSelectionControls();
|
|
}
|
|
|
|
function renderDashcamFiles(summary) {
|
|
fileListBody.replaceChildren();
|
|
if (summary.status !== 'ok') {
|
|
fileSummary.textContent = summary.message || `Unable to list files: ${summary.status}`;
|
|
deleteAllBtn.disabled = true;
|
|
return;
|
|
}
|
|
|
|
const files = summary.files || [];
|
|
|
|
files.forEach(file => {
|
|
const row = document.createElement('tr');
|
|
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';
|
|
checkbox.dataset.selectPath = file.path;
|
|
checkbox.setAttribute('aria-label', `Select ${file.path}`);
|
|
selection.appendChild(checkbox);
|
|
row.appendChild(selection);
|
|
|
|
const pathCell = createCell(file.path);
|
|
pathCell.className = 'dash-col-file';
|
|
row.appendChild(pathCell);
|
|
|
|
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 btn-icon-mobile';
|
|
importButton.dataset.importPath = file.path;
|
|
importButton.innerHTML = '<i class="fas fa-download"></i><span class="btn-label"> Import</span>';
|
|
actionsDiv.appendChild(importButton);
|
|
|
|
const deleteButton = document.createElement('button');
|
|
deleteButton.type = 'button';
|
|
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 = '<i class="fas fa-trash"></i><span class="btn-label"> Delete</span>';
|
|
actionsDiv.appendChild(deleteButton);
|
|
actionsTd.appendChild(actionsDiv);
|
|
row.appendChild(actionsTd);
|
|
fileListBody.appendChild(row);
|
|
});
|
|
|
|
fileSummary.textContent = `${files.length} video(s), ${YarisUI.formatBytes(summary.total_size || 0)} total`;
|
|
if (summary.errors && summary.errors.length) {
|
|
fileSummary.textContent += ` (${summary.errors.length} file error(s))`;
|
|
}
|
|
deleteAllBtn.disabled = files.length === 0;
|
|
updateSelectionControls();
|
|
}
|
|
|
|
function refreshDashcamFiles() {
|
|
fileSummary.textContent = 'Scanning...';
|
|
filesRefreshBtn.disabled = true;
|
|
return fetchJson('/api/dash/files')
|
|
.then(renderDashcamFiles)
|
|
.catch(err => {
|
|
fileListBody.replaceChildren();
|
|
fileSummary.textContent = `Unable to list dashcam files: ${err.message}`;
|
|
deleteAllBtn.disabled = true;
|
|
updateSelectionControls();
|
|
})
|
|
.finally(() => {
|
|
filesRefreshBtn.disabled = false;
|
|
});
|
|
}
|
|
|
|
function deleteDashcamFile(path) {
|
|
return fetchJson('/api/dash/file', {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ path }),
|
|
}).then(result => {
|
|
fileSummary.textContent = `Deleted ${result.deleted_count} video(s). Refreshing...`;
|
|
return refreshDashcamFiles();
|
|
}).catch(err => {
|
|
fileSummary.textContent = `Delete failed: ${err.message}`;
|
|
});
|
|
}
|
|
|
|
function describeImportResult(summary) {
|
|
let text = `Selected ${summary.found} video(s), copied ${summary.copied}, skipped ${summary.skipped} (already imported).`;
|
|
if (summary.errors && summary.errors.length) {
|
|
text += ` Errors: ${summary.errors.join('; ')}`;
|
|
}
|
|
return text;
|
|
}
|
|
|
|
function importDashcamFiles(paths) {
|
|
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 => {
|
|
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;
|
|
});
|
|
return summary;
|
|
}).catch(err => {
|
|
showImportResult(`Selected import failed: ${err.message}`);
|
|
return null;
|
|
}).finally(() => setImportBusy(false));
|
|
}
|
|
|
|
function describeDevice(device) {
|
|
const label = device.label ? ` "${device.label}"` : '';
|
|
return `${device.path}${label} (${device.size}, ${device.fstype || 'unknown fs'})`;
|
|
}
|
|
|
|
function scanForDashcam() {
|
|
deviceStatus.textContent = 'Scanning...';
|
|
importBtn.disabled = true;
|
|
|
|
fetch('/api/dash/devices')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const devices = data.devices || [];
|
|
if (devices.length === 0) {
|
|
deviceStatus.textContent = 'No USB dashcam/storage device detected. Plug it in and scan again.';
|
|
return;
|
|
}
|
|
deviceStatus.textContent = 'Detected: ' + devices.map(describeDevice).join(', ');
|
|
importBtn.disabled = false;
|
|
})
|
|
.catch(err => {
|
|
deviceStatus.textContent = 'Error while scanning: ' + err;
|
|
});
|
|
}
|
|
|
|
function importDashcam() {
|
|
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') {
|
|
showImportResult(summary.message || ('Import failed: ' + summary.status));
|
|
return;
|
|
}
|
|
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('; ')}`;
|
|
}
|
|
showImportResult(text);
|
|
refreshDashcamFiles();
|
|
})
|
|
.catch(err => {
|
|
showImportResult('Error while importing: ' + err);
|
|
})
|
|
.finally(() => setImportBusy(false));
|
|
}
|
|
|
|
scanBtn.addEventListener('click', scanForDashcam);
|
|
importBtn.addEventListener('click', importDashcam);
|
|
filesRefreshBtn.addEventListener('click', refreshDashcamFiles);
|
|
fileListBody.addEventListener('click', event => {
|
|
const importButton = event.target.closest('[data-import-path]');
|
|
if (importButton) {
|
|
importDashcamFiles([importButton.dataset.importPath]);
|
|
return;
|
|
}
|
|
|
|
const deleteButton = event.target.closest('[data-delete-path]');
|
|
if (deleteButton) {
|
|
deleteButton.disabled = true;
|
|
deleteDashcamFile(deleteButton.dataset.deletePath).finally(() => {
|
|
deleteButton.disabled = false;
|
|
});
|
|
}
|
|
});
|
|
fileListBody.addEventListener('change', event => {
|
|
if (event.target.matches('[data-select-path]')) {
|
|
updateSelectionControls();
|
|
}
|
|
});
|
|
importSelectedBtn.addEventListener('click', () => {
|
|
const paths = selectedPaths();
|
|
if (paths.length) {
|
|
importDashcamFiles(paths);
|
|
}
|
|
});
|
|
selectAllBtn.addEventListener('click', () => {
|
|
const checkboxes = Array.from(fileListBody.querySelectorAll('[data-select-path]'));
|
|
const shouldSelect = checkboxes.some(checkbox => !checkbox.checked);
|
|
checkboxes.forEach(checkbox => {
|
|
checkbox.checked = shouldSelect;
|
|
});
|
|
updateSelectionControls();
|
|
});
|
|
deleteAllBtn.addEventListener('click', () => {
|
|
if (!window.confirm(deleteAllBtn.dataset.confirm)) {
|
|
return;
|
|
}
|
|
deleteAllBtn.disabled = true;
|
|
fetchJson('/api/dash/files', { method: 'DELETE' })
|
|
.then(result => {
|
|
fileSummary.textContent = `Deleted ${result.deleted_count} video(s). Refreshing...`;
|
|
return refreshDashcamFiles();
|
|
})
|
|
.catch(err => {
|
|
fileSummary.textContent = `Delete all failed: ${err.message}`;
|
|
deleteAllBtn.disabled = false;
|
|
});
|
|
});
|
|
|
|
// Auto-scan once when the page loads.
|
|
scanForDashcam();
|
|
refreshDashcamFiles();
|
|
|
|
// --- Remote server upload status/toggle ---
|