194 lines
7.2 KiB
JavaScript
194 lines
7.2 KiB
JavaScript
/**
|
|
* 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 += `
|
|
<tr id="row-${idx}" data-path="${esc(f.path)}">
|
|
<td class="col-check">
|
|
<input type="checkbox" class="file-checkbox" id="chk-${idx}"
|
|
data-path="${esc(f.path)}"
|
|
aria-label="Select ${esc(f.name)} for compression" />
|
|
</td>
|
|
<td class="col-name">
|
|
<label for="chk-${idx}" class="file-name-cell" style="cursor:pointer">
|
|
<span class="file-name">${esc(f.name)}</span>
|
|
<span class="file-path" title="${esc(f.path)}">${esc(pathDir)}</span>
|
|
</label>
|
|
</td>
|
|
<td class="col-size"><strong>${f.size_gb.toFixed(3)}</strong> GB</td>
|
|
<td class="col-bitrate">
|
|
<span class="bitrate-badge">
|
|
${esc(f.bit_rate_mbps ? f.bit_rate_mbps + ' Mbps' : 'Unknown')}
|
|
</span>
|
|
</td>
|
|
<td class="col-target">
|
|
<span class="bitrate-badge target">
|
|
${esc(f.target_bit_rate_mbps ? f.target_bit_rate_mbps + ' Mbps' : '—')}
|
|
</span>
|
|
</td>
|
|
<td class="col-codec">
|
|
<span class="codec-tag ${codecMod}"
|
|
title="Encoder: ${isHevc ? 'libx265' : 'libx264'}">
|
|
${esc(codecLabel)}
|
|
</span>
|
|
</td>
|
|
</tr>`;
|
|
});
|
|
|
|
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 =
|
|
'<span class="btn-icon-prefix" aria-hidden="true">⊙</span> 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.');
|
|
});
|
|
}
|