feat: update status handling in Audiobook component with enhanced visual indicators
This commit is contained in:
@@ -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 } 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, Flame, 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'
|
||||
@@ -32,16 +32,16 @@ function LazyAudioPlayer({ audioUrl, jobId, compact }: { audioUrl: string; jobId
|
||||
return <div ref={ref}>{visible && <AudioPlayer audioUrl={audioUrl} jobId={jobId} compact={compact} />}</div>
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
pending: 'secondary',
|
||||
analyzing: 'default',
|
||||
characters_ready: 'default',
|
||||
parsing: 'default',
|
||||
processing: 'default',
|
||||
ready: 'default',
|
||||
generating: 'default',
|
||||
done: 'outline',
|
||||
error: 'destructive',
|
||||
const STATUS_CONFIG: Record<string, { Icon: React.ElementType; iconCls: string; cardBg: string; cardBgSel: string }> = {
|
||||
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' },
|
||||
}
|
||||
|
||||
const STEP_HINT_STATUSES = ['pending', 'analyzing', 'characters_ready', 'ready', 'generating']
|
||||
@@ -1073,34 +1073,23 @@ function ProjectListSidebar({
|
||||
? Wand2
|
||||
: Book
|
||||
const iconClass = isNsfw
|
||||
? 'h-3.5 w-3.5 shrink-0 mt-0.5 text-orange-500'
|
||||
? 'h-3.5 w-3.5 shrink-0 text-orange-500'
|
||||
: p.source_type === 'ai_generated'
|
||||
? 'h-3.5 w-3.5 shrink-0 mt-0.5 text-violet-500'
|
||||
: 'h-3.5 w-3.5 shrink-0 mt-0.5 text-muted-foreground'
|
||||
const showProgress = p.segment_total > 0 && ['processing', 'generating', 'done'].includes(p.status)
|
||||
? 'h-3.5 w-3.5 shrink-0 text-violet-500'
|
||||
: 'h-3.5 w-3.5 shrink-0 text-muted-foreground'
|
||||
return (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => onSelect(p.id)}
|
||||
className={`w-full text-left rounded-lg border p-3 flex flex-col gap-1.5 transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 ${
|
||||
className={`w-full text-left rounded-lg border p-3 flex flex-col gap-1.5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 ${
|
||||
selectedId === p.id
|
||||
? 'bg-background border-border shadow-sm'
|
||||
: 'bg-background/50 border-border/40'
|
||||
? `${STATUS_CONFIG[p.status]?.cardBgSel ?? 'bg-background'} border-border shadow-sm`
|
||||
: `${STATUS_CONFIG[p.status]?.cardBg ?? 'bg-background/50'} border-border/40 hover:opacity-90`
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<ProjectIcon className={iconClass} />
|
||||
<span className="text-sm font-medium leading-snug break-words min-w-0 flex-1">{p.title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 ml-5">
|
||||
<Badge variant={(STATUS_COLORS[p.status] || 'secondary') as any} className="text-[10px] h-4 px-1">
|
||||
{t(`status.${p.status}`, { defaultValue: p.status })}
|
||||
</Badge>
|
||||
{showProgress && (
|
||||
<span className="text-[10px] text-muted-foreground tabular-nums">
|
||||
{p.segment_done}/{p.segment_total}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm font-medium truncate flex-1 text-center" title={p.title}>{p.title}</span>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
@@ -1194,12 +1183,6 @@ function CharactersPanel({
|
||||
}
|
||||
}
|
||||
|
||||
const genderLabel = (gender: string) => {
|
||||
if (gender === '男') return t('projectCard.characters.genderMale')
|
||||
if (gender === '女') return t('projectCard.characters.genderFemale')
|
||||
if (gender === '未知') return t('projectCard.characters.genderUnknown')
|
||||
return gender
|
||||
}
|
||||
|
||||
const charCount = detail?.characters.length ?? 0
|
||||
const hasChaptersOutline = (detail?.chapters.length ?? 0) > 0
|
||||
@@ -1313,12 +1296,7 @@ function CharactersPanel({
|
||||
<div className="flex flex-col gap-1 text-sm">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<span className="font-medium truncate">{char.name}</span>
|
||||
{char.gender && (
|
||||
<Badge variant="outline" className={`text-xs shrink-0 ${char.gender === '男' ? 'border-blue-400/50 text-blue-400' : char.gender === '女' ? 'border-pink-400/50 text-pink-400' : 'border-muted-foreground/40 text-muted-foreground'}`}>
|
||||
{genderLabel(char.gender)}
|
||||
</Badge>
|
||||
)}
|
||||
<span className={`font-medium truncate ${char.gender === '男' ? 'text-blue-400' : char.gender === '女' ? 'text-pink-400' : ''}`}>{char.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{char.use_indextts2 && (
|
||||
@@ -2320,15 +2298,19 @@ export default function Audiobook() {
|
||||
<EmptyState />
|
||||
) : (
|
||||
<>
|
||||
<div className="shrink-0 border-b px-4 py-2 flex items-start justify-between gap-2">
|
||||
<div className="flex items-start gap-2 min-w-0 flex-1">
|
||||
<div className="shrink-0 border-b px-4 py-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
{(() => {
|
||||
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 mt-0.5 text-orange-500' : selectedProject.source_type === 'ai_generated' ? 'h-4 w-4 shrink-0 mt-0.5 text-violet-500' : 'h-4 w-4 shrink-0 mt-0.5 text-muted-foreground'
|
||||
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'
|
||||
return <Icon className={cls} />
|
||||
})()}
|
||||
<span className="font-medium break-words">{selectedProject.title}</span>
|
||||
<span className="font-medium break-words min-w-0">{selectedProject.title}</span>
|
||||
{(() => {
|
||||
const sc = STATUS_CONFIG[displayStatus]
|
||||
return sc ? <sc.Icon className={`${sc.iconCls} shrink-0`} /> : null
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0 flex-wrap justify-end">
|
||||
{isTurboMode && (
|
||||
@@ -2336,22 +2318,19 @@ export default function Audiobook() {
|
||||
{t('status.turboActive')}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant={(STATUS_COLORS[displayStatus] || 'secondary') as any}>
|
||||
{t(`status.${displayStatus}`, { defaultValue: displayStatus })}
|
||||
</Badge>
|
||||
{status === 'pending' && (
|
||||
<Button size="sm" onClick={handleAnalyze} disabled={loadingAction}>
|
||||
<Button size="sm" variant="ghost" onClick={handleAnalyze} disabled={loadingAction}>
|
||||
{t('projectCard.analyze')}
|
||||
</Button>
|
||||
)}
|
||||
{status === 'ready' && (
|
||||
<Button size="sm" onClick={handleProcessAll} disabled={loadingAction}>
|
||||
<Button size="sm" variant="ghost" onClick={handleProcessAll} disabled={loadingAction}>
|
||||
{loadingAction ? <Loader2 className="h-3 w-3 animate-spin" /> : null}
|
||||
{t('projectCard.processAll')}
|
||||
</Button>
|
||||
)}
|
||||
{status === 'done' && (
|
||||
<Button size="sm" variant="outline" onClick={() => handleDownload()} disabled={loadingAction}>
|
||||
<Button size="sm" variant="ghost" onClick={() => handleDownload()} disabled={loadingAction}>
|
||||
<Download className="h-3 w-3" />{t('projectCard.downloadAll')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user