Fix Tabel Apbdes, & fix muciplayer in background
This commit is contained in:
313
src/app/context/MusicContext.tsx
Normal file
313
src/app/context/MusicContext.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
'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<void>;
|
||||
}
|
||||
|
||||
const MusicContext = createContext<MusicContextType | undefined>(undefined);
|
||||
|
||||
export function MusicProvider({ children }: { children: ReactNode }) {
|
||||
// State
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentSong, setCurrentSong] = useState<Musik | null>(null);
|
||||
const [currentSongIndex, setCurrentSongIndex] = useState(-1);
|
||||
const [musikData, setMusikData] = useState<Musik[]>([]);
|
||||
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<HTMLAudioElement | null>(null);
|
||||
const isSeekingRef = useRef(false);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
|
||||
// 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', () => {
|
||||
if (isRepeat) {
|
||||
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, isRepeat]);
|
||||
|
||||
// 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 (
|
||||
<MusicContext.Provider value={value}>{children}</MusicContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useMusic() {
|
||||
const context = useContext(MusicContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useMusic must be used within a MusicProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user