import { ArrowLeftIcon, HeartFillIcon, HeartIcon } from '@primer/octicons-react' import { useEffect, useRef, useState } from 'react' import { JellyImg } from '../components/JellyImg' import { Progressbar } from '../components/Main' import { Squircle } from '../components/Squircle' import { DeletingIcon, DownloadedIcon, DownloadingIcon, LyricsIcon, MoreIcon, QueueIcon, TracksIcon, } from '../components/SvgIcons' import { useDropdownContext } from '../context/DropdownContext/DropdownContext' import { useHistoryContext } from '../context/HistoryContext/HistoryContext' import { useNavigate } from 'react-router-dom' import { usePlaybackContext } from '../context/PlaybackContext/PlaybackContext' import { usePageTitle } from '../context/PageTitleContext/PageTitleContext' import { useJellyfinTrackInfo } from '../hooks/Jellyfin/useJellyfinTrackInfo' import { useDuration } from '../hooks/useDuration' import { useFavorites } from '../hooks/useFavorites' import { Lyrics } from './Lyrics' import './NowPlaying.css' import './NowPlayingLyrics.css' import '../styles/lyricsModal.css' export const NowPlaying = () => { const { setPageTitle } = usePageTitle() const history = useHistoryContext() const navigate = useNavigate() const previousPage = history?.goBack ?? (() => window.history.back()) // Add error boundary for this component try { const { playlistTitle, currentTrack, bitrate, currentPlaylist, currentTrackIndex } = usePlaybackContext() const playback = usePlaybackContext() const duration = useDuration() const { isOpen, selectedItem, onContextMenu } = useDropdownContext() const { addToFavorites, removeFromFavorites } = useFavorites() // const { setDisabled } = useScrollContext() // Marquee activation for long titles const [isMarqueeActive, setIsMarqueeActive] = useState(false) const marqueeViewportRef = useRef(null) const marqueeTextRef = useRef(null) // Lyrics modal state const [showLyrics, setShowLyrics] = useState(false) // Queue modal state const [showQueue, setShowQueue] = useState(false) // Touch gesture handling for mobile swipe-to-close const [touchStart, setTouchStart] = useState(null) const [touchEnd, setTouchEnd] = useState(null) // Minimum swipe distance (in px) const minSwipeDistance = 50 const onTouchStart = (e: React.TouchEvent) => { setTouchEnd(null) setTouchStart(e.targetTouches[0].clientX) } const onTouchMove = (e: React.TouchEvent) => { setTouchEnd(e.targetTouches[0].clientX) } const onTouchEnd = () => { if (!touchStart || !touchEnd) return const distance = touchStart - touchEnd const isRightSwipe = distance < -minSwipeDistance // Swipe right to close if (isRightSwipe) { if (showLyrics) setShowLyrics(false) if (showQueue) setShowQueue(false) } } useEffect(() => { setPageTitle('Now Playing'); }, [setPageTitle]); // Measure if title overflows to toggle marquee useEffect(() => { const measure = () => { try { const viewport = marqueeViewportRef.current const text = marqueeTextRef.current if (!viewport || !text) return const shouldMarquee = text.scrollWidth > viewport.clientWidth + 2 setIsMarqueeActive(shouldMarquee) } catch (error) { console.warn('Error measuring marquee text:', error) setIsMarqueeActive(false) } } // Delay to ensure DOM has rendered new title const t = setTimeout(measure, 0) const handleResize = () => { try { measure() } catch (error) { console.warn('Error in resize handler:', error) } } window.addEventListener('resize', handleResize) return () => { clearTimeout(t) window.removeEventListener('resize', handleResize) } }, [currentTrack?.Name]) // const handleVolumeChange = (e: ChangeEvent) => { // const newVolume = parseFloat(e.target.value) // playback.setVolume(newVolume) // } // const handleVolumeScroll = (e: WheelEvent) => { // e.stopPropagation() // const step = e.deltaY > 0 ? -0.02 : 0.02 // const newVolume = Math.max(0, Math.min(1, playback.volume + step)) // playback.setVolume(newVolume) // } // Do not alter the app's main scroll when opening Now Playing; this breaks back-navigation restoration. // If needed, scope any scroll adjustments to a local container within Now Playing. const handleMoreClick = (e: React.MouseEvent) => { if (!currentTrack) { return } e.stopPropagation() onContextMenu(e, { item: currentTrack }, true, { add_to_favorite: true, remove_from_favorite: true }) } // Prefer a single primary artist for display const fullArtistList = currentTrack?.Artists && currentTrack.Artists.length > 0 ? currentTrack.Artists.join(', ') : 'No Artist' const primaryArtistName = (currentTrack as any)?.AlbumArtist || (currentTrack?.Artists && currentTrack.Artists.length > 0 ? currentTrack.Artists[0] : 'No Artist') // Get IDs for navigation const albumId = currentTrack?.AlbumId const primaryArtistId = currentTrack?.ArtistItems?.[0]?.Id // Helper functions for navigation const handleSongTitleClick = (e?: React.MouseEvent | React.TouchEvent) => { try { if (e) { e.preventDefault(); e.stopPropagation(); } if (albumId) { navigate(`/album/${albumId}`) } } catch (error) { console.error('Error navigating to album:', error) } } const handleArtistClick = (e?: React.MouseEvent | React.TouchEvent) => { try { if (e) { e.preventDefault(); e.stopPropagation(); } if (primaryArtistId) { navigate(`/artist/${primaryArtistId}`) } else if (currentTrack?.ArtistItems?.[0]?.Id) { navigate(`/artist/${currentTrack.ArtistItems[0].Id}`) } } catch (error) { console.error('Error navigating to artist:', error) } } return ( <>
{ if (currentTrack?.ParentId) { navigate(`/album/${currentTrack.ParentId}`) } else if (currentTrack?.AlbumId) { navigate(`/album/${currentTrack.AlbumId}`) } }} style={{ cursor: 'pointer' }}>
Playing From
{playlistTitle || 'No Playlist'}
{ if (currentTrack?.Id) { try { if (currentTrack?.UserData?.IsFavorite) { await removeFromFavorites(currentTrack) } else { await addToFavorites(currentTrack) } } catch (error) { console.error('Failed to update favorite status:', error) } } }} > {currentTrack?.UserData?.IsFavorite ? ( ) : ( )}
{currentTrack && ( )} {!currentTrack && (
)}
{currentTrack?.Name || 'No Track Played'}
{primaryArtistName}
{playback.formatTime(duration.progress)}
{bitrate === 320000 ? 'High' : bitrate === 256000 ? 'Medium' : bitrate === 192000 ? 'Low' : bitrate === 128000 ? 'Minimal' : 'Source'}
{bitrate === 320000 ? ( '320' ) : bitrate === 256000 ? ( '256' ) : bitrate === 192000 ? ( '192' ) : bitrate === 128000 ? ( '128' ) : ( )} {' '} kbps
{currentTrack?.offlineState && (
{currentTrack?.offlineState === 'downloading' && (
)} {currentTrack?.offlineState === 'downloaded' && (
)} {currentTrack?.offlineState === 'deleting' && (
)} {currentTrack?.offlineState === 'accepted' && (
)} {currentTrack?.offlineState === 'processing' && (
)} {currentTrack?.offlineState === 'finished' && (
)} {currentTrack?.offlineState === 'uploading' && (
)}
)}
{playback.formatTime(duration.duration)}
{ if (showLyrics) { setShowLyrics(false) } else { setShowQueue(false) setShowLyrics(true) } }} >
{ if (showQueue) { setShowQueue(false) } else { setShowLyrics(false) setShowQueue(true) } }} >
{currentTrack && }
{/* Lyrics Panel */}

