Feat: Now-Playing serverseitig syncen + in Topbar verschieben

Backend:
- nowPlaying Map trackt aktuell gespielten Sound pro Guild
- SSE broadcast { type: 'nowplaying' } bei play und stop
- nowplaying im SSE-Snapshot für neue Clients
- playFilePath Helper broadcastet ebenfalls (Party Mode)

Frontend:
- SSE-Handler für nowplaying Events (sync über alle Clients)
- Now-Playing als Pill-Badge in der Topbar (rechts, neben Channel)
- Bottombar komplett entfernt
- Fade-in Animation und accent-farbige Pill
- --accent-rgb CSS Variable für alle Themes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bot 2026-03-01 16:00:22 +01:00
parent 4661c366fb
commit f90401a009
3 changed files with 58 additions and 46 deletions

View file

@ -164,6 +164,8 @@ const guildAudioState = new Map<string, GuildAudioState>();
// Partymode: serverseitige Steuerung (global pro Guild)
const partyTimers = new Map<string, NodeJS.Timeout>();
const partyActive = new Set<string>();
// Now-Playing: aktuell gespielter Sound pro Guild
const nowPlaying = new Map<string, string>();
// SSE-Klienten für Broadcasts (z.B. Partymode Status)
const sseClients = new Set<Response>();
function sseBroadcast(payload: any) {
@ -265,6 +267,10 @@ async function playFilePath(guildId: string, channelId: string, filePath: string
state.player.play(resource);
state.currentResource = resource;
state.currentVolume = useVolume;
// Now-Playing broadcast
const soundLabel = relativeKey ? path.parse(relativeKey).name : path.parse(filePath).name;
nowPlaying.set(guildId, soundLabel);
sseBroadcast({ type: 'nowplaying', guildId, name: soundLabel });
if (relativeKey) incrementPlaysFor(relativeKey);
}
@ -1080,6 +1086,9 @@ app.post('/api/play', async (req: Request, res: Response) => {
persistedState.volumes[guildId] = volumeToUse;
writePersistedState(persistedState);
console.log(`${new Date().toISOString()} | player.play() called for ${soundName}`);
// Now-Playing broadcast
nowPlaying.set(guildId!, soundName!);
sseBroadcast({ type: 'nowplaying', guildId, name: soundName });
// Plays zählen (relativer Key verfügbar?)
if (relativePath) incrementPlaysFor(relativePath);
return res.json({ ok: true });
@ -1139,6 +1148,9 @@ app.post('/api/stop', (req: Request, res: Response) => {
const state = guildAudioState.get(guildId);
if (!state) return res.status(404).json({ error: 'Kein aktiver Player' });
state.player.stop(true);
// Now-Playing löschen
nowPlaying.delete(guildId);
sseBroadcast({ type: 'nowplaying', guildId, name: '' });
// Partymode für diese Guild ebenfalls stoppen
try {
const t = partyTimers.get(guildId);
@ -1240,7 +1252,7 @@ app.get('/api/events', (req: Request, res: Response) => {
// Snapshot senden
try {
res.write(`data: ${JSON.stringify({ type: 'snapshot', party: Array.from(partyActive), selected: persistedState.selectedChannels ?? {}, volumes: persistedState.volumes ?? {} })}\n\n`);
res.write(`data: ${JSON.stringify({ type: 'snapshot', party: Array.from(partyActive), selected: persistedState.selectedChannels ?? {}, volumes: persistedState.volumes ?? {}, nowplaying: Object.fromEntries(nowPlaying) })}\n\n`);
} catch {}
// Ping, damit Proxies die Verbindung offen halten