import { Play, Pause, SkipBack, SkipForward, Repeat, Shuffle, Volume2, Download, PlusCircle, Mic2, Heart, Loader2, ListMusic, MonitorSpeaker, Maximize2, MoreHorizontal, Info, ChevronUp } from 'lucide-react'; import { usePlayer } from "../context/PlayerContext"; import { useEffect, useRef, useState } from "react"; import { useNavigate, useLocation } from "react-router-dom"; import TechSpecs from './TechSpecs'; import AddToPlaylistModal from "./AddToPlaylistModal"; import Lyrics from './Lyrics'; import QueueModal from './QueueModal'; import { useDominantColor } from '../hooks/useDominantColor'; import { useLyrics } from '../hooks/useLyrics'; export default function PlayerBar() { const { currentTrack, isPlaying, isBuffering, togglePlay, setBuffering, likedTracks, toggleLike, nextTrack, prevTrack, shuffle, toggleShuffle, repeatMode, toggleRepeat, audioQuality, isLyricsOpen, toggleLyrics, closeLyrics, openLyrics, isFullScreenOpen, setIsFullScreenOpen } = usePlayer(); const dominantColor = useDominantColor(currentTrack?.cover_url); const audioRef = useRef(null); const iframeRef = useRef(null); const [progress, setProgress] = useState(0); const [duration, setDuration] = useState(0); const [hasInteractedWithLyrics, setHasInteractedWithLyrics] = useState(false); const { currentLine } = useLyrics( currentTrack?.title || '', currentTrack?.artist || '', progress, isLyricsOpen || hasInteractedWithLyrics // Only fetch if opened or previously interacted ); // Swipe Logic const touchStartY = useRef(null); const handleTouchStart = (e: React.TouchEvent) => { touchStartY.current = e.touches[0].clientY; }; const handleTouchEnd = (e: React.TouchEvent) => { if (touchStartY.current === null) return; const touchEndY = e.changedTouches[0].clientY; const diffY = touchStartY.current - touchEndY; // Swipe Up (positive diff) > 100px (Increased threshold to prevent accidental triggers) if (diffY > 100) { setHasInteractedWithLyrics(true); openLyrics(); // Explicitly Open Lyrics } touchStartY.current = null; }; const [volume, setVolume] = useState(1); const navigate = useNavigate(); const location = useLocation(); // Modal State const [isAddToPlaylistOpen, setIsAddToPlaylistOpen] = useState(false); const [isTechSpecsOpen, setIsTechSpecsOpen] = useState(false); const [isQueueOpen, setIsQueueOpen] = useState(false); const [isInfoOpen, setIsInfoOpen] = useState(false); const [playerMode, setPlayerMode] = useState<'audio' | 'video'>('audio'); const [isIdle, setIsIdle] = useState(false); const [isVideoReady, setIsVideoReady] = useState(false); const idleTimerRef = useRef(null); const resetIdleTimer = () => { setIsIdle(false); if (idleTimerRef.current) clearTimeout(idleTimerRef.current); if (playerMode === 'video' && isPlaying) { idleTimerRef.current = setTimeout(() => { setIsIdle(true); }, 3000); } }; // Force close lyrics on mount (Defensive fix for "Open on first play") useEffect(() => { closeLyrics(); }, []); // Auto-close fullscreen player on navigation useEffect(() => { setPlayerMode('audio'); setIsFullScreenOpen(false); }, [location.pathname]); // Reset to audio mode when track changes useEffect(() => { setPlayerMode('audio'); setIsIdle(false); setIsVideoReady(false); }, [currentTrack?.id]); // Handle idle timer when playing video useEffect(() => { resetIdleTimer(); return () => { if (idleTimerRef.current) clearTimeout(idleTimerRef.current); }; }, [isPlaying, playerMode]); // Handle audio/video mode switching const handleModeSwitch = (mode: 'audio' | 'video') => { if (mode === 'video') { // Pause audio ref but DON'T toggle isPlaying state to false // The iframe useEffect will pick up isPlaying=true and start the video audioRef.current?.pause(); setIsVideoReady(false); // If currently playing, start video playback after iframe loads if (isPlaying && iframeRef.current && iframeRef.current.contentWindow) { setTimeout(() => { if (iframeRef.current?.contentWindow) { iframeRef.current.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'playVideo' }), '*'); } }, 1000); } } else { // Switching back to audio if (isPlaying) { audioRef.current?.play().catch(() => { }); } } setPlayerMode(mode); }; // Handle play/pause for video mode - send command to YouTube iframe only const handleVideoPlayPause = () => { if (playerMode !== 'video' || !iframeRef.current || !iframeRef.current.contentWindow) return; // Send play/pause command directly to YouTube const action = isPlaying ? 'pauseVideo' : 'playVideo'; try { iframeRef.current.contentWindow.postMessage(JSON.stringify({ event: 'command', func: action }), '*'); } catch (e) { // Ignore cross-origin errors } // Toggle local state for UI sync only (audio won't play since it's paused) togglePlay(); }; // ... (rest of useEffects) // ... inside return ... const isDragging = useRef(false); // Audio source effect useEffect(() => { if (currentTrack && audioRef.current && currentTrack.url) { const isSameUrl = audioRef.current.src === currentTrack.url || (currentTrack.url.startsWith('/') && audioRef.current.src.endsWith(currentTrack.url)) || (audioRef.current.src.includes(currentTrack.id)); if (isSameUrl) return; audioRef.current.src = currentTrack.url; if (isPlaying) { audioRef.current.play().catch(e => { if (e.name !== 'AbortError') console.error("Play error:", e); }); } } }, [currentTrack?.url]); // Play/Pause effect - skip when in video mode (YouTube controls playback) useEffect(() => { if (playerMode === 'video') return; // Skip audio control in video mode if (audioRef.current) { if (isPlaying) { audioRef.current.play().catch(e => { if (e.name !== 'AbortError') console.error("Play error:", e); }); if ('mediaSession' in navigator) navigator.mediaSession.playbackState = "playing"; } else { audioRef.current.pause(); if ('mediaSession' in navigator) navigator.mediaSession.playbackState = "paused"; } } }, [isPlaying, playerMode]); // Volume Effect useEffect(() => { if (audioRef.current) { audioRef.current.volume = volume; } }, [volume]); // Note: YouTube iframe play/pause sync is handled via URL autoplay parameter // Cross-origin restrictions prevent reliable postMessage control const handleTimeUpdate = () => { if (audioRef.current) { // Only update progress if NOT dragging, to prevent stutter/fighting if (!isDragging.current) { setProgress(audioRef.current.currentTime); } if (!isNaN(audioRef.current.duration)) { setDuration(audioRef.current.duration); } // Update position state for lock screen if ('mediaSession' in navigator && !isNaN(audioRef.current.duration)) { try { navigator.mediaSession.setPositionState({ duration: audioRef.current.duration, playbackRate: audioRef.current.playbackRate, position: audioRef.current.currentTime }); } catch { /* ignore */ } } } }; // Called while dragging - updates visual slider only const handleSeek = (e: React.ChangeEvent) => { isDragging.current = true; const time = parseFloat(e.target.value); setProgress(time); }; // Called on release - commits the seek to audio engine const handleSeekCommit = () => { if (audioRef.current) { audioRef.current.currentTime = progress; } // Small delay to prevent onTimeUpdate from jumping back immediately setTimeout(() => { isDragging.current = false; }, 200); }; const handleVolume = (e: React.ChangeEvent) => { setVolume(parseFloat(e.target.value)); }; const handleDownload = () => { if (!currentTrack) return; const url = `/api/download?id=${currentTrack.id}&title=${encodeURIComponent(currentTrack.title)}`; window.open(url, '_blank'); }; const formatTime = (time: number) => { if (isNaN(time)) return "0:00"; const minutes = Math.floor(time / 60); const seconds = Math.floor(time % 60); return `${minutes}:${seconds.toString().padStart(2, '0')}`; }; if (!currentTrack) return null; return ( <>
{ if (window.innerWidth < 1024) { setIsFullScreenOpen(true); } }} >
{/* Mobile Full Screen Player Overlay */}
{/* Header / Close */}
{ setPlayerMode('audio'); setIsFullScreenOpen(false); }} className="text-white p-2 hover:bg-white/10 rounded-full transition cursor-pointer">
{/* Song / Video Toggle */}
{/* Content Area */}
{playerMode === 'video' ? ( /* CINEMATIC VIDEO MODE: Full Background Video */
{/* Slight scale to hide any possible edges */} {!isVideoReady && (
)}
{/* Overlay Gradient for cinematic feel */}
) : ( /* SONG MODE: Centered Case */
{currentTrack.title}
)} {/* Controls Overlay (Bottom) */}
{/* Metadata */}

