feat: implement MusicPlayer component with volume control and playlist management
This commit is contained in:
@@ -3,6 +3,7 @@ import { ProtectedRoute } from '@/components/ProtectedRoute'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AuthProvider } from '@/components/AuthProvider'
|
||||
import { SocketProvider } from '@/contexts/SocketContext'
|
||||
import { MusicPlayerProvider } from '@/contexts/MusicPlayerContext'
|
||||
import { AccountPage } from '@/pages/AccountPage'
|
||||
import { ActivityPage } from '@/pages/ActivityPage'
|
||||
import { AdminUsersPage } from '@/pages/AdminUsersPage'
|
||||
@@ -21,7 +22,8 @@ function App() {
|
||||
<Toaster />
|
||||
<AuthProvider>
|
||||
<SocketProvider>
|
||||
<Router>
|
||||
<MusicPlayerProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
@@ -110,7 +112,8 @@ function App() {
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</Router>
|
||||
</MusicPlayerProvider>
|
||||
</SocketProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { PageHeader } from '@/components/PageHeader'
|
||||
import { AppSidebar } from '@/components/sidebar/AppSidebar'
|
||||
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar'
|
||||
import { MusicPlayer } from '@/components/MusicPlayer'
|
||||
import { useMusicPlayer } from '@/contexts/MusicPlayerContext'
|
||||
import { type ReactNode } from 'react'
|
||||
|
||||
interface AppLayoutProps {
|
||||
@@ -27,6 +29,7 @@ export function AppLayout({
|
||||
<div className="container mx-auto p-6">{children}</div>
|
||||
</main>
|
||||
</SidebarInset>
|
||||
<MusicPlayer />
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
|
||||
385
src/components/MusicPlayer.tsx
Normal file
385
src/components/MusicPlayer.tsx
Normal file
@@ -0,0 +1,385 @@
|
||||
import { useRef } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { useMusicPlayer } from '@/contexts/MusicPlayerContext'
|
||||
import {
|
||||
Play,
|
||||
Pause,
|
||||
Square,
|
||||
SkipBack,
|
||||
SkipForward,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
Repeat,
|
||||
Repeat1,
|
||||
Shuffle,
|
||||
List,
|
||||
Maximize2,
|
||||
Minimize2
|
||||
} from 'lucide-react'
|
||||
|
||||
export function MusicPlayer() {
|
||||
const {
|
||||
isPlaying,
|
||||
currentTime,
|
||||
duration,
|
||||
volume,
|
||||
isMuted,
|
||||
playMode,
|
||||
currentTrack,
|
||||
playlist,
|
||||
currentTrackIndex,
|
||||
isMinimized,
|
||||
showPlaylist,
|
||||
togglePlayPause,
|
||||
stop,
|
||||
previousTrack,
|
||||
nextTrack,
|
||||
seekTo,
|
||||
setVolume,
|
||||
toggleMute,
|
||||
setPlayMode,
|
||||
playTrack,
|
||||
toggleMaximize,
|
||||
togglePlaylistVisibility,
|
||||
} = useMusicPlayer()
|
||||
|
||||
const progressBarRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
if (!currentTrack) {
|
||||
return null
|
||||
}
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = Math.floor(seconds % 60)
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newVolume = parseFloat(e.target.value)
|
||||
setVolume(newVolume)
|
||||
}
|
||||
|
||||
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (progressBarRef.current) {
|
||||
const rect = progressBarRef.current.getBoundingClientRect()
|
||||
const clickX = e.clientX - rect.left
|
||||
const percentage = clickX / rect.width
|
||||
const newTime = percentage * duration
|
||||
seekTo(newTime)
|
||||
}
|
||||
}
|
||||
|
||||
const getPlayModeIcon = () => {
|
||||
switch (playMode) {
|
||||
case 'loop-playlist':
|
||||
return <Repeat className="h-4 w-4" />
|
||||
case 'loop-one':
|
||||
return <Repeat1 className="h-4 w-4" />
|
||||
case 'random':
|
||||
return <Shuffle className="h-4 w-4" />
|
||||
default:
|
||||
return <Play className="h-4 w-4" />
|
||||
}
|
||||
}
|
||||
|
||||
const handlePlayModeToggle = () => {
|
||||
const modes = ['continuous', 'loop-playlist', 'loop-one', 'random'] as const
|
||||
const currentIndex = modes.indexOf(playMode)
|
||||
const nextIndex = (currentIndex + 1) % modes.length
|
||||
setPlayMode(modes[nextIndex])
|
||||
}
|
||||
|
||||
const progressPercentage = (currentTime / duration) * 100
|
||||
|
||||
if (isMinimized) {
|
||||
return (
|
||||
<Card className="fixed bottom-4 right-4 w-80 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border shadow-lg z-50">
|
||||
{/* Thumbnail */}
|
||||
{currentTrack.thumbnail && (
|
||||
<div className="relative h-32 w-full overflow-hidden rounded-t-lg">
|
||||
<img
|
||||
src={currentTrack.thumbnail}
|
||||
alt={currentTrack.title}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute top-2 right-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={toggleMaximize}
|
||||
className="h-8 w-8 p-0 bg-black/50 hover:bg-black/70"
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
{/* Track info */}
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-medium text-sm leading-tight line-clamp-1">
|
||||
{currentTrack.title}
|
||||
</h3>
|
||||
{currentTrack.artist && (
|
||||
<p className="text-xs text-muted-foreground line-clamp-1">
|
||||
{currentTrack.artist}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="space-y-2">
|
||||
<div
|
||||
ref={progressBarRef}
|
||||
className="w-full h-2 bg-muted rounded-full cursor-pointer"
|
||||
onClick={handleProgressClick}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-primary rounded-full transition-all"
|
||||
style={{ width: `${progressPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>{formatTime(currentTime)}</span>
|
||||
<span>{formatTime(duration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main controls */}
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<Button variant="ghost" size="sm" onClick={previousTrack}>
|
||||
<SkipBack className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="default" size="sm" onClick={togglePlayPause}>
|
||||
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={stop}>
|
||||
<Square className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={nextTrack}>
|
||||
<SkipForward className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Secondary controls */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handlePlayModeToggle}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
{getPlayModeIcon()}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={togglePlaylistVisibility}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Volume control */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={toggleMute}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
{isMuted || volume === 0 ? (
|
||||
<VolumeX className="h-4 w-4" />
|
||||
) : (
|
||||
<Volume2 className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={isMuted ? 0 : volume}
|
||||
onChange={handleVolumeChange}
|
||||
className="w-16 h-1 bg-muted rounded-lg appearance-none cursor-pointer slider"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Playlist */}
|
||||
{showPlaylist && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto">
|
||||
<h4 className="font-medium text-sm">Playlist</h4>
|
||||
{playlist.map((track, index) => (
|
||||
<div
|
||||
key={track.id}
|
||||
className={`flex items-center space-x-2 p-2 rounded cursor-pointer hover:bg-muted/50 ${
|
||||
index === currentTrackIndex ? 'bg-muted' : ''
|
||||
}`}
|
||||
onClick={() => playTrack(index)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs font-medium line-clamp-1">{track.title}</p>
|
||||
{track.artist && (
|
||||
<p className="text-xs text-muted-foreground line-clamp-1">
|
||||
{track.artist}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatTime(track.duration)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Maximized view - overlay
|
||||
return (
|
||||
<Card className="fixed inset-0 bg-background/95 backdrop-blur-md supports-[backdrop-filter]:bg-background/80 z-50 flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<h2 className="text-lg font-semibold">Music Player</h2>
|
||||
<Button variant="ghost" size="sm" onClick={toggleMaximize}>
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex">
|
||||
{/* Main player area */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-8">
|
||||
{/* Large thumbnail */}
|
||||
{currentTrack.thumbnail && (
|
||||
<div className="w-80 h-80 rounded-lg overflow-hidden mb-6 shadow-lg">
|
||||
<img
|
||||
src={currentTrack.thumbnail}
|
||||
alt={currentTrack.title}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Track info */}
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold mb-2">{currentTrack.title}</h1>
|
||||
{currentTrack.artist && (
|
||||
<p className="text-lg text-muted-foreground">{currentTrack.artist}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="w-full max-w-md space-y-4 mb-8">
|
||||
<div
|
||||
ref={progressBarRef}
|
||||
className="w-full h-3 bg-muted rounded-full cursor-pointer"
|
||||
onClick={handleProgressClick}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-primary rounded-full transition-all"
|
||||
style={{ width: `${progressPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-muted-foreground">
|
||||
<span>{formatTime(currentTime)}</span>
|
||||
<span>{formatTime(duration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main controls */}
|
||||
<div className="flex items-center justify-center space-x-4 mb-6">
|
||||
<Button variant="ghost" size="lg" onClick={previousTrack}>
|
||||
<SkipBack className="h-6 w-6" />
|
||||
</Button>
|
||||
<Button variant="default" size="lg" onClick={togglePlayPause} className="h-14 w-14">
|
||||
{isPlaying ? <Pause className="h-8 w-8" /> : <Play className="h-8 w-8" />}
|
||||
</Button>
|
||||
<Button variant="ghost" size="lg" onClick={stop}>
|
||||
<Square className="h-6 w-6" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="lg" onClick={nextTrack}>
|
||||
<SkipForward className="h-6 w-6" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Secondary controls */}
|
||||
<div className="flex items-center space-x-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handlePlayModeToggle}
|
||||
>
|
||||
{getPlayModeIcon()}
|
||||
</Button>
|
||||
|
||||
{/* Volume control */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={toggleMute}
|
||||
>
|
||||
{isMuted || volume === 0 ? (
|
||||
<VolumeX className="h-5 w-5" />
|
||||
) : (
|
||||
<Volume2 className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={isMuted ? 0 : volume}
|
||||
onChange={handleVolumeChange}
|
||||
className="w-24 h-2 bg-muted rounded-lg appearance-none cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Playlist sidebar */}
|
||||
<div className="w-80 border-l bg-muted/30 flex flex-col">
|
||||
<div className="p-4 border-b">
|
||||
<h3 className="font-semibold">Playlist</h3>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-2">
|
||||
{playlist.map((track, index) => (
|
||||
<div
|
||||
key={track.id}
|
||||
className={`flex items-center space-x-3 p-3 rounded-lg cursor-pointer hover:bg-background/50 transition-colors ${
|
||||
index === currentTrackIndex ? 'bg-background shadow-sm' : ''
|
||||
}`}
|
||||
onClick={() => playTrack(index)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium line-clamp-1">{track.title}</p>
|
||||
{track.artist && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-1">
|
||||
{track.artist}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatTime(track.duration)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -117,4 +117,38 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Custom slider styles for volume control */
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-track {
|
||||
@apply h-1 bg-muted rounded-lg;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
@apply h-4 w-4 bg-primary rounded-full cursor-pointer;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-track {
|
||||
@apply h-1 bg-muted rounded-lg border-0;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
@apply h-4 w-4 bg-primary rounded-full cursor-pointer border-0;
|
||||
}
|
||||
|
||||
/* Line clamp utilities */
|
||||
.line-clamp-1 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user