feat: enhance AudiobookCharacterResponse and AudioPlayer for compact mode support

This commit is contained in:
2026-03-13 17:14:33 +08:00
parent e024910411
commit dbfcff3476
6 changed files with 70 additions and 37 deletions

View File

@@ -70,3 +70,11 @@
min-height: 40px;
min-width: 40px;
}
.compact {
padding: 0.25rem 0.5rem;
}
.compact .downloadButton {
display: none;
}

View File

@@ -12,9 +12,10 @@ interface AudioPlayerProps {
audioUrl: string
jobId: number
text?: string
compact?: boolean
}
const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
const AudioPlayer = memo(({ audioUrl, jobId, compact }: AudioPlayerProps) => {
const { t } = useTranslation('common')
const { theme } = useTheme()
const [blobUrl, setBlobUrl] = useState<string>('')
@@ -97,13 +98,13 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
const player = new WaveformPlayer(containerRef.current, {
url: blobUrl,
waveformStyle: 'mirror',
height: 60,
barWidth: 3,
height: compact ? 32 : 60,
barWidth: compact ? 2 : 3,
barSpacing: 1,
samples: 200,
samples: compact ? 80 : 200,
waveformColor,
progressColor,
showTime: true,
showTime: !compact,
showPlaybackSpeed: false,
autoplay: false,
enableMediaSession: true,
@@ -157,6 +158,17 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
return null
}
if (compact) {
return (
<audio
src={blobUrl}
controls
className="w-full h-8"
style={{ colorScheme: 'dark' }}
/>
)
}
return (
<div className={styles.audioPlayerWrapper}>
<div ref={containerRef} className={styles.waveformContainer} />

View File

@@ -48,6 +48,8 @@ export interface AudiobookCharacter {
description?: string
instruct?: string
voice_design_id?: number
voice_design_name?: string
voice_design_speaker?: string
use_indextts2?: boolean
}

View File

@@ -16,7 +16,7 @@ import { RotateCcw } from 'lucide-react'
import apiClient, { formatApiError, adminApi, authApi } from '@/lib/api'
import { useAuth } from '@/contexts/AuthContext'
function LazyAudioPlayer({ audioUrl, jobId }: { audioUrl: string; jobId: number }) {
function LazyAudioPlayer({ audioUrl, jobId, compact }: { audioUrl: string; jobId: number; compact?: boolean }) {
const [visible, setVisible] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
@@ -29,7 +29,7 @@ function LazyAudioPlayer({ audioUrl, jobId }: { audioUrl: string; jobId: number
observer.observe(el)
return () => observer.disconnect()
}, [])
return <div ref={ref}>{visible && <AudioPlayer audioUrl={audioUrl} jobId={jobId} />}</div>
return <div ref={ref}>{visible && <AudioPlayer audioUrl={audioUrl} jobId={jobId} compact={compact} />}</div>
}
const STATUS_COLORS: Record<string, string> = {
@@ -1333,22 +1333,23 @@ function CharactersPanel({
)}
</div>
</div>
{char.instruct && <span className="text-xs text-muted-foreground truncate">{char.instruct}</span>}
<div className="flex items-center gap-1">
{char.description && <span className="text-xs text-muted-foreground">{char.description}</span>}
{char.instruct && <span className="text-xs text-muted-foreground/70">{char.instruct}</span>}
<div className="text-xs text-muted-foreground/60">
{char.voice_design_id
? <Badge variant="outline" className="text-xs">{t('projectCard.characters.voiceDesign', { id: char.voice_design_id })}</Badge>
: <Badge variant="secondary" className="text-xs">{t('projectCard.characters.noVoice')}</Badge>
}
? (char.voice_design_name || `#${char.voice_design_id}`)
: t('projectCard.characters.noVoice')}
</div>
</div>
)}
{!editingCharId && char.voice_design_id && (
<div className="mt-2 flex items-center justify-between gap-2 bg-muted/30 rounded-md p-1.5 border border-muted/50">
<div className="mt-2 flex items-center gap-2">
<div className="flex-1 min-w-0">
<LazyAudioPlayer
key={`audio-${char.id}-${voiceKeys[char.id] || 0}`}
audioUrl={`${audiobookApi.getCharacterAudioUrl(project.id, char.id)}?t=${voiceKeys[char.id] || 0}`}
jobId={char.id}
compact
/>
</div>
{status === 'characters_ready' && (
@@ -2321,7 +2322,12 @@ export default function Audiobook() {
<>
<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">
<Book className="h-4 w-4 shrink-0 text-muted-foreground mt-0.5" />
{(() => {
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'
return <Icon className={cls} />
})()}
<span className="font-medium break-words">{selectedProject.title}</span>
</div>
<div className="flex items-center gap-1 shrink-0 flex-wrap justify-end">