'use client'; import React, { useEffect, useRef, useState } from 'react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { useAudioPlayer } from '@/app/components/AudioPlayerContext'; import { FullScreenPlayer } from '@/app/components/FullScreenPlayer'; import { FaPlay, FaPause, FaVolumeHigh, FaForward, FaBackward, FaCompress, FaVolumeXmark, FaExpand } from "react-icons/fa6"; import ColorThief from '@neutrixs/colorthief'; import { Progress } from '@/components/ui/progress'; import { useToast } from '@/hooks/use-toast'; export const AudioPlayer: React.FC = () => { const { currentTrack, playPreviousTrack, addToQueue, playNextTrack, clearQueue, queue } = useAudioPlayer(); const router = useRouter(); const audioRef = useRef(null); const preloadAudioRef = useRef(null); const [progress, setProgress] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [showVolumeSlider, setShowVolumeSlider] = useState(false); const [volume, setVolume] = useState(1); const [isClient, setIsClient] = useState(false); const [isMinimized, setIsMinimized] = useState(false); const [isFullScreen, setIsFullScreen] = useState(false); const audioCurrent = audioRef.current; const { toast } = useToast(); const handleOpenQueue = () => { setIsFullScreen(false); router.push('/queue'); }; useEffect(() => { setIsClient(true); // Load saved volume const savedVolume = localStorage.getItem('navidrome-volume'); if (savedVolume) { try { const volumeValue = parseFloat(savedVolume); if (volumeValue >= 0 && volumeValue <= 1) { setVolume(volumeValue); } } catch (error) { console.error('Failed to parse saved volume:', error); } } // Clean up old localStorage entries with track IDs const keysToRemove: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('navidrome-track-time-')) { keysToRemove.push(key); } } keysToRemove.forEach(key => localStorage.removeItem(key)); }, []); // Apply volume to audio element when volume changes useEffect(() => { const audioCurrent = audioRef.current; if (audioCurrent) { audioCurrent.volume = volume; } // Save volume to localStorage localStorage.setItem('navidrome-volume', volume.toString()); }, [volume]); // Save position when component unmounts or track changes useEffect(() => { const audioCurrent = audioRef.current; return () => { if (audioCurrent && currentTrack && audioCurrent.currentTime > 10) { localStorage.setItem('navidrome-current-track-time', audioCurrent.currentTime.toString()); } }; }, [currentTrack]); useEffect(() => { const audioCurrent = audioRef.current; if (currentTrack && audioCurrent && audioCurrent.src !== currentTrack.url) { // Always clear current track time when changing tracks localStorage.removeItem('navidrome-current-track-time'); audioCurrent.src = currentTrack.url; // Check for saved timestamp (only restore if more than 10 seconds in) const savedTime = localStorage.getItem('navidrome-current-track-time'); if (savedTime) { const time = parseFloat(savedTime); // Only restore if we were at least 10 seconds in and not near the end if (time > 10 && time < (currentTrack.duration - 30)) { const restorePosition = () => { if (audioCurrent.readyState >= 2) { // HAVE_CURRENT_DATA audioCurrent.currentTime = time; audioCurrent.removeEventListener('loadeddata', restorePosition); } }; if (audioCurrent.readyState >= 2) { audioCurrent.currentTime = time; } else { audioCurrent.addEventListener('loadeddata', restorePosition); } } // Always clear after attempting to restore localStorage.removeItem('navidrome-current-track-time'); } // Auto-play only if the track has the autoPlay flag if (currentTrack.autoPlay) { audioCurrent.play().then(() => { setIsPlaying(true); }).catch((error) => { console.error('Failed to auto-play:', error); setIsPlaying(false); }); } else { setIsPlaying(false); } } }, [currentTrack]); useEffect(() => { const audioCurrent = audioRef.current; let lastSavedTime = 0; const updateProgress = () => { if (audioCurrent && currentTrack) { setProgress((audioCurrent.currentTime / audioCurrent.duration) * 100); // Save current time every 30 seconds, but only if we've moved forward significantly const currentTime = audioCurrent.currentTime; if (Math.abs(currentTime - lastSavedTime) >= 30 && currentTime > 10) { localStorage.setItem('navidrome-current-track-time', currentTime.toString()); lastSavedTime = currentTime; } } }; const handleTrackEnd = () => { if (currentTrack) { // Clear saved time when track ends localStorage.removeItem('navidrome-current-track-time'); } playNextTrack(); }; const handleSeeked = () => { if (audioCurrent && currentTrack) { // Save immediately when user seeks localStorage.setItem('navidrome-current-track-time', audioCurrent.currentTime.toString()); lastSavedTime = audioCurrent.currentTime; } }; const handlePlay = () => { setIsPlaying(true); }; const handlePause = () => { setIsPlaying(false); }; if (audioCurrent) { audioCurrent.addEventListener('timeupdate', updateProgress); audioCurrent.addEventListener('ended', handleTrackEnd); audioCurrent.addEventListener('seeked', handleSeeked); audioCurrent.addEventListener('play', handlePlay); audioCurrent.addEventListener('pause', handlePause); } return () => { if (audioCurrent) { audioCurrent.removeEventListener('timeupdate', updateProgress); audioCurrent.removeEventListener('ended', handleTrackEnd); audioCurrent.removeEventListener('seeked', handleSeeked); audioCurrent.removeEventListener('play', handlePlay); audioCurrent.removeEventListener('pause', handlePause); } }; }, [playNextTrack, currentTrack]); // Media Session API integration useEffect(() => { if (!isClient || !currentTrack || !('mediaSession' in navigator)) return; // Set metadata navigator.mediaSession.metadata = new MediaMetadata({ title: currentTrack.name, artist: currentTrack.artist, album: currentTrack.album, artwork: currentTrack.coverArt ? [ { src: currentTrack.coverArt, sizes: '512x512', type: 'image/jpeg' } ] : undefined, }); // Set playback state navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused'; // Set action handlers navigator.mediaSession.setActionHandler('play', () => { const audioCurrent = audioRef.current; if (audioCurrent) { audioCurrent.play(); setIsPlaying(true); } }); navigator.mediaSession.setActionHandler('pause', () => { const audioCurrent = audioRef.current; if (audioCurrent) { audioCurrent.pause(); setIsPlaying(false); } }); navigator.mediaSession.setActionHandler('previoustrack', () => { playPreviousTrack(); }); navigator.mediaSession.setActionHandler('nexttrack', () => { playNextTrack(); }); navigator.mediaSession.setActionHandler('seekto', (details) => { const audioCurrent = audioRef.current; if (audioCurrent && details.seekTime !== undefined) { audioCurrent.currentTime = details.seekTime; } }); return () => { if ('mediaSession' in navigator) { navigator.mediaSession.setActionHandler('play', null); navigator.mediaSession.setActionHandler('pause', null); navigator.mediaSession.setActionHandler('previoustrack', null); navigator.mediaSession.setActionHandler('nexttrack', null); navigator.mediaSession.setActionHandler('seekto', null); } }; }, [currentTrack, isPlaying, isClient, playPreviousTrack, playNextTrack]); const handleProgressClick = (e: React.MouseEvent) => { if (audioCurrent && currentTrack) { const rect = e.currentTarget.getBoundingClientRect(); const clickX = e.clientX - rect.left; const newTime = (clickX / rect.width) * audioCurrent.duration; audioCurrent.currentTime = newTime; // Save the new position immediately localStorage.setItem('navidrome-current-track-time', newTime.toString()); } }; const togglePlayPause = () => { if (audioCurrent) { if (isPlaying) { audioCurrent.pause(); setIsPlaying(false); } else { audioCurrent.play().then(() => { setIsPlaying(true); }).catch((error) => { console.error('Failed to play audio:', error); setIsPlaying(false); }); } } }; const handleVolumeChange = (e: React.ChangeEvent) => { const newVolume = parseFloat(e.target.value); setVolume(newVolume); }; function formatTime(seconds: number): string { if (isNaN(seconds) || seconds < 0) { return "0:00"; } const minutes = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60).toString().padStart(2, "0"); return `${minutes}:${secs}`; } if (!isClient || !currentTrack) { return null; } // Mini player (collapsed state) if (isMinimized) { return (
setIsMinimized(false)} >
{currentTrack.name}

{currentTrack.name}

{currentTrack.artist}

); } // Compact floating player (default state) return (
{currentTrack.name}

{currentTrack.name}

{currentTrack.artist}

{/* Control buttons */}
); }; // {/* Progress bar */} //
// // {formatTime(audioCurrent?.currentTime ?? 0)} // // // // {formatTime(audioCurrent?.duration ?? 0)} // //