"use client"; import { useAudio } from "@/lib/audio-context"; import { api } from "@/lib/api"; import { useIsMobile, useIsTablet } from "@/hooks/useMediaQuery"; import Image from "next/image"; import Link from "next/link"; import { Play, Pause, Maximize2, Music as MusicIcon, SkipBack, SkipForward, Repeat, Repeat1, Shuffle, MonitorUp, RotateCcw, RotateCw, Loader2, AudioWaveform, ChevronLeft, ChevronUp, ChevronDown, } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/utils/cn"; import { useState, useRef, useEffect } from "react"; import { KeyboardShortcutsTooltip } from "./KeyboardShortcutsTooltip"; import { EnhancedVibeOverlay } from "./VibeOverlayEnhanced"; export function MiniPlayer() { const { currentTrack, currentAudiobook, currentPodcast, playbackType, isPlaying, isBuffering, isShuffle, repeatMode, currentTime, duration: playbackDuration, canSeek, downloadProgress, vibeMode, queue, currentIndex, pause, resume, next, previous, toggleShuffle, toggleRepeat, seek, skipForward, skipBackward, setPlayerMode, setUpcoming, startVibeMode, stopVibeMode, } = useAudio(); const isMobile = useIsMobile(); const isTablet = useIsTablet(); const isMobileOrTablet = isMobile || isTablet; const [isVibeLoading, setIsVibeLoading] = useState(false); const [isMinimized, setIsMinimized] = useState(false); const [isDismissed, setIsDismissed] = useState(false); const [swipeOffset, setSwipeOffset] = useState(0); const [isVibePanelExpanded, setIsVibePanelExpanded] = useState(false); const touchStartX = useRef(null); const lastMediaIdRef = useRef(null); // Get current track's audio features for vibe comparison const currentTrackFeatures = queue[currentIndex]?.audioFeatures || null; // Reset dismissed/minimized state when a new track starts playing const currentMediaId = currentTrack?.id || currentAudiobook?.id || currentPodcast?.id; useEffect(() => { if (currentMediaId && currentMediaId !== lastMediaIdRef.current) { lastMediaIdRef.current = currentMediaId; setIsDismissed(false); setIsMinimized(false); } }, [currentMediaId]); // Handle Vibe Match toggle - finds tracks that sound like the current track const handleVibeToggle = async () => { if (!currentTrack?.id) return; // If vibe mode is on, turn it off if (vibeMode) { stopVibeMode(); toast.success("Vibe mode off"); return; } // Otherwise, start vibe mode setIsVibeLoading(true); try { const response = await api.getRadioTracks( "vibe", currentTrack.id, 50 ); if (response.tracks && response.tracks.length > 0) { // Get the source track's features from the API response 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, }; // Start vibe mode with the queue IDs (include current track) const queueIds = [ currentTrack.id, ...response.tracks.map((t: any) => t.id), ]; startVibeMode(sourceFeatures, queueIds); // Add vibe tracks as upcoming (after current song finishes) setUpcoming(response.tracks, true); // preserveOrder=true for vibe mode toast.success(`Vibe mode on`, { description: `${response.tracks.length} matching tracks queued up next`, icon: , }); } else { toast.error("Couldn't find matching tracks in your library"); } } catch (error) { console.error("Failed to start vibe match:", error); toast.error("Failed to match vibe"); } finally { setIsVibeLoading(false); } }; const hasMedia = !!(currentTrack || currentAudiobook || currentPodcast); // Get current media info let title = ""; let subtitle = ""; let coverUrl: 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, 100) : null; mediaLink = currentTrack.album?.id ? `/album/${currentTrack.album.id}` : null; } else if (playbackType === "audiobook" && currentAudiobook) { title = currentAudiobook.title; subtitle = currentAudiobook.author; coverUrl = currentAudiobook.coverUrl ? api.getCoverArtUrl(currentAudiobook.coverUrl, 100) : null; mediaLink = `/audiobooks/${currentAudiobook.id}`; } else if (playbackType === "podcast" && currentPodcast) { title = currentPodcast.title; subtitle = currentPodcast.podcastTitle; coverUrl = currentPodcast.coverUrl ? api.getCoverArtUrl(currentPodcast.coverUrl, 100) : null; const podcastId = currentPodcast.id.split(":")[0]; mediaLink = `/podcasts/${podcastId}`; } else { title = "Not Playing"; subtitle = "Select something to play"; } // Check if controls should be enabled (only for tracks) const canSkip = playbackType === "track"; // Calculate progress percentage 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 ); })(); const progress = duration > 0 ? Math.min(100, Math.max(0, (currentTime / duration) * 100)) : 0; // Handle progress bar click const handleProgressClick = (e: React.MouseEvent) => { if (!canSeek) return; const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const percentage = x / rect.width; const newTime = percentage * duration; seek(newTime); }; const seekEnabled = hasMedia && canSeek; // ============================================ // MOBILE/TABLET: Spotify-style compact player // ============================================ if (isMobileOrTablet) { // Don't render if no media if (!hasMedia) return null; // Handle swipe gestures: // - Swipe RIGHT: minimize to tab // - Swipe LEFT + playing: open overlay // - Swipe LEFT + not playing: dismiss completely 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; // Track both directions, cap at ±150px setSwipeOffset(Math.max(-150, Math.min(150, deltaX))); }; const handleTouchEnd = () => { if (touchStartX.current === null) return; // Swipe RIGHT (positive) → minimize to tab if (swipeOffset > 80) { setIsMinimized(true); } // Swipe LEFT (negative) → open overlay OR dismiss else if (swipeOffset < -80) { if (isPlaying) { // If playing, open full-screen overlay setPlayerMode("overlay"); } else { // If not playing, dismiss completely setIsDismissed(true); } } // Reset setSwipeOffset(0); touchStartX.current = null; }; // Completely dismissed - don't render anything if (isDismissed) { return null; } // Minimized tab - small pill on RIGHT to bring player back if (isMinimized) { return ( ); } // Calculate opacity for swipe feedback const swipeOpacity = 1 - Math.abs(swipeOffset) / 200; return (
{/* Gradient background - richer, more vibrant colors */}
{/* Edge glow effects */}
{/* Progress bar at top */}
{/* Player content */}
setPlayerMode("overlay")} > {/* Album Art */}
{coverUrl ? ( {title} ) : (
)}
{/* Track Info */}

{title}

{subtitle}

{/* Controls - Vibe & Play/Pause */}
e.stopPropagation()} > {/* Vibe Button */} {/* Play/Pause */}
); } // ============================================ // DESKTOP: Full-featured mini player // ============================================ return (
{/* Collapsible Vibe Panel - slides up from player */} {vibeMode && (
setIsVibePanelExpanded(false)} />
)} {/* Vibe Tab - shows when vibe mode is active */} {vibeMode && ( )}
{/* Subtle top glow */}
{/* Progress Bar */}
{seekEnabled && (
)}
{/* Player Content */}
{/* Artwork & Track Info */}
{/* Artwork */} {mediaLink ? (
{coverUrl ? ( {title} ) : ( )}
) : (
)} {/* Track Info */}
{mediaLink ? (

{title}

) : (

{title}

)}

{subtitle}

{/* Mode Switch Buttons */}
{/* Playback Controls */}
{/* Shuffle */} {/* Skip Backward 30s */} {/* Previous */} {/* Play/Pause */} {/* Next */} {/* Skip Forward 30s */} {/* Repeat */} {/* Vibe Mode Toggle */} {/* Keyboard Shortcuts */}
); }