feat: implement MusicPlayer component with volume control and playlist management

This commit is contained in:
JSC
2025-07-07 16:10:06 +02:00
parent 3fad1d773e
commit 8012a53235
5 changed files with 771 additions and 2 deletions

View 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>
)
}