Compare commits
4 Commits
3fad1d773e
...
b8bac2b6a9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8bac2b6a9 | ||
|
|
32fab283be | ||
|
|
2230fa32e5 | ||
|
|
8012a53235 |
@@ -3,6 +3,7 @@ import { ProtectedRoute } from '@/components/ProtectedRoute'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { AuthProvider } from '@/components/AuthProvider'
|
import { AuthProvider } from '@/components/AuthProvider'
|
||||||
import { SocketProvider } from '@/contexts/SocketContext'
|
import { SocketProvider } from '@/contexts/SocketContext'
|
||||||
|
import { MusicPlayerProvider } from '@/contexts/MusicPlayerContext'
|
||||||
import { AccountPage } from '@/pages/AccountPage'
|
import { AccountPage } from '@/pages/AccountPage'
|
||||||
import { ActivityPage } from '@/pages/ActivityPage'
|
import { ActivityPage } from '@/pages/ActivityPage'
|
||||||
import { AdminUsersPage } from '@/pages/AdminUsersPage'
|
import { AdminUsersPage } from '@/pages/AdminUsersPage'
|
||||||
@@ -21,7 +22,8 @@ function App() {
|
|||||||
<Toaster />
|
<Toaster />
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<SocketProvider>
|
<SocketProvider>
|
||||||
<Router>
|
<MusicPlayerProvider>
|
||||||
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/register" element={<RegisterPage />} />
|
<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 />} />
|
||||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
</MusicPlayerProvider>
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { AppSidebar } from '@/components/sidebar/AppSidebar'
|
import { AppSidebar } from '@/components/sidebar/AppSidebar'
|
||||||
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar'
|
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar'
|
||||||
|
import { MusicPlayer } from '@/components/MusicPlayer'
|
||||||
|
import { useMusicPlayer } from '@/contexts/MusicPlayerContext'
|
||||||
import { type ReactNode } from 'react'
|
import { type ReactNode } from 'react'
|
||||||
|
|
||||||
interface AppLayoutProps {
|
interface AppLayoutProps {
|
||||||
@@ -27,6 +29,7 @@ export function AppLayout({
|
|||||||
<div className="container mx-auto p-6">{children}</div>
|
<div className="container mx-auto p-6">{children}</div>
|
||||||
</main>
|
</main>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
|
<MusicPlayer />
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// Listen for refresh token expiration events
|
// Listen for refresh token expiration events
|
||||||
const handleRefreshTokenExpired = () => {
|
const handleRefreshTokenExpired = () => {
|
||||||
console.log('Refresh token expired, logging out user')
|
|
||||||
setUser(null)
|
setUser(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
386
src/components/MusicPlayer.tsx
Normal file
386
src/components/MusicPlayer.tsx
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
// Show player if there's a playlist, even if no current track is playing
|
||||||
|
if (playlist.length === 0) {
|
||||||
|
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 || 'No track selected'}
|
||||||
|
</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 || 'No track selected'}</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
318
src/contexts/MusicPlayerContext.tsx
Normal file
318
src/contexts/MusicPlayerContext.tsx
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
import { createContext, useContext, useState, useRef, useEffect, type ReactNode } from 'react'
|
||||||
|
import { useSocket } from './SocketContext'
|
||||||
|
import { apiService } from '@/services/api'
|
||||||
|
|
||||||
|
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
|
||||||
|
loadPlaylist: (playlistId: 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 { socket } = useSocket()
|
||||||
|
|
||||||
|
// Playback state
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false)
|
||||||
|
const [currentTime, setCurrentTime] = useState(0)
|
||||||
|
const [duration, setDuration] = useState(0)
|
||||||
|
const [volume, setVolumeState] = useState(80)
|
||||||
|
const [isMuted, setIsMuted] = useState(false)
|
||||||
|
const [playMode, setPlayModeState] = useState<PlayMode>('continuous')
|
||||||
|
|
||||||
|
// Playlist state
|
||||||
|
const [currentTrackIndex, setCurrentTrackIndex] = useState(0)
|
||||||
|
const [playlist, setPlaylist] = useState<Track[]>([])
|
||||||
|
const [currentPlaylistId, setCurrentPlaylistId] = useState<number | null>(null)
|
||||||
|
const [currentTrack, setCurrentTrack] = useState<Track | null>(null)
|
||||||
|
|
||||||
|
// UI state
|
||||||
|
const [isMinimized, setIsMinimized] = useState(true)
|
||||||
|
const [showPlaylist, setShowPlaylist] = useState(false)
|
||||||
|
|
||||||
|
// Fetch initial player state on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchInitialState = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiService.get('/api/player/state')
|
||||||
|
let state = await response.json()
|
||||||
|
|
||||||
|
// If no playlist is loaded, try to load the main playlist
|
||||||
|
if (!state.playlist_id) {
|
||||||
|
try {
|
||||||
|
await apiService.post('/api/player/load-main-playlist')
|
||||||
|
// Fetch state again after loading main playlist
|
||||||
|
const newResponse = await apiService.get('/api/player/state')
|
||||||
|
state = await newResponse.json()
|
||||||
|
} catch (loadError) {
|
||||||
|
console.warn('Failed to load main playlist:', loadError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all state from backend
|
||||||
|
setIsPlaying(state.is_playing || false)
|
||||||
|
setCurrentTime(state.current_time || 0)
|
||||||
|
setDuration(state.duration || 0)
|
||||||
|
setVolumeState(state.volume || 80)
|
||||||
|
setIsMuted(state.volume === 0)
|
||||||
|
setPlayModeState(state.play_mode || 'continuous')
|
||||||
|
setCurrentTrackIndex(state.current_track_index || 0)
|
||||||
|
setPlaylist(state.playlist || [])
|
||||||
|
setCurrentPlaylistId(state.playlist_id || null)
|
||||||
|
setCurrentTrack(state.current_track || null)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch initial player state:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchInitialState()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Listen for real-time player updates via SocketIO
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePlayerStateUpdate = (state: any) => {
|
||||||
|
setIsPlaying(state.is_playing || false)
|
||||||
|
setCurrentTime(state.current_time || 0)
|
||||||
|
setDuration(state.duration || 0)
|
||||||
|
setVolumeState(state.volume || 80)
|
||||||
|
setIsMuted(state.volume === 0)
|
||||||
|
setPlayModeState(state.play_mode || 'continuous')
|
||||||
|
setCurrentTrackIndex(state.current_track_index || 0)
|
||||||
|
setPlaylist(state.playlist || [])
|
||||||
|
setCurrentPlaylistId(state.playlist_id || null)
|
||||||
|
setCurrentTrack(state.current_track || null)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('player_state_update', handlePlayerStateUpdate)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off('player_state_update', handlePlayerStateUpdate)
|
||||||
|
}
|
||||||
|
}, [socket])
|
||||||
|
|
||||||
|
const play = async () => {
|
||||||
|
try {
|
||||||
|
await apiService.post('/api/player/play')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to play:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pause = async () => {
|
||||||
|
try {
|
||||||
|
await apiService.post('/api/player/pause')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to pause:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stop = async () => {
|
||||||
|
try {
|
||||||
|
await apiService.post('/api/player/stop')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to stop:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePlayPause = async () => {
|
||||||
|
if (isPlaying) {
|
||||||
|
await pause()
|
||||||
|
} else {
|
||||||
|
await play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const seekTo = async (time: number) => {
|
||||||
|
try {
|
||||||
|
const position = duration > 0 ? time / duration : 0
|
||||||
|
await apiService.post('/api/player/seek', { position })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to seek:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setVolume = async (newVolume: number) => {
|
||||||
|
try {
|
||||||
|
await apiService.post('/api/player/volume', { volume: newVolume })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set volume:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMute = async () => {
|
||||||
|
try {
|
||||||
|
const newVolume = isMuted ? 80 : 0
|
||||||
|
await apiService.post('/api/player/volume', { volume: newVolume })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to toggle mute:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextTrack = async () => {
|
||||||
|
try {
|
||||||
|
await apiService.post('/api/player/next')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to skip to next track:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousTrack = async () => {
|
||||||
|
try {
|
||||||
|
await apiService.post('/api/player/previous')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to skip to previous track:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const playTrack = async (trackIndex: number) => {
|
||||||
|
try {
|
||||||
|
await apiService.post('/api/player/play-track', { index: trackIndex })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to play track:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadPlaylist = async (playlistId: number) => {
|
||||||
|
try {
|
||||||
|
await apiService.post('/api/player/playlist', { playlist_id: playlistId })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load playlist:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addToPlaylist = (track: Track) => {
|
||||||
|
// This would need to be implemented via API
|
||||||
|
console.log('Adding to playlist not yet implemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFromPlaylist = (trackId: string) => {
|
||||||
|
// This would need to be implemented via API
|
||||||
|
console.log('Removing from playlist not yet implemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearPlaylist = () => {
|
||||||
|
// This would need to be implemented via API
|
||||||
|
console.log('Clearing playlist not yet implemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
const setPlayMode = async (mode: PlayMode) => {
|
||||||
|
try {
|
||||||
|
await apiService.post('/api/player/mode', { mode })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set play mode:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMaximize = () => {
|
||||||
|
setIsMinimized(!isMinimized)
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePlaylistVisibility = () => {
|
||||||
|
setShowPlaylist(!showPlaylist)
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: MusicPlayerContextType = {
|
||||||
|
// Playback state
|
||||||
|
isPlaying,
|
||||||
|
currentTime,
|
||||||
|
duration,
|
||||||
|
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,
|
||||||
|
loadPlaylist,
|
||||||
|
addToPlaylist,
|
||||||
|
removeFromPlaylist,
|
||||||
|
clearPlaylist,
|
||||||
|
toggleMaximize,
|
||||||
|
togglePlaylistVisibility,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MusicPlayerContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</MusicPlayerContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({ children }) => {
|
|||||||
|
|
||||||
// Set up event listeners
|
// Set up event listeners
|
||||||
newSocket.on("connect", () => {
|
newSocket.on("connect", () => {
|
||||||
|
|
||||||
// Send authentication after connection
|
// Send authentication after connection
|
||||||
newSocket.emit("authenticate", {});
|
newSocket.emit("authenticate", {});
|
||||||
});
|
});
|
||||||
@@ -75,7 +76,9 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({ children }) => {
|
|||||||
|
|
||||||
// Connect/disconnect based on authentication state
|
// Connect/disconnect based on authentication state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!socket || loading) return;
|
if (!socket || loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (user && !isConnected) {
|
if (user && !isConnected) {
|
||||||
socket.connect();
|
socket.connect();
|
||||||
|
|||||||
@@ -118,3 +118,37 @@
|
|||||||
@apply bg-background text-foreground;
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,11 +82,8 @@ class ApiService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
console.log('Token refreshed successfully')
|
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
console.log('Token refresh failed:', response.status)
|
|
||||||
|
|
||||||
// If refresh token is also expired (401), trigger logout
|
// If refresh token is also expired (401), trigger logout
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
this.handleLogout()
|
this.handleLogout()
|
||||||
@@ -103,9 +100,6 @@ class ApiService {
|
|||||||
* Handle logout when refresh token expires
|
* Handle logout when refresh token expires
|
||||||
*/
|
*/
|
||||||
private handleLogout() {
|
private handleLogout() {
|
||||||
// Clear any local storage or state if needed
|
|
||||||
console.log('Refresh token expired, user needs to login again')
|
|
||||||
|
|
||||||
// Dispatch a custom event that the AuthContext can listen to
|
// Dispatch a custom event that the AuthContext can listen to
|
||||||
window.dispatchEvent(new CustomEvent('auth:refresh-token-expired'))
|
window.dispatchEvent(new CustomEvent('auth:refresh-token-expired'))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user