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 nlp from 'compromise';
const MAX_BLOCK_LENGTH = 450;
const MAX_BLOCK_LENGTH = 350;
const preprocessSentenceForAudio = (text: string): string => {
return text

View file

@ -5,84 +5,18 @@ import { useParams } from 'next/navigation';
import dynamic from 'next/dynamic';
import { useEPUB } from '@/contexts/EPUBContext';
import { useTTS } from '@/contexts/TTSContext';
import { useConfig } from '@/contexts/ConfigContext';
import { DocumentSkeleton } from '@/components/DocumentSkeleton';
import TTSPlayer from '@/components/player/TTSPlayer';
import { setLastDocumentLocation } from '@/utils/indexedDB';
import type { Rendition, Book, NavItem } from 'epubjs';
import { ReactReaderStyle, type IReactReaderStyle } from 'react-reader';
import { useConfig } from '@/contexts/ConfigContext';
import { useEPUBTheme, getThemeStyles } from '@/hooks/useEPUBTheme';
const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), {
ssr: false,
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 {
className?: string;
}
@ -96,11 +30,11 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
const rendition = useRef<Rendition | undefined>(undefined);
const toc = useRef<NavItem[]>([]);
const locationRef = useRef<string | number>(currDocPage);
const [reloadKey, setReloadKey] = useState(0);
const [initialPrevLocLoad, setInitialPrevLocLoad] = useState(false);
const { updateTheme } = useEPUBTheme(epubTheme, rendition.current);
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
if (location === 'next' && rendition.current) {
rendition.current.next();
@ -111,32 +45,32 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
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) {
// Save the location to IndexedDB
if (id) {
console.log('Saving location:', location);
setLastDocumentLocation(id as string, location.toString());
}
}
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);
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) {
// Save the location to IndexedDB
if (id) {
console.log('Saving location:', location);
setLastDocumentLocation(id as string, location.toString());
}
}
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]);
// Replace the debounced text extraction with a proper implementation using useMemo
@ -174,46 +108,6 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
}
}, [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
useEffect(() => {
registerLocationChangeHandler(handleLocationChanged);
@ -230,7 +124,7 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
</div>
<div className="flex-1 -mt-16 pt-16">
<ReactReader
key={reloadKey} // Add this line to force remount
key={'epub-reader'}
location={locationRef.current}
locationChanged={handleLocationChanged}
url={currDocData}
@ -239,10 +133,9 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
showToc={true}
readerStyles={epubTheme && getThemeStyles() || undefined}
getRendition={(_rendition: Rendition) => {
updateTheme(_rendition);
bookRef.current = _rendition.book;
rendition.current = _rendition;
updateTheme();
}}
/>
</div>

View file

@ -21,7 +21,6 @@ interface EPUBContextType {
currDocText: string | undefined;
setCurrentDocument: (id: string) => Promise<void>;
clearCurrDoc: () => void;
onDocumentLoadSuccess: ({ numPages }: { numPages: number }) => void;
extractPageText: (book: Book, rendition: Rendition) => Promise<string>;
}
@ -35,14 +34,6 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
const [currDocName, setCurrDocName] = 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
*/
@ -106,7 +97,6 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
// Context value memoization
const contextValue = useMemo(
() => ({
onDocumentLoadSuccess,
setCurrentDocument,
currDocData,
currDocName,
@ -117,7 +107,6 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
extractPageText,
}),
[
onDocumentLoadSuccess,
setCurrentDocument,
currDocData,
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 };
};