React web app for the self-hosted music streaming project, carved out from the music-app monorepo. Uses @music-app/shared for shared types and utilities.
69 lines
2.1 KiB
TypeScript
69 lines
2.1 KiB
TypeScript
import { useState, useRef, useCallback } from 'react'
|
|
|
|
interface ProgressBarProps {
|
|
progress: number
|
|
duration: number
|
|
onSeek: (time: number) => void
|
|
className?: string
|
|
}
|
|
|
|
export function ProgressBar({ progress, duration, onSeek, className = '' }: ProgressBarProps) {
|
|
const [isDragging, setIsDragging] = useState(false)
|
|
const barRef = useRef<HTMLDivElement>(null)
|
|
|
|
const percent = duration > 0 ? (progress / duration) * 100 : 0
|
|
|
|
const handleSeek = useCallback((clientX: number) => {
|
|
if (!barRef.current) return
|
|
const rect = barRef.current.getBoundingClientRect()
|
|
const x = Math.max(0, Math.min(clientX - rect.left, rect.width))
|
|
const ratio = x / rect.width
|
|
onSeek(duration * ratio)
|
|
}, [duration, onSeek])
|
|
|
|
const handleClick = (e: React.MouseEvent) => handleSeek(e.clientX)
|
|
|
|
const handleMouseDown = (e: React.MouseEvent) => {
|
|
setIsDragging(true)
|
|
handleSeek(e.clientX)
|
|
}
|
|
|
|
const handleMouseMove = (e: React.MouseEvent) => {
|
|
if (isDragging) handleSeek(e.clientX)
|
|
}
|
|
|
|
const handleMouseUp = () => setIsDragging(false)
|
|
|
|
const formatTime = (sec: number) => {
|
|
const m = Math.floor(sec / 60)
|
|
const s = Math.floor(sec % 60)
|
|
return `${m}:${s.toString().padStart(2, '0')}`
|
|
}
|
|
|
|
return (
|
|
<div className={`w-full ${className}`}>
|
|
<div
|
|
ref={barRef}
|
|
className="relative h-1 bg-music-border rounded-full cursor-pointer group"
|
|
onClick={handleClick}
|
|
onMouseDown={handleMouseDown}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseUp={handleMouseUp}
|
|
onMouseLeave={handleMouseUp}
|
|
>
|
|
<div
|
|
className="absolute left-0 top-0 h-full bg-music-accent rounded-full transition-none"
|
|
style={{ width: `${percent}%` }}
|
|
/>
|
|
<div
|
|
className="absolute top-1/2 -translate-y-1/2 w-3 h-3 bg-music-accent rounded-full opacity-0 group-hover:opacity-100 transition-opacity -ml-1.5"
|
|
style={{ left: `${percent}%` }}
|
|
/>
|
|
</div>
|
|
<div className="flex justify-between mt-1 text-xs text-music-muted">
|
|
<span>{formatTime(progress)}</span>
|
|
<span>{formatTime(duration)}</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
} |