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, Headphones, } from 'lucide-react'; import AlertModal from './AlertModal'; import AudioVisualizer from './AudioVisualizer'; interface AudioPlayerProps { episode: Episode | null; onClose: () => void; userId?: string; mode?: 'floating' | 'inline'; } // Extracted as stable component to prevent remount on every currentTime update interface MiniPlayerProps { episode: Episode; podcast: { title: string; imageUrl?: string } | null; isPlaying: boolean; isResuming: boolean; currentTime: number; duration: number; playbackSpeed: number; progress: number; fullWidth?: boolean; onTogglePlay: () => void; onSeekRelative: (delta: number) => void; onChangeSpeed: () => void; onExpand: () => void; onClose: () => void; onProgressChange: (e: React.ChangeEvent) => void; } function MiniPlayer({ episode, podcast, isPlaying, isResuming, currentTime, duration, playbackSpeed, progress, fullWidth, onTogglePlay, onSeekRelative, onChangeSpeed, onExpand, onClose, onProgressChange, }: MiniPlayerProps) { const fmt = (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')}`; }; return (
{(episode.imageUrl || podcast?.imageUrl) ? :
🎙️
}

{episode.title}

{isResuming ?

Reprise...

:

{podcast?.title || ''}

}
{fmt(currentTime)}/{fmt(duration)}
); } 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 [isExpanded, setIsExpanded] = useState(false); const [showMiniBar, setShowMiniBar] = useState(false); const inlineRef = useRef(null); useEffect(() => { setUserId(userId ?? null); }, [userId, setUserId]); // IntersectionObserver with debounce + hysteresis to prevent flicker const miniTimer = useRef | null>(null); const miniVisibleSince = useRef(0); useEffect(() => { if (mode !== 'inline' || !inlineRef.current) return; const obs = new IntersectionObserver( ([entry]) => { if (miniTimer.current) clearTimeout(miniTimer.current); const delay = entry.isIntersecting ? 400 : 150; miniTimer.current = setTimeout(() => { if (!entry.isIntersecting) { miniVisibleSince.current = Date.now(); setShowMiniBar(true); } else if (Date.now() - miniVisibleSince.current > 800) { setShowMiniBar(false); } }, delay); }, { threshold: 0.1, rootMargin: '-80px 0px 0px 0px' } ); obs.observe(inlineRef.current); return () => { obs.disconnect(); if (miniTimer.current) clearTimeout(miniTimer.current); }; }, [mode, episode?._id]); const podcast = typeof episode?.podcastId === 'object' ? episode.podcastId : null; const fmt = (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 handleProgress = (e: React.ChangeEvent) => seek(parseFloat(e.target.value)); const handleVolume = (e: React.ChangeEvent) => setVolume(parseFloat(e.target.value)); const closeAlert = () => { setError(null); onClose(); }; const miniPlayerProps = { episode, podcast, isPlaying, isResuming, currentTime, duration, playbackSpeed, progress, onTogglePlay: togglePlay, onSeekRelative: seekRelative, onChangeSpeed: changeSpeed, onExpand: () => setIsExpanded(true), onClose, onProgressChange: handleProgress, }; // ── INLINE MODE ── if (mode === 'inline') { return ( <>
{isPlaying && (
Lecture en cours

{podcast?.title || ''}

{episode.title}

{isResuming && (

Reprise de lecture...

)}
{/* Audio Visualizer */}
{/* Progress */}
{fmt(currentTime)}{fmt(duration)}
{showMiniBar && createPortal(, document.body)} ); } // ── FLOATING MODE ── return ( <> {!isExpanded ? ( ) : (
En cours de lecture
{(episode.imageUrl || podcast?.imageUrl) ? :
🎙️
}

{podcast?.title}

{episode.title}

{episode.description}

{fmt(currentTime)}{fmt(duration)}
)} ); }