Lyrics

{/* Queue Panel */}

Queue

{currentTrack && ( <>

Now Playing

{currentTrack.Name}
{primaryArtistName}
{currentPlaylist && currentTrackIndex !== undefined && currentPlaylist.slice(currentTrackIndex + 1).length > 0 && (

Playing Next

{currentPlaylist.slice(currentTrackIndex + 1).slice(0, 50).map((track: any, index: number) => { const queueIndex = currentTrackIndex + 1 + index; return (
{ e.preventDefault(); e.currentTarget.classList.add('drag-over'); }} onDragLeave={(e) => { e.currentTarget.classList.remove('drag-over'); }} onDrop={(e) => { e.preventDefault(); e.currentTarget.classList.remove('drag-over'); const draggedIndex = parseInt(e.dataTransfer.getData('text/plain')); const dropIndex = queueIndex; if (draggedIndex !== dropIndex) { playback.moveItemInPlaylist(draggedIndex, dropIndex); } }} onContextMenu={(e) => { e.preventDefault(); onContextMenu(e, { item: track }, true, { add_to_favorite: true, remove_from_favorite: true, move_to_top: true }); }} onTouchStart={(e) => { // Long press detection for mobile const touchTimer = setTimeout(() => { // Trigger context menu on long press const contextEvent = new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: e.touches[0].clientX, clientY: e.touches[0].clientY }); e.currentTarget.dispatchEvent(contextEvent); }, 500); // 500ms long press const clearTimer = () => { clearTimeout(touchTimer); e.currentTarget.removeEventListener('touchend', clearTimer); e.currentTarget.removeEventListener('touchmove', clearTimer); }; e.currentTarget.addEventListener('touchend', clearTimer); e.currentTarget.addEventListener('touchmove', clearTimer); }} onClick={() => { // Jump to this track in the queue playback.playTrack(queueIndex); }} > {index + 1}
{track.Name}
{(track as any)?.AlbumArtist || (track?.Artists && track.Artists.length > 0 ? track.Artists[0] : 'No Artist')}
{ e.dataTransfer.setData('text/plain', queueIndex.toString()); e.currentTarget.closest('.queue-track')?.classList.add('dragging'); e.stopPropagation(); }} onDragEnd={(e) => { e.currentTarget.closest('.queue-track')?.classList.remove('dragging'); }} onMouseDown={(e) => e.stopPropagation()} onTouchStart={(e) => e.stopPropagation()} > ⋮⋮
); })} {currentPlaylist.slice(currentTrackIndex + 1).length > 50 && (
+{currentPlaylist.slice(currentTrackIndex + 1).length - 50} more tracks
)}
)} )}
); } catch (error) { console.error('Error in NowPlaying component:', error); // Return a safe fallback UI return (
window.history.back()} className="return_icon" title="Back">

Something went wrong. Please try refreshing the page.

); } } const TrackBitrate = ({ trackId }: { trackId: string }) => { const trackInfo = useJellyfinTrackInfo(trackId) const bitrate = Math.round((trackInfo.MediaSources?.[0].Bitrate || 0) / 1000) return <>{bitrate} }