165 lines
5.7 KiB
TypeScript
165 lines
5.7 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useRef, useEffect } from 'react'
|
|
|
|
interface AudioPlayerProps {
|
|
songId: string
|
|
songTitle: string
|
|
artistName: string
|
|
coverUrl?: string
|
|
audioUrl: string
|
|
onPlayCountIncrement?: () => void
|
|
}
|
|
|
|
export function AudioPlayer({
|
|
songId,
|
|
songTitle,
|
|
artistName,
|
|
coverUrl,
|
|
audioUrl,
|
|
onPlayCountIncrement
|
|
}: AudioPlayerProps) {
|
|
const [isPlaying, setIsPlaying] = useState(false)
|
|
const [currentTime, setCurrentTime] = useState(0)
|
|
const [duration, setDuration] = useState(0)
|
|
const [volume, setVolume] = useState(0.8)
|
|
const audioRef = useRef<HTMLAudioElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (audioRef.current) {
|
|
audioRef.current.volume = volume
|
|
}
|
|
}, [volume])
|
|
|
|
useEffect(() => {
|
|
const audio = audioRef.current
|
|
if (!audio) return
|
|
|
|
const updateTime = () => setCurrentTime(audio.currentTime)
|
|
const updateDuration = () => setDuration(audio.duration)
|
|
|
|
audio.addEventListener('timeupdate', updateTime)
|
|
audio.addEventListener('loadedmetadata', updateDuration)
|
|
|
|
return () => {
|
|
audio.removeEventListener('timeupdate', updateTime)
|
|
audio.removeEventListener('loadedmetadata', updateDuration)
|
|
}
|
|
}, [])
|
|
|
|
const togglePlay = () => {
|
|
if (audioRef.current) {
|
|
if (isPlaying) {
|
|
audioRef.current.pause()
|
|
} else {
|
|
audioRef.current.play()
|
|
if (onPlayCountIncrement && currentTime === 0) {
|
|
onPlayCountIncrement()
|
|
}
|
|
}
|
|
setIsPlaying(!isPlaying)
|
|
}
|
|
}
|
|
|
|
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const newTime = parseFloat(e.target.value)
|
|
setCurrentTime(newTime)
|
|
if (audioRef.current) {
|
|
audioRef.current.currentTime = newTime
|
|
}
|
|
}
|
|
|
|
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setVolume(parseFloat(e.target.value))
|
|
}
|
|
|
|
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="fixed bottom-0 left-0 right-0 bg-zinc-900 border-t border-zinc-800 p-4 z-50">
|
|
<audio ref={audioRef} src={audioUrl} />
|
|
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="flex items-center gap-4">
|
|
{/* Song Info */}
|
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
{coverUrl && (
|
|
<img
|
|
src={coverUrl}
|
|
alt={songTitle}
|
|
className="w-12 h-12 rounded object-cover"
|
|
/>
|
|
)}
|
|
<div className="min-w-0">
|
|
<p className="text-white font-medium truncate">{songTitle}</p>
|
|
<p className="text-zinc-400 text-sm truncate">{artistName}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Controls */}
|
|
<div className="flex flex-col items-center flex-1 gap-2">
|
|
<button
|
|
onClick={togglePlay}
|
|
className="w-10 h-10 rounded-full bg-purple-500 hover:bg-purple-600 flex items-center justify-center transition"
|
|
>
|
|
{isPlaying ? (
|
|
<svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M6 4h3v12H6V4zm5 0h3v12h-3V4z" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-5 h-5 text-white ml-0.5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M6.3 4.1c-.4-.2-.8 0-.8.4v11c0 .4.4.6.8.4l9-5.5c.3-.2.3-.6 0-.8l-9-5.5z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
|
|
{/* Progress Bar */}
|
|
<div className="flex items-center gap-2 w-full max-w-md">
|
|
<span className="text-xs text-zinc-400 tabular-nums">
|
|
{formatTime(currentTime)}
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max={duration || 0}
|
|
value={currentTime}
|
|
onChange={handleSeek}
|
|
className="flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer"
|
|
style={{
|
|
background: `linear-gradient(to right, rgb(168 85 247) 0%, rgb(168 85 247) ${(currentTime / duration) * 100}%, rgb(63 63 70) ${(currentTime / duration) * 100}%, rgb(63 63 70) 100%)`
|
|
}}
|
|
/>
|
|
<span className="text-xs text-zinc-400 tabular-nums">
|
|
{formatTime(duration)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Volume */}
|
|
<div className="flex items-center gap-2 flex-1 justify-end">
|
|
<svg className="w-5 h-5 text-zinc-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" />
|
|
</svg>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
value={volume}
|
|
onChange={handleVolumeChange}
|
|
className="w-24 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer"
|
|
style={{
|
|
background: `linear-gradient(to right, rgb(168 85 247) 0%, rgb(168 85 247) ${volume * 100}%, rgb(63 63 70) ${volume * 100}%, rgb(63 63 70) 100%)`
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|