Fix epub use theme and saved location

This commit is contained in:
Richard Roberson 2025-02-17 23:03:28 -07:00
parent 737cf5c9f5
commit 9ae6dd9e82
4 changed files with 149 additions and 148 deletions

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import nlp from 'compromise'; import nlp from 'compromise';
const MAX_BLOCK_LENGTH = 450; const MAX_BLOCK_LENGTH = 350;
const preprocessSentenceForAudio = (text: string): string => { const preprocessSentenceForAudio = (text: string): string => {
return text return text

View file

@ -5,84 +5,18 @@ import { useParams } from 'next/navigation';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useEPUB } from '@/contexts/EPUBContext'; import { useEPUB } from '@/contexts/EPUBContext';
import { useTTS } from '@/contexts/TTSContext'; import { useTTS } from '@/contexts/TTSContext';
import { useConfig } from '@/contexts/ConfigContext';
import { DocumentSkeleton } from '@/components/DocumentSkeleton'; import { DocumentSkeleton } from '@/components/DocumentSkeleton';
import TTSPlayer from '@/components/player/TTSPlayer'; import TTSPlayer from '@/components/player/TTSPlayer';
import { setLastDocumentLocation } from '@/utils/indexedDB'; import { setLastDocumentLocation } from '@/utils/indexedDB';
import type { Rendition, Book, NavItem } from 'epubjs'; import type { Rendition, Book, NavItem } from 'epubjs';
import { ReactReaderStyle, type IReactReaderStyle } from 'react-reader'; import { useEPUBTheme, getThemeStyles } from '@/hooks/useEPUBTheme';
import { useConfig } from '@/contexts/ConfigContext';
const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), { const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), {
ssr: false, ssr: false,
loading: () => <DocumentSkeleton /> loading: () => <DocumentSkeleton />
}); });
const getThemeStyles = (): IReactReaderStyle => {
const baseStyle = {
...ReactReaderStyle,
readerArea: {
...ReactReaderStyle.readerArea,
transition: undefined,
}
};
const colors = {
background: getComputedStyle(document.documentElement).getPropertyValue('--background'),
foreground: getComputedStyle(document.documentElement).getPropertyValue('--foreground'),
base: getComputedStyle(document.documentElement).getPropertyValue('--base'),
offbase: getComputedStyle(document.documentElement).getPropertyValue('--offbase'),
muted: getComputedStyle(document.documentElement).getPropertyValue('--muted'),
};
return {
...baseStyle,
arrow: {
...baseStyle.arrow,
color: colors.foreground,
},
arrowHover: {
...baseStyle.arrowHover,
color: colors.muted,
},
readerArea: {
...baseStyle.readerArea,
backgroundColor: colors.base,
},
titleArea: {
...baseStyle.titleArea,
color: colors.foreground,
display: 'none',
},
tocArea: {
...baseStyle.tocArea,
background: colors.base,
},
tocButtonExpanded: {
...baseStyle.tocButtonExpanded,
background: colors.offbase,
},
tocButtonBar: {
...baseStyle.tocButtonBar,
background: colors.muted,
},
tocButton: {
...baseStyle.tocButton,
color: colors.muted,
},
tocAreaButton: {
...baseStyle.tocAreaButton,
color: colors.muted,
backgroundColor: colors.offbase,
padding: '0.25rem',
paddingLeft: '0.5rem',
paddingRight: '0.5rem',
marginBottom: '0.25rem',
borderRadius: '0.25rem',
borderColor: 'transparent',
},
};
};
interface EPUBViewerProps { interface EPUBViewerProps {
className?: string; className?: string;
} }
@ -96,11 +30,11 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
const rendition = useRef<Rendition | undefined>(undefined); const rendition = useRef<Rendition | undefined>(undefined);
const toc = useRef<NavItem[]>([]); const toc = useRef<NavItem[]>([]);
const locationRef = useRef<string | number>(currDocPage); const locationRef = useRef<string | number>(currDocPage);
const [reloadKey, setReloadKey] = useState(0);
const [initialPrevLocLoad, setInitialPrevLocLoad] = useState(false); const [initialPrevLocLoad, setInitialPrevLocLoad] = useState(false);
const { updateTheme } = useEPUBTheme(epubTheme, rendition.current);
const handleLocationChanged = useCallback((location: string | number, initial = false) => { const handleLocationChanged = useCallback((location: string | number, initial = false) => {
if (!bookRef.current?.isOpen) return; if (!bookRef.current?.isOpen || !rendition.current) return;
// Handle special 'next' and 'prev' cases, which // Handle special 'next' and 'prev' cases, which
if (location === 'next' && rendition.current) { if (location === 'next' && rendition.current) {
rendition.current.next(); rendition.current.next();
@ -111,32 +45,32 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
return; return;
} }
if (bookRef.current && rendition.current) {
const { displayed, href } = rendition.current.location.start;
const chapter = toc.current.find((item) => item.href === href);
console.log('Displayed:', displayed, 'Chapter:', chapter);
if (locationRef.current !== 1) { const { displayed, href } = rendition.current.location.start;
// Save the location to IndexedDB const chapter = toc.current.find((item) => item.href === href);
if (id) {
console.log('Saving location:', location); console.log('Displayed:', displayed, 'Chapter:', chapter);
setLastDocumentLocation(id as string, location.toString());
} if (locationRef.current !== 1) {
} // Save the location to IndexedDB
if (id) {
setEPUBPageInChapter(displayed.page, displayed.total, chapter?.label || ''); console.log('Saving location:', location);
setLastDocumentLocation(id as string, location.toString());
// Add a small delay for initial load to ensure rendition is ready
if (initial) {
rendition.current.display(location.toString()).then(() => {
setInitialPrevLocLoad(true);
});
} else {
locationRef.current = location;
extractPageText(bookRef.current, rendition.current);
} }
} }
setEPUBPageInChapter(displayed.page, displayed.total, chapter?.label || '');
// Add a small delay for initial load to ensure rendition is ready
if (initial) {
rendition.current.display(location.toString()).then(() => {
setInitialPrevLocLoad(true);
});
} else {
locationRef.current = location;
extractPageText(bookRef.current, rendition.current);
}
}, [id, setEPUBPageInChapter, extractPageText]); }, [id, setEPUBPageInChapter, extractPageText]);
// Replace the debounced text extraction with a proper implementation using useMemo // Replace the debounced text extraction with a proper implementation using useMemo
@ -174,46 +108,6 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
} }
}, [extractPageText, debouncedExtractText, initialPrevLocLoad]); }, [extractPageText, debouncedExtractText, initialPrevLocLoad]);
const updateTheme = useCallback((rendition: Rendition) => {
if (!epubTheme) return; // Only apply theme if enabled
const colors = {
foreground: getComputedStyle(document.documentElement).getPropertyValue('--foreground'),
base: getComputedStyle(document.documentElement).getPropertyValue('--base'),
};
rendition.themes.override('color', colors.foreground);
rendition.themes.override('background', colors.base);
}, [epubTheme]);
// Watch for theme changes
useEffect(() => {
if (!epubTheme || !bookRef.current?.isOpen || !rendition.current) return;
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
if (epubTheme) {
setReloadKey(prev => prev + 1);
}
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, [epubTheme]);
// Watch for epubTheme changes
useEffect(() => {
if (!epubTheme || !bookRef.current?.isOpen || !rendition.current) return;
setReloadKey(prev => prev + 1);
}, [epubTheme]);
// Register the location change handler // Register the location change handler
useEffect(() => { useEffect(() => {
registerLocationChangeHandler(handleLocationChanged); registerLocationChangeHandler(handleLocationChanged);
@ -230,7 +124,7 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
</div> </div>
<div className="flex-1 -mt-16 pt-16"> <div className="flex-1 -mt-16 pt-16">
<ReactReader <ReactReader
key={reloadKey} // Add this line to force remount key={'epub-reader'}
location={locationRef.current} location={locationRef.current}
locationChanged={handleLocationChanged} locationChanged={handleLocationChanged}
url={currDocData} url={currDocData}
@ -239,10 +133,9 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
showToc={true} showToc={true}
readerStyles={epubTheme && getThemeStyles() || undefined} readerStyles={epubTheme && getThemeStyles() || undefined}
getRendition={(_rendition: Rendition) => { getRendition={(_rendition: Rendition) => {
updateTheme(_rendition);
bookRef.current = _rendition.book; bookRef.current = _rendition.book;
rendition.current = _rendition; rendition.current = _rendition;
updateTheme();
}} }}
/> />
</div> </div>

View file

@ -21,7 +21,6 @@ interface EPUBContextType {
currDocText: string | undefined; currDocText: string | undefined;
setCurrentDocument: (id: string) => Promise<void>; setCurrentDocument: (id: string) => Promise<void>;
clearCurrDoc: () => void; clearCurrDoc: () => void;
onDocumentLoadSuccess: ({ numPages }: { numPages: number }) => void;
extractPageText: (book: Book, rendition: Rendition) => Promise<string>; extractPageText: (book: Book, rendition: Rendition) => Promise<string>;
} }
@ -35,14 +34,6 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
const [currDocName, setCurrDocName] = useState<string>(); const [currDocName, setCurrDocName] = useState<string>();
const [currDocText, setCurrDocText] = useState<string>(); const [currDocText, setCurrDocText] = useState<string>();
/**
* Handles successful document load
*/
const onDocumentLoadSuccess = useCallback(({ numPages }: { numPages: number }) => {
console.log('EPUB loaded:', numPages);
setCurrDocPages(numPages);
}, [setCurrDocPages]);
/** /**
* Clears the current document state * Clears the current document state
*/ */
@ -106,7 +97,6 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
// Context value memoization // Context value memoization
const contextValue = useMemo( const contextValue = useMemo(
() => ({ () => ({
onDocumentLoadSuccess,
setCurrentDocument, setCurrentDocument,
currDocData, currDocData,
currDocName, currDocName,
@ -117,7 +107,6 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
extractPageText, extractPageText,
}), }),
[ [
onDocumentLoadSuccess,
setCurrentDocument, setCurrentDocument,
currDocData, currDocData,
currDocName, currDocName,

119
src/hooks/useEPUBTheme.ts Normal file
View file

@ -0,0 +1,119 @@
import { useCallback, useEffect } from 'react';
import { Rendition } from 'epubjs';
import { ReactReaderStyle, IReactReaderStyle } from 'react-reader';
export const getThemeStyles = (): IReactReaderStyle => {
const baseStyle = {
...ReactReaderStyle,
readerArea: {
...ReactReaderStyle.readerArea,
transition: undefined,
}
};
const colors = {
background: getComputedStyle(document.documentElement).getPropertyValue('--background'),
foreground: getComputedStyle(document.documentElement).getPropertyValue('--foreground'),
base: getComputedStyle(document.documentElement).getPropertyValue('--base'),
offbase: getComputedStyle(document.documentElement).getPropertyValue('--offbase'),
muted: getComputedStyle(document.documentElement).getPropertyValue('--muted'),
};
return {
...baseStyle,
arrow: {
...baseStyle.arrow,
color: colors.foreground,
},
arrowHover: {
...baseStyle.arrowHover,
color: colors.muted,
},
readerArea: {
...baseStyle.readerArea,
backgroundColor: colors.base,
},
titleArea: {
...baseStyle.titleArea,
color: colors.foreground,
display: 'none',
},
tocArea: {
...baseStyle.tocArea,
background: colors.base,
},
tocButtonExpanded: {
...baseStyle.tocButtonExpanded,
background: colors.offbase,
},
tocButtonBar: {
...baseStyle.tocButtonBar,
background: colors.muted,
},
tocButton: {
...baseStyle.tocButton,
color: colors.muted,
},
tocAreaButton: {
...baseStyle.tocAreaButton,
color: colors.muted,
backgroundColor: colors.offbase,
padding: '0.25rem',
paddingLeft: '0.5rem',
paddingRight: '0.5rem',
marginBottom: '0.25rem',
borderRadius: '0.25rem',
borderColor: 'transparent',
},
};
};
export const useEPUBTheme = (epubTheme: boolean, rendition: Rendition | undefined) => {
const updateTheme = useCallback(() => {
if (!epubTheme || !rendition) return;
const colors = {
foreground: getComputedStyle(document.documentElement).getPropertyValue('--foreground'),
base: getComputedStyle(document.documentElement).getPropertyValue('--base'),
};
// Register theme rules instead of using override
rendition.themes.registerRules('theme-light', {
'body': {
'color': colors.foreground,
'background-color': colors.base
}
});
// Select the theme to apply it
rendition.themes.select('theme-light');
}, [epubTheme, rendition]);
// Watch for theme changes
useEffect(() => {
if (!epubTheme || !rendition) return;
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
updateTheme();
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, [epubTheme, rendition, updateTheme]);
// Watch for epubTheme changes
useEffect(() => {
if (!epubTheme || !rendition) return;
updateTheme();
}, [epubTheme, rendition, updateTheme]);
return { updateTheme };
};