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 `
+
+
+
+ ${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 `
+
+
+
+ ${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