diff --git a/src/app/(app)/app/layout.tsx b/src/app/(app)/app/layout.tsx new file mode 100644 index 0000000..f8abbbb --- /dev/null +++ b/src/app/(app)/app/layout.tsx @@ -0,0 +1,20 @@ +'use client'; + +import type { ReactNode } from 'react'; + +import { ConfigProvider } from '@/contexts/ConfigContext'; +import { DocumentProvider } from '@/contexts/DocumentContext'; +import { DexieMigrationModal } from '@/components/documents/DexieMigrationModal'; + +export default function AppHomeLayout({ children }: { children: ReactNode }) { + return ( + + + <> + {children} + + + + + ); +} diff --git a/src/app/(app)/epub/[id]/layout.tsx b/src/app/(app)/epub/[id]/layout.tsx new file mode 100644 index 0000000..701e1e7 --- /dev/null +++ b/src/app/(app)/epub/[id]/layout.tsx @@ -0,0 +1,17 @@ +'use client'; + +import type { ReactNode } from 'react'; + +import { ConfigProvider } from '@/contexts/ConfigContext'; +import { TTSProvider } from '@/contexts/TTSContext'; +import { EPUBProvider } from '@/contexts/EPUBContext'; + +export default function EpubReaderLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/src/app/(app)/epub/[id]/page.tsx b/src/app/(app)/epub/[id]/page.tsx index 878beff..b3ee043 100644 --- a/src/app/(app)/epub/[id]/page.tsx +++ b/src/app/(app)/epub/[id]/page.tsx @@ -25,7 +25,7 @@ export default function EPUBPage() { const canExportAudiobook = useFeatureFlag('enableAudiobookExport'); const { id } = useParams(); const router = useRouter(); - const { setCurrentDocument, currDocName, clearCurrDoc, createFullAudioBook: createEPUBAudioBook, regenerateChapter: regenerateEPUBChapter } = useEPUB(); + const { setCurrentDocument, currDocName, clearCurrDoc, createFullAudioBook: createEPUBAudioBook, regenerateChapter: regenerateEPUBChapter, bookRef } = useEPUB(); const { stop } = useTTS(); const { isAtLimit } = useAuthRateLimit(); const [error, setError] = useState(null); @@ -36,6 +36,7 @@ export default function EPUBPage() { const [maxPadPx, setMaxPadPx] = useState(0); const inFlightDocIdRef = useRef(null); const loadedDocIdRef = useRef(null); + const didInitPadPctRef = useRef(false); useEffect(() => { setIsLoading(true); @@ -118,7 +119,13 @@ export default function EPUBPage() { // Nudge EPUB renderer to reflow on horizontal padding changes useEffect(() => { - // Some EPUB renderers listen to window resize; emit a synthetic event + // Some EPUB renderers listen to window resize; emit a synthetic event only + // for user-driven pad changes. Skipping initial mount avoids startup races + // that can interrupt first-play TTS requests in tests/browsers like Firefox. + if (!didInitPadPctRef.current) { + didInitPadPctRef.current = true; + return; + } window.dispatchEvent(new Event('resize')); }, [padPct]); @@ -235,6 +242,7 @@ export default function EPUBPage() { isOpen={activeSidebar === 'segments'} setIsOpen={(isOpen) => setActiveSidebar((prev) => isOpen ? 'segments' : (prev === 'segments' ? null : prev))} documentId={id as string} + epubBookRef={bookRef} /> ); diff --git a/src/app/(app)/html/[id]/layout.tsx b/src/app/(app)/html/[id]/layout.tsx new file mode 100644 index 0000000..45189a7 --- /dev/null +++ b/src/app/(app)/html/[id]/layout.tsx @@ -0,0 +1,17 @@ +'use client'; + +import type { ReactNode } from 'react'; + +import { ConfigProvider } from '@/contexts/ConfigContext'; +import { TTSProvider } from '@/contexts/TTSContext'; +import { HTMLProvider } from '@/contexts/HTMLContext'; + +export default function HtmlReaderLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/src/app/(app)/pdf/[id]/layout.tsx b/src/app/(app)/pdf/[id]/layout.tsx new file mode 100644 index 0000000..f5ac472 --- /dev/null +++ b/src/app/(app)/pdf/[id]/layout.tsx @@ -0,0 +1,17 @@ +'use client'; + +import type { ReactNode } from 'react'; + +import { ConfigProvider } from '@/contexts/ConfigContext'; +import { TTSProvider } from '@/contexts/TTSContext'; +import { PDFProvider } from '@/contexts/PDFContext'; + +export default function PdfReaderLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 361f3d3..c777ca0 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -1,21 +1,13 @@ 'use client'; import { ReactNode, useState } from 'react'; -import { usePathname } from 'next/navigation'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { DocumentProvider } from '@/contexts/DocumentContext'; -import { PDFProvider } from '@/contexts/PDFContext'; -import { EPUBProvider } from '@/contexts/EPUBContext'; -import { TTSProvider } from '@/contexts/TTSContext'; import { ThemeProvider } from '@/contexts/ThemeContext'; -import { ConfigProvider } from '@/contexts/ConfigContext'; -import { HTMLProvider } from '@/contexts/HTMLContext'; import { AuthRateLimitProvider } from '@/contexts/AuthRateLimitContext'; import { RuntimeConfigProvider } from '@/contexts/RuntimeConfigContext'; import { PrivacyModal } from '@/components/PrivacyModal'; import { AuthLoader } from '@/components/auth/AuthLoader'; -import { DexieMigrationModal } from '@/components/documents/DexieMigrationModal'; interface ProvidersProps { children: ReactNode; @@ -26,7 +18,6 @@ interface ProvidersProps { } export function Providers({ children, authEnabled, authBaseUrl, allowAnonymousAuthSessions, githubAuthEnabled }: ProvidersProps) { - const pathname = usePathname(); const [queryClient] = useState(() => new QueryClient({ defaultOptions: { queries: { @@ -35,62 +26,26 @@ export function Providers({ children, authEnabled, authBaseUrl, allowAnonymousAu }, }, })); - const isAuthPage = pathname?.startsWith('/signin') || pathname?.startsWith('/signup'); - - const content = isAuthPage ? ( - - - - - <> - {children} - {authEnabled && } - - - - - - ) : ( - - - - - - - - - - - <> - {children} - {authEnabled && } - - - - - - - - - - - - - ); return ( - {content} + + + + + <> + {children} + {authEnabled && } + + + + + ); } diff --git a/src/components/reader/SegmentsSidebar.tsx b/src/components/reader/SegmentsSidebar.tsx index 6dfbdf8..6994b8c 100644 --- a/src/components/reader/SegmentsSidebar.tsx +++ b/src/components/reader/SegmentsSidebar.tsx @@ -1,7 +1,8 @@ 'use client'; -import { Fragment, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Fragment, type ReactNode, type RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Popover, PopoverButton, PopoverPanel, Transition } from '@headlessui/react'; +import type { Book } from 'epubjs'; import toast from 'react-hot-toast'; import { useTTS } from '@/contexts/TTSContext'; import { useConfig } from '@/contexts/ConfigContext'; @@ -9,7 +10,6 @@ import { RefreshIcon, InfoIcon } from '@/components/icons/Icons'; import { ReaderSidebarShell } from '@/components/reader/ReaderSidebarShell'; import { compareSegmentLocators, locatorGroupKey, locatorIdentityKey } from '@/lib/shared/tts-locator'; import { buildSegmentKey, buildSegmentKeyPrefix } from '@/lib/shared/tts-segment-plan'; -import { useEPUB } from '@/contexts/EPUBContext'; import { canonicalizeEpubSegmentsAgainstSpineText, type CanonicalizedEpubSegment, @@ -36,6 +36,7 @@ interface SegmentsSidebarProps { isOpen: boolean; setIsOpen: (open: boolean) => void; documentId: string; + epubBookRef?: RefObject; } type FetchState = @@ -172,7 +173,7 @@ function isElementFullyVisibleWithinContainer(element: HTMLElement, container: H return elRect.top >= containerRect.top && elRect.bottom <= containerRect.bottom; } -export function SegmentsSidebar({ isOpen, setIsOpen, documentId }: SegmentsSidebarProps) { +export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }: SegmentsSidebarProps) { const { sentences, currentSentenceIndex, @@ -192,7 +193,6 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId }: SegmentsSideb ttsSegmentMaxBlockLength, updateConfigKey, } = useConfig(); - const { bookRef } = useEPUB(); /** * Canonicalized per-sentence identities for the currently rendered page. @@ -206,7 +206,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId }: SegmentsSideb setSynthRowCanonical([]); return; } - const book = bookRef.current; + const book = epubBookRef?.current; if (!book?.isOpen) { setSynthRowCanonical([]); return; @@ -234,7 +234,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId }: SegmentsSideb if (!cancelled) setSynthRowCanonical(next); })(); return () => { cancelled = true; }; - }, [bookRef, currDocPage, sentences, documentId, activeReaderType, ttsSegmentMaxBlockLength]); + }, [epubBookRef, currDocPage, sentences, documentId, activeReaderType, ttsSegmentMaxBlockLength]); const [state, setState] = useState({ kind: 'idle' }); const [isClearing, setIsClearing] = useState(false); @@ -427,7 +427,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId }: SegmentsSideb // have a spine concept. const inferredCurrentLocator: TTSSegmentLocator | null = (() => { if (activeReaderType === 'epub' && typeof currDocPage === 'string' && currDocPage.length > 0) { - const book = bookRef.current; + const book = epubBookRef?.current; const spine = book && book.isOpen ? resolveSpineFromCfi(book, currDocPage) : null; if (spine) { return { @@ -545,7 +545,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId }: SegmentsSideb }); return entries; - }, [state, currDocPage, currDocPageNumber, sentences, bookRef, documentId, activeReaderType, synthRowCanonical]); + }, [state, currDocPage, currDocPageNumber, sentences, epubBookRef, documentId, activeReaderType, synthRowCanonical]); const totalVariants = state.kind === 'ready' ? rowsToRender.reduce((sum, r) => sum + r.row.variants.length, 0) diff --git a/src/hooks/epub/useEPUBResize.ts b/src/hooks/epub/useEPUBResize.ts index 2a215d3..506bd35 100644 --- a/src/hooks/epub/useEPUBResize.ts +++ b/src/hooks/epub/useEPUBResize.ts @@ -1,15 +1,36 @@ -import { useEffect, RefObject, useState } from 'react'; +import { useEffect, RefObject, useRef, useState } from 'react'; import { debounce } from '@/lib/client/pdf'; export function useEPUBResize(containerRef: RefObject) { const [isResizing, setIsResizing] = useState(false); const [dimensions, setDimensions] = useState(null); + const hasBaselineRectRef = useRef(false); + const lastRectRef = useRef<{ width: number; height: number } | null>(null); - useEffect(() => { + useEffect(() => { const debouncedResize = debounce((...args: unknown[]) => { const entries = args[0] as ResizeObserverEntry[]; - console.log('Debounced resize', entries[0].contentRect); - setDimensions(entries[0].contentRect); + const rect = entries[0]?.contentRect; + if (!rect) return; + + const nextWidth = Math.round(rect.width); + const nextHeight = Math.round(rect.height); + const prev = lastRectRef.current; + lastRectRef.current = { width: nextWidth, height: nextHeight }; + + // First callback after observer attach reflects initial layout, not a user + // resize. Treat it as baseline to avoid pausing TTS on page load. + if (!hasBaselineRectRef.current) { + hasBaselineRectRef.current = true; + setDimensions(rect); + return; + } + + if (prev && prev.width === nextWidth && prev.height === nextHeight) { + return; + } + + setDimensions(rect); setIsResizing((prev) => { if (!prev) return true; return prev; @@ -17,9 +38,6 @@ export function useEPUBResize(containerRef: RefObject) { }, 150); const resizeObserver = new ResizeObserver((entries) => { - // if (!isResizing) { - // setIsResizing(true); - // } debouncedResize(entries); }); @@ -28,7 +46,6 @@ export function useEPUBResize(containerRef: RefObject) { if (mutation.addedNodes.length) { const container = containerRef.current?.querySelector('.epub-container'); if (container) { - console.log('Observer attached to epub-container'); resizeObserver.observe(container); mutationObserver.disconnect(); break; @@ -45,7 +62,6 @@ export function useEPUBResize(containerRef: RefObject) { const container = containerRef.current.querySelector('.epub-container'); if (container) { - console.log('Container already exists, attaching observer'); resizeObserver.observe(container); mutationObserver.disconnect(); } @@ -58,4 +74,4 @@ export function useEPUBResize(containerRef: RefObject) { }, [containerRef]); return { isResizing, setIsResizing, dimensions }; -} \ No newline at end of file +}