feat: enhance AudiobookCharacterResponse and AudioPlayer for compact mode support
This commit is contained in:
@@ -70,3 +70,11 @@
|
||||
min-height: 40px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.compact {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.compact .downloadButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user