/** * scan.js * ------- * Directory scan and file selection table. * * Handles the "Scan for Files" button, the /api/scan fetch, rendering the * results table, and the select-all / deselect-all controls. * * Exports * ------- * initScan() — attach all event listeners; call once at startup */ import { state, els, announce } from './state.js'; import { esc } from './utils.js'; // ─── Status helper ──────────────────────────────────────────────────────────── function setScanStatus(msg, type) { els.scanStatus.textContent = msg; els.scanStatus.style.color = type === 'error' ? 'var(--text-danger)' : type === 'success' ? 'var(--text-success)' : type === 'warn' ? 'var(--accent)' : 'var(--text-muted)'; } // ─── File table ─────────────────────────────────────────────────────────────── function updateSelectionUI() { els.selectionSummary.textContent = `${state.selectedPaths.size} of ${state.scannedFiles.length} selected`; els.compressBtn.disabled = state.selectedPaths.size === 0; } /** * Build and inject the file selection table from the scan results. * Attaches checkbox change handlers for each row. * @param {Array} files — enriched file objects from /api/scan */ export function renderFileTable(files) { let html = ''; files.forEach((f, idx) => { const codec = (f.codec || 'unknown').toLowerCase(); const isHevc = ['hevc', 'h265', 'x265'].includes(codec); const isH264 = ['h264', 'avc', 'x264'].includes(codec); const codecLabel = isHevc ? 'H.265 / HEVC' : isH264 ? 'H.264 / AVC' : codec.toUpperCase(); const codecMod = isHevc ? 'hevc' : isH264 ? 'h264' : ''; const pathDir = f.path.replace(f.name, ''); html += ` ${f.size_gb.toFixed(3)} GB ${esc(f.bit_rate_mbps ? f.bit_rate_mbps + ' Mbps' : 'Unknown')} ${esc(f.target_bit_rate_mbps ? f.target_bit_rate_mbps + ' Mbps' : '—')} ${esc(codecLabel)} `; }); els.fileTbody.innerHTML = html; els.fileTbody.querySelectorAll('.file-checkbox').forEach(chk => { chk.addEventListener('change', () => { const row = chk.closest('tr'); if (chk.checked) { state.selectedPaths.add(chk.dataset.path); row.classList.add('selected'); } else { state.selectedPaths.delete(chk.dataset.path); row.classList.remove('selected'); } updateSelectionUI(); }); }); updateSelectionUI(); } // ─── Public init ───────────────────────────────────────────────────────────── /** * Attach event listeners for the scan button and file selection controls. * Call once during app initialisation. */ export function initScan() { // ── Scan button ────────────────────────────────────────────────────────── els.scanBtn.addEventListener('click', async () => { const directory = els.dirInput.value.trim(); const minSize = parseFloat(els.minSizeInput.value); if (!directory) { setScanStatus('Please enter a directory path.', 'error'); els.dirInput.focus(); return; } if (isNaN(minSize) || minSize <= 0) { setScanStatus('Please enter a valid minimum size > 0.', 'error'); els.minSizeInput.focus(); return; } els.scanBtn.disabled = true; els.scanBtn.textContent = '⟳ Scanning…'; setScanStatus('Scanning directory, please wait…', 'info'); announce('Scanning directory for video files.'); els.sectionFiles.hidden = true; try { const resp = await fetch('/api/scan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ directory, min_size_gb: minSize }), }); const data = await resp.json(); if (!resp.ok) { setScanStatus(`Error: ${data.error}`, 'error'); announce(`Scan failed: ${data.error}`); return; } state.scannedFiles = data.files; state.selectedPaths.clear(); if (!data.files.length) { setScanStatus(`No video files larger than ${minSize} GB found.`, 'warn'); announce('No video files found matching your criteria.'); return; } setScanStatus(`Found ${data.files.length} file(s).`, 'success'); announce(`Scan complete. Found ${data.files.length} video files.`); renderFileTable(data.files); els.sectionFiles.hidden = false; els.sectionFiles.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (err) { setScanStatus(`Network error: ${err.message}`, 'error'); } finally { els.scanBtn.disabled = false; els.scanBtn.innerHTML = ' Scan for Files'; } }); // ── Select all ─────────────────────────────────────────────────────────── els.selectAllBtn.addEventListener('click', () => { els.fileTbody.querySelectorAll('.file-checkbox').forEach(chk => { chk.checked = true; state.selectedPaths.add(chk.dataset.path); chk.closest('tr').classList.add('selected'); }); updateSelectionUI(); announce(`All ${state.scannedFiles.length} files selected.`); }); // ── Deselect all ───────────────────────────────────────────────────────── els.deselectAllBtn.addEventListener('click', () => { els.fileTbody.querySelectorAll('.file-checkbox').forEach(chk => { chk.checked = false; state.selectedPaths.delete(chk.dataset.path); chk.closest('tr').classList.remove('selected'); }); updateSelectionUI(); announce('All files deselected.'); }); }