import { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { Episode } from '../services/episodeService'; import { useAudio } from '../context/AudioContext'; import { Play, Pause, Volume2, VolumeX, ChevronDown, Maximize2, X, RotateCcw, RotateCw, } from 'lucide-react'; import AlertModal from './AlertModal'; import AudioVisualizer from './AudioVisualizer'; import { useTheme } from '../context/ThemeContext'; interface AudioPlayerProps { episode: Episode | null; onClose: () => void; userId?: string; mode?: 'floating' | 'inline'; } export default function AudioPlayer({ episode, onClose, userId, mode = 'floating' }: AudioPlayerProps) { const { isPlaying, currentTime, duration, volume, playbackSpeed, isMuted, isResuming, error, togglePlay, seek, seekRelative, setVolume, toggleMute, changeSpeed, setError, setUserId, } = useAudio(); const { theme } = useTheme(); const [isExpanded, setIsExpanded] = useState(false); const [showMiniBar, setShowMiniBar] = useState(false); const inlineWrapperRef = useRef(null); // Sync userId into context so context can save/load progress useEffect(() => { setUserId(userId ?? null); }, [userId, setUserId]); // IntersectionObserver: show mini-bar when inline player scrolls out of view useEffect(() => { if (mode !== 'inline' || !inlineWrapperRef.current) return; const observer = new IntersectionObserver( ([entry]) => { setShowMiniBar(!entry.isIntersecting); }, { threshold: 0, rootMargin: '-40px 0px 0px 0px' } ); observer.observe(inlineWrapperRef.current); return () => observer.disconnect(); }, [mode, episode?._id]); const podcast = typeof episode?.podcastId === 'object' ? episode.podcastId : null; const formatTime = (s: number) => { if (!isFinite(s)) return '0:00'; const m = Math.floor(s / 60); const sec = Math.floor(s % 60); return `${m}:${sec.toString().padStart(2, '0')}`; }; if (!episode) return null; const progress = duration > 0 ? (currentTime / duration) * 100 : 0; const inverseTheme = theme === 'light' ? 'dark-theme' : 'light-theme'; const handleProgressChange = (e: React.ChangeEvent) => { seek(parseFloat(e.target.value)); }; const handleVolumeChange = (e: React.ChangeEvent) => { setVolume(parseFloat(e.target.value)); }; // ── INLINE MODE (embedded in hero card) ── if (mode === 'inline') { return ( <>
{ setError(null); onClose(); }} /> {/* Glow backdrop */}
{/* Artwork */}
{/* Halo aura derrière l'artwork */} {isPlaying && ( {/* Info + controls */}
Lecture en cours

{podcast?.title || ''}

{episode.title}

{isResuming && (

Reprise de lecture...

)}
{/* Progress */}
{formatTime(currentTime)} {formatTime(duration)}
{/* Visualiseur audio */}
{/* Controls */}
{/* Mini sticky bar appears when inline player scrolls out of view */} {showMiniBar && createPortal(
{/* Artwork */}
{(episode.imageUrl || podcast?.imageUrl) ? :
🎙️
}
{/* Title */}

{episode.title}

{podcast?.title || ''}

{/* Controls */}
{/* Time + scroll up */}
{formatTime(currentTime)} / {formatTime(duration)}
{/* Progress bar */}
, document.body )} ); } // ── FLOATING MODE (all pages except dashboard) ── return ( <> { setError(null); onClose(); }} /> {/* ── COMPACT UNIFIED MINI-BAR ── */} {!isExpanded && (
{/* Artwork */}
{(episode.imageUrl || podcast?.imageUrl) ? :
🎙️
}
{/* Title */}

{episode.title}

{isResuming ? (

Reprise de lecture...

) : (

{podcast?.title || ''}

)}
{/* Controls */}
{/* Time */}
{formatTime(currentTime)} / {formatTime(duration)}
{/* Speed + Volume + Expand + Close */}
{/* Progress bar */}
)} {/* ── EXPANDED VIEW ── */} {isExpanded && (
{/* Top bar */}

En cours de lecture

{/* Artwork */}
{(episode.imageUrl || podcast?.imageUrl) ? :
🎙️
}
{/* Info + controls */}

{podcast?.title}

{episode.title}

{episode.description}

{/* Progress */}
{formatTime(currentTime)} {formatTime(duration)}
{/* Controls */}
{/* Volume */}
)} ); }