174 lines
5.5 KiB
TypeScript
174 lines
5.5 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useRef, useEffect } from 'react'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Play, Pause, Volume2 } from 'lucide-react'
|
||
|
||
interface VoicePlayerProps {
|
||
audioUrl: string
|
||
duration?: number
|
||
isCurrentUser?: boolean
|
||
}
|
||
|
||
export function VoicePlayer({ audioUrl, duration = 0, isCurrentUser = false }: VoicePlayerProps) {
|
||
const [isPlaying, setIsPlaying] = useState(false)
|
||
const [currentTime, setCurrentTime] = useState(0)
|
||
const [audioDuration, setAudioDuration] = useState(duration > 0 ? duration : 0)
|
||
const [isLoading, setIsLoading] = useState(false)
|
||
|
||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||
|
||
// Обновляем длительность при изменении props
|
||
useEffect(() => {
|
||
if (duration > 0 && (!audioDuration || audioDuration === 0)) {
|
||
setAudioDuration(duration)
|
||
}
|
||
}, [duration, audioDuration])
|
||
|
||
useEffect(() => {
|
||
// Создаем аудио элемент
|
||
audioRef.current = new Audio(audioUrl)
|
||
const audio = audioRef.current
|
||
|
||
|
||
|
||
const handleLoadedMetadata = () => {
|
||
if (audio.duration && isFinite(audio.duration) && !isNaN(audio.duration)) {
|
||
setAudioDuration(audio.duration)
|
||
} else {
|
||
setAudioDuration(duration || 0)
|
||
}
|
||
setIsLoading(false)
|
||
}
|
||
|
||
const handleTimeUpdate = () => {
|
||
if (audio.currentTime && isFinite(audio.currentTime) && !isNaN(audio.currentTime)) {
|
||
setCurrentTime(audio.currentTime)
|
||
}
|
||
}
|
||
|
||
const handleEnded = () => {
|
||
setIsPlaying(false)
|
||
setCurrentTime(0)
|
||
}
|
||
|
||
const handleCanPlay = () => {
|
||
setIsLoading(false)
|
||
}
|
||
|
||
const handleLoadStart = () => {
|
||
setIsLoading(true)
|
||
}
|
||
|
||
const handleError = () => {
|
||
console.error('Audio loading error')
|
||
setIsLoading(false)
|
||
setIsPlaying(false)
|
||
}
|
||
|
||
audio.addEventListener('loadedmetadata', handleLoadedMetadata)
|
||
audio.addEventListener('timeupdate', handleTimeUpdate)
|
||
audio.addEventListener('ended', handleEnded)
|
||
audio.addEventListener('canplay', handleCanPlay)
|
||
audio.addEventListener('loadstart', handleLoadStart)
|
||
audio.addEventListener('error', handleError)
|
||
|
||
return () => {
|
||
if (audio) {
|
||
audio.removeEventListener('loadedmetadata', handleLoadedMetadata)
|
||
audio.removeEventListener('timeupdate', handleTimeUpdate)
|
||
audio.removeEventListener('ended', handleEnded)
|
||
audio.removeEventListener('canplay', handleCanPlay)
|
||
audio.removeEventListener('loadstart', handleLoadStart)
|
||
audio.removeEventListener('error', handleError)
|
||
audio.pause()
|
||
}
|
||
}
|
||
}, [audioUrl, duration])
|
||
|
||
const togglePlayPause = () => {
|
||
const audio = audioRef.current
|
||
if (!audio) return
|
||
|
||
if (isPlaying) {
|
||
audio.pause()
|
||
setIsPlaying(false)
|
||
} else {
|
||
audio.play().then(() => {
|
||
setIsPlaying(true)
|
||
}).catch((error) => {
|
||
console.error('Error playing audio:', error)
|
||
setIsPlaying(false)
|
||
})
|
||
}
|
||
}
|
||
|
||
const formatTime = (time: number) => {
|
||
if (isNaN(time) || !isFinite(time) || time < 0) return '0:00'
|
||
const minutes = Math.floor(time / 60)
|
||
const seconds = Math.floor(time % 60)
|
||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||
}
|
||
|
||
const getProgress = () => {
|
||
if (!audioDuration || audioDuration === 0 || isNaN(audioDuration) || !isFinite(audioDuration)) return 0
|
||
if (!currentTime || isNaN(currentTime) || !isFinite(currentTime)) return 0
|
||
return Math.min((currentTime / audioDuration) * 100, 100)
|
||
}
|
||
|
||
return (
|
||
<div className={`flex items-center space-x-4 p-3 rounded-lg min-w-[200px] max-w-sm ${
|
||
isCurrentUser
|
||
? 'bg-blue-500/20 border border-blue-500/30'
|
||
: 'bg-white/10 border border-white/20'
|
||
}`}>
|
||
{/* Кнопка воспроизведения */}
|
||
<Button
|
||
onClick={togglePlayPause}
|
||
disabled={isLoading}
|
||
variant="ghost"
|
||
size="sm"
|
||
className={`p-2 rounded-full ${
|
||
isCurrentUser
|
||
? 'text-blue-300 hover:text-blue-200 hover:bg-blue-500/30'
|
||
: 'text-white hover:text-white/80 hover:bg-white/20'
|
||
}`}
|
||
>
|
||
{isLoading ? (
|
||
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||
) : isPlaying ? (
|
||
<Pause className="h-4 w-4" />
|
||
) : (
|
||
<Play className="h-4 w-4" />
|
||
)}
|
||
</Button>
|
||
|
||
{/* Визуализация волны / прогресс бар */}
|
||
<div className="flex-1 space-y-2">
|
||
<div className="flex items-center space-x-3">
|
||
<Volume2 className={`h-3 w-3 ${
|
||
isCurrentUser ? 'text-blue-300' : 'text-white/60'
|
||
}`} />
|
||
<div className="flex-1 h-1 bg-white/20 rounded-full overflow-hidden">
|
||
<div
|
||
className={`h-full transition-all duration-300 ${
|
||
isCurrentUser ? 'bg-blue-400' : 'bg-white/60'
|
||
}`}
|
||
style={{ width: `${getProgress()}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Время */}
|
||
<div className="flex justify-between items-center text-xs">
|
||
<span className={`${isCurrentUser ? 'text-blue-300/70' : 'text-white/50'} min-w-[2rem]`}>
|
||
{formatTime(currentTime)}
|
||
</span>
|
||
<span className={`${isCurrentUser ? 'text-blue-300/70' : 'text-white/50'} min-w-[2rem] text-right`}>
|
||
{formatTime(audioDuration)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|