feat: Radio volume control - server-wide slider synced via SSE
- Server: inlineVolume on AudioResource, POST /api/radio/volume endpoint - Volume persisted per guild, broadcast via SSE to all clients - Frontend: volume slider in bottom bar with debounced API calls - Volume icon changes based on level (muted/low/normal) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
971fd2c3fc
commit
d1ae2db00b
3 changed files with 128 additions and 3 deletions
|
|
@ -65,7 +65,9 @@ export default function RadioTab({ data }: { data: any }) {
|
|||
const [favorites, setFavorites] = useState<Favorite[]>([]);
|
||||
const [showFavorites, setShowFavorites] = useState(false);
|
||||
const [playingLoading, setPlayingLoading] = useState(false);
|
||||
const [volume, setVolume] = useState(0.5);
|
||||
const searchTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
const volumeTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
// ── Fetch initial data ──
|
||||
useEffect(() => {
|
||||
|
|
@ -90,7 +92,14 @@ export default function RadioTab({ data }: { data: any }) {
|
|||
useEffect(() => {
|
||||
if (data?.playing) setNowPlaying(data.playing);
|
||||
if (data?.favorites) setFavorites(data.favorites);
|
||||
}, [data]);
|
||||
// Volume from snapshot or radio_volume event
|
||||
if (data?.volumes && selectedGuild && data.volumes[selectedGuild] != null) {
|
||||
setVolume(data.volumes[selectedGuild]);
|
||||
}
|
||||
if (data?.volume != null && data?.guildId === selectedGuild) {
|
||||
setVolume(data.volume);
|
||||
}
|
||||
}, [data, selectedGuild]);
|
||||
|
||||
// ── Point click handler (stable ref) ──
|
||||
const handlePointClickRef = useRef<(point: any) => void>(undefined);
|
||||
|
|
@ -274,6 +283,20 @@ export default function RadioTab({ data }: { data: any }) {
|
|||
} catch {}
|
||||
}, [selectedPlace]);
|
||||
|
||||
// ── Volume handler (debounced) ──
|
||||
const handleVolume = useCallback((val: number) => {
|
||||
setVolume(val);
|
||||
if (!selectedGuild) return;
|
||||
if (volumeTimeout.current) clearTimeout(volumeTimeout.current);
|
||||
volumeTimeout.current = setTimeout(() => {
|
||||
fetch('/api/radio/volume', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ guildId: selectedGuild, volume: val }),
|
||||
}).catch(console.error);
|
||||
}, 100);
|
||||
}, [selectedGuild]);
|
||||
|
||||
const isFavorite = (stationId: string) => favorites.some(f => f.stationId === stationId);
|
||||
const currentPlaying = selectedGuild ? nowPlaying[selectedGuild] : null;
|
||||
const currentGuild = guilds.find(g => g.id === selectedGuild);
|
||||
|
|
@ -438,6 +461,17 @@ export default function RadioTab({ data }: { data: any }) {
|
|||
<span className="radio-np-name">{currentPlaying.stationName}</span>
|
||||
<span className="radio-np-loc">{currentPlaying.placeName}{currentPlaying.country ? `, ${currentPlaying.country}` : ''}</span>
|
||||
</div>
|
||||
<div className="radio-volume">
|
||||
<span className="radio-volume-icon">{volume === 0 ? '\u{1F507}' : volume < 0.4 ? '\u{1F509}' : '\u{1F50A}'}</span>
|
||||
<input
|
||||
type="range"
|
||||
className="radio-volume-slider"
|
||||
min={0} max={1} step={0.01}
|
||||
value={volume}
|
||||
onChange={e => handleVolume(Number(e.target.value))}
|
||||
/>
|
||||
<span className="radio-volume-val">{Math.round(volume * 100)}%</span>
|
||||
</div>
|
||||
<span className="radio-np-ch">{'\u{1F50A}'} {currentPlaying.channelName}</span>
|
||||
<button className="radio-btn-stop" onClick={handleStop}>{'\u23F9'} Stop</button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue