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.
This commit is contained in:
@@ -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}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -580,6 +589,15 @@ audio{width:100%;height:36px;border-radius:4px;display:block;
|
||||
aria-describedby="threshold-hint">
|
||||
<span id="threshold-hint" class="controls-hint">RMS 0–1 · sections above this value are marked loud</span>
|
||||
</div>
|
||||
<div class="filter-bar" role="search" aria-label="Filter recordings">
|
||||
<label for="filter-name">Search:</label>
|
||||
<input type="text" id="filter-name" placeholder="filename…" aria-label="Filter by filename">
|
||||
<label for="filter-from">From:</label>
|
||||
<input type="date" id="filter-from" aria-label="From date">
|
||||
<label for="filter-to">To:</label>
|
||||
<input type="date" id="filter-to" aria-label="To date">
|
||||
<button id="filter-clear" aria-label="Clear all filters">✕ Clear</button>
|
||||
</div>
|
||||
<div class="wrap" id="main">
|
||||
<table aria-label="Recordings archive">
|
||||
<thead>
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user