import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { closeReadAloudPlayer, getReadAloudServerSnapshot, getReadAloudSnapshot, subscribeReadAloud, type ReadAloudSnapshot } from '@/lib/read-aloud' import { cn } from '@/lib/utils' import type { TFunction } from 'i18next' import { useCallback, useSyncExternalStore } from 'react' import { useTranslation } from 'react-i18next' function formatClock(ts: number | null): string { if (ts == null) return '—' try { return new Date(ts).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' }) } catch { return '—' } } /** Lighter scrim than default bg-black/80; content stays above overlay (z-230 vs z-220). */ const READ_ALOUD_OVERLAY_CLASS = 'z-[220] bg-black/35 backdrop-blur-sm dark:bg-black/40' function sectionAriaLabel(i: number, snap: ReadAloudSnapshot, t: TFunction): string { if (i < snap.chunksPlayed) { return t('Read-aloud section done', { index: i + 1 }) } if (i > snap.currentChunkIndex) { return t('Read-aloud section pending', { index: i + 1 }) } switch (snap.phase) { case 'requesting': return t('Read-aloud section fetching', { index: i + 1 }) case 'buffering': case 'preparing': return t('Read-aloud section preparing audio', { index: i + 1 }) case 'playing': return t('Read-aloud section playing', { index: i + 1 }) case 'paused': return t('Read-aloud section paused', { index: i + 1 }) default: return t('Read-aloud section pending', { index: i + 1 }) } } function phaseLabel(s: ReadAloudSnapshot, t: (k: string) => string): string { switch (s.phase) { case 'idle': return t('Read-aloud idle') case 'preparing': return t('Preparing read-aloud…') case 'requesting': return t('Requesting audio…') case 'buffering': return t('Loading audio…') case 'playing': return t('Playing') case 'paused': return t('Paused') case 'done': return t('Read-aloud finished') case 'error': return t('Read-aloud error') default: return s.phase } } export default function ReadAloudPlayerModal(): JSX.Element { const { t } = useTranslation() const snap = useSyncExternalStore( subscribeReadAloud, getReadAloudSnapshot, getReadAloudServerSnapshot ) const onOpenChange = useCallback((open: boolean) => { if (!open) { closeReadAloudPlayer() } }, []) const showChunks = snap.engine === 'piper' && snap.totalChunks > 0 const nChunks = snap.totalChunks const overallPct = nChunks > 0 ? Math.min(100, ((snap.chunksPlayed + snap.chunkPlaybackRatio) / nChunks) * 100) : 0 return ( {t('Read aloud')}
{snap.title ? (

{snap.title}

) : null}

{phaseLabel(snap, t)}

{snap.engine === 'piper' ? (

{t('TTS endpoint')}: {snap.backend || '—'}

) : snap.engine === 'webspeech' ? (

{t('Using browser speech synthesis')}

) : null} {snap.readAloudPiperSkipped || snap.readAloudPiperTryStartedAt != null || snap.usedPiperFallback ? (

{t('Read-aloud Piper status heading')}

{snap.readAloudPiperSkipped ? (

{t('Read-aloud Piper skipped notice')}

) : null} {snap.readAloudPiperTryStartedAt != null ? (

{t('Read-aloud Piper attempt started', { time: formatClock(snap.readAloudPiperTryStartedAt) })}

) : null} {!snap.readAloudPiperSkipped && snap.backend ? (

{t('Read-aloud Piper endpoint tried', { url: snap.backend })}

) : null}
) : null} {snap.engine === 'webspeech' && snap.usedPiperFallback ? (

{t('Read-aloud Piper fallback notice')}

{snap.piperFallbackDetail ? (

{t('Read-aloud Piper fallback detail label')}:{' '} {snap.piperFallbackDetail}

) : null}
) : null} {showChunks ? (

{t('Read-aloud section progress', { current: snap.currentChunkIndex + 1, total: snap.totalChunks })}

{Array.from({ length: snap.totalChunks }, (_, i) => { const done = i < snap.chunksPlayed const active = i === snap.currentChunkIndex const fetching = active && snap.phase === 'requesting' const decoding = active && (snap.phase === 'buffering' || snap.phase === 'preparing') const playing = active && snap.phase === 'playing' const paused = active && snap.phase === 'paused' return (
{(playing || paused) && !done ? (
) : null}
) })}

{snap.phase === 'requesting' ? t('Read-aloud legend fetching') : snap.phase === 'buffering' || snap.phase === 'preparing' ? t('Read-aloud legend buffering') : snap.phase === 'playing' ? t('Read-aloud legend playing') : snap.phase === 'paused' ? t('Read-aloud legend paused') : null}

) : null}
{t('Request sent')}
{formatClock(snap.requestSentAt)}
{t('Response received')}
{formatClock(snap.responseReceivedAt)}
{t('Playback started')}
{formatClock(snap.playbackStartedAt)}
{t('Characters')}
{snap.charCount > 0 ? snap.charCount.toLocaleString() : '—'}
{snap.error ? (

{snap.error}

) : null}
) }