Updates galore. Improved folder structure, componentized, and notifications upon completion.
This commit is contained in:
parent
b48784e2ad
commit
7e0502ca40
33 changed files with 3565 additions and 728 deletions
194
static/js/modules/scan.js
Normal file
194
static/js/modules/scan.js
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
/**
|
||||
* 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.');
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue