190 lines
6.2 KiB
TypeScript
190 lines
6.2 KiB
TypeScript
import { type FC, useState, useEffect, useRef } from 'react'
|
|
import { Microphone, Play, Pause } from 'iconsax-react'
|
|
|
|
type Props = {
|
|
label?: string
|
|
placeholder?: string
|
|
value?: string
|
|
onChange?: (value: string) => void
|
|
onRecordingComplete?: (audioBlob: Blob) => void
|
|
}
|
|
|
|
const VoiceRecorder: FC<Props> = ({
|
|
label = 'پیام شما',
|
|
placeholder = 'پیام شما',
|
|
value,
|
|
onChange,
|
|
onRecordingComplete
|
|
}) => {
|
|
const [isRecording, setIsRecording] = useState(false)
|
|
const [audioUrl, setAudioUrl] = useState<string | null>(null)
|
|
const [isPlaying, setIsPlaying] = useState(false)
|
|
const [currentTime, setCurrentTime] = useState(0)
|
|
const [duration, setDuration] = useState(0)
|
|
|
|
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
|
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
|
const chunksRef = useRef<Blob[]>([])
|
|
const animationFrameRef = useRef<number | null>(null)
|
|
|
|
const startRecording = async () => {
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
|
|
const mediaRecorder = new MediaRecorder(stream)
|
|
mediaRecorderRef.current = mediaRecorder
|
|
chunksRef.current = []
|
|
|
|
mediaRecorder.ondataavailable = (event) => {
|
|
if (event.data.size > 0) {
|
|
chunksRef.current.push(event.data)
|
|
}
|
|
}
|
|
|
|
mediaRecorder.onstop = () => {
|
|
const audioBlob = new Blob(chunksRef.current, { type: 'audio/webm' })
|
|
const url = URL.createObjectURL(audioBlob)
|
|
setAudioUrl(url)
|
|
onRecordingComplete?.(audioBlob)
|
|
stream.getTracks().forEach(track => track.stop())
|
|
}
|
|
|
|
mediaRecorder.start()
|
|
setIsRecording(true)
|
|
} catch (error) {
|
|
console.error('خطا در دسترسی به میکروفون:', error)
|
|
}
|
|
}
|
|
|
|
const stopRecording = () => {
|
|
if (mediaRecorderRef.current && isRecording) {
|
|
mediaRecorderRef.current.stop()
|
|
setIsRecording(false)
|
|
}
|
|
}
|
|
|
|
const togglePlayPause = () => {
|
|
if (!audioRef.current) return
|
|
|
|
if (isPlaying) {
|
|
audioRef.current.pause()
|
|
} else {
|
|
audioRef.current.play()
|
|
}
|
|
setIsPlaying(!isPlaying)
|
|
}
|
|
|
|
const updateProgress = () => {
|
|
if (audioRef.current) {
|
|
setCurrentTime(audioRef.current.currentTime)
|
|
if (isPlaying) {
|
|
animationFrameRef.current = requestAnimationFrame(updateProgress)
|
|
}
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (audioUrl && audioRef.current) {
|
|
audioRef.current.addEventListener('loadedmetadata', () => {
|
|
if (audioRef.current) {
|
|
setDuration(audioRef.current.duration)
|
|
}
|
|
})
|
|
|
|
audioRef.current.addEventListener('ended', () => {
|
|
setIsPlaying(false)
|
|
setCurrentTime(0)
|
|
})
|
|
}
|
|
}, [audioUrl])
|
|
|
|
useEffect(() => {
|
|
if (isPlaying) {
|
|
updateProgress()
|
|
}
|
|
return () => {
|
|
if (animationFrameRef.current) {
|
|
cancelAnimationFrame(animationFrameRef.current)
|
|
}
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [isPlaying])
|
|
|
|
const generateWaveBars = () => {
|
|
const bars = []
|
|
const barCount = 100
|
|
const progress = duration > 0 ? currentTime / duration : 0
|
|
|
|
for (let i = 0; i < barCount; i++) {
|
|
const position = i / barCount
|
|
const isPassed = position <= progress
|
|
const baseHeight = 3
|
|
const randomHeight = Math.random() * 20 + baseHeight
|
|
|
|
bars.push(
|
|
<div
|
|
key={i}
|
|
className="w-[2px] rounded-full transition-all duration-100"
|
|
style={{
|
|
height: `${randomHeight}px`,
|
|
backgroundColor: isPassed ? '#3B82F6' : '#E5E7EB'
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
|
|
return bars
|
|
}
|
|
|
|
return (
|
|
<div className="w-full">
|
|
{label && <div className="text-sm mb-1">{label}</div>}
|
|
|
|
<div className="w-full">
|
|
<div className="w-full h-10 bg-white border border-border rounded-xl flex items-center px-4 gap-2">
|
|
<input
|
|
type="text"
|
|
placeholder={placeholder}
|
|
value={value}
|
|
onChange={(e) => onChange?.(e.target.value)}
|
|
className="flex-1 bg-transparent outline-none text-sm"
|
|
/>
|
|
<button
|
|
onClick={isRecording ? stopRecording : startRecording}
|
|
className="flex-shrink-0"
|
|
>
|
|
<Microphone
|
|
size={20}
|
|
color={isRecording ? '#EF4444' : '#000'}
|
|
variant={isRecording ? 'Bold' : 'Outline'}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{audioUrl && (
|
|
<div className="w-full h-12 bg-white border border-border rounded-xl flex items-center px-3 gap-3 mt-3">
|
|
<button
|
|
onClick={togglePlayPause}
|
|
className="w-8 h-8 rounded-full bg-black flex items-center justify-center flex-shrink-0"
|
|
>
|
|
{isPlaying ? (
|
|
<Pause size={16} color="#fff" variant="Bold" />
|
|
) : (
|
|
<Play size={16} color="#fff" variant="Bold" />
|
|
)}
|
|
</button>
|
|
|
|
<div className="flex-1 flex items-center gap-[2px] h-8">
|
|
{generateWaveBars()}
|
|
</div>
|
|
|
|
<audio ref={audioRef} src={audioUrl} className="hidden" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default VoiceRecorder
|