frontend: implements volume slider

This commit is contained in:
2025-02-15 21:16:04 -08:00
parent 9a8f7b9ac5
commit a06bc3960e
3 changed files with 70 additions and 26 deletions

View File

@@ -13,7 +13,7 @@ const AddSongPanel: React.FC<AddSongPanelProps> = ({ onAddURL }) => {
} }
return ( return (
<div className="flex items-center justify-center h-fit bg-black/50 rounded-b-2xl text-white"> <div className="flex items-center justify-center h-fit bg-black/50 md:rounded-b-2xl text-white">
<div className="flex flex-row items-center gap-4 w-full px-8 py-4"> <div className="flex flex-row items-center gap-4 w-full px-8 py-4">
<input <input
type="text" type="text"

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import SongTable from './SongTable'; import SongTable from './SongTable';
import NowPlaying from './NowPlaying'; import NowPlaying from './NowPlaying';
import AddSongPanel from './AddSongPanel'; import AddSongPanel from './AddSongPanel';
@@ -8,20 +8,26 @@ const App: React.FC = () => {
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [nowPlayingSong, setNowPlayingSong] = useState<string | null>(null); const [nowPlayingSong, setNowPlayingSong] = useState<string | null>(null);
const [nowPlayingFileName, setNowPlayingFileName] = useState<string | null>(null); const [nowPlayingFileName, setNowPlayingFileName] = useState<string | null>(null);
const [volume, setVolume] = useState(100);
const [volumeSettingIsLocked, setVolumeSettingIsLocked] = useState(false);
const [songs, setSongs] = useState<PlaylistItem[]>([]); const [songs, setSongs] = useState<PlaylistItem[]>([]);
const [ws, setWs] = useState<WebSocket | null>(null); const [_, setWs] = useState<WebSocket | null>(null);
const fetchPlaylist = async () => { const fetchPlaylist = useCallback(async () => {
const playlist = await API.getPlaylist(); const playlist = await API.getPlaylist();
setSongs(playlist); setSongs(playlist);
}; }, []);
const fetchNowPlaying = async () => { const fetchNowPlaying = useCallback(async () => {
const nowPlaying = await API.getNowPlaying(); const nowPlaying = await API.getNowPlaying();
setNowPlayingSong(nowPlaying.nowPlaying); setNowPlayingSong(nowPlaying.nowPlaying);
setNowPlayingFileName(nowPlaying.currentFile); setNowPlayingFileName(nowPlaying.currentFile);
setIsPlaying(!nowPlaying.isPaused); setIsPlaying(!nowPlaying.isPaused);
};
if (!volumeSettingIsLocked) {
setVolume(nowPlaying.volume);
}
}, [volumeSettingIsLocked]);
const handleAddURL = (url: string) => { const handleAddURL = (url: string) => {
const urlToAdd = url.trim(); const urlToAdd = url.trim();
@@ -71,8 +77,12 @@ const App: React.FC = () => {
fetchNowPlaying(); fetchNowPlaying();
}; };
const watchForEvents = () => { const handleVolumeSettingChange = async (volume: number) => {
const ws = API.subscribeToEvents((event) => { setVolume(volume);
await API.setVolume(volume);
};
const handleWebSocketEvent = useCallback((event: any) => {
switch (event.event) { switch (event.event) {
case 'user_modify': case 'user_modify':
case 'end-file': case 'end-file':
@@ -81,26 +91,29 @@ const App: React.FC = () => {
fetchNowPlaying(); fetchNowPlaying();
break; break;
} }
}); }, [fetchPlaylist, fetchNowPlaying]);
return ws;
};
// Initial data fetch
useEffect(() => { useEffect(() => {
fetchPlaylist(); fetchPlaylist();
fetchNowPlaying(); fetchNowPlaying();
setWs(watchForEvents()); }, [fetchPlaylist, fetchNowPlaying]);
// WebSocket connection
useEffect(() => {
const ws = API.subscribeToEvents(handleWebSocketEvent);
setWs(ws);
return () => { return () => {
if (ws) { if (ws) {
ws.close(); ws.close();
} }
}; };
}, []); }, [handleWebSocketEvent]);
return ( return (
<div className="flex items-center justify-center h-screen w-screen bg-black py-10"> <div className="flex items-center justify-center h-screen w-screen bg-black md:py-10">
<div className="bg-violet-900 w-full md:max-w-xl h-full md:max-h-xl md:border md:rounded-2xl flex flex-col"> <div className="bg-violet-900 w-full md:max-w-2xl h-full md:max-h-xl md:border md:rounded-2xl flex flex-col">
<NowPlaying <NowPlaying
className="flex flex-row md:rounded-t-2xl" className="flex flex-row md:rounded-t-2xl"
songName={nowPlayingSong || "(Not Playing)"} songName={nowPlayingSong || "(Not Playing)"}
@@ -109,6 +122,10 @@ const App: React.FC = () => {
onPlayPause={togglePlayPause} onPlayPause={togglePlayPause}
onSkip={handleSkip} onSkip={handleSkip}
onPrevious={handlePrevious} onPrevious={handlePrevious}
volume={volume}
onVolumeSettingChange={handleVolumeSettingChange}
onVolumeWillChange={() => setVolumeSettingIsLocked(true)}
onVolumeDidChange={() => setVolumeSettingIsLocked(false)}
/> />
{songs.length > 0 ? ( {songs.length > 0 ? (

View File

@@ -1,14 +1,24 @@
import React, { HTMLAttributes } from 'react'; import React, { HTMLAttributes } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { FaPlay, FaPause, FaStepForward, FaStepBackward } from 'react-icons/fa'; import { FaPlay, FaPause, FaStepForward, FaStepBackward, FaVolumeUp } from 'react-icons/fa';
interface NowPlayingProps extends HTMLAttributes<HTMLDivElement> { interface NowPlayingProps extends HTMLAttributes<HTMLDivElement> {
songName: string; songName: string;
fileName: string; fileName: string;
isPlaying: boolean; isPlaying: boolean;
volume: number;
onPlayPause: () => void; onPlayPause: () => void;
onSkip: () => void; onSkip: () => void;
onPrevious: () => void; onPrevious: () => void;
// Sent when the volume setting actually changes value
onVolumeSettingChange: (volume: number) => void;
// Sent when the volume is about to start changing
onVolumeWillChange: (volume: number) => void;
// Sent when the volume has changed
onVolumeDidChange: (volume: number) => void;
} }
const NowPlaying: React.FC<NowPlayingProps> = (props) => { const NowPlaying: React.FC<NowPlayingProps> = (props) => {
@@ -20,13 +30,30 @@ const NowPlaying: React.FC<NowPlayingProps> = (props) => {
<div className="text-white text-lg font-bold truncate">{props.songName}</div> <div className="text-white text-lg font-bold truncate">{props.songName}</div>
<div className="text-white text-sm truncate">{props.fileName}</div> <div className="text-white text-sm truncate">{props.fileName}</div>
</div> </div>
<div className="flex flex-row gap-4"> <div className="flex flex-row items-center gap-4">
<div className="flex items-center gap-2 text-white">
<FaVolumeUp size={20} />
<input
type="range"
min="0"
max="100"
value={props.volume}
onMouseDown={() => props.onVolumeWillChange(props.volume)}
onMouseUp={() => props.onVolumeDidChange(props.volume)}
onChange={(e) => props.onVolumeSettingChange(Number(e.target.value))}
className="w-24 h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:rounded-full hover:[&::-webkit-slider-thumb]:bg-violet-300"
/>
</div>
<button className="text-white hover:text-violet-300 transition-colors" onClick={props.onPrevious}> <button className="text-white hover:text-violet-300 transition-colors" onClick={props.onPrevious}>
<FaStepBackward size={24} /> <FaStepBackward size={24} />
</button> </button>
<button className="text-white hover:text-violet-300 transition-colors" onClick={props.onPlayPause}> <button className="text-white hover:text-violet-300 transition-colors" onClick={props.onPlayPause}>
{props.isPlaying ? <FaPause size={24} /> : <FaPlay size={24} />} {props.isPlaying ? <FaPause size={24} /> : <FaPlay size={24} />}
</button> </button>
<button className="text-white hover:text-violet-300 transition-colors" onClick={props.onSkip}> <button className="text-white hover:text-violet-300 transition-colors" onClick={props.onSkip}>
<FaStepForward size={24} /> <FaStepForward size={24} />
</button> </button>