"use client"; import { useRef, useCallback, useEffect } from "react"; import { useAudioPlayer } from "./audio-context"; import { cn } from "@/lib/utils"; import Link from "next/link"; import Image from "next/image"; import { Play, Pause, SkipBack, SkipForward, Volume2, VolumeX, Volume1, X, ExternalLink, Loader2, } from "lucide-react"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const FALLBACK_COVER = "/images/cover.png"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function formatTime(seconds: number): string { if (!isFinite(seconds) || seconds < 0) return "0:00"; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; return `${m}:${String(s).padStart(2, "0")}`; } function VolumeIcon({ volume, isMuted, className, }: { volume: number; isMuted: boolean; className?: string; }) { if (isMuted || volume === 0) return ; if (volume < 0.5) return ; return ; } // --------------------------------------------------------------------------- // Progress Bar // --------------------------------------------------------------------------- function ProgressBar({ currentTime, duration, onSeek, thin = false, }: { currentTime: number; duration: number; onSeek: (time: number) => void; thin?: boolean; }) { const barRef = useRef(null); const draggingRef = useRef(false); const progress = duration > 0 ? (currentTime / duration) * 100 : 0; const calculateSeekTime = useCallback( (clientX: number) => { const bar = barRef.current; if (!bar || duration <= 0) return; const rect = bar.getBoundingClientRect(); const fraction = Math.max( 0, Math.min(1, (clientX - rect.left) / rect.width), ); onSeek(fraction * duration); }, [duration, onSeek], ); const handlePointerDown = useCallback( (e: React.PointerEvent) => { e.preventDefault(); draggingRef.current = true; (e.target as HTMLElement).setPointerCapture(e.pointerId); calculateSeekTime(e.clientX); }, [calculateSeekTime], ); const handlePointerMove = useCallback( (e: React.PointerEvent) => { if (!draggingRef.current) return; calculateSeekTime(e.clientX); }, [calculateSeekTime], ); const handlePointerUp = useCallback(() => { draggingRef.current = false; }, []); if (thin) { return (
); } return (
{formatTime(currentTime)}
{formatTime(duration)}
); } // --------------------------------------------------------------------------- // Transport Button // --------------------------------------------------------------------------- function TransportButton({ onClick, disabled = false, label, size = "default", children, className, }: { onClick: () => void; disabled?: boolean; label: string; size?: "default" | "large"; children: React.ReactNode; className?: string; }) { return ( ); } // --------------------------------------------------------------------------- // Floating Player // --------------------------------------------------------------------------- export function FloatingPlayer() { const { currentTrack, isPlaying, currentTime, duration, volume, isMuted, isLoading, playlist, currentIndex, togglePlayPause, stop, seekTo, setVolume, toggleMute, next, previous, } = useAudioPlayer(); const playerRef = useRef(null); // Add bottom padding to body when player is visible useEffect(() => { if (!currentTrack) return; document.body.style.paddingBottom = "96px"; return () => { document.body.style.paddingBottom = ""; }; }, [currentTrack]); // Keyboard shortcuts when player is focused const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { switch (e.key) { case " ": e.preventDefault(); togglePlayPause(); break; case "ArrowLeft": e.preventDefault(); seekTo(Math.max(0, currentTime - 10)); break; case "ArrowRight": e.preventDefault(); seekTo(Math.min(duration, currentTime + 10)); break; case "ArrowUp": e.preventDefault(); setVolume(Math.min(1, volume + 0.05)); break; case "ArrowDown": e.preventDefault(); setVolume(Math.max(0, volume - 0.05)); break; case "m": case "M": e.preventDefault(); toggleMute(); break; case "Escape": e.preventDefault(); stop(); break; } }, [ togglePlayPause, seekTo, setVolume, toggleMute, stop, currentTime, duration, volume, ], ); if (!currentTrack) return null; const hasNext = currentIndex >= 0 && currentIndex < playlist.length - 1; const hasPrev = currentIndex > 0; const coverSrc = currentTrack.coverImage || FALLBACK_COVER; return (
{/* Thin progress bar — absolutely positioned at top edge */} {/* Time row — desktop (hidden on mobile, mobile has its own) */}
{formatTime(currentTime)} {formatTime(duration)}
{/* ── Desktop layout ── */}
{/* Left: Cover + Track info */}
{/* Cover image */}

{currentTrack.title}

{currentTrack.subtitle && (

{currentTrack.subtitle}

)}
{/* Center: Transport controls */}
{isLoading ? ( ) : isPlaying ? ( ) : ( )}
{/* Right: Volume + Source link + Close */}
{/* Volume control */}
setVolume(Number(e.target.value) / 100)} aria-label="Volume" className={cn( "w-16 h-1 appearance-none bg-white/15 rounded-full cursor-pointer", "[&::-webkit-slider-thumb]:appearance-none", "[&::-webkit-slider-thumb]:size-3", "[&::-webkit-slider-thumb]:rounded-full", "[&::-webkit-slider-thumb]:bg-white", "[&::-webkit-slider-thumb]:cursor-pointer", "[&::-moz-range-thumb]:size-3", "[&::-moz-range-thumb]:rounded-full", "[&::-moz-range-thumb]:bg-white", "[&::-moz-range-thumb]:border-0", "[&::-moz-range-thumb]:cursor-pointer", )} />
{/* Source link */} {currentTrack.sourceUrl && ( )} {/* Close */}
{/* ── Mobile layout ── */}
{/* Time row — right below seeker */}
{formatTime(currentTime)} {formatTime(duration)}
{/* Row 1: Cover + Title + Source + Close */}
{/* Cover image */}
{/* Track info */}

{currentTrack.title}

{currentTrack.subtitle && (

{currentTrack.subtitle}

)}
{/* Source link */} {currentTrack.sourceUrl && ( )} {/* Close */}
{/* Row 2: Prev + Play/Pause + Next — centered */}
{isLoading ? ( ) : isPlaying ? ( ) : ( )}
); }