"use client"; import { useRef, useState, useEffect } from "react"; import { api } from "@/lib/api"; import { useToast } from "@/lib/toast-context"; import { Button } from "@/components/ui/Button"; import { Modal } from "@/components/ui/Modal"; import { Play, Pause, SkipForward, SkipBack, Volume2, VolumeX, X, RotateCcw, } from "lucide-react"; import { cn } from "@/utils/cn"; import { formatTime } from "@/utils/formatTime"; interface Episode { id: string; title: string; description?: string; duration: number; publishedAt: string; episodeNumber?: number; season?: number; progress?: { currentTime: number; progress: number; isFinished: boolean; lastPlayedAt: Date; }; } interface PodcastPlayerProps { podcastId: string; episode: Episode; onClose: () => void; onEpisodeChange?: (episode: Episode) => void; } export function PodcastPlayer({ podcastId, episode, onClose, onEpisodeChange, }: PodcastPlayerProps) { const audioRef = useRef(null); const progressIntervalRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState( episode.progress?.currentTime || 0 ); const [duration, setDuration] = useState(episode.duration || 0); const [volume, setVolume] = useState(1); const [isMuted, setIsMuted] = useState(false); const [playbackSpeed, setPlaybackSpeed] = useState(1); const [showConfirmModal, setShowConfirmModal] = useState(false); const [isRemovingProgress, setIsRemovingProgress] = useState(false); const { toast } = useToast(); // Resume from last position useEffect(() => { if (audioRef.current && episode.progress?.currentTime) { audioRef.current.currentTime = episode.progress.currentTime; setCurrentTime(episode.progress.currentTime); } }, [episode]); // Save progress periodically while playing useEffect(() => { if (!isPlaying || !audioRef.current) { if (progressIntervalRef.current) { clearInterval(progressIntervalRef.current); progressIntervalRef.current = null; } return; } // Save progress every 10 seconds progressIntervalRef.current = setInterval(async () => { if (audioRef.current && currentTime > 0) { const duration = audioRef.current.duration || episode.duration || 0; const isFinished = duration - currentTime < 30; try { await api.updatePodcastEpisodeProgress( podcastId, episode.id, currentTime, duration, isFinished ); } catch (error) { console.error("Failed to sync podcast progress:", error); } } }, 10000); return () => { if (progressIntervalRef.current) { clearInterval(progressIntervalRef.current); } }; }, [isPlaying, currentTime, podcastId, episode.id, episode.duration]); // Track time updates const handleTimeUpdate = () => { if (audioRef.current) { setCurrentTime(audioRef.current.currentTime); } }; // Track duration const handleLoadedMetadata = () => { if (audioRef.current) { setDuration(audioRef.current.duration); } }; // Play/pause handler const handlePlayPause = () => { if (!audioRef.current) return; if (isPlaying) { audioRef.current.pause(); } else { audioRef.current.play(); } }; // Save on pause const handlePause = async () => { setIsPlaying(false); if (audioRef.current) { try { await api.updatePodcastEpisodeProgress( podcastId, episode.id, audioRef.current.currentTime, audioRef.current.duration || duration, false ); } catch (error) { console.error("Failed to save podcast progress on pause:", error); } } }; // Save when finished const handleEnded = async () => { setIsPlaying(false); if (audioRef.current) { try { await api.updatePodcastEpisodeProgress( podcastId, episode.id, audioRef.current.duration, audioRef.current.duration, true ); toast.success("Episode finished!"); onEpisodeChange?.(episode); } catch (error) { console.error("Failed to save podcast progress on end:", error); } } }; const handleSkip = (seconds: number) => { if (audioRef.current) { audioRef.current.currentTime += seconds; } }; const handleSeek = (value: number) => { if (audioRef.current) { audioRef.current.currentTime = value; setCurrentTime(value); } }; const handleVolumeChange = (value: number) => { setVolume(value); if (audioRef.current) { audioRef.current.volume = value; } setIsMuted(value === 0); }; const handleMuteToggle = () => { if (isMuted) { handleVolumeChange(volume || 0.5); } else { handleVolumeChange(0); } }; const handleSpeedChange = (speed: number) => { setPlaybackSpeed(speed); if (audioRef.current) { audioRef.current.playbackRate = speed; } }; const handleRemoveProgress = async () => { setShowConfirmModal(false); setIsRemovingProgress(true); try { await api.deletePodcastEpisodeProgress(podcastId, episode.id); if (audioRef.current) { audioRef.current.currentTime = 0; setCurrentTime(0); } toast.success("Progress removed"); onEpisodeChange?.(episode); } catch (error) { console.error("Failed to remove progress:", error); toast.error("Failed to remove progress"); } finally { setIsRemovingProgress(false); } }; const progress = duration > 0 ? (currentTime / duration) * 100 : 0; const streamUrl = `${ process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:3006" }/podcasts/${podcastId}/episodes/${episode.id}/stream`; return ( <>
{/* Audio Element */}