refactor(epub): move reader state to route-local hook
This commit is contained in:
parent
2f067c0a90
commit
fd6a06e467
4 changed files with 40 additions and 41 deletions
|
|
@ -4,13 +4,12 @@ import type { ReactNode } from 'react';
|
||||||
|
|
||||||
import { ConfigProvider } from '@/contexts/ConfigContext';
|
import { ConfigProvider } from '@/contexts/ConfigContext';
|
||||||
import { TTSProvider } from '@/contexts/TTSContext';
|
import { TTSProvider } from '@/contexts/TTSContext';
|
||||||
import { EPUBProvider } from '@/contexts/EPUBContext';
|
|
||||||
|
|
||||||
export default function EpubReaderLayout({ children }: { children: ReactNode }) {
|
export default function EpubReaderLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<TTSProvider>
|
<TTSProvider>
|
||||||
<EPUBProvider>{children}</EPUBProvider>
|
{children}
|
||||||
</TTSProvider>
|
</TTSProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useEPUB } from '@/contexts/EPUBContext';
|
|
||||||
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
|
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
|
||||||
import { EPUBViewer } from '@/components/views/EPUBViewer';
|
import { EPUBViewer } from '@/components/views/EPUBViewer';
|
||||||
import { DocumentSettings } from '@/components/documents/DocumentSettings';
|
import { DocumentSettings } from '@/components/documents/DocumentSettings';
|
||||||
|
|
@ -20,12 +19,21 @@ import { resolveDocumentId } from '@/lib/client/dexie';
|
||||||
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
|
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
|
||||||
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
import { useFeatureFlag } from '@/contexts/RuntimeConfigContext';
|
import { useFeatureFlag } from '@/contexts/RuntimeConfigContext';
|
||||||
|
import { useEPUB } from './useEpubDocument';
|
||||||
|
|
||||||
export default function EPUBPage() {
|
export default function EPUBPage() {
|
||||||
const canExportAudiobook = useFeatureFlag('enableAudiobookExport');
|
const canExportAudiobook = useFeatureFlag('enableAudiobookExport');
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { setCurrentDocument, currDocName, clearCurrDoc, createFullAudioBook: createEPUBAudioBook, regenerateChapter: regenerateEPUBChapter, bookRef } = useEPUB();
|
const epubState = useEPUB();
|
||||||
|
const {
|
||||||
|
setCurrentDocument,
|
||||||
|
currDocName,
|
||||||
|
clearCurrDoc,
|
||||||
|
createFullAudioBook: createEPUBAudioBook,
|
||||||
|
regenerateChapter: regenerateEPUBChapter,
|
||||||
|
bookRef,
|
||||||
|
} = epubState;
|
||||||
const { stop } = useTTS();
|
const { stop } = useTTS();
|
||||||
const { isAtLimit } = useAuthRateLimit();
|
const { isAtLimit } = useAuthRateLimit();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -209,7 +217,7 @@ export default function EPUBPage() {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full" style={{ paddingLeft: `${Math.round(maxPadPx * ((100 - padPct) / 100))}px`, paddingRight: `${Math.round(maxPadPx * ((100 - padPct) / 100))}px` }}>
|
<div className="h-full w-full" style={{ paddingLeft: `${Math.round(maxPadPx * ((100 - padPct) / 100))}px`, paddingRight: `${Math.round(maxPadPx * ((100 - padPct) / 100))}px` }}>
|
||||||
<EPUBViewer className="h-full" />
|
<EPUBViewer className="h-full" epubState={epubState} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useState,
|
useState,
|
||||||
ReactNode,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
|
|
@ -36,7 +33,7 @@ import {
|
||||||
type EpubCanonicalWordToken,
|
type EpubCanonicalWordToken,
|
||||||
} from '@/lib/client/epub/epub-word-highlight';
|
} from '@/lib/client/epub/epub-word-highlight';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { useConfig } from './ConfigContext';
|
import { useConfig } from '@/contexts/ConfigContext';
|
||||||
import type {
|
import type {
|
||||||
EpubRenderedLocationWalker,
|
EpubRenderedLocationWalker,
|
||||||
TTSSentenceAlignment,
|
TTSSentenceAlignment,
|
||||||
|
|
@ -47,7 +44,7 @@ import type { AudiobookGenerationSettings, TTSSegmentLocator } from '@/types/cli
|
||||||
import { isStableEpubLocator } from '@/types/client';
|
import { isStableEpubLocator } from '@/types/client';
|
||||||
import type { CanonicalTtsSegment } from '@/lib/shared/tts-segment-plan';
|
import type { CanonicalTtsSegment } from '@/lib/shared/tts-segment-plan';
|
||||||
|
|
||||||
interface EPUBContextType {
|
export interface EPUBState {
|
||||||
currDocData: ArrayBuffer | undefined;
|
currDocData: ArrayBuffer | undefined;
|
||||||
currDocName: string | undefined;
|
currDocName: string | undefined;
|
||||||
currDocPages: number | undefined;
|
currDocPages: number | undefined;
|
||||||
|
|
@ -90,8 +87,6 @@ interface EPUBContextType {
|
||||||
clearWordHighlights: () => void;
|
clearWordHighlights: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EPUBContext = createContext<EPUBContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
const EPUB_CONTINUATION_CHARS = 5000;
|
const EPUB_CONTINUATION_CHARS = 5000;
|
||||||
|
|
||||||
const stepToNextNode = (node: Node | null, root: Node): Node | null => {
|
const stepToNextNode = (node: Node | null, root: Node): Node | null => {
|
||||||
|
|
@ -526,12 +521,9 @@ const resolveVisibleSegmentRange = (
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider component for EPUB functionality
|
* Route-local EPUB reader hook.
|
||||||
* Manages the state and operations for EPUB document handling
|
|
||||||
* @param {Object} props - Component props
|
|
||||||
* @param {ReactNode} props.children - Child components to be wrapped by the provider
|
|
||||||
*/
|
*/
|
||||||
export function EPUBProvider({ children }: { children: ReactNode }) {
|
export function useEPUB(): EPUBState {
|
||||||
const { setText: setTTSText, currDocPage, currDocPages, setCurrDocPages, stop, skipToLocation, setIsEPUB } = useTTS();
|
const { setText: setTTSText, currDocPage, currDocPages, setCurrDocPages, stop, skipToLocation, setIsEPUB } = useTTS();
|
||||||
const { authEnabled } = useAuthConfig();
|
const { authEnabled } = useAuthConfig();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
@ -1141,8 +1133,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Context value memoization
|
return useMemo(
|
||||||
const contextValue = useMemo(
|
|
||||||
() => ({
|
() => ({
|
||||||
setCurrentDocument,
|
setCurrentDocument,
|
||||||
currDocData,
|
currDocData,
|
||||||
|
|
@ -1190,23 +1181,4 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
|
||||||
clearWordHighlights,
|
clearWordHighlights,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
|
||||||
<EPUBContext.Provider value={contextValue}>
|
|
||||||
{children}
|
|
||||||
</EPUBContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom hook to consume the EPUB context
|
|
||||||
* @returns {EPUBContextType} The EPUB context value
|
|
||||||
* @throws {Error} When used outside of EPUBProvider
|
|
||||||
*/
|
|
||||||
export function useEPUB() {
|
|
||||||
const context = useContext(EPUBContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error('useEPUB must be used within an EPUBProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { useEPUB } from '@/contexts/EPUBContext';
|
|
||||||
import { useTTS } from '@/contexts/TTSContext';
|
import { useTTS } from '@/contexts/TTSContext';
|
||||||
import { useConfig } from '@/contexts/ConfigContext';
|
import { useConfig } from '@/contexts/ConfigContext';
|
||||||
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
|
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
|
||||||
import { useEPUBTheme, getThemeStyles } from '@/hooks/epub/useEPUBTheme';
|
import { useEPUBTheme, getThemeStyles } from '@/hooks/epub/useEPUBTheme';
|
||||||
import { useEPUBResize } from '@/hooks/epub/useEPUBResize';
|
import { useEPUBResize } from '@/hooks/epub/useEPUBResize';
|
||||||
import { DotsVerticalIcon, ChevronLeftIcon, ChevronRightIcon } from '@/components/icons/Icons';
|
import { DotsVerticalIcon, ChevronLeftIcon, ChevronRightIcon } from '@/components/icons/Icons';
|
||||||
|
import type { EPUBState } from '@/app/(app)/epub/[id]/useEpubDocument';
|
||||||
|
|
||||||
const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), {
|
const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
|
@ -17,9 +17,29 @@ const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactRe
|
||||||
|
|
||||||
interface EPUBViewerProps {
|
interface EPUBViewerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
epubState: Pick<
|
||||||
|
EPUBState,
|
||||||
|
| 'currDocData'
|
||||||
|
| 'currDocName'
|
||||||
|
| 'currDocPage'
|
||||||
|
| 'currDocPages'
|
||||||
|
| 'locationRef'
|
||||||
|
| 'handleLocationChanged'
|
||||||
|
| 'bookRef'
|
||||||
|
| 'renditionRef'
|
||||||
|
| 'tocRef'
|
||||||
|
| 'setRendition'
|
||||||
|
| 'extractPageText'
|
||||||
|
| 'highlightSegment'
|
||||||
|
| 'clearHighlights'
|
||||||
|
| 'highlightWordIndex'
|
||||||
|
| 'clearWordHighlights'
|
||||||
|
| 'walkUpcomingRenderedLocations'
|
||||||
|
| 'resolveEpubLocator'
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EPUBViewer({ className = '' }: EPUBViewerProps) {
|
export function EPUBViewer({ className = '', epubState }: EPUBViewerProps) {
|
||||||
const [isTocOpen, setIsTocOpen] = useState(false);
|
const [isTocOpen, setIsTocOpen] = useState(false);
|
||||||
const {
|
const {
|
||||||
currDocData,
|
currDocData,
|
||||||
|
|
@ -39,7 +59,7 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
|
||||||
clearWordHighlights,
|
clearWordHighlights,
|
||||||
walkUpcomingRenderedLocations,
|
walkUpcomingRenderedLocations,
|
||||||
resolveEpubLocator,
|
resolveEpubLocator,
|
||||||
} = useEPUB();
|
} = epubState;
|
||||||
const {
|
const {
|
||||||
registerLocationChangeHandler,
|
registerLocationChangeHandler,
|
||||||
registerEpubLocationWalker,
|
registerEpubLocationWalker,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue