feat: add ChapterPlayer component for audio chapter playback in Audiobook

This commit is contained in:
2026-03-13 16:21:11 +08:00
parent 786254cb81
commit 3393be4967
2 changed files with 232 additions and 1 deletions

View File

@@ -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 } 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 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
@@ -10,6 +10,7 @@ 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 { ChapterPlayer } from '@/components/ChapterPlayer'
import { audiobookApi, type AudiobookProject, type AudiobookProjectDetail, type AudiobookCharacter, type AudiobookSegment, type ScriptGenerationRequest, type NsfwScriptGenerationRequest } from '@/lib/api/audiobook'
import { RotateCcw } from 'lucide-react'
import apiClient, { formatApiError, adminApi, authApi } from '@/lib/api'
@@ -1443,6 +1444,7 @@ function ChaptersPanel({
const [savingSegId, setSavingSegId] = useState<number | null>(null)
const [regeneratingSegs, setRegeneratingSegs] = useState<Set<number>>(new Set())
const [audioVersions, setAudioVersions] = useState<Record<number, number>>({})
const [chapterPlayerChIdx, setChapterPlayerChIdx] = useState<number | null>(null)
const prevSegStatusRef = useRef<Record<number, string>>({})
const initialExpandDoneRef = useRef(false)
@@ -1670,6 +1672,15 @@ function ChaptersPanel({
}}>
<RefreshCw className="h-3 w-3 mr-0.5" /><Volume2 className="h-3 w-3 mr-1" />{t('projectCard.chapters.generate')}
</Button>
<Button
size="icon"
variant={chapterPlayerChIdx === ch.chapter_index ? 'secondary' : 'ghost'}
className="h-6 w-6"
onClick={() => setChapterPlayerChIdx(prev => prev === ch.chapter_index ? null : ch.chapter_index)}
title="播放本章"
>
<Headphones className="h-3 w-3" />
</Button>
<Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => onDownload(ch.chapter_index)} title={t('projectCard.downloadAll')}>
<Download className="h-3 w-3" />
</Button>
@@ -1827,6 +1838,23 @@ function ChaptersPanel({
</div>
)}
{chapterPlayerChIdx !== null && (() => {
const activeCh = detail?.chapters.find(c => c.chapter_index === chapterPlayerChIdx)
const activeSegs = segments.filter(s => s.chapter_index === chapterPlayerChIdx)
if (!activeCh) return null
return (
<div className="border-t shrink-0">
<ChapterPlayer
projectId={project.id}
chapterIndex={chapterPlayerChIdx}
chapterTitle={activeCh.title || t('projectCard.chapters.defaultTitle', { index: chapterPlayerChIdx + 1 })}
segments={activeSegs}
onClose={() => setChapterPlayerChIdx(null)}
/>
</div>
)
})()}
{doneCount > 0 && (
<div className="px-3 py-2 border-t shrink-0">
<SequentialPlayer segments={segments} projectId={project.id} onPlayingChange={onSequentialPlayingChange} />