From de667821b7e2517e7efe4bf6b19420f23af73114 Mon Sep 17 00:00:00 2001 From: Jonathan Schuster Date: Wed, 29 Apr 2026 20:37:14 +0200 Subject: [PATCH] feat: add filename and date-range filters to recordings list Client-side filter bar with live filename search and from/to date pickers. Rendering is split into renderFiles() + applyFilters() so filters can be re-applied without re-fetching. Subtitle shows 'N of M shown' when a filter is active. Clear button resets all fields. --- web.py | 106 ++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 27 deletions(-) diff --git a/web.py b/web.py index 0594e0e..0415f49 100644 --- a/web.py +++ b/web.py @@ -564,6 +564,15 @@ button.chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px} .player-row td{padding:0 10px 10px;background:var(--bg);border-bottom:1px solid var(--brd)} audio{width:100%;height:36px;border-radius:4px;display:block; color-scheme:dark;accent-color:var(--accent)} +/* filter bar */ +.filter-bar{display:flex;align-items:center;gap:10px;padding:8px 28px; + border-bottom:1px solid var(--brd);background:var(--surf);flex-wrap:wrap} +.filter-bar label{font-size:13px;color:var(--muted);white-space:nowrap} +.filter-bar input[type=text]{width:180px;background:var(--bg);border:1px solid var(--brd); + color:var(--txt);padding:3px 6px;border-radius:4px;font-size:13px} +.filter-bar input[type=date]{background:var(--bg);border:1px solid var(--brd); + color:var(--txt);padding:3px 6px;border-radius:4px;font-size:13px;color-scheme:dark} +.filter-bar input:focus{outline:2px solid var(--accent);outline-offset:1px} @@ -580,6 +589,15 @@ audio{width:100%;height:36px;border-radius:4px;display:block; aria-describedby="threshold-hint"> RMS 0–1 · sections above this value are marked loud +
@@ -623,6 +641,8 @@ const recMap = new Map(); // idx -> [{start,end}], populated after analysis const sectionMap = new Map(); let activePlayerIdx = null; +// full file list from server, annotated with stable _idx +let allFiles = []; function togglePlayer(idx, filename) { const prow = document.getElementById('prow-'+idx); @@ -780,10 +800,13 @@ async function deleteFile(idx, filename) { document.getElementById('row-'+idx)?.remove(); document.getElementById('prow-'+idx)?.remove(); recMap.delete(idx); - const remaining = document.querySelectorAll('tr.data-row').length; - document.getElementById('subtitle').textContent = - `${remaining} recording${remaining!==1?'s':''} found`; - if (!remaining) document.getElementById('empty').style.display = ''; + allFiles = allFiles.filter(f => f._idx !== idx); + const visible = document.querySelectorAll('tr.data-row').length; + const total = allFiles.length; + document.getElementById('subtitle').textContent = total === visible + ? `${total} recording${total!==1?'s':''} found` + : `${visible} of ${total} recording${total!==1?'s':''} shown`; + if (!visible) document.getElementById('empty').style.display = ''; updateStorage(); } else { const d = await r.json().catch(()=>({})); @@ -807,32 +830,21 @@ async function updateStorage() { } catch(e) {} } -async function load() { - const refreshBtn = document.getElementById('refresh-btn'); - refreshBtn.disabled = true; - document.getElementById('subtitle').textContent = 'Loading…'; - recMap.clear(); - - let files; - try { - files = await (await fetch('/api/files')).json(); - } catch(e) { - document.getElementById('subtitle').textContent = 'Error loading files'; - refreshBtn.disabled = false; - return; - } - +function renderFiles(files) { const tbody = document.getElementById('tbody'); tbody.innerHTML = ''; + recMap.clear(); + sectionMap.clear(); - const n = files.length; - document.getElementById('subtitle').textContent = - `${n} recording${n!==1?'s':''} found`; - document.getElementById('empty').style.display = n ? 'none' : ''; - updateStorage(); - if (!n) { refreshBtn.disabled = false; return; } + const total = allFiles.length; + const visible = files.length; + document.getElementById('subtitle').textContent = total === visible + ? `${total} recording${total!==1?'s':''} found` + : `${visible} of ${total} recording${total!==1?'s':''} shown`; + document.getElementById('empty').style.display = visible ? 'none' : ''; - files.forEach((f, i) => { + files.forEach(f => { + const i = f._idx; const ext = f.ext; const canAnalyse = ext === 'wav' || ext === 'flac'; const isRec = !!f.recording; @@ -909,10 +921,40 @@ async function load() { .addEventListener('click', () => deleteFile(i, f.name)); } - // ---- register for live-status polling ---- recMap.set(i, f.name); }); +} +function applyFilters() { + const nameQ = document.getElementById('filter-name').value.toLowerCase().trim(); + const fromD = document.getElementById('filter-from').value; + const toD = document.getElementById('filter-to').value; + const filtered = allFiles.filter(f => { + if (nameQ && !f.name.toLowerCase().includes(nameQ)) return false; + if (fromD && f.date < fromD + ' 00:00:00') return false; + if (toD && f.date > toD + ' 23:59:59') return false; + return true; + }); + renderFiles(filtered); +} + +async function load() { + const refreshBtn = document.getElementById('refresh-btn'); + refreshBtn.disabled = true; + document.getElementById('subtitle').textContent = 'Loading…'; + + let files; + try { + files = await (await fetch('/api/files')).json(); + } catch(e) { + document.getElementById('subtitle').textContent = 'Error loading files'; + refreshBtn.disabled = false; + return; + } + + allFiles = files.map((f, i) => ({...f, _idx: i})); + updateStorage(); + applyFilters(); refreshBtn.disabled = false; } @@ -933,6 +975,16 @@ async function pollStatus() { document.getElementById('refresh-btn').addEventListener('click', load); +document.getElementById('filter-name').addEventListener('input', applyFilters); +document.getElementById('filter-from').addEventListener('change', applyFilters); +document.getElementById('filter-to').addEventListener('change', applyFilters); +document.getElementById('filter-clear').addEventListener('click', () => { + document.getElementById('filter-name').value = ''; + document.getElementById('filter-from').value = ''; + document.getElementById('filter-to').value = ''; + applyFilters(); +}); + // Seed threshold input from server config, then start fetch('/api/config').then(r => r.json()).then(cfg => { if (cfg.threshold != null)