From f90401a009051d1c7c639423b5d473aab8bdfca3 Mon Sep 17 00:00:00 2001 From: Bot Date: Sun, 1 Mar 2026 16:00:22 +0100 Subject: [PATCH] Feat: Now-Playing serverseitig syncen + in Topbar verschieben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/src/index.ts | 14 ++++++++++- web/src/App.tsx | 29 ++++++++++++--------- web/src/styles.css | 61 +++++++++++++++++++++------------------------ 3 files changed, 58 insertions(+), 46 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index e962928..174ea64 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -164,6 +164,8 @@ const guildAudioState = new Map(); // Partymode: serverseitige Steuerung (global pro Guild) const partyTimers = new Map(); const partyActive = new Set(); +// Now-Playing: aktuell gespielter Sound pro Guild +const nowPlaying = new Map(); // SSE-Klienten für Broadcasts (z.B. Partymode Status) const sseClients = new Set(); 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 diff --git a/web/src/App.tsx b/web/src/App.tsx index c88c4c8..b382aaa 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -154,12 +154,20 @@ export default function App() { const g = selectedRef.current?.split(':')[0]; if (g && typeof vols[g] === 'number') setVolume(vols[g]); } catch { } + try { + const np = msg?.nowplaying || {}; + const g = selectedRef.current?.split(':')[0]; + if (g && typeof np[g] === 'string') setLastPlayed(np[g]); + } catch { } } else if (msg?.type === 'channel') { const g = selectedRef.current?.split(':')[0]; if (msg.guildId === g) setSelected(`${msg.guildId}:${msg.channelId}`); } else if (msg?.type === 'volume') { const g = selectedRef.current?.split(':')[0]; if (msg.guildId === g && typeof msg.volume === 'number') setVolume(msg.volume); + } else if (msg?.type === 'nowplaying') { + const g = selectedRef.current?.split(':')[0]; + if (msg.guildId === g) setLastPlayed(msg.name || ''); } }); return () => { try { unsub(); } catch { } }; @@ -362,6 +370,15 @@ export default function App() {
+ {lastPlayed && ( +
+
+
+
+
+ {lastPlayed} +
+ )} {selected && (
@@ -594,18 +611,6 @@ export default function App() { )} - {/* ═══ BOTTOM BAR ═══ */} -
-
-
-
-
-
- Spielt: - {lastPlayed || '—'} -
-
- {/* ═══ CONTEXT MENU ═══ */} {ctxMenu && (