UI: Volume-Regler in Toolbar verschoben, Spielt-Anzeige persistent

- Volume-Control von Bottom-Bar in Toolbar verschoben (links von Random)
- Pill-förmiges Design passend zu Size-Slider
- "Spielt" Anzeige bleibt bis Stop oder neuer Sound sichtbar
- Anfangsbuchstaben nur beim ersten Sound jeder Gruppe

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bot 2026-03-01 15:37:40 +01:00
parent 5f0b06550e
commit 4661c366fb
2 changed files with 54 additions and 46 deletions

View file

@ -216,12 +216,12 @@ export default function App() {
try { try {
await playSound(s.name, guildId, channelId, volume, s.relativePath); await playSound(s.name, guildId, channelId, volume, s.relativePath);
setLastPlayed(s.name); setLastPlayed(s.name);
setTimeout(() => setLastPlayed(''), 4000);
} catch (e: any) { notify(e?.message || 'Play fehlgeschlagen', 'error'); } } catch (e: any) { notify(e?.message || 'Play fehlgeschlagen', 'error'); }
} }
async function handleStop() { async function handleStop() {
if (!selected) return; if (!selected) return;
setLastPlayed('');
try { await fetch(`/api/stop?guildId=${encodeURIComponent(guildId)}`, { method: 'POST' }); } catch { } try { await fetch(`/api/stop?guildId=${encodeURIComponent(guildId)}`, { method: 'POST' }); } catch { }
} }
@ -421,6 +421,34 @@ export default function App() {
<div className="toolbar-spacer" /> <div className="toolbar-spacer" />
<div className="volume-control">
<span
className="material-icons vol-icon"
onClick={() => {
const newVol = volume > 0 ? 0 : 0.5;
setVolume(newVol);
if (guildId) setVolumeLive(guildId, newVol).catch(() => {});
}}
>
{volume === 0 ? 'volume_off' : volume < 0.5 ? 'volume_down' : 'volume_up'}
</span>
<input
type="range"
className="vol-slider"
min={0}
max={1}
step={0.01}
value={volume}
onChange={async e => {
const v = parseFloat(e.target.value);
setVolume(v);
if (guildId) try { await setVolumeLive(guildId, v); } catch { }
}}
style={{ '--vol': `${Math.round(volume * 100)}%` } as React.CSSProperties}
/>
<span className="vol-pct">{Math.round(volume * 100)}%</span>
</div>
<button className="tb-btn random" onClick={handleRandom} title="Zufälliger Sound"> <button className="tb-btn random" onClick={handleRandom} title="Zufälliger Sound">
<span className="material-icons tb-icon">shuffle</span> <span className="material-icons tb-icon">shuffle</span>
Random Random
@ -576,33 +604,6 @@ export default function App() {
<span className="np-label">Spielt:</span> <span className="np-label">Spielt:</span>
<span className="np-name">{lastPlayed || '—'}</span> <span className="np-name">{lastPlayed || '—'}</span>
</div> </div>
<div className="volume-section">
<span
className="material-icons volume-icon"
onClick={() => {
const newVol = volume > 0 ? 0 : 0.5;
setVolume(newVol);
if (guildId) setVolumeLive(guildId, newVol).catch(() => {});
}}
>
{volume === 0 ? 'volume_off' : volume < 0.5 ? 'volume_down' : 'volume_up'}
</span>
<input
type="range"
className="volume-slider"
min={0}
max={1}
step={0.01}
value={volume}
onChange={async e => {
const v = parseFloat(e.target.value);
setVolume(v);
if (guildId) try { await setVolumeLive(guildId, v); } catch { }
}}
style={{ '--vol': `${Math.round(volume * 100)}%` } as React.CSSProperties}
/>
<span className="volume-pct">{Math.round(volume * 100)}%</span>
</div>
</div> </div>
{/* ═══ CONTEXT MENU ═══ */} {/* ═══ CONTEXT MENU ═══ */}

View file

@ -1025,57 +1025,64 @@ input, select {
.np-wave-bar:nth-child(3) { height: 6px; animation-delay: 240ms; } .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(4) { height: 11px; animation-delay: 80ms; }
/* ── Volume ── */ /* ── Volume Control (Toolbar) ── */
.volume-section { .volume-control {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 6px;
margin-left: auto; padding: 4px 10px;
border-radius: 20px;
background: var(--bg-tertiary);
border: 1px solid rgba(255, 255, 255, .06);
} }
.volume-icon { .vol-icon {
font-size: 18px; font-size: 16px;
color: var(--text-muted); color: var(--text-faint);
cursor: pointer; cursor: pointer;
transition: color var(--transition); transition: color var(--transition);
user-select: none;
} }
.volume-icon:hover { .vol-icon:hover {
color: var(--text-normal); color: var(--text-normal);
} }
.volume-slider { .vol-slider {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
width: 90px; width: 80px;
height: 3px; height: 3px;
border-radius: 2px; border-radius: 2px;
background: linear-gradient(to right, var(--accent) 0%, var(--accent) var(--vol, 80%), var(--bg-tertiary) var(--vol, 80%)); background: linear-gradient(to right, var(--accent) 0%, var(--accent) var(--vol, 80%), var(--bg-modifier-selected) var(--vol, 80%));
outline: none; outline: none;
cursor: pointer; cursor: pointer;
} }
.volume-slider::-webkit-slider-thumb { .vol-slider::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
width: 12px; width: 12px;
height: 12px; height: 12px;
border-radius: 50%; border-radius: 50%;
background: var(--white); background: var(--accent);
box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
cursor: pointer; cursor: pointer;
transition: transform var(--transition);
} }
.volume-slider::-moz-range-thumb { .vol-slider::-webkit-slider-thumb:hover {
transform: scale(1.3);
}
.vol-slider::-moz-range-thumb {
width: 12px; width: 12px;
height: 12px; height: 12px;
border-radius: 50%; border-radius: 50%;
background: var(--white); background: var(--accent);
box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
border: none; border: none;
cursor: pointer; cursor: pointer;
} }
.volume-pct { .vol-pct {
font-size: 11px; font-size: 11px;
color: var(--text-faint); color: var(--text-faint);
min-width: 28px; min-width: 28px;