'use client'; import { useParams, useRouter } from "next/navigation"; import Link from 'next/link'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useEPUB } from '@/contexts/EPUBContext'; import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton'; import { EPUBViewer } from '@/components/views/EPUBViewer'; import { DocumentSettings } from '@/components/documents/DocumentSettings'; import { Header } from '@/components/Header'; import { useTTS } from "@/contexts/TTSContext"; import TTSPlayer from '@/components/player/TTSPlayer'; import { RateLimitPauseButton } from '@/components/player/RateLimitPauseButton'; import { DocumentHeaderMenu } from '@/components/documents/DocumentHeaderMenu'; import { SegmentsSidebar } from '@/components/reader/SegmentsSidebar'; import { AudiobookExportModal } from '@/components/AudiobookExportModal'; import type { TTSAudiobookChapter } from '@/types/tts'; import type { AudiobookGenerationSettings } from '@/types/client'; import { resolveDocumentId } from '@/lib/client/dexie'; import { RateLimitBanner } from '@/components/auth/RateLimitBanner'; import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext'; import { useFeatureFlag } from '@/contexts/RuntimeConfigContext'; export default function EPUBPage() { const canExportAudiobook = useFeatureFlag('enableAudiobookExport'); const { id } = useParams(); const router = useRouter(); const { setCurrentDocument, currDocName, clearCurrDoc, createFullAudioBook: createEPUBAudioBook, regenerateChapter: regenerateEPUBChapter } = useEPUB(); const { stop } = useTTS(); const { isAtLimit } = useAuthRateLimit(); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); const [activeSidebar, setActiveSidebar] = useState(null); const [containerHeight, setContainerHeight] = useState('auto'); const [padPct, setPadPct] = useState(100); // 0..100 (100 = full width, 0 = max padding) const [maxPadPx, setMaxPadPx] = useState(0); const inFlightDocIdRef = useRef(null); const loadedDocIdRef = useRef(null); useEffect(() => { setIsLoading(true); setError(null); setActiveSidebar(null); inFlightDocIdRef.current = null; loadedDocIdRef.current = null; }, [id]); const loadDocument = useCallback(async () => { console.log('Loading new epub (from page.tsx)'); let didRedirect = false; let startedLoad = false; try { if (!id) { setError('Document not found'); return; } const resolved = await resolveDocumentId(id as string); if (resolved !== (id as string)) { didRedirect = true; router.replace(`/epub/${resolved}`); return; } if (loadedDocIdRef.current === resolved) { return; } if (inFlightDocIdRef.current === resolved) { return; } startedLoad = true; inFlightDocIdRef.current = resolved; stop(); // Reset TTS when loading new document await setCurrentDocument(resolved); loadedDocIdRef.current = resolved; } catch (err) { console.error('Error loading document:', err); setError('Failed to load document'); } finally { if (startedLoad) { inFlightDocIdRef.current = null; } if (!didRedirect && startedLoad) { setIsLoading(false); } } }, [id, router, setCurrentDocument, stop]); useEffect(() => { if (!isLoading) return; loadDocument(); }, [loadDocument, isLoading]); // Compute available height = viewport - (header height + tts bar height) useEffect(() => { const compute = () => { const header = document.querySelector('[data-app-header]') as HTMLElement | null; const ttsbar = document.querySelector('[data-app-ttsbar]') as HTMLElement | null; const headerH = header ? header.getBoundingClientRect().height : 0; const ttsH = ttsbar ? ttsbar.getBoundingClientRect().height : 0; const vh = window.innerHeight; const h = Math.max(0, vh - headerH - ttsH); setContainerHeight(`${h}px`); // compute max horizontal padding while preserving a minimum readable width, // but still allow some padding on small screens const vw = window.innerWidth; const desiredMin = 640; // target readable min width const minContent = Math.min(desiredMin, Math.max(320, vw - 32)); const maxPad = Math.max(0, Math.floor((vw - minContent) / 2)); setMaxPadPx(maxPad); }; compute(); window.addEventListener('resize', compute); return () => window.removeEventListener('resize', compute); }, []); // Nudge EPUB renderer to reflow on horizontal padding changes useEffect(() => { // Some EPUB renderers listen to window resize; emit a synthetic event window.dispatchEvent(new Event('resize')); }, [padPct]); const handleGenerateAudiobook = useCallback(async ( onProgress: (progress: number) => void, signal: AbortSignal, onChapterComplete: (chapter: TTSAudiobookChapter) => void, settings: AudiobookGenerationSettings ) => { return createEPUBAudioBook(onProgress, signal, onChapterComplete, id as string, settings.format, settings); }, [createEPUBAudioBook, id]); const handleRegenerateChapter = useCallback(async ( chapterIndex: number, bookId: string, settings: AudiobookGenerationSettings, signal: AbortSignal ) => { return regenerateEPUBChapter(chapterIndex, bookId, settings.format, signal, settings); }, [regenerateEPUBChapter]); if (error) { return (

{error}

clearCurrDoc()} className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition-all duration-200 ease-in-out hover:scale-[1.04] hover:text-accent" > Back to Documents
); } return ( <>
clearCurrDoc()} className="inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase transition-all duration-200 ease-in-out hover:scale-[1.04] hover:text-accent" aria-label="Back to documents" > Documents } title={isLoading ? 'Loading…' : (currDocName || '')} right={
setPadPct(p => Math.min(p + 10, 100))} onZoomDecrease={() => setPadPct(p => Math.max(p - 10, 0))} onOpenSettings={() => setActiveSidebar((prev) => prev === 'settings' ? null : 'settings')} onOpenAudiobook={() => setActiveSidebar((prev) => prev === 'audiobook' ? null : 'audiobook')} onOpenSegments={() => setActiveSidebar((prev) => prev === 'segments' ? null : 'segments')} isSettingsOpen={activeSidebar === 'settings'} isAudiobookOpen={activeSidebar === 'audiobook'} isSegmentsOpen={activeSidebar === 'segments'} showAudiobookExport={canExportAudiobook} minZoom={0} maxZoom={100} />
} />
{isLoading ? (
) : (
)}
{canExportAudiobook && ( setActiveSidebar((prev) => isOpen ? 'audiobook' : (prev === 'audiobook' ? null : prev))} documentType="epub" documentId={id as string} onGenerateAudiobook={handleGenerateAudiobook} onRegenerateChapter={handleRegenerateChapter} /> )} {isAtLimit ? (
) : ( )} setActiveSidebar((prev) => isOpen ? 'settings' : (prev === 'settings' ? null : prev))} /> setActiveSidebar((prev) => isOpen ? 'segments' : (prev === 'segments' ? null : prev))} documentId={id as string} /> ); }