129 lines
4.7 KiB
TypeScript
129 lines
4.7 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useMusicPlayer, isRadioTrack } from './music-provider'
|
|
import { FullScreenPlayer } from './full-screen-player'
|
|
import { QueueView } from './queue-view'
|
|
import { Slider } from '@/components/ui/slider'
|
|
import { Play, Pause, SkipBack, SkipForward, ListMusic, Shuffle, Radio } from 'lucide-react'
|
|
|
|
function formatTime(secs: number) {
|
|
if (!secs || !isFinite(secs)) return '0:00'
|
|
const m = Math.floor(secs / 60)
|
|
const s = Math.floor(secs % 60)
|
|
return `${m}:${s.toString().padStart(2, '0')}`
|
|
}
|
|
|
|
export function MiniPlayer() {
|
|
const { state, togglePlay, seek, nextTrack, prevTrack, setFullScreen, toggleShuffle } = useMusicPlayer()
|
|
const [queueOpen, setQueueOpen] = useState(false)
|
|
|
|
if (!state.currentTrack) return null
|
|
|
|
const track = state.currentTrack
|
|
const isRadio = isRadioTrack(track)
|
|
|
|
return (
|
|
<>
|
|
<div className="fixed bottom-0 inset-x-0 z-50 bg-card border-t border-border shadow-lg">
|
|
{/* Progress bar (thin, above the player) — disabled for radio */}
|
|
<div className="px-2">
|
|
{isRadio ? (
|
|
<div className="h-1 w-full bg-rose-500/40 rounded-full overflow-hidden">
|
|
<div className="h-full w-full bg-rose-500 animate-pulse" />
|
|
</div>
|
|
) : (
|
|
<Slider
|
|
value={[state.progress]}
|
|
max={state.duration || 1}
|
|
step={1}
|
|
onValueChange={([v]) => seek(v)}
|
|
className="h-1 [&_[data-slot=slider]]:h-1 [&_span[data-slot=scroll-bar]]:hidden"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 px-4 py-2 h-14">
|
|
{/* Cover art - clickable to open fullscreen */}
|
|
<button
|
|
onClick={() => setFullScreen(true)}
|
|
className="flex-shrink-0 w-10 h-10 rounded-md overflow-hidden bg-muted"
|
|
>
|
|
{track.coverArt ? (
|
|
<img
|
|
src={`/api/music/cover/${track.coverArt}?size=80`}
|
|
alt={track.album}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center bg-muted">
|
|
{isRadio && <Radio className="h-5 w-5 text-rose-400" />}
|
|
</div>
|
|
)}
|
|
</button>
|
|
|
|
{/* Track info - clickable to open fullscreen */}
|
|
<button
|
|
onClick={() => setFullScreen(true)}
|
|
className="flex-1 min-w-0 text-left"
|
|
>
|
|
<div className="text-sm font-medium truncate">{track.title}</div>
|
|
<div className="text-xs text-muted-foreground truncate">{track.artist}</div>
|
|
</button>
|
|
|
|
{/* Time / LIVE badge */}
|
|
{isRadio ? (
|
|
<span className="text-[10px] font-bold px-1.5 py-0.5 rounded bg-rose-500/20 text-rose-400 animate-pulse hidden sm:block">
|
|
LIVE
|
|
</span>
|
|
) : (
|
|
<span className="text-xs text-muted-foreground tabular-nums hidden sm:block">
|
|
{formatTime(state.progress)} / {formatTime(state.duration)}
|
|
</span>
|
|
)}
|
|
|
|
{/* Controls */}
|
|
<div className="flex items-center gap-1">
|
|
{!isRadio && (
|
|
<button
|
|
onClick={toggleShuffle}
|
|
className={`p-1.5 rounded-full transition-colors ${state.shuffleEnabled ? 'text-purple-400' : 'text-muted-foreground hover:bg-muted/50'}`}
|
|
title={state.shuffleEnabled ? 'Disable shuffle' : 'Enable shuffle'}
|
|
>
|
|
<Shuffle className="h-3.5 w-3.5" />
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={prevTrack}
|
|
className="p-1.5 hover:bg-muted/50 rounded-full transition-colors"
|
|
>
|
|
<SkipBack className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={togglePlay}
|
|
className="p-2 bg-primary text-primary-foreground rounded-full hover:bg-primary/90 transition-colors"
|
|
>
|
|
{state.isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4 ml-0.5" />}
|
|
</button>
|
|
<button
|
|
onClick={nextTrack}
|
|
className="p-1.5 hover:bg-muted/50 rounded-full transition-colors"
|
|
>
|
|
<SkipForward className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => setQueueOpen(true)}
|
|
className="p-1.5 hover:bg-muted/50 rounded-full transition-colors text-muted-foreground"
|
|
>
|
|
<ListMusic className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<FullScreenPlayer />
|
|
<QueueView open={queueOpen} onClose={() => setQueueOpen(false)} />
|
|
</>
|
|
)
|
|
}
|