feat: implement MusicPlayer component with volume control and playlist management
This commit is contained in:
344
src/contexts/MusicPlayerContext.tsx
Normal file
344
src/contexts/MusicPlayerContext.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
import { createContext, useContext, useState, useRef, useEffect, type ReactNode } from 'react'
|
||||
|
||||
export interface Track {
|
||||
id: string
|
||||
title: string
|
||||
artist?: string
|
||||
duration: number
|
||||
thumbnail?: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export type PlayMode = 'continuous' | 'loop-playlist' | 'loop-one' | 'random'
|
||||
|
||||
interface MusicPlayerContextType {
|
||||
// Playback state
|
||||
isPlaying: boolean
|
||||
currentTime: number
|
||||
duration: number
|
||||
volume: number
|
||||
isMuted: boolean
|
||||
playMode: PlayMode
|
||||
|
||||
// Current track and playlist
|
||||
currentTrack: Track | null
|
||||
playlist: Track[]
|
||||
currentTrackIndex: number
|
||||
|
||||
// UI state
|
||||
isMinimized: boolean
|
||||
showPlaylist: boolean
|
||||
|
||||
// Actions
|
||||
play: () => void
|
||||
pause: () => void
|
||||
stop: () => void
|
||||
togglePlayPause: () => void
|
||||
seekTo: (time: number) => void
|
||||
setVolume: (volume: number) => void
|
||||
toggleMute: () => void
|
||||
setPlayMode: (mode: PlayMode) => void
|
||||
nextTrack: () => void
|
||||
previousTrack: () => void
|
||||
playTrack: (trackIndex: number) => void
|
||||
addToPlaylist: (track: Track) => void
|
||||
removeFromPlaylist: (trackId: string) => void
|
||||
clearPlaylist: () => void
|
||||
toggleMaximize: () => void
|
||||
togglePlaylistVisibility: () => void
|
||||
}
|
||||
|
||||
const MusicPlayerContext = createContext<MusicPlayerContextType | undefined>(undefined)
|
||||
|
||||
export function useMusicPlayer() {
|
||||
const context = useContext(MusicPlayerContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useMusicPlayer must be used within a MusicPlayerProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
interface MusicPlayerProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function MusicPlayerProvider({ children }: MusicPlayerProviderProps) {
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||
|
||||
// Playback state
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
const [duration, setDuration] = useState(0)
|
||||
const [volume, setVolumeState] = useState(0.8)
|
||||
const [isMuted, setIsMuted] = useState(false)
|
||||
const [playMode, setPlayMode] = useState<PlayMode>('continuous')
|
||||
|
||||
// Playlist state
|
||||
const [currentTrackIndex, setCurrentTrackIndex] = useState(0)
|
||||
const [playlist, setPlaylist] = useState<Track[]>([
|
||||
{
|
||||
id: '1',
|
||||
title: 'Cheryl Lynn - Got To Be Real',
|
||||
artist: 'Cheryl Lynn',
|
||||
duration: 240,
|
||||
thumbnail: '/api/sounds/stream/thumbnails/Cheryl Lynn - Got To Be Real Official Audio_fI569nw0YUQ.webp',
|
||||
url: '/api/sounds/stream/Cheryl Lynn - Got To Be Real Official Audio_fI569nw0YUQ.opus'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'The Whispers - And The Beat Goes On',
|
||||
artist: 'The Whispers',
|
||||
duration: 280,
|
||||
thumbnail: '/api/sounds/stream/thumbnails/The Whispers - And The Beat Goes On Official Video_pEmX5HR9ZxU.jpg',
|
||||
url: '/api/sounds/stream/The Whispers - And The Beat Goes On Official Video_pEmX5HR9ZxU.opus'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'OLD RAP VS NEW RAP',
|
||||
artist: 'Mister V, Jhon Rachid, Maskey',
|
||||
duration: 320,
|
||||
thumbnail: '/api/sounds/stream/thumbnails/OLD RAP VS NEW RAP Ft Mister V Jhon Rachid Maskey _PAFYcOFE3DY.webp',
|
||||
url: '/api/sounds/stream/OLD RAP VS NEW RAP Ft Mister V Jhon Rachid Maskey _PAFYcOFE3DY.opus'
|
||||
}
|
||||
])
|
||||
|
||||
// UI state
|
||||
const [isMinimized, setIsMinimized] = useState(true)
|
||||
const [showPlaylist, setShowPlaylist] = useState(false)
|
||||
|
||||
const currentTrack = playlist[currentTrackIndex] || null
|
||||
|
||||
// Initialize audio element
|
||||
useEffect(() => {
|
||||
audioRef.current = new Audio()
|
||||
|
||||
const audio = audioRef.current
|
||||
|
||||
const handleTimeUpdate = () => {
|
||||
setCurrentTime(audio.currentTime)
|
||||
}
|
||||
|
||||
const handleLoadedMetadata = () => {
|
||||
setDuration(audio.duration)
|
||||
}
|
||||
|
||||
const handleEnded = () => {
|
||||
handleTrackEnd()
|
||||
}
|
||||
|
||||
audio.addEventListener('timeupdate', handleTimeUpdate)
|
||||
audio.addEventListener('loadedmetadata', handleLoadedMetadata)
|
||||
audio.addEventListener('ended', handleEnded)
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener('timeupdate', handleTimeUpdate)
|
||||
audio.removeEventListener('loadedmetadata', handleLoadedMetadata)
|
||||
audio.removeEventListener('ended', handleEnded)
|
||||
audio.pause()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Update audio source when track changes
|
||||
useEffect(() => {
|
||||
if (audioRef.current && currentTrack) {
|
||||
audioRef.current.src = currentTrack.url
|
||||
audioRef.current.load()
|
||||
}
|
||||
}, [currentTrack])
|
||||
|
||||
// Update audio volume and mute state
|
||||
useEffect(() => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.volume = isMuted ? 0 : volume
|
||||
}
|
||||
}, [volume, isMuted])
|
||||
|
||||
const handleTrackEnd = () => {
|
||||
switch (playMode) {
|
||||
case 'loop-one':
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = 0
|
||||
audioRef.current.play()
|
||||
}
|
||||
break
|
||||
case 'loop-playlist':
|
||||
nextTrack()
|
||||
break
|
||||
case 'random':
|
||||
playRandomTrack()
|
||||
break
|
||||
case 'continuous':
|
||||
if (currentTrackIndex < playlist.length - 1) {
|
||||
nextTrack()
|
||||
} else {
|
||||
stop()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const play = () => {
|
||||
if (audioRef.current && currentTrack) {
|
||||
audioRef.current.play()
|
||||
setIsPlaying(true)
|
||||
}
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause()
|
||||
setIsPlaying(false)
|
||||
}
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause()
|
||||
audioRef.current.currentTime = 0
|
||||
setIsPlaying(false)
|
||||
setCurrentTime(0)
|
||||
}
|
||||
}
|
||||
|
||||
const togglePlayPause = () => {
|
||||
if (isPlaying) {
|
||||
pause()
|
||||
} else {
|
||||
play()
|
||||
}
|
||||
}
|
||||
|
||||
const seekTo = (time: number) => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = time
|
||||
setCurrentTime(time)
|
||||
}
|
||||
}
|
||||
|
||||
const setVolume = (newVolume: number) => {
|
||||
setVolumeState(newVolume)
|
||||
setIsMuted(newVolume === 0)
|
||||
}
|
||||
|
||||
const toggleMute = () => {
|
||||
setIsMuted(!isMuted)
|
||||
}
|
||||
|
||||
const nextTrack = () => {
|
||||
if (playlist.length === 0) return
|
||||
|
||||
const nextIndex = (currentTrackIndex + 1) % playlist.length
|
||||
setCurrentTrackIndex(nextIndex)
|
||||
|
||||
// Auto-play if currently playing
|
||||
if (isPlaying) {
|
||||
setTimeout(() => play(), 100)
|
||||
}
|
||||
}
|
||||
|
||||
const previousTrack = () => {
|
||||
if (playlist.length === 0) return
|
||||
|
||||
const prevIndex = currentTrackIndex === 0 ? playlist.length - 1 : currentTrackIndex - 1
|
||||
setCurrentTrackIndex(prevIndex)
|
||||
|
||||
// Auto-play if currently playing
|
||||
if (isPlaying) {
|
||||
setTimeout(() => play(), 100)
|
||||
}
|
||||
}
|
||||
|
||||
const playRandomTrack = () => {
|
||||
if (playlist.length <= 1) return
|
||||
|
||||
let randomIndex
|
||||
do {
|
||||
randomIndex = Math.floor(Math.random() * playlist.length)
|
||||
} while (randomIndex === currentTrackIndex)
|
||||
|
||||
setCurrentTrackIndex(randomIndex)
|
||||
setTimeout(() => play(), 100)
|
||||
}
|
||||
|
||||
const playTrack = (trackIndex: number) => {
|
||||
if (trackIndex >= 0 && trackIndex < playlist.length) {
|
||||
setCurrentTrackIndex(trackIndex)
|
||||
setTimeout(() => play(), 100)
|
||||
}
|
||||
}
|
||||
|
||||
const addToPlaylist = (track: Track) => {
|
||||
setPlaylist(prev => [...prev, track])
|
||||
}
|
||||
|
||||
const removeFromPlaylist = (trackId: string) => {
|
||||
setPlaylist(prev => {
|
||||
const newPlaylist = prev.filter(track => track.id !== trackId)
|
||||
// Adjust current track index if necessary
|
||||
const removedIndex = prev.findIndex(track => track.id === trackId)
|
||||
if (removedIndex !== -1 && removedIndex < currentTrackIndex) {
|
||||
setCurrentTrackIndex(current => Math.max(0, current - 1))
|
||||
} else if (removedIndex === currentTrackIndex && newPlaylist.length > 0) {
|
||||
setCurrentTrackIndex(current => Math.min(current, newPlaylist.length - 1))
|
||||
}
|
||||
return newPlaylist
|
||||
})
|
||||
}
|
||||
|
||||
const clearPlaylist = () => {
|
||||
stop()
|
||||
setPlaylist([])
|
||||
setCurrentTrackIndex(0)
|
||||
}
|
||||
|
||||
const toggleMaximize = () => {
|
||||
setIsMinimized(!isMinimized)
|
||||
}
|
||||
|
||||
const togglePlaylistVisibility = () => {
|
||||
setShowPlaylist(!showPlaylist)
|
||||
}
|
||||
|
||||
const value: MusicPlayerContextType = {
|
||||
// Playback state
|
||||
isPlaying,
|
||||
currentTime,
|
||||
duration: currentTrack?.duration || 0,
|
||||
volume,
|
||||
isMuted,
|
||||
playMode,
|
||||
|
||||
// Current track and playlist
|
||||
currentTrack,
|
||||
playlist,
|
||||
currentTrackIndex,
|
||||
|
||||
// UI state
|
||||
isMinimized,
|
||||
showPlaylist,
|
||||
|
||||
// Actions
|
||||
play,
|
||||
pause,
|
||||
stop,
|
||||
togglePlayPause,
|
||||
seekTo,
|
||||
setVolume,
|
||||
toggleMute,
|
||||
setPlayMode,
|
||||
nextTrack,
|
||||
previousTrack,
|
||||
playTrack,
|
||||
addToPlaylist,
|
||||
removeFromPlaylist,
|
||||
clearPlaylist,
|
||||
toggleMaximize,
|
||||
togglePlaylistVisibility,
|
||||
}
|
||||
|
||||
return (
|
||||
<MusicPlayerContext.Provider value={value}>
|
||||
{children}
|
||||
</MusicPlayerContext.Provider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user