125 lines
3.1 KiB
TypeScript
125 lines
3.1 KiB
TypeScript
import { useRef, useState, useEffect, useCallback, memo } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import AudioPlayerLib from 'react-h5-audio-player'
|
|
import 'react-h5-audio-player/lib/styles.css'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Download } from 'lucide-react'
|
|
import apiClient from '@/lib/api'
|
|
import styles from './AudioPlayer.module.css'
|
|
|
|
interface AudioPlayerProps {
|
|
audioUrl: string
|
|
jobId: number
|
|
}
|
|
|
|
const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
|
|
const { t } = useTranslation('common')
|
|
const [blobUrl, setBlobUrl] = useState<string>('')
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [loadError, setLoadError] = useState<string | null>(null)
|
|
const previousAudioUrlRef = useRef<string>('')
|
|
|
|
useEffect(() => {
|
|
if (!audioUrl || audioUrl === previousAudioUrlRef.current) return
|
|
|
|
let active = true
|
|
const prevBlobUrl = blobUrl
|
|
|
|
const fetchAudio = async () => {
|
|
setIsLoading(true)
|
|
setLoadError(null)
|
|
|
|
if (prevBlobUrl) {
|
|
URL.revokeObjectURL(prevBlobUrl)
|
|
}
|
|
|
|
try {
|
|
const response = await apiClient.get(audioUrl, { responseType: 'blob' })
|
|
if (active) {
|
|
const url = URL.createObjectURL(response.data)
|
|
setBlobUrl(url)
|
|
previousAudioUrlRef.current = audioUrl
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load audio:", error)
|
|
if (active) {
|
|
setLoadError(t('failedToLoadAudio'))
|
|
}
|
|
} finally {
|
|
if (active) {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
fetchAudio()
|
|
|
|
return () => {
|
|
active = false
|
|
}
|
|
}, [audioUrl, blobUrl, t])
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (blobUrl) URL.revokeObjectURL(blobUrl)
|
|
}
|
|
}, [])
|
|
|
|
const handleDownload = useCallback(() => {
|
|
const link = document.createElement('a')
|
|
link.href = blobUrl || audioUrl
|
|
link.download = `tts-${jobId}-${Date.now()}.wav`
|
|
link.click()
|
|
}, [blobUrl, audioUrl, jobId])
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center p-4 border rounded-lg">
|
|
<span className="text-sm text-muted-foreground">{t('loadingAudio')}</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (loadError) {
|
|
return (
|
|
<div className="flex items-center justify-center p-4 border rounded-lg">
|
|
<span className="text-sm text-destructive">{loadError}</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!blobUrl) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className={styles.audioPlayerWrapper}>
|
|
<AudioPlayerLib
|
|
src={blobUrl}
|
|
layout="horizontal"
|
|
customAdditionalControls={[
|
|
<Button
|
|
key="download"
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={handleDownload}
|
|
className={styles.downloadButton}
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
</Button>
|
|
]}
|
|
customVolumeControls={[]}
|
|
showJumpControls={false}
|
|
volume={1}
|
|
preload="metadata"
|
|
autoPlayAfterSrcChange={false}
|
|
/>
|
|
</div>
|
|
)
|
|
})
|
|
|
|
AudioPlayer.displayName = 'AudioPlayer'
|
|
|
|
export { AudioPlayer }
|