{currentTrack.title}

{ setPlayerMode('audio'); setIsFullScreenOpen(false); navigate(`/artist/${encodeURIComponent(currentTrack.artist)}`); }} className={`text-white/70 font-medium cursor-pointer hover:text-white hover:underline transition drop-shadow-md ${playerMode === 'video' ? 'text-base md:text-xl' : 'text-lg md:text-2xl'}`} > {currentTrack.artist}

{/* Secondary Actions */}
{/* Scrubber & Controls */}
{/* Scrubber */}
{formatTime(progress)} {formatTime(duration)}
{/* Main Playback Controls */}
{/* Song Info Modal (Mobile) */} {isInfoOpen && (

Song Info

Title

{currentTrack.title}

Artist

{ setPlayerMode('audio'); setIsInfoOpen(false); setIsFullScreenOpen(false); navigate(`/artist/${encodeURIComponent(currentTrack.artist)}`); }} > {currentTrack.artist}

Album

{currentTrack.album || 'Single'}

Source: YouTube Music

{currentTrack.duration &&

Duration: {formatTime(currentTrack.duration)}

}
)} {/* Modals */} setIsQueueOpen(false)} /> setIsTechSpecsOpen(false)} quality={audioQuality} trackTitle={currentTrack?.title || ''} /> {isAddToPlaylistOpen && currentTrack && ( setIsAddToPlaylistOpen(false)} /> )} {isLyricsOpen && ( )} ); }