Spaces:
Build error
Build error
| "use client"; | |
| import { useState, useRef, useEffect } from "react"; | |
| import { Play, Pause, Volume2, VolumeX } from "lucide-react"; | |
| import { Slider } from "@/components/ui/slider"; | |
| import { Button } from "@/components/ui/button"; | |
| import { cn } from "@/lib/utils"; | |
| interface AudioPlayerProps { | |
| src: string; | |
| className?: string; | |
| autoplay?: boolean; | |
| onPlayStateChange?: (isPlaying: boolean) => void; | |
| } | |
| export function AudioPlayer({ src, className, autoplay = false, onPlayStateChange }: AudioPlayerProps) { | |
| const [isPlaying, setIsPlaying] = useState(autoplay); | |
| const [duration, setDuration] = useState(0); | |
| const [currentTime, setCurrentTime] = useState(0); | |
| const [volume, setVolume] = useState(1); | |
| const [isMuted, setIsMuted] = useState(false); | |
| const audioRef = useRef<HTMLAudioElement>(null); | |
| useEffect(() => { | |
| const audio = audioRef.current; | |
| if (!audio) return; | |
| if (autoplay) { | |
| audio.play().catch(() => { | |
| /* Autoplay blocked by browser */ | |
| }); | |
| } | |
| const setAudioData = () => { | |
| setDuration(audio.duration); | |
| setVolume(audio.volume); | |
| }; | |
| const setAudioTime = () => { | |
| setCurrentTime(audio.currentTime); | |
| }; | |
| const handleEnded = () => { | |
| setIsPlaying(false); | |
| onPlayStateChange?.(false); | |
| }; | |
| // Add event listeners | |
| audio.addEventListener("loadeddata", setAudioData); | |
| audio.addEventListener("timeupdate", setAudioTime); | |
| audio.addEventListener("ended", handleEnded); | |
| return () => { | |
| audio.removeEventListener("loadeddata", setAudioData); | |
| audio.removeEventListener("timeupdate", setAudioTime); | |
| audio.removeEventListener("ended", handleEnded); | |
| }; | |
| }, [src, autoplay, onPlayStateChange]); | |
| const togglePlay = () => { | |
| const audio = audioRef.current; | |
| if (!audio) return; | |
| if (isPlaying) { | |
| audio.pause(); | |
| onPlayStateChange?.(false); | |
| } else { | |
| audio.play(); | |
| onPlayStateChange?.(true); | |
| } | |
| setIsPlaying(!isPlaying); | |
| }; | |
| const toggleMute = () => { | |
| const audio = audioRef.current; | |
| if (!audio) return; | |
| audio.muted = !isMuted; | |
| setIsMuted(!isMuted); | |
| }; | |
| const handleVolumeChange = (value: number[]) => { | |
| const audio = audioRef.current; | |
| if (!audio) return; | |
| const newVolume = value[0]; | |
| audio.volume = newVolume; | |
| setVolume(newVolume); | |
| if (newVolume > 0 && isMuted) { | |
| setIsMuted(false); | |
| audio.muted = false; | |
| } | |
| }; | |
| const handleSeek = (value: number[]) => { | |
| const audio = audioRef.current; | |
| if (!audio) return; | |
| audio.currentTime = value[0]; | |
| setCurrentTime(value[0]); | |
| }; | |
| const formatTime = (time: number) => { | |
| const minutes = Math.floor(time / 60); | |
| const seconds = Math.floor(time % 60); | |
| return `${minutes}:${seconds.toString().padStart(2, "0")}`; | |
| }; | |
| return ( | |
| <div className={cn("flex flex-col gap-2 w-full bg-background/50 rounded-lg border p-3", className)}> | |
| <audio ref={audioRef} src={src} preload="metadata" /> | |
| <div className="flex items-center gap-2"> | |
| {!isPlaying ? ( | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 shrink-0 bg-primary/10 hover:bg-primary/20 text-primary" | |
| onClick={togglePlay} | |
| aria-label="Play" | |
| > | |
| <Play className="h-4 w-4 ml-0.5 fill-current" /> | |
| </Button> | |
| ) : ( | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 shrink-0 bg-secondary hover:bg-secondary/80" | |
| onClick={togglePlay} | |
| aria-label="Pause" | |
| > | |
| <Pause className="h-4 w-4 fill-current" /> | |
| </Button> | |
| )} | |
| <div className="flex-1 flex flex-col gap-1 ml-1"> | |
| <Slider | |
| value={[currentTime]} | |
| max={duration || 100} // Default to 100 to avoid 0 width slider initially | |
| step={0.1} | |
| onValueChange={handleSeek} | |
| className="w-full cursor-pointer" | |
| aria-label="Seek" | |
| /> | |
| <div className="flex justify-between text-[10px] text-muted-foreground px-0.5"> | |
| <span>{formatTime(currentTime)}</span> | |
| <span>{formatTime(duration)}</span> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2 group relative"> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground" | |
| onClick={toggleMute} | |
| aria-label={isMuted ? "Unmute" : "Mute"} | |
| > | |
| {isMuted || volume === 0 ? ( | |
| <VolumeX className="h-4 w-4" /> | |
| ) : ( | |
| <Volume2 className="h-4 w-4" /> | |
| )} | |
| </Button> | |
| <div className="w-0 overflow-hidden group-hover:w-20 transition-all duration-300"> | |
| <Slider | |
| value={[isMuted ? 0 : volume]} | |
| max={1} | |
| step={0.01} | |
| onValueChange={handleVolumeChange} | |
| className="w-20 pr-2" | |
| aria-label="Volume" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |