From 794ad98cf03c98bf89c876b95134e70fecc92efa Mon Sep 17 00:00:00 2001 From: tcpsyn Date: Sat, 21 Mar 2026 03:34:44 -0600 Subject: [PATCH] Replace music dropdown with genre quick-select buttons - One-click genre buttons play random track from that genre - Active genre highlighted, now-playing bar shows track name - Only genres with tracks shown, crossfade on genre switch - M key replays active genre or picks random Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/css/style.css | 100 ++++++++++++++++++++++++++++++++----- frontend/index.html | 16 +++--- frontend/js/app.js | 109 ++++++++++++++++++++++++++--------------- 3 files changed, 165 insertions(+), 60 deletions(-) diff --git a/frontend/css/style.css b/frontend/css/style.css index 832dceb..e3312b7 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -727,19 +727,6 @@ section h2 { margin-bottom: 10px; } -.music-section select optgroup { - color: var(--accent); - font-weight: bold; - font-style: normal; - padding: 4px 0; -} - -.music-section select option { - color: var(--text); - font-weight: normal; - padding: 2px 8px; -} - .music-controls { display: flex; gap: 8px; @@ -766,6 +753,83 @@ section h2 { accent-color: var(--accent); } +/* Genre Quick-Select */ +.genre-section { + grid-column: span 3; +} + +.genre-grid { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 8px; +} + +.genre-btn { + background: var(--bg); + color: var(--text); + border: 1px solid rgba(232, 121, 29, 0.12); + padding: 6px 12px; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 0.8rem; + transition: all 0.15s; + white-space: nowrap; +} + +.genre-btn:hover { + border-color: var(--accent); + background: #2a1e10; + color: #fff; +} + +.genre-btn.active { + background: var(--accent); + border-color: var(--accent); + color: #fff; + font-weight: 600; +} + +.now-playing { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; +} + +.now-playing-text { + font-size: 0.75rem; + color: var(--text-muted); + flex: 0 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.now-playing-stop { + background: var(--bg); + color: var(--text); + border: 1px solid rgba(232, 121, 29, 0.15); + padding: 4px 10px; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 0.75rem; + flex-shrink: 0; + transition: all 0.15s; +} + +.now-playing-stop:hover { + border-color: var(--accent); + background: #2a1e10; +} + +.now-playing-volume { + width: 80px; + flex-shrink: 0; + accent-color: var(--accent); +} + /* Soundboard */ .sounds-section { grid-column: span 2; @@ -1666,6 +1730,16 @@ section h2 { font-size: 0.8rem; } +.media-row .genre-section { + grid-column: span 3; +} + +@media (max-width: 700px) { + .media-row .genre-section { + grid-column: span 1; + } +} + /* Devon (Intern) */ .message.devon { border-left: 3px solid var(--devon); diff --git a/frontend/index.html b/frontend/index.html index e102df4..8e2c020 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -142,13 +142,13 @@
-
-

Music

- -
- - - +
+

Music M

+
+
@@ -359,6 +359,6 @@
- + diff --git a/frontend/js/app.js b/frontend/js/app.js index 06753a0..970c806 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -310,7 +310,6 @@ function initEventListeners() { }); // Music - now server-side - document.getElementById('play-btn')?.addEventListener('click', playMusic); document.getElementById('stop-btn')?.addEventListener('click', stopMusic); document.getElementById('volume')?.addEventListener('input', setMusicVolume); @@ -964,94 +963,126 @@ async function sendTypedMessage() { // --- Music (Server-Side) --- +let genreMap = {}; // { genre: [track, ...] } +let activeGenre = null; +let currentTrackName = ''; + async function loadMusic() { try { const res = await fetch('/api/music'); const data = await res.json(); tracks = data.tracks || []; - const select = document.getElementById('track-select'); - if (!select) return; - - const previousValue = select.value; - select.innerHTML = ''; - // Group tracks by genre - const genres = {}; + genreMap = {}; tracks.forEach(track => { const genre = track.genre || 'Other'; - if (!genres[genre]) genres[genre] = []; - genres[genre].push(track); + if (!genreMap[genre]) genreMap[genre] = []; + genreMap[genre].push(track); }); - // Sort genre names, but put "Other" last - const genreOrder = Object.keys(genres).sort((a, b) => { + // Sort genre names, "Other" last + const genreOrder = Object.keys(genreMap).sort((a, b) => { if (a === 'Other') return 1; if (b === 'Other') return -1; return a.localeCompare(b); }); + // Build genre buttons + const container = document.getElementById('genre-buttons'); + if (!container) return; + container.innerHTML = ''; + genreOrder.forEach(genre => { - const group = document.createElement('optgroup'); - group.label = genre; - // Shuffle within each genre group - const genreTracks = genres[genre]; - for (let i = genreTracks.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [genreTracks[i], genreTracks[j]] = [genreTracks[j], genreTracks[i]]; - } - genreTracks.forEach(track => { - const option = document.createElement('option'); - option.value = track.file; - option.textContent = track.name; - group.appendChild(option); - }); - select.appendChild(group); + const btn = document.createElement('button'); + btn.className = 'genre-btn'; + btn.textContent = genre; + btn.dataset.genre = genre; + btn.addEventListener('click', () => playGenre(genre)); + container.appendChild(btn); }); - // Restore previous selection if it still exists - if (previousValue && [...select.options].some(o => o.value === previousValue)) { - select.value = previousValue; - } - - console.log('Loaded', tracks.length, 'tracks'); + console.log('Loaded', tracks.length, 'tracks in', genreOrder.length, 'genres'); } catch (err) { console.error('loadMusic error:', err); } } -async function playMusic() { - await loadMusic(); - const select = document.getElementById('track-select'); - const track = select?.value; - if (!track) return; +async function playGenre(genre) { + const genreTracks = genreMap[genre]; + if (!genreTracks || genreTracks.length === 0) return; + + // Pick a random track from the genre + const track = genreTracks[Math.floor(Math.random() * genreTracks.length)]; try { const res = await fetch('/api/music/play', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ track, action: 'play' }) + body: JSON.stringify({ track: track.file, action: 'play' }) }); if (!res.ok) throw new Error(res.status); isMusicPlaying = true; + activeGenre = genre; + currentTrackName = track.name; + updateMusicUI(); } catch (err) { log('Music play failed: ' + err.message); } } +async function playMusic() { + // M key toggle — if nothing playing, pick random genre + if (!activeGenre) { + const genres = Object.keys(genreMap); + if (genres.length === 0) { + await loadMusic(); + const g = Object.keys(genreMap); + if (g.length === 0) return; + return playGenre(g[Math.floor(Math.random() * g.length)]); + } + return playGenre(genres[Math.floor(Math.random() * genres.length)]); + } + return playGenre(activeGenre); +} + + async function stopMusic() { try { const res = await fetch('/api/music/stop', { method: 'POST' }); if (!res.ok) throw new Error(res.status); isMusicPlaying = false; + activeGenre = null; + currentTrackName = ''; + updateMusicUI(); } catch (err) { log('Music stop failed: ' + err.message); } } +function updateMusicUI() { + // Highlight active genre button + document.querySelectorAll('.genre-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.genre === activeGenre); + }); + + // Show/hide now playing bar + const nowPlaying = document.getElementById('now-playing'); + const nowText = document.getElementById('now-playing-text'); + if (nowPlaying && nowText) { + if (isMusicPlaying && currentTrackName) { + nowText.textContent = currentTrackName; + nowPlaying.classList.remove('hidden'); + } else { + nowPlaying.classList.add('hidden'); + } + } +} + + let _volumeDebounce = null; function setMusicVolume(e) { const volume = e.target.value / 100;