diff --git a/web/src/components/ConnectionStatus.jsx b/web/src/components/ConnectionStatus.jsx new file mode 100644 index 0000000..2a36ea8 --- /dev/null +++ b/web/src/components/ConnectionStatus.jsx @@ -0,0 +1,82 @@ +import { useState, useEffect } from 'react'; + +const API_URL = import.meta.env.VITE_API_URL || '/api'; + +export default function ConnectionStatus() { + const [channels, setChannels] = useState([]); + const [loading, setLoading] = useState(false); + const [disconnecting, setDisconnecting] = useState(false); + + useEffect(() => { + loadChannels(); + const interval = setInterval(loadChannels, 5000); + return () => clearInterval(interval); + }, []); + + const loadChannels = async () => { + try { + setLoading(true); + const response = await fetch(`${API_URL}/channels`); + const data = await response.json(); + setChannels(data.channels || []); + } catch (error) { + console.error('Failed to load channels:', error); + } finally { + setLoading(false); + } + }; + + const handleDisconnect = async () => { + try { + setDisconnecting(true); + const response = await fetch(`${API_URL}/channels/disconnect`, { + method: 'POST' + }); + + if (response.ok) { + loadChannels(); + } + } catch (error) { + console.error('Failed to disconnect:', error); + } finally { + setDisconnecting(false); + } + }; + + const currentChannel = channels.find(c => c.current); + + if (loading && !currentChannel) { + return ( +
+
Loading...
+
+ ); + } + + if (!currentChannel) { + return ( +
+
+
Disconnected
+
+ ); + } + + return ( +
+
+
+
+ Connected to {currentChannel.name} +
+
+ +
+ ); +} diff --git a/web/src/components/Queue.jsx b/web/src/components/Queue.jsx new file mode 100644 index 0000000..4aaf6ac --- /dev/null +++ b/web/src/components/Queue.jsx @@ -0,0 +1,151 @@ +import { useState, useEffect } from 'react'; + +const API_URL = import.meta.env.VITE_API_URL || '/api'; + +function formatDuration(seconds) { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; +} + +export default function Queue({ onQueueUpdate }) { + const [queue, setQueue] = useState([]); + const [currentTrack, setCurrentTrack] = useState(null); + const [loading, setLoading] = useState(false); + const [draggedIndex, setDraggedIndex] = useState(null); + const [dropTargetIndex, setDropTargetIndex] = useState(null); + + const loadQueue = async () => { + try { + setLoading(true); + const response = await fetch(`${API_URL}/queue`); + const data = await response.json(); + setQueue(data.tracks || []); + setCurrentTrack(data.currentTrack); + } catch (error) { + console.error('Failed to load queue:', error); + } finally { + setLoading(false); + } + }; + + const handleReorder = async (from, to) => { + if (from === to) return; + + try { + await fetch(`${API_URL}/queue/reorder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ from, to }) + }); + loadQueue(); + if (onQueueUpdate) onQueueUpdate(); + } catch (error) { + console.error('Failed to reorder queue:', error); + } + }; + + const handleDragStart = (e, index) => { + setDraggedIndex(index); + e.dataTransfer.effectAllowed = 'move'; + }; + + const handleDragOver = (e, index) => { + e.preventDefault(); + setDropTargetIndex(index); + }; + + const handleDragLeave = () => { + setDropTargetIndex(null); + }; + + const handleDrop = (e, toIndex) => { + e.preventDefault(); + if (draggedIndex !== null && draggedIndex !== toIndex) { + handleReorder(draggedIndex, toIndex); + } + setDraggedIndex(null); + setDropTargetIndex(null); + }; + + const handleDragEnd = () => { + setDraggedIndex(null); + setDropTargetIndex(null); + }; + + useEffect(() => { + loadQueue(); + }, []); + + return ( +
+
+

+ Playback Queue ({queue.length} tracks) +

+
+ + {loading && ( +
Loading...
+ )} + + {!loading && queue.length === 0 && ( +
+

No tracks in library

+

Upload some MP3 files to get started

+
+ )} + + {!loading && queue.length > 0 && ( +
+ {queue.map((track, index) => { + const isCurrentTrack = currentTrack && track.id === currentTrack.id && index === 0; + + return ( +
handleDragStart(e, index)} + onDragOver={(e) => handleDragOver(e, index)} + onDragLeave={handleDragLeave} + onDrop={(e) => handleDrop(e, index)} + onDragEnd={handleDragEnd} + className={`p-3 rounded-lg transition-all cursor-move ${ + isCurrentTrack + ? 'bg-green-600/20 border-2 border-green-500/50' + : draggedIndex === index + ? 'opacity-50 scale-95' + : dropTargetIndex === index + ? 'bg-purple-600/20 border-2 border-purple-500/50' + : 'bg-slate-700/30 hover:bg-slate-700/50' + }`} + > +
+
+ {isCurrentTrack ? '▶' : index + 1} +
+
+
+ {track.title} +
+
+ {track.artist} +
+
+ {track.duration > 0 && ( +
+ {formatDuration(track.duration)} +
+ )} +
+ ⋮⋮ +
+
+
+ ); + })} +
+ )} +
+ ); +}