'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 } = useAudioPlayer(); const router = useRouter(); const audioRef = 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); // 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)); }, []); // 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) { 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); } } else { // Clear saved time if we're not restoring it localStorage.removeItem('navidrome-current-track-time'); } } else { // Clear saved time if no saved time exists localStorage.removeItem('navidrome-current-track-time'); } audioCurrent.play(); setIsPlaying(true); } }, [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; } }; if (audioCurrent) { audioCurrent.addEventListener('timeupdate', updateProgress); audioCurrent.addEventListener('ended', handleTrackEnd); audioCurrent.addEventListener('seeked', handleSeeked); } return () => { if (audioCurrent) { audioCurrent.removeEventListener('timeupdate', updateProgress); audioCurrent.removeEventListener('ended', handleTrackEnd); audioCurrent.removeEventListener('seeked', handleSeeked); } }; }, [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(); } else { audioCurrent.play(); } setIsPlaying(!isPlaying); } }; const handleVolumeChange = (e: React.ChangeEvent) => { const newVolume = parseFloat(e.target.value); setVolume(newVolume); if (audioCurrent) { audioCurrent.volume = 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)} // //