openreader/src/components/player/TTSPlayer.tsx
Richard R 2e4f36f5c5 feat(pdf): introduce parser versioning and playback readiness state
Add PDF_PARSER_VERSION constant and propagate parser versioning throughout
the PDF parsing, job, and API layers. Implement normalization of parse state
to ensure compatibility with the current parser version, and enable reuse of
parsed PDF results when possible. Add isPlaybackReady state to document hooks
and TTS player, improving playback UX by disabling controls until content is
ready. Update tests to reflect new playback readiness logic.

BREAKING CHANGE: PDF parse state and job logic now require explicit parserVersion;
older parse states may be treated as pending until reprocessed.
2026-06-03 18:51:29 -06:00

87 lines
2.7 KiB
TypeScript

'use client';
import { useTTS } from '@/contexts/TTSContext';
import {
PlayIcon,
PauseIcon,
SkipForwardIcon,
SkipBackwardIcon,
} from '@/components/icons/Icons';
import { LoadingSpinner } from '@/components/Spinner';
import { VoicesControl } from '@/components/player/VoicesControl';
import { SpeedControl } from '@/components/player/SpeedControl';
import { Navigator } from '@/components/player/Navigator';
import { IconButton } from '@/components/ui';
export default function TTSPlayer({ currentPage, numPages, isPlaybackReady = true }: {
currentPage?: number;
numPages?: number | undefined;
isPlaybackReady?: boolean;
}) {
const {
isPlaying,
togglePlay,
skipForward,
skipBackward,
isProcessing,
setSpeedAndRestart,
setAudioPlayerSpeedAndRestart,
setVoiceAndRestart,
availableVoices,
skipToLocation,
} = useTTS();
return (
<div className="sticky bottom-0 z-30 w-full border-t border-line-soft bg-surface" data-app-ttsbar>
<div className="px-2 md:px-3 pt-1 pb-[max(0.375rem,env(safe-area-inset-bottom))] flex items-center justify-center gap-1 min-h-10">
{/* Speed control */}
<SpeedControl
setSpeedAndRestart={setSpeedAndRestart}
setAudioPlayerSpeedAndRestart={setAudioPlayerSpeedAndRestart}
/>
{/* Page Navigation */}
{currentPage && numPages && (
<Navigator
currentPage={currentPage}
numPages={numPages}
skipToLocation={skipToLocation}
/>
)}
{/* Playback Controls */}
<IconButton
onClick={skipBackward}
aria-label="Skip backward"
disabled={isProcessing || !isPlaybackReady}
className="relative"
>
{isProcessing ? <LoadingSpinner /> : <SkipBackwardIcon className="w-5 h-5" />}
</IconButton>
<IconButton
onClick={togglePlay}
aria-label={isPlaying ? 'Pause' : 'Play'}
disabled={!isPlaying && (!isPlaybackReady || isProcessing)}
className="relative"
>
{!isPlaying && !isPlaybackReady
? <LoadingSpinner />
: (isPlaying ? <PauseIcon className="w-5 h-5" /> : <PlayIcon className="w-5 h-5" />)}
</IconButton>
<IconButton
onClick={skipForward}
aria-label="Skip forward"
disabled={isProcessing || !isPlaybackReady}
className="relative"
>
{isProcessing ? <LoadingSpinner /> : <SkipForwardIcon className="w-5 h-5" />}
</IconButton>
{/* Voice control */}
<VoicesControl availableVoices={availableVoices} setVoiceAndRestart={setVoiceAndRestart} />
</div>
</div>
);
}