Add selected dashcam imports
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
+110
-3
@@ -10,12 +10,16 @@ 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;
|
||||
|
||||
function fetchJson(url, options) {
|
||||
return fetch(url, options).then(response => response.json().then(data => {
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.status || `HTTP ${response.status}`);
|
||||
const details = data.errors && data.errors.length ? data.errors.join('; ') : null;
|
||||
throw new Error(data.message || details || data.status || `HTTP ${response.status}`);
|
||||
}
|
||||
return data;
|
||||
}));
|
||||
@@ -36,6 +40,33 @@ function createCell(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') {
|
||||
@@ -51,12 +82,29 @@ function renderDashcamFiles(summary) {
|
||||
row.dataset.path = file.path;
|
||||
row.dataset.size = file.size;
|
||||
row.dataset.modified = file.modified_at;
|
||||
|
||||
const selection = document.createElement('td');
|
||||
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);
|
||||
|
||||
row.appendChild(createCell(file.path));
|
||||
row.appendChild(createCell(formatBytes(file.size)));
|
||||
row.appendChild(createCell(new Date(file.modified_at * 1000).toLocaleString()));
|
||||
|
||||
const actions = document.createElement('td');
|
||||
actions.className = 'data-list-actions';
|
||||
const importButton = document.createElement('button');
|
||||
importButton.type = 'button';
|
||||
importButton.className = 'btn btn-sm btn-outline-primary';
|
||||
importButton.dataset.importPath = file.path;
|
||||
importButton.innerHTML = '<i class="fas fa-download me-1"></i>Import';
|
||||
actions.appendChild(importButton);
|
||||
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.type = 'button';
|
||||
deleteButton.className = 'btn btn-sm btn-outline-danger';
|
||||
@@ -73,6 +121,7 @@ function renderDashcamFiles(summary) {
|
||||
fileSummary.textContent += ` (${summary.errors.length} file error(s))`;
|
||||
}
|
||||
deleteAllBtn.disabled = files.length === 0;
|
||||
updateSelectionControls();
|
||||
}
|
||||
|
||||
function refreshDashcamFiles() {
|
||||
@@ -84,6 +133,7 @@ function refreshDashcamFiles() {
|
||||
fileListBody.replaceChildren();
|
||||
fileSummary.textContent = `Unable to list dashcam files: ${err.message}`;
|
||||
deleteAllBtn.disabled = true;
|
||||
updateSelectionControls();
|
||||
})
|
||||
.finally(() => {
|
||||
filesRefreshBtn.disabled = false;
|
||||
@@ -103,6 +153,38 @@ function deleteDashcamFile(path) {
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
importStatus.textContent = `Importing ${paths.length} selected video(s)...`;
|
||||
setImportBusy(true);
|
||||
return fetchJson('/api/dash/import', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ paths }),
|
||||
}).then(summary => {
|
||||
importStatus.textContent = 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 => {
|
||||
importStatus.textContent = `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'})`;
|
||||
@@ -130,7 +212,7 @@ function scanForDashcam() {
|
||||
|
||||
function importDashcam() {
|
||||
importStatus.textContent = 'Importing... this can take a while for large videos.';
|
||||
importBtn.disabled = true;
|
||||
setImportBusy(true);
|
||||
|
||||
fetch('/api/dash/import', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
@@ -150,7 +232,7 @@ function importDashcam() {
|
||||
importStatus.textContent = 'Error while importing: ' + err;
|
||||
})
|
||||
.finally(() => {
|
||||
importBtn.disabled = false;
|
||||
setImportBusy(false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -158,6 +240,12 @@ 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;
|
||||
@@ -166,6 +254,25 @@ fileListBody.addEventListener('click', event => {
|
||||
});
|
||||
}
|
||||
});
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user