import { ArrowLeftIcon, HeartFillIcon, HeartIcon } from '@primer/octicons-react' import { ChangeEvent, useEffect, useRef, useState, WheelEvent } from 'react' import { Link } from 'react-router-dom' 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 { usePlaybackContext } from '../context/PlaybackContext/PlaybackContext' import { useScrollContext } from '../context/ScrollContext/ScrollContext' import { usePageTitle } from '../context/PageTitleContext/PageTitleContext' import { useJellyfinTrackInfo } from '../hooks/Jellyfin/useJellyfinTrackInfo' import { useDuration } from '../hooks/useDuration' import { useFavorites } from '../hooks/useFavorites' import './NowPlayingLyrics.css' export const NowPlaying = () => { const { setPageTitle } = usePageTitle() const history = useHistoryContext() const previousPage = history?.goBack ?? (() => window.history.back()) const { playlistTitle, currentTrack, bitrate } = 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) useEffect(() => { setPageTitle('Now Playing'); }, [setPageTitle]); // Measure if title overflows to toggle marquee useEffect(() => { const measure = () => { const viewport = marqueeViewportRef.current const text = marqueeTextRef.current if (!viewport || !text) return const shouldMarquee = text.scrollWidth > viewport.clientWidth + 2 setIsMarqueeActive(shouldMarquee) } // Delay to ensure DOM has rendered new title const t = setTimeout(measure, 0) window.addEventListener('resize', measure) return () => { clearTimeout(t) window.removeEventListener('resize', measure) } }, [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') return ( <>
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'} {isMarqueeActive && ( <> • {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)}
Volume: {(playback.volume * 100).toFixed(0)}%
setDisabled(true)} onMouseLeave={() => setDisabled(false)} />
{currentTrack && }
) } const TrackBitrate = ({ trackId }: { trackId: string }) => { const trackInfo = useJellyfinTrackInfo(trackId) const bitrate = Math.round((trackInfo.MediaSources?.[0].Bitrate || 0) / 1000) return <>{bitrate} }