diff --git a/frontend/src/components/AudioPlayer.tsx b/frontend/src/components/AudioPlayer.tsx index b0d5726..3fdae3a 100644 --- a/frontend/src/components/AudioPlayer.tsx +++ b/frontend/src/components/AudioPlayer.tsx @@ -1,7 +1,6 @@ import { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { Episode } from '../services/episodeService'; -import { episodeService } from '../services/episodeService'; import { useAudio } from '../context/AudioContext'; import { Play, @@ -13,7 +12,6 @@ import { X, RotateCcw, RotateCw, - AlertCircle } from 'lucide-react'; import AlertModal from './AlertModal'; import { useTheme } from '../context/ThemeContext'; @@ -26,71 +24,34 @@ interface AudioPlayerProps { } export default function AudioPlayer({ episode, onClose, userId, mode = 'floating' }: AudioPlayerProps) { - const { isPlaying, setIsPlaying, togglePlay } = useAudio(); + const { + isPlaying, + currentTime, + duration, + volume, + playbackSpeed, + isMuted, + isResuming, + error, + togglePlay, + seek, + seekRelative, + setVolume, + toggleMute, + changeSpeed, + setError, + setUserId, + } = useAudio(); const { theme } = useTheme(); - const audioRef = useRef(null); - const [currentTime, setCurrentTime] = useState(0); - const [duration, setDuration] = useState(0); - const [volume, setVolume] = useState(() => parseFloat(localStorage.getItem('podcastic-volume') || '1')); - const [playbackSpeed, setPlaybackSpeed] = useState(() => parseFloat(localStorage.getItem('podcastic-speed') || '1')); - const [isMuted, setIsMuted] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); - const [isResuming, setIsResuming] = useState(false); - const [error, setError] = useState(null); const [showMiniBar, setShowMiniBar] = useState(false); const inlineWrapperRef = useRef(null); - - // Refs for stable tracking without re-renders - const currentTimeRef = useRef(0); - const episodeIdRef = useRef(null); - const userIdRef = useRef(null); - // Sync refs with state + // Sync userId into context so context can save/load progress useEffect(() => { - currentTimeRef.current = currentTime; - }, [currentTime]); - - useEffect(() => { - episodeIdRef.current = episode?._id || null; - userIdRef.current = userId || null; - }, [episode, userId]); - - const podcast = typeof episode?.podcastId === 'object' ? episode.podcastId : null; - - // Sync settings to audio element - useEffect(() => { - if (audioRef.current) { - audioRef.current.volume = isMuted ? 0 : volume; - audioRef.current.playbackRate = playbackSpeed; - } - }, [volume, isMuted, playbackSpeed, episode]); - - // Save settings - useEffect(() => { - localStorage.setItem('podcastic-volume', volume.toString()); - localStorage.setItem('podcastic-speed', playbackSpeed.toString()); - }, [volume, playbackSpeed]); - - // Save progress periodically and on unmount - useEffect(() => { - if (!episode || !userId) return; - - // Save every 20 seconds only if playing - const interval = setInterval(() => { - if (isPlaying && episodeIdRef.current) { - episodeService.saveProgress(episodeIdRef.current, Math.floor(currentTimeRef.current), false).catch(() => {}); - } - }, 20000); - - // Cleanup: SAVE FINAL PROGRESS ON UNMOUNT OR EPISODE CHANGE - return () => { - clearInterval(interval); - if (episodeIdRef.current && userIdRef.current) { - console.log(`[AudioPlayer] Final save for ${episodeIdRef.current} at ${Math.floor(currentTimeRef.current)}s`); - episodeService.saveProgress(episodeIdRef.current, Math.floor(currentTimeRef.current), false).catch(() => {}); - } - }; - }, [episode?._id, userId, isPlaying]); // Restart if episode or isPlaying changes to maintain correct interval behavior + setUserId(userId ?? null); + }, [userId, setUserId]); // IntersectionObserver: show mini-bar when inline player scrolls out of view useEffect(() => { @@ -105,58 +66,7 @@ export default function AudioPlayer({ episode, onClose, userId, mode = 'floating return () => observer.disconnect(); }, [mode, episode?._id]); - // Load previous progress - useEffect(() => { - if (!episode || !userId) return; - setIsResuming(true); - episodeService.getProgress(episode._id).then((r) => { - if (r.progress?.position && audioRef.current) { - console.log(`[AudioPlayer] Resuming at ${r.progress.position}s`); - audioRef.current.currentTime = r.progress.position; - setCurrentTime(r.progress.position); - } - setIsResuming(false); - }).catch(() => { - setIsResuming(false); - }); - }, [episode, userId]); - - const handlePlayPause = () => { - if (!audioRef.current) return; - if (isPlaying) audioRef.current.pause(); - else audioRef.current.play(); - }; - - const changeSpeed = () => { - const speeds = [1, 1.25, 1.5, 2, 0.75]; - const nextIndex = (speeds.indexOf(playbackSpeed) + 1) % speeds.length; - setPlaybackSpeed(speeds[nextIndex]); - }; - - const handleSeek = (delta: number) => { - if (!audioRef.current) return; - audioRef.current.currentTime = Math.max(0, Math.min(duration, currentTime + delta)); - }; - - const handleProgressChange = (e: React.ChangeEvent) => { - const t = parseFloat(e.target.value); - setCurrentTime(t); - if (audioRef.current) audioRef.current.currentTime = t; - }; - - const handleVolumeChange = (e: React.ChangeEvent) => { - const v = parseFloat(e.target.value); - setVolume(v); - setIsMuted(v === 0); - if (audioRef.current) audioRef.current.volume = v; - }; - - const toggleMute = () => { - if (!audioRef.current) return; - const muted = !isMuted; - setIsMuted(muted); - audioRef.current.volume = muted ? 0 : volume; - }; + const podcast = typeof episode?.podcastId === 'object' ? episode.podcastId : null; const formatTime = (s: number) => { if (!isFinite(s)) return '0:00'; @@ -168,26 +78,21 @@ export default function AudioPlayer({ episode, onClose, userId, mode = 'floating 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 ( <>
-
- {/* Mini sticky bar appears when inline player scrolls out of view */} @@ -320,19 +224,19 @@ export default function AudioPlayer({ episode, onClose, userId, mode = 'floating {/* Controls */}
- - -
- {/* Time + close */} + {/* Time + scroll up */}
{formatTime(currentTime)} / @@ -365,32 +269,21 @@ export default function AudioPlayer({ episode, onClose, userId, mode = 'floating ); } + // ── FLOATING MODE (all pages except dashboard) ── return ( <> -