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) <noreply@anthropic.com>
This commit is contained in:
+87
-13
@@ -727,19 +727,6 @@ section h2 {
|
|||||||
margin-bottom: 10px;
|
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 {
|
.music-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -766,6 +753,83 @@ section h2 {
|
|||||||
accent-color: var(--accent);
|
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 */
|
/* Soundboard */
|
||||||
.sounds-section {
|
.sounds-section {
|
||||||
grid-column: span 2;
|
grid-column: span 2;
|
||||||
@@ -1666,6 +1730,16 @@ section h2 {
|
|||||||
font-size: 0.8rem;
|
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) */
|
/* Devon (Intern) */
|
||||||
.message.devon {
|
.message.devon {
|
||||||
border-left: 3px solid var(--devon);
|
border-left: 3px solid var(--devon);
|
||||||
|
|||||||
+8
-8
@@ -142,13 +142,13 @@
|
|||||||
|
|
||||||
<!-- Music / Ads / Idents -->
|
<!-- Music / Ads / Idents -->
|
||||||
<div class="media-row">
|
<div class="media-row">
|
||||||
<section class="music-section">
|
<section class="music-section genre-section">
|
||||||
<h2>Music</h2>
|
<h2>Music <span class="shortcut-label">M</span></h2>
|
||||||
<select id="track-select"></select>
|
<div id="genre-buttons" class="genre-grid"></div>
|
||||||
<div class="music-controls">
|
<div id="now-playing" class="now-playing hidden">
|
||||||
<button id="play-btn">Play <span class="shortcut-label">M</span></button>
|
<span id="now-playing-text" class="now-playing-text"></span>
|
||||||
<button id="stop-btn">Stop</button>
|
<button id="stop-btn" class="now-playing-stop">Stop</button>
|
||||||
<input type="range" id="volume" min="0" max="100" value="30">
|
<input type="range" id="volume" min="0" max="100" value="30" class="now-playing-volume">
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -359,6 +359,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/app.js?v=22"></script>
|
<script src="/js/app.js?v=23"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+70
-39
@@ -310,7 +310,6 @@ function initEventListeners() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Music - now server-side
|
// Music - now server-side
|
||||||
document.getElementById('play-btn')?.addEventListener('click', playMusic);
|
|
||||||
document.getElementById('stop-btn')?.addEventListener('click', stopMusic);
|
document.getElementById('stop-btn')?.addEventListener('click', stopMusic);
|
||||||
document.getElementById('volume')?.addEventListener('input', setMusicVolume);
|
document.getElementById('volume')?.addEventListener('input', setMusicVolume);
|
||||||
|
|
||||||
@@ -964,94 +963,126 @@ async function sendTypedMessage() {
|
|||||||
|
|
||||||
|
|
||||||
// --- Music (Server-Side) ---
|
// --- Music (Server-Side) ---
|
||||||
|
let genreMap = {}; // { genre: [track, ...] }
|
||||||
|
let activeGenre = null;
|
||||||
|
let currentTrackName = '';
|
||||||
|
|
||||||
async function loadMusic() {
|
async function loadMusic() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/music');
|
const res = await fetch('/api/music');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
tracks = data.tracks || [];
|
tracks = data.tracks || [];
|
||||||
|
|
||||||
const select = document.getElementById('track-select');
|
|
||||||
if (!select) return;
|
|
||||||
|
|
||||||
const previousValue = select.value;
|
|
||||||
select.innerHTML = '';
|
|
||||||
|
|
||||||
// Group tracks by genre
|
// Group tracks by genre
|
||||||
const genres = {};
|
genreMap = {};
|
||||||
tracks.forEach(track => {
|
tracks.forEach(track => {
|
||||||
const genre = track.genre || 'Other';
|
const genre = track.genre || 'Other';
|
||||||
if (!genres[genre]) genres[genre] = [];
|
if (!genreMap[genre]) genreMap[genre] = [];
|
||||||
genres[genre].push(track);
|
genreMap[genre].push(track);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort genre names, but put "Other" last
|
// Sort genre names, "Other" last
|
||||||
const genreOrder = Object.keys(genres).sort((a, b) => {
|
const genreOrder = Object.keys(genreMap).sort((a, b) => {
|
||||||
if (a === 'Other') return 1;
|
if (a === 'Other') return 1;
|
||||||
if (b === 'Other') return -1;
|
if (b === 'Other') return -1;
|
||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Build genre buttons
|
||||||
|
const container = document.getElementById('genre-buttons');
|
||||||
|
if (!container) return;
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
genreOrder.forEach(genre => {
|
genreOrder.forEach(genre => {
|
||||||
const group = document.createElement('optgroup');
|
const btn = document.createElement('button');
|
||||||
group.label = genre;
|
btn.className = 'genre-btn';
|
||||||
// Shuffle within each genre group
|
btn.textContent = genre;
|
||||||
const genreTracks = genres[genre];
|
btn.dataset.genre = genre;
|
||||||
for (let i = genreTracks.length - 1; i > 0; i--) {
|
btn.addEventListener('click', () => playGenre(genre));
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
container.appendChild(btn);
|
||||||
[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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Restore previous selection if it still exists
|
console.log('Loaded', tracks.length, 'tracks in', genreOrder.length, 'genres');
|
||||||
if (previousValue && [...select.options].some(o => o.value === previousValue)) {
|
|
||||||
select.value = previousValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Loaded', tracks.length, 'tracks');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('loadMusic error:', err);
|
console.error('loadMusic error:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function playMusic() {
|
async function playGenre(genre) {
|
||||||
await loadMusic();
|
const genreTracks = genreMap[genre];
|
||||||
const select = document.getElementById('track-select');
|
if (!genreTracks || genreTracks.length === 0) return;
|
||||||
const track = select?.value;
|
|
||||||
if (!track) return;
|
// Pick a random track from the genre
|
||||||
|
const track = genreTracks[Math.floor(Math.random() * genreTracks.length)];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/music/play', {
|
const res = await fetch('/api/music/play', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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);
|
if (!res.ok) throw new Error(res.status);
|
||||||
isMusicPlaying = true;
|
isMusicPlaying = true;
|
||||||
|
activeGenre = genre;
|
||||||
|
currentTrackName = track.name;
|
||||||
|
updateMusicUI();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('Music play failed: ' + err.message);
|
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() {
|
async function stopMusic() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/music/stop', { method: 'POST' });
|
const res = await fetch('/api/music/stop', { method: 'POST' });
|
||||||
if (!res.ok) throw new Error(res.status);
|
if (!res.ok) throw new Error(res.status);
|
||||||
isMusicPlaying = false;
|
isMusicPlaying = false;
|
||||||
|
activeGenre = null;
|
||||||
|
currentTrackName = '';
|
||||||
|
updateMusicUI();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('Music stop failed: ' + err.message);
|
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;
|
let _volumeDebounce = null;
|
||||||
function setMusicVolume(e) {
|
function setMusicVolume(e) {
|
||||||
const volume = e.target.value / 100;
|
const volume = e.target.value / 100;
|
||||||
|
|||||||
Reference in New Issue
Block a user