From e0bad5e5acc2b1616bb307d598caa7b15c721e3a Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 22 Jun 2026 02:35:55 -0700 Subject: [PATCH] feat: card-based Sample Library layout with grouping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redesigned WAV Browser as a structured Sample Library with card layout: - WAVs grouped by library (ISO name) and program (volume name) - Library cards with collapsible headers showing program/sample counts - Program sections within each library - Sample cards in a compact grid with play-on-click Updated playWav to show a clean sample name in the player bar, hide the player when playback ends, and use sample-card selectors for active-state highlighting. Clean display names (underscores → spaces, stripped .wav extensions) --- web/ui/app.js | 105 ++++++++++++++++++++++++++++++------ web/ui/index.html | 17 +++--- web/ui/style.css | 132 ++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 208 insertions(+), 46 deletions(-) diff --git a/web/ui/app.js b/web/ui/app.js index 29d57f2..675ffb4 100644 --- a/web/ui/app.js +++ b/web/ui/app.js @@ -200,42 +200,109 @@ let animationFrame = null; document.getElementById('wav-browse-btn').addEventListener('click', browseWavs); +function groupWavs(paths) { + const libs = {}; + for (const p of paths) { + const parts = p.split('/'); + let lib, prog, file; + if (parts.length === 1) { + file = parts[0]; + } else if (parts.length === 2) { + lib = parts[0]; prog = parts[0]; file = parts[1]; + } else { + lib = parts[0]; prog = parts[1]; file = parts[parts.length - 1]; + } + if (!libs[lib]) libs[lib] = {}; + if (!libs[lib][prog]) libs[lib][prog] = []; + libs[lib][prog].push({ fullPath: p, name: file.replace(/\.wav$/i, '') }); + } + return libs; +} + +function cleanName(name) { + return name.replace(/_+/g, ' ').trim(); +} + +function renderSampleCards(samples) { + return samples.map(s => ` +
+ ${esc(s.name)} + +
+ `).join(''); +} + +function renderPrograms(programs) { + const progNames = Object.keys(programs).sort(); + return progNames.map(prog => { + const samples = programs[prog]; + return ` +
+
+

${esc(cleanName(prog))}

+ ${samples.length} samples +
+
+ ${renderSampleCards(samples)} +
+
`; + }).join(''); +} + +function renderLibraries(libs) { + const libNames = Object.keys(libs).sort(); + return libNames.map(lib => { + const programs = libs[lib]; + const progCount = Object.keys(programs).length; + const sampleCount = Object.values(programs).reduce((sum, s) => sum + s.length, 0); + return ` +
+
+

${esc(cleanName(lib))}

+ ${progCount} programs · ${sampleCount} samples +
+
+ ${renderPrograms(programs)} +
+
`; + }).join(''); +} + async function browseWavs() { const dir = document.getElementById('wav-dir').value; + const results = document.getElementById('wav-results'); + const summary = document.getElementById('wav-summary'); const list = document.getElementById('wav-list'); list.innerHTML = 'Loading...'; - console.log('browseWavs: dir =', dir); try { const url = `${API}/list-wavs?dir=${encodeURIComponent(dir)}`; - console.log('browseWavs: fetching', url); const resp = await fetch(url); - console.log('browseWavs: response status', resp.status, 'ok', resp.ok); if (!resp.ok) { list.innerHTML = 'HTTP ' + resp.status + ''; + results.style.display = 'block'; return; } const data = await resp.json(); - console.log('browseWavs: parsed data', data, 'type', typeof data, 'isArray', Array.isArray(data)); if (!data || data.error) { list.innerHTML = 'Error: ' + (data && data.error ? data.error : 'server error') + ''; + results.style.display = 'block'; return; } if (!Array.isArray(data) || data.length === 0) { list.innerHTML = 'No WAV files found'; + results.style.display = 'block'; return; } - console.log('browseWavs: found', data.length, 'wavs'); - list.innerHTML = data.map(f => ` -
- ${esc(f)} - -
- `).join(''); + const libs = groupWavs(data); + const libCount = Object.keys(libs).length; + summary.textContent = libCount + ' librar' + (libCount === 1 ? 'y' : 'ies') + ' · ' + data.length + ' samples'; + list.innerHTML = renderLibraries(libs); + results.style.display = 'block'; } catch (e) { - console.error('browseWavs: error', e); list.innerHTML = 'Failed: ' + e.message; + results.style.display = 'block'; } } @@ -255,8 +322,11 @@ async function playWav(filename) { animationFrame = null; } + const name = filename.split('/').pop().replace(/\.wav$/i, ''); + const player = document.getElementById('wav-player'); const nowPlaying = document.getElementById('wav-now-playing'); - nowPlaying.textContent = 'Loading: ' + filename; + player.style.display = 'block'; + nowPlaying.textContent = 'Loading: ' + name; try { const resp = await fetch(`/wavs/${encodeURIComponent(filename)}`); @@ -276,6 +346,7 @@ async function playWav(filename) { document.getElementById('wav-progress-fill').style.width = '0%'; document.getElementById('wav-time').textContent = formatTime(0); clearActiveItem(); + player.style.display = 'none'; } }; @@ -284,7 +355,7 @@ async function playWav(filename) { isPlaying = true; pauseOffset = 0; - nowPlaying.textContent = '▶ ' + filename; + nowPlaying.textContent = '▶ ' + name; setActiveItem(filename); updateProgress(); } catch (e) { @@ -308,13 +379,13 @@ function updateProgress() { } function setActiveItem(filename) { - document.querySelectorAll('.wav-item').forEach(el => { - el.classList.toggle('playing', el.dataset.file === filename); + document.querySelectorAll('.sample-card').forEach(el => { + el.classList.toggle('playing', el.dataset.wav === filename); }); } function clearActiveItem() { - document.querySelectorAll('.wav-item').forEach(el => { + document.querySelectorAll('.sample-card').forEach(el => { el.classList.remove('playing'); }); } diff --git a/web/ui/index.html b/web/ui/index.html index 1794e9b..889edb8 100644 --- a/web/ui/index.html +++ b/web/ui/index.html @@ -40,18 +40,21 @@
-

WAV Browser

+

Sample Library

-
-
-
-
- 0:00 +
diff --git a/web/ui/style.css b/web/ui/style.css index 0262c08..32aae1a 100644 --- a/web/ui/style.css +++ b/web/ui/style.css @@ -272,42 +272,130 @@ h2 { min-width: 40px; } -.wav-list { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 8px; +.no-files { + color: var(--text2); + font-size: 13px; + padding: 12px 0; } -.wav-item { +.wav-summary { + font-size: 13px; + color: var(--text2); + margin-top: 12px; + margin-bottom: 12px; + min-height: 18px; +} + +/* Library cards */ +.wav-library { + display: flex; + flex-direction: column; + gap: 12px; +} + +.library-card { + background: var(--bg2); + border: 1px solid var(--bg3); + border-radius: var(--radius); + overflow: hidden; +} + +.library-header { + padding: 14px 18px; + cursor: pointer; + user-select: none; + background: var(--bg); + border-bottom: 1px solid transparent; + transition: border-color 0.15s; +} +.library-header:hover { + border-color: var(--accent); +} +.library-header h3 { + font-size: 15px; + font-weight: 600; + color: var(--text); + margin: 0; + text-transform: none; + letter-spacing: 0; +} +.library-header .lib-meta { + font-size: 12px; + color: var(--text2); + margin-top: 4px; +} + +.library-programs { + padding: 10px 18px 16px; +} + +/* Program cards */ +.program-card { + margin-top: 14px; +} +.program-card:first-child { + margin-top: 0; +} + +.program-header { + cursor: pointer; + user-select: none; + margin-bottom: 8px; +} +.program-header h4 { + font-size: 13px; + font-weight: 600; + color: var(--accent); + margin: 0; + text-transform: none; + letter-spacing: 0; + display: inline; +} +.program-header .prog-meta { + font-size: 11px; + color: var(--text2); + margin-left: 8px; +} + +.program-samples { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 6px; +} + +/* Sample cards */ +.sample-card { background: var(--bg); border: 1px solid var(--bg3); border-radius: var(--radius); - padding: 10px 12px; - display: flex; - align-items: center; - gap: 8px; + padding: 8px 10px; cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + min-height: 64px; + transition: border-color 0.15s, background 0.15s; } -.wav-item:hover { +.sample-card:hover { border-color: var(--accent); } -.wav-item.playing { +.sample-card.playing { border-color: var(--green); background: rgba(78, 204, 163, 0.1); } -.wav-item .wav-name { - flex: 1; - font-size: 13px; +.sample-card .sample-name { + font-size: 11px; + color: var(--text); + text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + max-width: 100%; } -.wav-item .wav-play-btn { - background: var(--accent2); - color: white; - border: none; - border-radius: 4px; - padding: 4px 10px; - font-size: 11px; - cursor: pointer; +.sample-card .sample-play-icon { + font-size: 14px; + color: var(--accent2); + line-height: 1; } \ No newline at end of file