import React, { useState, useEffect, useCallback, ReactNode } from 'react'; import SongTable from './SongTable'; import NowPlaying from './NowPlaying'; import AddSongPanel from './AddSongPanel'; import RenameFavoriteModal from './RenameFavoriteModal'; import { TabView, Tab } from './TabView'; import { API, getDisplayTitle, PlaylistItem, ServerEvent } from '../api/player'; import { FaMusic, FaHeart, FaPlus, FaEdit } from 'react-icons/fa'; import useWebSocket from 'react-use-websocket'; import classNames from 'classnames'; import { useScreenShare } from '../hooks/useScreenShare'; enum Tabs { Playlist = "playlist", Favorites = "favorites", } const EmptyContent: React.FC<{ label: string}> = ({label}) => (
{label}
); interface SonglistContentProps { songs: PlaylistItem[]; isPlaying: boolean; auxControlProvider?: (song: PlaylistItem) => ReactNode; onNeedsRefresh: () => void; } const PlaylistContent: React.FC = ({ songs, isPlaying, auxControlProvider, 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, auxControlProvider, onNeedsRefresh }) => { const handleDelete = (index: number) => { API.removeFromFavorites(songs[index].filename); 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 [isIdle, setIsIdle] = 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 [isRenameModalOpen, setIsRenameModalOpen] = useState(false); const [favoriteToRename, setFavoriteToRename] = useState(null); const { isScreenSharing, isScreenSharingSupported, toggleScreenShare, stopScreenShare } = useScreenShare(); 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); setIsIdle(nowPlaying.playingItem ? !nowPlaying.playingItem.playing : true); }, [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 handleStop = async () => { stopScreenShare(); await API.stop(); 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]); const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/api/events`; console.log('Connecting to WebSocket at', wsUrl); useWebSocket(wsUrl, { 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]); const AuxButton: React.FC<{ children: ReactNode, className: string, title: string, onClick: () => void }> = (props) => ( ); const playlistAuxControlProvider = (song: PlaylistItem) => { const isFavorite = favorites.some(f => f.filename === song.filename); return ( { if (isFavorite) { API.removeFromFavorites(song.filename); } else { API.addToFavorites(song.filename); } }} > ); }; const favoritesAuxControlProvider = (song: PlaylistItem) => { const isInPlaylist = playlist.some(p => p.filename === song.filename); return (
{ setFavoriteToRename(song); setIsRenameModalOpen(true); }} > { if (isInPlaylist) { API.removeFromPlaylist(playlist.findIndex(p => p.filename === song.filename)); } else { API.addToPlaylist(song.filename); } }} >
); }; return (
setVolumeSettingIsLocked(true)} onVolumeDidChange={() => setVolumeSettingIsLocked(false)} isScreenSharingSupported={isScreenSharingSupported} /> }> }> ({ ...f, playing: f.filename === nowPlayingFileName }))} isPlaying={isPlaying} onNeedsRefresh={refreshContent} auxControlProvider={favoritesAuxControlProvider} /> setIsRenameModalOpen(false)} favorite={favoriteToRename} />
); }; export default App;