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
+}