From 202f2fa83b69620f8538d78e1fc31d9885fc0513 Mon Sep 17 00:00:00 2001 From: bdim404 Date: Thu, 12 Mar 2026 16:05:19 +0800 Subject: [PATCH] feat: Refactor AudioPlayer and Audiobook components to improve loading state handling and integrate dialog components --- .../src/components/AudioPlayer.tsx | 32 ++- qwen3-tts-frontend/src/pages/Audiobook.tsx | 220 ++++++++++++------ 2 files changed, 179 insertions(+), 73 deletions(-) diff --git a/qwen3-tts-frontend/src/components/AudioPlayer.tsx b/qwen3-tts-frontend/src/components/AudioPlayer.tsx index facd353..ac35679 100644 --- a/qwen3-tts-frontend/src/components/AudioPlayer.tsx +++ b/qwen3-tts-frontend/src/components/AudioPlayer.tsx @@ -20,12 +20,17 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => { const [blobUrl, setBlobUrl] = useState('') const [isLoading, setIsLoading] = useState(false) const [loadError, setLoadError] = useState(null) + const [isPending, setIsPending] = useState(false) + const [retryCount, setRetryCount] = useState(0) const previousAudioUrlRef = useRef('') + const retryTimerRef = useRef | null>(null) const containerRef = useRef(null) const playerInstanceRef = useRef(null) useEffect(() => { - if (!audioUrl || audioUrl === previousAudioUrlRef.current) return + if (!audioUrl) return + const cacheKey = `${audioUrl}::${retryCount}` + if (cacheKey === previousAudioUrlRef.current) return let active = true const prevBlobUrl = blobUrl @@ -33,6 +38,7 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => { const fetchAudio = async () => { setIsLoading(true) setLoadError(null) + setIsPending(false) if (prevBlobUrl) { URL.revokeObjectURL(prevBlobUrl) @@ -43,12 +49,20 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => { if (active) { const url = URL.createObjectURL(response.data) setBlobUrl(url) - previousAudioUrlRef.current = audioUrl + previousAudioUrlRef.current = cacheKey } - } catch (error) { - console.error("Failed to load audio:", error) + } catch (error: unknown) { if (active) { - setLoadError(t('failedToLoadAudio')) + const status = (error as { response?: { status?: number } })?.response?.status + if (status === 404) { + setIsPending(true) + retryTimerRef.current = setTimeout(() => { + if (active) setRetryCount(c => c + 1) + }, 3000) + } else { + console.error("Failed to load audio:", error) + setLoadError(t('failedToLoadAudio')) + } } } finally { if (active) { @@ -61,8 +75,12 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => { return () => { active = false + if (retryTimerRef.current) { + clearTimeout(retryTimerRef.current) + retryTimerRef.current = null + } } - }, [audioUrl, blobUrl, t]) + }, [audioUrl, retryCount, blobUrl, t]) useEffect(() => { return () => { @@ -119,7 +137,7 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => { link.click() }, [blobUrl, audioUrl, jobId]) - if (isLoading) { + if (isLoading || isPending) { return (
{t('loadingAudio')} diff --git a/qwen3-tts-frontend/src/pages/Audiobook.tsx b/qwen3-tts-frontend/src/pages/Audiobook.tsx index 446da54..5a16474 100644 --- a/qwen3-tts-frontend/src/pages/Audiobook.tsx +++ b/qwen3-tts-frontend/src/pages/Audiobook.tsx @@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Badge } from '@/components/ui/badge' import { Progress } from '@/components/ui/progress' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Navbar } from '@/components/Navbar' import { AudioPlayer } from '@/components/AudioPlayer' import { audiobookApi, type AudiobookProject, type AudiobookProjectDetail, type AudiobookCharacter, type AudiobookSegment } from '@/lib/api/audiobook' @@ -213,7 +214,7 @@ function LogStream({ projectId, chapterId, active }: { projectId: number; chapte ) } -function LLMConfigPanel({ onSaved }: { onSaved?: () => void }) { +function LLMConfigDialog({ open, onClose }: { open: boolean; onClose: () => void }) { const { t } = useTranslation('audiobook') const [baseUrl, setBaseUrl] = useState('') const [apiKey, setApiKey] = useState('') @@ -222,8 +223,8 @@ function LLMConfigPanel({ onSaved }: { onSaved?: () => void }) { const [existing, setExisting] = useState<{ base_url?: string; model?: string; has_key: boolean } | null>(null) useEffect(() => { - audiobookApi.getLLMConfig().then(setExisting).catch(() => {}) - }, []) + if (open) audiobookApi.getLLMConfig().then(setExisting).catch(() => {}) + }, [open]) const handleSave = async () => { if (!baseUrl || !apiKey || !model) { @@ -237,7 +238,7 @@ function LLMConfigPanel({ onSaved }: { onSaved?: () => void }) { setApiKey('') const updated = await audiobookApi.getLLMConfig() setExisting(updated) - onSaved?.() + onClose() } catch (e: any) { toast.error(formatApiError(e)) } finally { @@ -246,28 +247,39 @@ function LLMConfigPanel({ onSaved }: { onSaved?: () => void }) { } return ( -
-
{t('llmConfigPanel.title')}
- {existing && ( -
- {t('llmConfigPanel.current', { - baseUrl: existing.base_url || t('llmConfigPanel.notSet'), - model: existing.model || t('llmConfigPanel.notSet'), - keyStatus: existing.has_key ? t('llmConfigPanel.hasKey') : t('llmConfigPanel.noKey'), - })} + { if (!v) onClose() }}> + + + {t('llmConfigPanel.title')} + +
+ {existing && ( +
+ {t('llmConfigPanel.current', { + baseUrl: existing.base_url || t('llmConfigPanel.notSet'), + model: existing.model || t('llmConfigPanel.notSet'), + keyStatus: existing.has_key ? t('llmConfigPanel.hasKey') : t('llmConfigPanel.noKey'), + })} +
+ )} + setBaseUrl(e.target.value)} /> + setApiKey(e.target.value)} /> + setModel(e.target.value)} /> +
+ + +
- )} - setBaseUrl(e.target.value)} /> - setApiKey(e.target.value)} /> - setModel(e.target.value)} /> - -
+ + ) } -function CreateProjectPanel({ onCreated }: { onCreated: () => void }) { +function CreateProjectDialog({ open, onClose, onCreated }: { open: boolean; onClose: () => void; onCreated: () => void }) { const { t } = useTranslation('audiobook') const [title, setTitle] = useState('') const [sourceType, setSourceType] = useState<'text' | 'epub'>('text') @@ -275,6 +287,8 @@ function CreateProjectPanel({ onCreated }: { onCreated: () => void }) { const [epubFile, setEpubFile] = useState(null) const [loading, setLoading] = useState(false) + const reset = () => { setTitle(''); setText(''); setEpubFile(null); setSourceType('text') } + const handleCreate = async () => { if (!title) { toast.error(t('createPanel.titleRequired')); return } if (sourceType === 'text' && !text) { toast.error(t('createPanel.textRequired')); return } @@ -287,8 +301,9 @@ function CreateProjectPanel({ onCreated }: { onCreated: () => void }) { await audiobookApi.uploadEpub(title, epubFile!) } toast.success(t('createPanel.createdSuccess')) - setTitle(''); setText(''); setEpubFile(null) + reset() onCreated() + onClose() } catch (e: any) { toast.error(formatApiError(e)) } finally { @@ -297,33 +312,42 @@ function CreateProjectPanel({ onCreated }: { onCreated: () => void }) { } return ( -
-
{t('createPanel.title')}
- setTitle(e.target.value)} /> -
- - -
- {sourceType === 'text' && ( -