import React, { useState, useEffect, useCallback } from 'react'; import SongTable from './SongTable'; import NowPlaying from './NowPlaying'; import AddSongPanel from './AddSongPanel'; import { TabView, Tab } from './TabView'; import { API, getDisplayTitle, PlaylistItem, ServerEvent } from '../api/player'; import { FaMusic, FaHeart } from 'react-icons/fa'; import useWebSocket from 'react-use-websocket'; enum Tabs { Playlist = "playlist", Favorites = "favorites", } const EmptyContent: React.FC<{ label: string}> = ({label}) => (
{label}
); interface SonglistContentProps { songs: PlaylistItem[]; isPlaying: boolean; onNeedsRefresh: () => void; } const PlaylistContent: React.FC = ({ songs, isPlaying, onNeedsRefresh }) => { const handleDelete = (index: number) => { API.removeFromPlaylist(index); onNeedsRefresh(); }; const handleSkipTo = (index: number) => { API.skipTo(index); onNeedsRefresh(); }; return ( songs.length > 0 ? ( ) : ( ) ); }; const FavoritesContent: React.FC = ({ songs, isPlaying, onNeedsRefresh }) => { const handleDelete = (index: number) => { API.removeFromFavorites(index); onNeedsRefresh(); }; const handleSkipTo = (index: number) => { API.replaceCurrentFile(songs[index].filename); API.play(); onNeedsRefresh(); }; return ( songs.length > 0 ? ( ) : ( ) ); }; const App: React.FC = () => { const [isPlaying, setIsPlaying] = useState(false); const [nowPlayingSong, setNowPlayingSong] = useState(null); const [nowPlayingFileName, setNowPlayingFileName] = useState(null); const [volume, setVolume] = useState(100); const [volumeSettingIsLocked, setVolumeSettingIsLocked] = useState(false); const [playlist, setPlaylist] = useState([]); const [favorites, setFavorites] = useState([]); const [selectedTab, setSelectedTab] = useState(Tabs.Playlist); const fetchPlaylist = useCallback(async () => { const playlist = await API.getPlaylist(); setPlaylist(playlist); }, []); const fetchFavorites = useCallback(async () => { const favorites = await API.getFavorites(); setFavorites(favorites); }, []); const fetchNowPlaying = useCallback(async () => { if (volumeSettingIsLocked) { // We are actively changing the volume, which we do actually want to send events // continuously to the server, but we don't want to refresh our state while doing that. return; } const nowPlaying = await API.getNowPlaying(); setNowPlayingSong(getDisplayTitle(nowPlaying.playingItem)); setNowPlayingFileName(nowPlaying.playingItem.filename); setIsPlaying(!nowPlaying.isPaused); setVolume(nowPlaying.volume); }, [volumeSettingIsLocked]); const handleAddURL = async (url: string) => { const urlToAdd = url.trim(); if (urlToAdd) { if (selectedTab === Tabs.Favorites) { await API.addToFavorites(urlToAdd); fetchFavorites(); } else { await API.addToPlaylist(urlToAdd); fetchPlaylist(); } if (!isPlaying) { await API.play(); } } }; const togglePlayPause = async () => { if (isPlaying) { await API.pause(); } else { await API.play(); } fetchNowPlaying(); }; const handleSkip = async () => { await API.skip(); fetchNowPlaying(); }; const handlePrevious = async () => { await API.previous(); fetchNowPlaying(); }; const handleVolumeSettingChange = async (volume: number) => { setVolume(volume); await API.setVolume(volume); }; const handleWebSocketEvent = useCallback((message: MessageEvent) => { const event = JSON.parse(message.data); switch (event.event) { case ServerEvent.PlaylistUpdate: case ServerEvent.NowPlayingUpdate: case ServerEvent.MetadataUpdate: case ServerEvent.MPDUpdate: fetchPlaylist(); fetchNowPlaying(); break; case ServerEvent.VolumeUpdate: if (!volumeSettingIsLocked) { fetchNowPlaying(); } break; case ServerEvent.FavoritesUpdate: fetchFavorites(); break; } }, [fetchPlaylist, fetchNowPlaying, fetchFavorites]); useWebSocket('/api/events', { onOpen: () => { console.log('WebSocket connected'); }, onClose: () => { console.log('WebSocket disconnected'); }, onError: (error) => { console.error('WebSocket error:', error); }, onMessage: handleWebSocketEvent, shouldReconnect: () => true, }); // Handle visibility changes useEffect(() => { const handleVisibilityChange = () => { if (document.visibilityState === 'visible') { fetchPlaylist(); fetchNowPlaying(); } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [fetchPlaylist, fetchNowPlaying]); const refreshContent = () => { fetchPlaylist(); fetchNowPlaying(); fetchFavorites(); } // Initial data fetch useEffect(() => { fetchPlaylist(); fetchNowPlaying(); fetchFavorites(); }, [fetchPlaylist, fetchNowPlaying, fetchFavorites]); return (
setVolumeSettingIsLocked(true)} onVolumeDidChange={() => setVolumeSettingIsLocked(false)} /> }> }> ({ ...f, playing: f.filename === nowPlayingFileName }))} isPlaying={isPlaying} onNeedsRefresh={refreshContent} />
); }; export default App;