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:
parent
4661c366fb
commit
f90401a009
3 changed files with 58 additions and 46 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</div>
|
||||
|
||||
<div className="topbar-right">
|
||||
{lastPlayed && (
|
||||
<div className="now-playing">
|
||||
<div className="np-waves active">
|
||||
<div className="np-wave-bar" /><div className="np-wave-bar" />
|
||||
<div className="np-wave-bar" /><div className="np-wave-bar" />
|
||||
</div>
|
||||
<span className="np-name">{lastPlayed}</span>
|
||||
</div>
|
||||
)}
|
||||
{selected && (
|
||||
<div className="connection">
|
||||
<span className="conn-dot" />
|
||||
|
|
@ -594,18 +611,6 @@ export default function App() {
|
|||
)}
|
||||
</main>
|
||||
|
||||
{/* ═══ BOTTOM BAR ═══ */}
|
||||
<div className="bottombar">
|
||||
<div className="now-playing">
|
||||
<div className={`np-waves ${lastPlayed ? 'active' : ''}`}>
|
||||
<div className="np-wave-bar" /><div className="np-wave-bar" />
|
||||
<div className="np-wave-bar" /><div className="np-wave-bar" />
|
||||
</div>
|
||||
<span className="np-label">Spielt:</span>
|
||||
<span className="np-name">{lastPlayed || '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ═══ CONTEXT MENU ═══ */}
|
||||
{ctxMenu && (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
--text-faint: #6d6f78;
|
||||
|
||||
--accent: #5865f2;
|
||||
--accent-rgb: 88, 101, 242;
|
||||
--accent-hover: #4752c4;
|
||||
--accent-glow: rgba(88, 101, 242, .45);
|
||||
|
||||
|
|
@ -53,6 +54,7 @@
|
|||
--bg-secondary: #241f35;
|
||||
--bg-tertiary: #2e2845;
|
||||
--accent: #9b59b6;
|
||||
--accent-rgb: 155, 89, 182;
|
||||
--accent-hover: #8e44ad;
|
||||
--accent-glow: rgba(155, 89, 182, .45);
|
||||
}
|
||||
|
|
@ -64,6 +66,7 @@
|
|||
--bg-secondary: #1c2e22;
|
||||
--bg-tertiary: #253a2c;
|
||||
--accent: #2ecc71;
|
||||
--accent-rgb: 46, 204, 113;
|
||||
--accent-hover: #27ae60;
|
||||
--accent-glow: rgba(46, 204, 113, .4);
|
||||
}
|
||||
|
|
@ -75,6 +78,7 @@
|
|||
--bg-secondary: #2f201c;
|
||||
--bg-tertiary: #3d2a24;
|
||||
--accent: #e67e22;
|
||||
--accent-rgb: 230, 126, 34;
|
||||
--accent-hover: #d35400;
|
||||
--accent-glow: rgba(230, 126, 34, .4);
|
||||
}
|
||||
|
|
@ -86,6 +90,7 @@
|
|||
--bg-secondary: #162a42;
|
||||
--bg-tertiary: #1e3652;
|
||||
--accent: #3498db;
|
||||
--accent-rgb: 52, 152, 219;
|
||||
--accent-hover: #2980b9;
|
||||
--accent-glow: rgba(52, 152, 219, .4);
|
||||
}
|
||||
|
|
@ -962,36 +967,25 @@ input, select {
|
|||
max-width: 260px;
|
||||
}
|
||||
|
||||
/* ────────────────────────────────────────────
|
||||
Bottom Bar
|
||||
──────────────────────────────────────────── */
|
||||
.bottombar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 0 20px;
|
||||
height: 48px;
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid rgba(0, 0, 0, .24);
|
||||
z-index: 10;
|
||||
flex-shrink: 0;
|
||||
transition: background .4s ease;
|
||||
}
|
||||
|
||||
/* ── Now Playing (Topbar) ── */
|
||||
.now-playing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background: rgba(var(--accent-rgb, 88, 101, 242), .12);
|
||||
border: 1px solid rgba(var(--accent-rgb, 88, 101, 242), .2);
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
max-width: 200px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
animation: np-fade-in 300ms ease;
|
||||
}
|
||||
|
||||
.np-label {
|
||||
color: var(--text-faint);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
@keyframes np-fade-in {
|
||||
from { opacity: 0; transform: translateX(10px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
.np-name {
|
||||
|
|
@ -1006,7 +1000,8 @@ input, select {
|
|||
display: none;
|
||||
gap: 1.5px;
|
||||
align-items: flex-end;
|
||||
height: 14px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.np-waves.active {
|
||||
|
|
@ -1014,16 +1009,16 @@ input, select {
|
|||
}
|
||||
|
||||
.np-wave-bar {
|
||||
width: 2.5px;
|
||||
width: 2px;
|
||||
background: var(--accent);
|
||||
border-radius: 1px;
|
||||
animation: wave 500ms ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.np-wave-bar:nth-child(1) { height: 4px; animation-delay: 0ms; }
|
||||
.np-wave-bar:nth-child(2) { height: 9px; animation-delay: 120ms; }
|
||||
.np-wave-bar:nth-child(3) { height: 6px; animation-delay: 240ms; }
|
||||
.np-wave-bar:nth-child(4) { height: 11px; animation-delay: 80ms; }
|
||||
.np-wave-bar:nth-child(1) { height: 3px; animation-delay: 0ms; }
|
||||
.np-wave-bar:nth-child(2) { height: 8px; animation-delay: 120ms; }
|
||||
.np-wave-bar:nth-child(3) { height: 5px; animation-delay: 240ms; }
|
||||
.np-wave-bar:nth-child(4) { height: 10px; animation-delay: 80ms; }
|
||||
|
||||
/* ── Volume Control (Toolbar) ── */
|
||||
.volume-control {
|
||||
|
|
@ -1398,10 +1393,6 @@ input, select {
|
|||
font-size: 11px;
|
||||
}
|
||||
|
||||
.bottombar {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.tb-btn span:not(.tb-icon) {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -1416,6 +1407,10 @@ input, select {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.now-playing {
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.toolbar .tb-btn {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue