You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
115 lines
3.3 KiB
115 lines
3.3 KiB
import { Button } from '@/components/ui/button' |
|
import { Slider } from '@/components/ui/slider' |
|
import { cn } from '@/lib/utils' |
|
import mediaManager from '@/services/media-manager.service' |
|
import { Pause, Play } from 'lucide-react' |
|
import { useEffect, useRef, useState } from 'react' |
|
|
|
interface AudioPlayerProps { |
|
src: string |
|
className?: string |
|
} |
|
|
|
export default function AudioPlayer({ src, className }: AudioPlayerProps) { |
|
const audioRef = useRef<HTMLAudioElement>(null) |
|
const [isPlaying, setIsPlaying] = useState(false) |
|
const [currentTime, setCurrentTime] = useState(0) |
|
const [duration, setDuration] = useState(0) |
|
const seekTimeoutRef = useRef<NodeJS.Timeout>() |
|
const isSeeking = useRef(false) |
|
|
|
useEffect(() => { |
|
const audio = audioRef.current |
|
if (!audio) return |
|
|
|
const updateTime = () => { |
|
if (!isSeeking.current) { |
|
setCurrentTime(audio.currentTime) |
|
} |
|
} |
|
const updateDuration = () => setDuration(audio.duration) |
|
const handleEnded = () => setIsPlaying(false) |
|
const handlePause = () => setIsPlaying(false) |
|
const handlePlay = () => setIsPlaying(true) |
|
|
|
audio.addEventListener('timeupdate', updateTime) |
|
audio.addEventListener('loadedmetadata', updateDuration) |
|
audio.addEventListener('ended', handleEnded) |
|
audio.addEventListener('pause', handlePause) |
|
audio.addEventListener('play', handlePlay) |
|
|
|
return () => { |
|
audio.removeEventListener('timeupdate', updateTime) |
|
audio.removeEventListener('loadedmetadata', updateDuration) |
|
audio.removeEventListener('ended', handleEnded) |
|
audio.removeEventListener('pause', handlePause) |
|
audio.removeEventListener('play', handlePlay) |
|
} |
|
}, []) |
|
|
|
const togglePlay = () => { |
|
const audio = audioRef.current |
|
if (!audio) return |
|
|
|
if (isPlaying) { |
|
audio.pause() |
|
setIsPlaying(false) |
|
} else { |
|
audio.play() |
|
setIsPlaying(true) |
|
mediaManager.play(audio) |
|
} |
|
} |
|
|
|
const handleSeek = (value: number[]) => { |
|
const audio = audioRef.current |
|
if (!audio) return |
|
|
|
isSeeking.current = true |
|
setCurrentTime(value[0]) |
|
|
|
if (seekTimeoutRef.current) { |
|
clearTimeout(seekTimeoutRef.current) |
|
} |
|
|
|
seekTimeoutRef.current = setTimeout(() => { |
|
audio.currentTime = value[0] |
|
isSeeking.current = false |
|
}, 300) |
|
} |
|
|
|
return ( |
|
<div |
|
className={cn( |
|
'flex items-center gap-3 py-2 pl-2 pr-4 border rounded-full max-w-md', |
|
className |
|
)} |
|
onClick={(e) => e.stopPropagation()} |
|
> |
|
<audio ref={audioRef} src={src} preload="metadata" /> |
|
|
|
{/* Play/Pause Button */} |
|
<Button size="icon" className="rounded-full shrink-0" onClick={togglePlay}> |
|
{isPlaying ? <Pause fill="currentColor" /> : <Play fill="currentColor" />} |
|
</Button> |
|
|
|
{/* Progress Section */} |
|
<div className="flex-1 relative"> |
|
<Slider value={[currentTime]} max={duration || 100} step={1} onValueChange={handleSeek} /> |
|
</div> |
|
|
|
<div className="text-sm font-mono text-muted-foreground"> |
|
{formatTime(Math.max(duration - currentTime, 0))} |
|
</div> |
|
</div> |
|
) |
|
} |
|
|
|
const formatTime = (time: number) => { |
|
if (time === Infinity || isNaN(time)) { |
|
return '-:--' |
|
} |
|
const minutes = Math.floor(time / 60) |
|
const seconds = Math.floor(time % 60) |
|
return `${minutes}:${seconds.toString().padStart(2, '0')}` |
|
}
|
|
|