"use client"; import { useAudio } from "@/lib/audio-context"; import { api } from "@/lib/api"; import Image from "next/image"; import Link from "next/link"; import { useRef, useState } from "react"; import { Play, Pause, SkipBack, SkipForward, ChevronDown, Music as MusicIcon, Shuffle, Repeat, Repeat1, AudioWaveform, Loader2, } from "lucide-react"; import { formatTime } from "@/utils/formatTime"; import { cn } from "@/utils/cn"; import { useIsMobile, useIsTablet } from "@/hooks/useMediaQuery"; import { toast } from "sonner"; import { VibeComparisonArt } from "./VibeOverlay"; import { useAudioState } from "@/lib/audio-state-context"; export function OverlayPlayer() { const { currentTrack, currentAudiobook, currentPodcast, playbackType, isPlaying, currentTime, canSeek, downloadProgress, isShuffle, repeatMode, vibeMode, queue, currentIndex, pause, resume, next, previous, returnToPreviousMode, seek, toggleShuffle, toggleRepeat, setUpcoming, startVibeMode, stopVibeMode, duration: playbackDuration, } = useAudio(); // Get current track's audio features for vibe comparison const currentTrackFeatures = queue[currentIndex]?.audioFeatures || null; const isMobile = useIsMobile(); const isTablet = useIsTablet(); const isMobileOrTablet = isMobile || isTablet; // Swipe state for track skipping const touchStartX = useRef(null); const [swipeOffset, setSwipeOffset] = useState(0); const [isVibeLoading, setIsVibeLoading] = useState(false); const duration = (() => { if (playbackType === "podcast" && currentPodcast?.duration) { return currentPodcast.duration; } if (playbackType === "audiobook" && currentAudiobook?.duration) { return currentAudiobook.duration; } return ( playbackDuration || currentTrack?.duration || currentAudiobook?.duration || currentPodcast?.duration || 0 ); })(); if (!currentTrack && !currentAudiobook && !currentPodcast) return null; const displayTime = (() => { if (currentTime > 0) return currentTime; if (playbackType === "audiobook" && currentAudiobook?.progress?.currentTime) { return currentAudiobook.progress.currentTime; } if (playbackType === "podcast" && currentPodcast?.progress?.currentTime) { return currentPodcast.progress.currentTime; } return currentTime; })(); const progress = duration > 0 ? Math.min(100, Math.max(0, (displayTime / duration) * 100)) : 0; const seekEnabled = canSeek; const canSkip = playbackType === "track"; const handleSeek = (e: React.MouseEvent | React.TouchEvent) => { if (!canSeek) return; const rect = e.currentTarget.getBoundingClientRect(); const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX; const x = clientX - rect.left; const percentage = x / rect.width; const time = percentage * duration; seek(time); }; // Swipe handlers for track skipping const handleTouchStart = (e: React.TouchEvent) => { touchStartX.current = e.touches[0].clientX; }; const handleTouchMove = (e: React.TouchEvent) => { if (touchStartX.current === null) return; const deltaX = e.touches[0].clientX - touchStartX.current; setSwipeOffset(Math.max(-100, Math.min(100, deltaX))); }; const handleTouchEnd = () => { if (touchStartX.current === null) return; if (canSkip) { if (swipeOffset > 60) { previous(); } else if (swipeOffset < -60) { next(); } } setSwipeOffset(0); touchStartX.current = null; }; // Handle Vibe toggle const handleVibeToggle = async () => { if (!currentTrack?.id) return; if (vibeMode) { stopVibeMode(); toast.success("Vibe mode off"); return; } setIsVibeLoading(true); try { const response = await api.getRadioTracks("vibe", currentTrack.id, 50); if (response.tracks && response.tracks.length > 0) { const sf = (response as any).sourceFeatures; const sourceFeatures = { bpm: sf?.bpm, energy: sf?.energy, valence: sf?.valence, arousal: sf?.arousal, danceability: sf?.danceability, keyScale: sf?.keyScale, instrumentalness: sf?.instrumentalness, analysisMode: sf?.analysisMode, // ML Mood predictions moodHappy: sf?.moodHappy, moodSad: sf?.moodSad, moodRelaxed: sf?.moodRelaxed, moodAggressive: sf?.moodAggressive, moodParty: sf?.moodParty, moodAcoustic: sf?.moodAcoustic, moodElectronic: sf?.moodElectronic, }; const queueIds = [currentTrack.id, ...response.tracks.map((t: any) => t.id)]; startVibeMode(sourceFeatures, queueIds); setUpcoming(response.tracks, true); // preserveOrder=true for vibe mode toast.success(`Vibe mode on`, { description: `${response.tracks.length} matching tracks queued`, icon: , }); } else { toast.error("Couldn't find matching tracks"); } } catch (error) { console.error("Failed to start vibe match:", error); toast.error("Failed to match vibe"); } finally { setIsVibeLoading(false); } }; // Get current media info let title = ""; let subtitle = ""; let coverUrl: string | null = null; let albumLink: string | null = null; let artistLink: string | null = null; let mediaLink: string | null = null; if (playbackType === "track" && currentTrack) { title = currentTrack.title; subtitle = currentTrack.artist?.name || "Unknown Artist"; coverUrl = currentTrack.album?.coverArt ? api.getCoverArtUrl(currentTrack.album.coverArt, 500) : null; albumLink = currentTrack.album?.id ? `/album/${currentTrack.album.id}` : null; artistLink = currentTrack.artist?.id ? `/artist/${currentTrack.artist.mbid || currentTrack.artist.id}` : null; mediaLink = albumLink; } else if (playbackType === "audiobook" && currentAudiobook) { title = currentAudiobook.title; subtitle = currentAudiobook.author; coverUrl = currentAudiobook.coverUrl ? api.getCoverArtUrl(currentAudiobook.coverUrl, 500) : null; mediaLink = `/audiobooks/${currentAudiobook.id}`; } else if (playbackType === "podcast" && currentPodcast) { title = currentPodcast.title; subtitle = currentPodcast.podcastTitle; coverUrl = currentPodcast.coverUrl ? api.getCoverArtUrl(currentPodcast.coverUrl, 500) : null; const podcastId = currentPodcast.id.split(":")[0]; mediaLink = `/podcasts/${podcastId}`; } return (
{/* Header with close button */}
{/* Now Playing indicator */} Now Playing
{/* Spacer for centering */}
{/* Main Content - Portrait vs Landscape */}
{/* Artwork Section */}
{/* Glow effect */}
{/* Album art OR Vibe Comparison when in vibe mode */}
{vibeMode && currentTrackFeatures ? ( ) : coverUrl ? ( {title} ) : (
)}
{/* Swipe hint indicators */} {canSkip && isMobileOrTablet && Math.abs(swipeOffset) > 20 && (
0 ? "-left-8" : "-right-8" )}> {swipeOffset > 0 ? ( ) : ( )}
)}
{/* Info & Controls Section */}
{/* Track Info */}
{mediaLink ? (

{title}

) : (

{title}

)} {artistLink ? (

{subtitle}

) : (

{subtitle}

)}
{/* Progress Bar */}
{formatTime(displayTime)} {formatTime(duration)}
{/* Main Controls */}
{/* Secondary Controls */}
{/* Safe area padding at bottom */}
); }