diff --git a/qwen3-tts-frontend/src/pages/Audiobook.tsx b/qwen3-tts-frontend/src/pages/Audiobook.tsx index 63a0c66..f84b12a 100644 --- a/qwen3-tts-frontend/src/pages/Audiobook.tsx +++ b/qwen3-tts-frontend/src/pages/Audiobook.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback, useRef } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' -import { Book, BookOpen, Plus, Trash2, RefreshCw, Download, ChevronDown, ChevronUp, Play, Square, Pencil, Check, X, Loader2, Zap, Settings2, PanelLeftClose, PanelLeftOpen, Wand2, Volume2, Bot, Flame, Headphones, Clock, CheckCircle2, AlertCircle, CircleDot } from 'lucide-react' +import { Book, BookOpen, Plus, Trash2, RefreshCw, Download, ChevronDown, ChevronUp, Play, Square, Pencil, Check, X, Loader2, Zap, Settings2, PanelLeftClose, PanelLeftOpen, Wand2, Volume2, Bot, Headphones, Clock, CheckCircle2, AlertCircle, CircleDot } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' @@ -33,18 +33,26 @@ function LazyAudioPlayer({ audioUrl, jobId, compact }: { audioUrl: string; jobId } const STATUS_CONFIG: Record = { - pending: { Icon: Clock, iconCls: 'h-3 w-3 text-muted-foreground', cardBg: 'bg-background/50', cardBgSel: 'bg-muted' }, - analyzing: { Icon: Loader2, iconCls: 'h-3 w-3 text-blue-500 animate-spin', cardBg: 'bg-blue-500/5', cardBgSel: 'bg-blue-500/20' }, - characters_ready: { Icon: Loader2, iconCls: 'h-3 w-3 text-blue-500 animate-spin', cardBg: 'bg-blue-500/5', cardBgSel: 'bg-blue-500/20' }, - parsing: { Icon: Loader2, iconCls: 'h-3 w-3 text-amber-500 animate-spin', cardBg: 'bg-amber-500/5', cardBgSel: 'bg-amber-500/20' }, - processing: { Icon: Loader2, iconCls: 'h-3 w-3 text-amber-500 animate-spin', cardBg: 'bg-amber-500/5', cardBgSel: 'bg-amber-500/20' }, - ready: { Icon: CircleDot, iconCls: 'h-3 w-3 text-green-500', cardBg: 'bg-green-500/5', cardBgSel: 'bg-green-500/20' }, - generating: { Icon: Loader2, iconCls: 'h-3 w-3 text-amber-500 animate-spin', cardBg: 'bg-amber-500/5', cardBgSel: 'bg-amber-500/20' }, - done: { Icon: CheckCircle2, iconCls: 'h-3 w-3 text-emerald-500', cardBg: 'bg-emerald-500/5', cardBgSel: 'bg-emerald-500/20' }, - error: { Icon: AlertCircle, iconCls: 'h-3 w-3 text-destructive', cardBg: 'bg-destructive/5', cardBgSel: 'bg-destructive/20' }, + pending: { Icon: Clock, iconCls: 'h-3 w-3 text-muted-foreground/40', cardBg: 'bg-background/50', cardBgSel: 'bg-muted' }, + analyzing: { Icon: Loader2, iconCls: 'h-3 w-3 text-sky-500 animate-spin', cardBg: 'bg-sky-500/5', cardBgSel: 'bg-sky-500/20' }, + characters_ready: { Icon: Loader2, iconCls: 'h-3 w-3 text-violet-500 animate-spin', cardBg: 'bg-violet-500/5', cardBgSel: 'bg-violet-500/20' }, + parsing: { Icon: Loader2, iconCls: 'h-3 w-3 text-amber-500 animate-spin', cardBg: 'bg-amber-500/5', cardBgSel: 'bg-amber-500/20' }, + processing: { Icon: Loader2, iconCls: 'h-3 w-3 text-red-500 animate-spin', cardBg: 'bg-orange-500/5', cardBgSel: 'bg-orange-500/20' }, + ready: { Icon: CircleDot, iconCls: 'h-3 w-3 text-cyan-500', cardBg: 'bg-cyan-500/5', cardBgSel: 'bg-cyan-500/20' }, + generating: { Icon: Loader2, iconCls: 'h-3 w-3 text-amber-400 animate-spin', cardBg: 'bg-amber-400/5', cardBgSel: 'bg-amber-400/20' }, + done: { Icon: CheckCircle2, iconCls: 'h-3 w-3 text-emerald-500', cardBg: 'bg-emerald-500/5', cardBgSel: 'bg-emerald-500/20' }, + error: { Icon: AlertCircle, iconCls: 'h-3 w-3 text-destructive', cardBg: 'bg-destructive/5', cardBgSel: 'bg-destructive/20' }, } -const STEP_HINT_STATUSES = ['pending', 'analyzing', 'characters_ready', 'ready', 'generating'] + +function NsfwIcon({ className }: { className?: string }) { + return ( + + + 18+ + + ) +} function SequentialPlayer({ segments, @@ -839,7 +847,7 @@ function NSFWScriptDialog({ open, onClose, onCreated }: { open: boolean; onClose - + NSFW 生成剧本 @@ -1046,8 +1054,8 @@ function ProjectListSidebar({ )} {hasNsfwAccess && ( - )} ) @@ -1189,9 +1196,9 @@ function CharactersPanel({ if (collapsed && hasChaptersOutline) { return ( -
-
- +
+
+ {t('projectCard.chapters.title', { count: detail!.chapters.length })} {ch.status === 'parsing' && ( -
+
)} - {/* Segments — flat list, each is its own card */} {chExpanded && chSegs.length > 0 && ( -
+
{chSegs.map(seg => { const isEditing = editingSegId === seg.id const isRegenerating = regeneratingSegs.has(seg.id) || seg.status === 'generating' @@ -1874,6 +1880,7 @@ export default function Audiobook() { const [detail, setDetail] = useState(null) const [segments, setSegments] = useState([]) const [loading, setLoading] = useState(true) + const [detailLoading, setDetailLoading] = useState(false) const [loadingAction, setLoadingAction] = useState(false) const [isPolling, setIsPolling] = useState(false) const [generatingChapterIndices, setGeneratingChapterIndices] = useState>(new Set()) @@ -1930,8 +1937,8 @@ export default function Audiobook() { setIsPolling(false) autoExpandedRef.current.clear() prevStatusRef.current = '' - fetchDetail() - fetchSegments() + setDetailLoading(true) + Promise.all([fetchDetail(), fetchSegments()]).finally(() => setDetailLoading(false)) }, [selectedProjectId, fetchDetail, fetchSegments]) const status = selectedProject?.status ?? '' @@ -2302,8 +2309,8 @@ export default function Audiobook() {
{(() => { const isNsfw = selectedProject.source_type === 'ai_generated' && !!(selectedProject.script_config as any)?.nsfw_mode - const Icon = selectedProject.source_type === 'epub' ? BookOpen : isNsfw ? Flame : selectedProject.source_type === 'ai_generated' ? Wand2 : Book - const cls = isNsfw ? 'h-4 w-4 shrink-0 text-orange-500' : selectedProject.source_type === 'ai_generated' ? 'h-4 w-4 shrink-0 text-violet-500' : 'h-4 w-4 shrink-0 text-muted-foreground' + const Icon = selectedProject.source_type === 'epub' ? BookOpen : isNsfw ? NsfwIcon : selectedProject.source_type === 'ai_generated' ? Zap : Book + const cls = isNsfw ? 'h-4 w-4 shrink-0 text-red-500' : 'h-4 w-4 shrink-0 text-muted-foreground' return })()} {selectedProject.title} @@ -2340,11 +2347,6 @@ export default function Audiobook() {
- {STEP_HINT_STATUSES.includes(status) && ( -
- {t(`stepHints.${status}`)} -
- )} {(status === 'analyzing' || (status === 'generating' && selectedProject.source_type === 'ai_generated')) && (
@@ -2418,6 +2420,20 @@ export default function Audiobook() {
)} + {detailLoading ? ( +
+
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+
+ {[...Array(5)].map((_, i) => ( +
+ ))} +
+
+ ) : (
setScrollToChapterId(null)} />
+ )} )}