feat: implement ChapterPlayer styling and refactor UI components for improved layout and functionality
This commit is contained in:
123
qwen3-tts-frontend/src/components/ChapterPlayer.module.css
Normal file
123
qwen3-tts-frontend/src/components/ChapterPlayer.module.css
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
.panel {
|
||||||
|
padding: 0.625rem 0.75rem 0.75rem;
|
||||||
|
background: rgb(249 115 22 / 0.12);
|
||||||
|
box-shadow: 0 -6px 24px rgba(0, 0, 0, 0.1);
|
||||||
|
border-top: 2px solid rgb(249 115 22 / 0.4);
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgb(249 115 22 / 0.85);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapterName {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: hsl(var(--foreground) / 0.75);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveformWrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid hsl(var(--border) / 0.6);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
background: hsl(var(--muted) / 0.3);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveformWrapper :global(.waveform-player) {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveformWrapper :global(.waveform-btn) {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
border-color: hsl(var(--border));
|
||||||
|
background: transparent;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveformWrapper :global(.waveform-btn:hover) {
|
||||||
|
color: rgb(249 115 22);
|
||||||
|
border-color: rgb(249 115 22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveformWrapper :global(.waveform-canvas) {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveformWrapper :global(.waveform-title) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveformWrapper :global(.waveform-body) {
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveformWrapper :global(.waveform-track) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveformWrapper :global(.waveform-info) {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveformWrapper :global(.waveform-text) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveformWrapper :global(.waveform-time) {
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveformInner {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitleName {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgb(249 115 22);
|
||||||
|
flex-shrink: 0;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitleText {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { X, Loader2 } from 'lucide-react'
|
import { X, Loader2 } from 'lucide-react'
|
||||||
import apiClient from '@/lib/api'
|
import apiClient from '@/lib/api'
|
||||||
import { audiobookApi, type AudiobookSegment } from '@/lib/api/audiobook'
|
import { audiobookApi, type AudiobookSegment } from '@/lib/api/audiobook'
|
||||||
import styles from './AudioPlayer.module.css'
|
import styles from './ChapterPlayer.module.css'
|
||||||
|
|
||||||
interface TimelineItem {
|
interface TimelineItem {
|
||||||
seg: AudiobookSegment
|
seg: AudiobookSegment
|
||||||
@@ -117,15 +117,15 @@ const ChapterPlayer = memo(({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const waveformColor = theme === 'dark' ? '#4b5563' : '#d1d5db'
|
const waveformColor = theme === 'dark' ? '#4b5563' : '#d1d5db'
|
||||||
const progressColor = theme === 'dark' ? '#a78bfa' : '#7c3aed'
|
const progressColor = '#f97316'
|
||||||
|
|
||||||
const player = new WaveformPlayer(containerRef.current, {
|
const player = new WaveformPlayer(containerRef.current, {
|
||||||
url: blobUrl,
|
url: blobUrl,
|
||||||
waveformStyle: 'mirror',
|
waveformStyle: 'mirror',
|
||||||
height: 60,
|
height: 56,
|
||||||
barWidth: 3,
|
barWidth: 2,
|
||||||
barSpacing: 1,
|
barSpacing: 1,
|
||||||
samples: 200,
|
samples: 260,
|
||||||
waveformColor,
|
waveformColor,
|
||||||
progressColor,
|
progressColor,
|
||||||
showTime: true,
|
showTime: true,
|
||||||
@@ -161,17 +161,16 @@ const ChapterPlayer = memo(({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-3 py-2.5 border-b bg-primary/5">
|
<div className={styles.panel}>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className={styles.header}>
|
||||||
<span className="text-xs font-medium flex items-center gap-2 min-w-0">
|
<span className={styles.title}>正在播放</span>
|
||||||
<span className="truncate text-foreground/80">{chapterTitle}</span>
|
<span className={styles.chapterName}>{chapterTitle}</span>
|
||||||
{isLoadingTimeline && !isLoadingChapter && (
|
{isLoadingTimeline && !isLoadingChapter && (
|
||||||
<span className="inline-flex items-center gap-1 text-[11px] text-muted-foreground shrink-0">
|
<span className="inline-flex items-center gap-1 text-[11px] text-muted-foreground shrink-0 ml-auto">
|
||||||
<Loader2 className="h-2.5 w-2.5 animate-spin" />字幕轨道加载中…
|
<Loader2 className="h-2.5 w-2.5 animate-spin" />字幕轨道加载中…
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
<Button type="button" size="icon" variant="ghost" className="h-5 w-5 shrink-0" onClick={onClose}>
|
||||||
<Button type="button" size="icon" variant="ghost" className="h-5 w-5" onClick={onClose}>
|
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,13 +181,13 @@ const ChapterPlayer = memo(({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className={styles.audioPlayerWrapper}>
|
<div className={styles.waveformWrapper}>
|
||||||
<div ref={containerRef} className={styles.waveformContainer} />
|
<div ref={containerRef} className={styles.waveformInner} />
|
||||||
</div>
|
</div>
|
||||||
{currentSeg && (
|
{currentSeg && (
|
||||||
<div className="mt-2 px-1 flex items-baseline gap-1.5">
|
<div className={styles.subtitle}>
|
||||||
<span className="text-[11px] font-semibold text-primary shrink-0">{currentSeg.character_name || '旁白'}</span>
|
<span className={styles.subtitleName}>{currentSeg.character_name || '旁白'}</span>
|
||||||
<span className="text-xs text-muted-foreground leading-relaxed">{currentSeg.text}</span>
|
<span className={styles.subtitleText}>{currentSeg.text}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1448,13 +1448,15 @@ function ChaptersPanel({
|
|||||||
const prevSegStatusRef = useRef<Record<number, string>>({})
|
const prevSegStatusRef = useRef<Record<number, string>>({})
|
||||||
const initialExpandDoneRef = useRef(false)
|
const initialExpandDoneRef = useRef(false)
|
||||||
const segRefs = useRef<Record<number, HTMLDivElement | null>>({})
|
const segRefs = useRef<Record<number, HTMLDivElement | null>>({})
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!scrollToChapterId) return
|
if (!scrollToChapterId) return
|
||||||
setExpandedChapters(prev => { const n = new Set(prev); n.add(scrollToChapterId); return n })
|
const el = document.getElementById(`ch-${scrollToChapterId}`)
|
||||||
setTimeout(() => {
|
const container = scrollContainerRef.current
|
||||||
document.getElementById(`ch-${scrollToChapterId}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
if (el && container) {
|
||||||
}, 50)
|
container.scrollTo({ top: el.offsetTop, behavior: 'smooth' })
|
||||||
|
}
|
||||||
onScrollToChapterDone()
|
onScrollToChapterDone()
|
||||||
}, [scrollToChapterId, onScrollToChapterDone])
|
}, [scrollToChapterId, onScrollToChapterDone])
|
||||||
|
|
||||||
@@ -1582,24 +1584,24 @@ function ChaptersPanel({
|
|||||||
{hasChapters && (
|
{hasChapters && (
|
||||||
<div className="flex items-center gap-1 flex-wrap">
|
<div className="flex items-center gap-1 flex-wrap">
|
||||||
{detail!.chapters.some(c => ['pending', 'error', 'ready'].includes(c.status)) && !isBackgroundGenerating && (
|
{detail!.chapters.some(c => ['pending', 'error', 'ready'].includes(c.status)) && !isBackgroundGenerating && (
|
||||||
<Button size="xs" variant="outline" disabled={loadingAction} onClick={onParseAll}>
|
<Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={onParseAll}>
|
||||||
{isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
|
{isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
|
||||||
{t(isAIMode ? 'projectCard.chapters.parseAllAI' : 'projectCard.chapters.parseAll')}
|
{t(isAIMode ? 'projectCard.chapters.parseAllAI' : 'projectCard.chapters.parseAll')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{detail!.chapters.some(c => c.status === 'ready') && (
|
{detail!.chapters.some(c => c.status === 'ready') && (
|
||||||
<Button size="xs" variant="outline" disabled={loadingAction} onClick={onGenerateAll}>
|
<Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={onGenerateAll}>
|
||||||
<Volume2 className="h-3 w-3 mr-1" />
|
<Volume2 className="h-3 w-3 mr-1" />
|
||||||
{t('projectCard.chapters.generateAll')}
|
{t('projectCard.chapters.generateAll')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{detail!.chapters.some(c => ['pending', 'error'].includes(c.status)) && detail!.chapters.some(c => c.status === 'ready') && (
|
{detail!.chapters.some(c => ['pending', 'error'].includes(c.status)) && detail!.chapters.some(c => c.status === 'ready') && (
|
||||||
<Button size="xs" disabled={loadingAction} onClick={onProcessAll}>
|
<Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={onProcessAll}>
|
||||||
{loadingAction ? <Loader2 className="h-3 w-3 animate-spin" /> : t('projectCard.chapters.processAll')}
|
{loadingAction ? <Loader2 className="h-3 w-3 animate-spin" /> : t('projectCard.chapters.processAll')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isAIMode && onContinueScript && (
|
{isAIMode && onContinueScript && (
|
||||||
<Button size="xs" variant="outline" disabled={loadingAction} onClick={onContinueScript}>
|
<Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={onContinueScript}>
|
||||||
<Plus className="h-3 w-3 mr-1" />
|
<Plus className="h-3 w-3 mr-1" />
|
||||||
{t('projectCard.chapters.continueScript')}
|
{t('projectCard.chapters.continueScript')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1611,7 +1613,7 @@ function ChaptersPanel({
|
|||||||
{!hasChapters ? (
|
{!hasChapters ? (
|
||||||
<div className="flex-1 flex items-center justify-center text-xs text-muted-foreground" />
|
<div className="flex-1 flex items-center justify-center text-xs text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto" ref={scrollContainerRef}>
|
||||||
{detail!.chapters.map(ch => {
|
{detail!.chapters.map(ch => {
|
||||||
const chSegs = segments.filter(s => s.chapter_index === ch.chapter_index)
|
const chSegs = segments.filter(s => s.chapter_index === ch.chapter_index)
|
||||||
const chDone = chSegs.filter(s => s.status === 'done').length
|
const chDone = chSegs.filter(s => s.status === 'done').length
|
||||||
@@ -1634,7 +1636,7 @@ function ChaptersPanel({
|
|||||||
onClick={toggleChExpand}
|
onClick={toggleChExpand}
|
||||||
>
|
>
|
||||||
<span className="shrink-0 text-muted-foreground">
|
<span className="shrink-0 text-muted-foreground">
|
||||||
{chExpanded ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronUp className="h-3.5 w-3.5" style={{ transform: 'rotate(180deg)' }} />}
|
{chExpanded ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs font-medium flex-1 truncate">{chTitle}</span>
|
<span className="text-xs font-medium flex-1 truncate">{chTitle}</span>
|
||||||
<span className="shrink-0 flex items-center gap-1.5" onClick={e => e.stopPropagation()}>
|
<span className="shrink-0 flex items-center gap-1.5" onClick={e => e.stopPropagation()}>
|
||||||
@@ -1644,7 +1646,7 @@ function ChaptersPanel({
|
|||||||
<Loader2 className="h-3 w-3 animate-spin" />排队中
|
<Loader2 className="h-3 w-3 animate-spin" />排队中
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<Button size="xs" variant="outline" onClick={() => onParseChapter(ch.id, ch.title)}>
|
<Button size="xs" variant="ghost" className="text-muted-foreground" onClick={() => onParseChapter(ch.id, ch.title)}>
|
||||||
{isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
|
{isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
|
||||||
{t(isAIMode ? 'projectCard.chapters.parseAI' : 'projectCard.chapters.parse')}
|
{t(isAIMode ? 'projectCard.chapters.parseAI' : 'projectCard.chapters.parse')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1657,14 +1659,14 @@ function ChaptersPanel({
|
|||||||
)}
|
)}
|
||||||
{ch.status === 'ready' && !chGenerating && !chAllDone && !generatingChapterIndices.has(ch.chapter_index) && (
|
{ch.status === 'ready' && !chGenerating && !chAllDone && !generatingChapterIndices.has(ch.chapter_index) && (
|
||||||
<>
|
<>
|
||||||
<Button size="xs" variant="outline" disabled={loadingAction} onClick={() => {
|
<Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={() => {
|
||||||
setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n })
|
setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n })
|
||||||
onGenerate(ch.chapter_index)
|
onGenerate(ch.chapter_index)
|
||||||
}}>
|
}}>
|
||||||
<Volume2 className="h-3 w-3 mr-1" />
|
<Volume2 className="h-3 w-3 mr-1" />
|
||||||
{t('projectCard.chapters.generate')}
|
{t('projectCard.chapters.generate')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="xs" variant="outline" disabled={loadingAction} onClick={() => onParseChapter(ch.id, ch.title)}>
|
<Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={() => onParseChapter(ch.id, ch.title)}>
|
||||||
{isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
|
{isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
|
||||||
{t(isAIMode ? 'projectCard.chapters.reparseAI' : 'projectCard.chapters.reparse')}
|
{t(isAIMode ? 'projectCard.chapters.reparseAI' : 'projectCard.chapters.reparse')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1678,7 +1680,7 @@ function ChaptersPanel({
|
|||||||
{(ch.status === 'done' || (ch.status === 'ready' && chAllDone)) && (
|
{(ch.status === 'done' || (ch.status === 'ready' && chAllDone)) && (
|
||||||
<>
|
<>
|
||||||
<span className="text-[11px] text-muted-foreground">{t('projectCard.chapters.doneBadge', { count: chDone })}</span>
|
<span className="text-[11px] text-muted-foreground">{t('projectCard.chapters.doneBadge', { count: chDone })}</span>
|
||||||
<Button size="xs" variant="outline" disabled={loadingAction} onClick={() => {
|
<Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={() => {
|
||||||
setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n })
|
setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n })
|
||||||
onGenerate(ch.chapter_index, true)
|
onGenerate(ch.chapter_index, true)
|
||||||
}}>
|
}}>
|
||||||
@@ -1699,7 +1701,7 @@ function ChaptersPanel({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{ch.status === 'error' && (
|
{ch.status === 'error' && (
|
||||||
<Button size="xs" variant="outline" className="text-destructive border-destructive/40" onClick={() => onParseChapter(ch.id, ch.title)}>
|
<Button size="xs" variant="ghost" className="text-destructive" onClick={() => onParseChapter(ch.id, ch.title)}>
|
||||||
{isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
|
{isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
|
||||||
{t(isAIMode ? 'projectCard.chapters.reparseAI' : 'projectCard.chapters.reparse')}
|
{t(isAIMode ? 'projectCard.chapters.reparseAI' : 'projectCard.chapters.reparse')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1729,24 +1731,19 @@ function ChaptersPanel({
|
|||||||
>
|
>
|
||||||
{/* Card header */}
|
{/* Card header */}
|
||||||
<div className="flex items-center gap-1.5 px-3 py-2 border-b border-border/50">
|
<div className="flex items-center gap-1.5 px-3 py-2 border-b border-border/50">
|
||||||
<Badge variant="outline" className="text-xs shrink-0 font-normal">
|
<span className="text-xs text-muted-foreground shrink-0">
|
||||||
{seg.character_name || t('projectCard.segments.unknownCharacter')}
|
<span className="text-sm text-foreground">{seg.character_name || t('projectCard.segments.unknownCharacter')}</span>
|
||||||
</Badge>
|
{!isEditing && seg.emo_text && (
|
||||||
{!isEditing && seg.emo_text && (
|
<>
|
||||||
<span className="text-[11px] text-muted-foreground shrink-0 flex items-center gap-0.5 flex-wrap">
|
{' | '}
|
||||||
{seg.emo_text.split('+').map(tok => {
|
{seg.emo_text.split('+').map((tok, i) => {
|
||||||
const [name, w] = tok.split(':')
|
const [name, w] = tok.split(':')
|
||||||
return (
|
return <span key={tok}>{i > 0 ? ' ' : ''}{name.trim()}{w ? `:${parseFloat(w).toFixed(2)}` : ''}</span>
|
||||||
<span key={tok} className="bg-muted rounded px-1">
|
})}
|
||||||
{name.trim()}{w && <span className="opacity-60">:{parseFloat(w).toFixed(2)}</span>}
|
{seg.emo_alpha != null && seg.emo_alpha !== 1 && ` :${seg.emo_alpha.toFixed(2)}`}
|
||||||
</span>
|
</>
|
||||||
)
|
)}
|
||||||
})}
|
</span>
|
||||||
{seg.emo_alpha != null && seg.emo_alpha !== 1 && (
|
|
||||||
<span className="opacity-60 ml-0.5">×{seg.emo_alpha.toFixed(2)}</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{isRegenerating && <Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />}
|
{isRegenerating && <Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />}
|
||||||
{!isRegenerating && seg.status === 'error' && (
|
{!isRegenerating && seg.status === 'error' && (
|
||||||
<Badge variant="destructive" className="text-xs">{t('projectCard.segments.errorBadge')}</Badge>
|
<Badge variant="destructive" className="text-xs">{t('projectCard.segments.errorBadge')}</Badge>
|
||||||
@@ -1856,7 +1853,7 @@ function ChaptersPanel({
|
|||||||
const activeSegs = segments.filter(s => s.chapter_index === chapterPlayerChIdx)
|
const activeSegs = segments.filter(s => s.chapter_index === chapterPlayerChIdx)
|
||||||
if (!activeCh) return null
|
if (!activeCh) return null
|
||||||
return (
|
return (
|
||||||
<div className="border-t shrink-0">
|
<div className="shrink-0">
|
||||||
<ChapterPlayer
|
<ChapterPlayer
|
||||||
projectId={project.id}
|
projectId={project.id}
|
||||||
chapterIndex={chapterPlayerChIdx}
|
chapterIndex={chapterPlayerChIdx}
|
||||||
|
|||||||
Reference in New Issue
Block a user