'use client'; import { createContext, useContext, useState, useRef, useEffect, useCallback, ReactNode, } from 'react'; interface MusicFile { id: string; name: string; realName: string; path: string; mimeType: string; link: string; } export interface Musik { id: string; judul: string; artis: string; deskripsi: string | null; durasi: string; genre: string | null; tahunRilis: number | null; audioFile: MusicFile | null; coverImage: MusicFile | null; isActive: boolean; } interface MusicContextType { // State isPlaying: boolean; currentSong: Musik | null; currentSongIndex: number; musikData: Musik[]; currentTime: number; duration: number; volume: number; isMuted: boolean; isRepeat: boolean; isShuffle: boolean; isLoading: boolean; isPlayerOpen: boolean; // Actions playSong: (song: Musik) => void; togglePlayPause: () => void; playNext: () => void; playPrev: () => void; seek: (time: number) => void; setVolume: (volume: number) => void; toggleMute: () => void; toggleRepeat: () => void; toggleShuffle: () => void; togglePlayer: () => void; loadMusikData: () => Promise; } const MusicContext = createContext(undefined); export function MusicProvider({ children }: { children: ReactNode }) { // State const [isPlaying, setIsPlaying] = useState(false); const [currentSong, setCurrentSong] = useState(null); const [currentSongIndex, setCurrentSongIndex] = useState(-1); const [musikData, setMusikData] = useState([]); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [volume, setVolumeState] = useState(70); const [isMuted, setIsMuted] = useState(false); const [isRepeat, setIsRepeat] = useState(false); const [isShuffle, setIsShuffle] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isPlayerOpen, setIsPlayerOpen] = useState(false); // Refs const audioRef = useRef(null); const isSeekingRef = useRef(false); const animationFrameRef = useRef(null); const isRepeatRef = useRef(false); // Ref untuk avoid stale closure // Sync ref dengan state useEffect(() => { isRepeatRef.current = isRepeat; }, [isRepeat]); // Load musik data const loadMusikData = useCallback(async () => { try { setIsLoading(true); const res = await fetch('/api/desa/musik/find-many?page=1&limit=50'); const data = await res.json(); if (data.success && data.data) { const activeMusik = data.data.filter((m: Musik) => m.isActive); setMusikData(activeMusik); } } catch (error) { console.error('Error fetching musik:', error); } finally { setIsLoading(false); } }, []); // Initialize audio element useEffect(() => { audioRef.current = new Audio(); audioRef.current.preload = 'metadata'; // Event listeners audioRef.current.addEventListener('loadedmetadata', () => { setDuration(Math.floor(audioRef.current!.duration)); }); audioRef.current.addEventListener('ended', () => { // Gunakan ref untuk avoid stale closure if (isRepeatRef.current) { audioRef.current!.currentTime = 0; audioRef.current!.play(); } else { playNext(); } }); // Load initial data loadMusikData(); return () => { if (audioRef.current) { audioRef.current.pause(); audioRef.current = null; } if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; // eslint-disable-next-line react-hooks/exhaustive-deps -- playNext is intentionally not in deps to avoid circular dependency }, [loadMusikData]); // Remove isRepeat dari deps karena sudah pakai ref // Update time with requestAnimationFrame for smooth progress const updateTime = useCallback(() => { if (audioRef.current && !audioRef.current.paused && !isSeekingRef.current) { setCurrentTime(Math.floor(audioRef.current.currentTime)); animationFrameRef.current = requestAnimationFrame(updateTime); } }, []); useEffect(() => { if (isPlaying) { animationFrameRef.current = requestAnimationFrame(updateTime); } else { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } } return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; }, [isPlaying, updateTime]); // Play song const playSong = useCallback( (song: Musik) => { if (!song?.audioFile?.link || !audioRef.current) return; const songIndex = musikData.findIndex(m => m.id === song.id); setCurrentSongIndex(songIndex); setCurrentSong(song); setIsPlaying(true); audioRef.current.src = song.audioFile.link; audioRef.current.load(); audioRef.current .play() .catch((err) => console.error('Error playing audio:', err)); }, [musikData] ); // Toggle play/pause const togglePlayPause = useCallback(() => { if (!audioRef.current || !currentSong) return; if (isPlaying) { audioRef.current.pause(); setIsPlaying(false); } else { audioRef.current .play() .then(() => setIsPlaying(true)) .catch((err) => console.error('Error playing audio:', err)); } }, [isPlaying, currentSong]); // Play next const playNext = useCallback(() => { if (musikData.length === 0) return; let nextIndex: number; if (isShuffle) { nextIndex = Math.floor(Math.random() * musikData.length); } else { nextIndex = (currentSongIndex + 1) % musikData.length; } const nextSong = musikData[nextIndex]; if (nextSong) { playSong(nextSong); } }, [musikData, isShuffle, currentSongIndex, playSong]); // Play previous const playPrev = useCallback(() => { if (musikData.length === 0) return; // If more than 3 seconds into song, restart it if (currentTime > 3) { if (audioRef.current) { audioRef.current.currentTime = 0; } return; } const prevIndex = currentSongIndex <= 0 ? musikData.length - 1 : currentSongIndex - 1; const prevSong = musikData[prevIndex]; if (prevSong) { playSong(prevSong); } }, [musikData, currentSongIndex, currentTime, playSong]); // Seek const seek = useCallback((time: number) => { if (!audioRef.current) return; audioRef.current.currentTime = time; setCurrentTime(time); }, []); // Set volume const setVolume = useCallback((vol: number) => { if (!audioRef.current) return; const normalizedVol = Math.max(0, Math.min(100, vol)) / 100; audioRef.current.volume = normalizedVol; setVolumeState(Math.max(0, Math.min(100, vol))); setIsMuted(normalizedVol === 0); }, []); // Toggle mute const toggleMute = useCallback(() => { if (!audioRef.current) return; const newMuted = !isMuted; audioRef.current.muted = newMuted; setIsMuted(newMuted); if (newMuted && volume > 0) { audioRef.current.volume = 0; } else if (!newMuted && volume > 0) { audioRef.current.volume = volume / 100; } }, [isMuted, volume]); // Toggle repeat const toggleRepeat = useCallback(() => { setIsRepeat((prev) => !prev); }, []); // Toggle shuffle const toggleShuffle = useCallback(() => { setIsShuffle((prev) => !prev); }, []); // Toggle player const togglePlayer = useCallback(() => { setIsPlayerOpen((prev) => !prev); }, []); const value: MusicContextType = { isPlaying, currentSong, currentSongIndex, musikData, currentTime, duration, volume, isMuted, isRepeat, isShuffle, isLoading, isPlayerOpen, playSong, togglePlayPause, playNext, playPrev, seek, setVolume, toggleMute, toggleRepeat, toggleShuffle, togglePlayer, loadMusikData, }; return ( {children} ); } export function useMusic() { const context = useContext(MusicContext); if (context === undefined) { throw new Error('useMusic must be used within a MusicProvider'); } return context; }