feat(ui): 3-Zeilen-Layout (Suche/Channel/Volume/Theme, Media-URL+Abspielen, Admin-Login); Enter-Start; Backend join bei URL-Play

This commit is contained in:
vibe-bot 2025-08-08 16:25:09 +02:00
parent 6d4dba3ad3
commit cfc3f899a2
3 changed files with 40 additions and 7 deletions

View file

@ -606,7 +606,19 @@ app.post('/api/play-url', async (req: Request, res: Response) => {
const guild = client.guilds.cache.get(guildId); const guild = client.guilds.cache.get(guildId);
if (!guild) return res.status(404).json({ error: 'Guild nicht gefunden' }); if (!guild) return res.status(404).json({ error: 'Guild nicht gefunden' });
let state = guildAudioState.get(guildId); let state = guildAudioState.get(guildId);
if (!state) return res.status(400).json({ error: 'Bitte zuerst einen Sound abspielen, um die Verbindung herzustellen' }); if (!state) {
const channel = guild.channels.cache.get(channelId);
if (!channel || (channel.type !== ChannelType.GuildVoice && channel.type !== ChannelType.GuildStageVoice)) {
return res.status(400).json({ error: 'Ungültiger Voice-Channel' });
}
const connection = joinVoiceChannel({ channelId, guildId, adapterCreator: guild.voiceAdapterCreator as any, selfDeaf: false, selfMute: false });
const player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } });
connection.subscribe(player);
state = { connection, player, guildId, channelId, currentVolume: getPersistedVolume(guildId) };
guildAudioState.set(guildId, state);
state.connection = await ensureConnectionReady(connection, channelId, guildId, guild);
attachVoiceLifecycle(state, guild);
}
const useVolume = typeof volume === 'number' ? Math.max(0, Math.min(1, volume)) : state.currentVolume ?? 1; const useVolume = typeof volume === 'number' ? Math.max(0, Math.min(1, volume)) : state.currentVolume ?? 1;

View file

@ -133,7 +133,7 @@ export default function App() {
)} )}
</header> </header>
<section className="controls glass"> <section className="controls glass row1">
<div className="control search"> <div className="control search">
<input <input
value={query} value={query}
@ -173,8 +173,23 @@ export default function App() {
<option value="rainbow">Rainbow Chaos</option> <option value="rainbow">Rainbow Chaos</option>
</select> </select>
</div> </div>
</section>
<section className="controls glass row2">
<div className="control" style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 8 }}> <div className="control" style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 8 }}>
<input value={mediaUrl} onChange={(e) => setMediaUrl(e.target.value)} placeholder="YouTube/Instagram/MP3 URL..." /> <input
value={mediaUrl}
onChange={(e) => setMediaUrl(e.target.value)}
onKeyDown={async (e) => {
if (e.key === 'Enter') {
if (!selected) { setError('Bitte Voice-Channel wählen'); return; }
const [guildId, channelId] = selected.split(':');
try { await playUrl(mediaUrl, guildId, channelId, volume); }
catch (err: any) { setError(err?.message || 'Play-URL fehlgeschlagen'); }
}
}}
placeholder="YouTube/Instagram/MP3 URL..."
/>
<button type="button" className="tab" onClick={async () => { <button type="button" className="tab" onClick={async () => {
if (!selected) { setError('Bitte Voice-Channel wählen'); return; } if (!selected) { setError('Bitte Voice-Channel wählen'); return; }
const [guildId, channelId] = selected.split(':'); const [guildId, channelId] = selected.split(':');
@ -182,7 +197,10 @@ export default function App() {
catch (e: any) { setError(e?.message || 'Play-URL fehlgeschlagen'); } catch (e: any) { setError(e?.message || 'Play-URL fehlgeschlagen'); }
}}> Abspielen</button> }}> Abspielen</button>
</div> </div>
{!isAdmin && ( </section>
{!isAdmin && (
<section className="controls glass row3">
<div className="control" style={{ display: 'flex', gap: 8 }}> <div className="control" style={{ display: 'flex', gap: 8 }}>
<input type="password" value={adminPwd} onChange={(e) => setAdminPwd(e.target.value)} placeholder="Admin Passwort" /> <input type="password" value={adminPwd} onChange={(e) => setAdminPwd(e.target.value)} placeholder="Admin Passwort" />
<button type="button" className="tab" onClick={async () => { <button type="button" className="tab" onClick={async () => {
@ -191,8 +209,8 @@ export default function App() {
else alert('Login fehlgeschlagen'); else alert('Login fehlgeschlagen');
}}>Login</button> }}>Login</button>
</div> </div>
)} </section>
</section> )}
{/* Admin Toolbar */} {/* Admin Toolbar */}
{isAdmin && ( {isAdmin && (

View file

@ -88,7 +88,10 @@ header p { opacity: .8; }
} }
.badge { align-self: flex-start; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.18); padding: 6px 10px; border-radius: 999px; font-size: 13px; } .badge { align-self: flex-start; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.18); padding: 6px 10px; border-radius: 999px; font-size: 13px; }
.controls { display: grid; grid-template-columns: 1fr minmax(240px, 300px) 220px 180px minmax(260px, 1fr); gap: 12px; align-items: center; margin-bottom: 18px; } .controls { display: grid; grid-template-columns: 1fr minmax(240px, 320px) 260px 200px; gap: 12px; align-items: center; margin-bottom: 12px; }
.controls.row2 { grid-template-columns: minmax(400px, 1fr); }
.controls.row3 { grid-template-columns: 1fr; }
.controls.glass { padding: 18px; }
.controls.glass { .controls.glass {
backdrop-filter: saturate(140%) blur(20px); backdrop-filter: saturate(140%) blur(20px);
background: linear-gradient(135deg, rgba(255,255,255,.14), rgba(255,255,255,.06)); background: linear-gradient(135deg, rgba(255,255,255,.14), rgba(255,255,255,.06));