Fix epub use theme and saved location
This commit is contained in:
parent
737cf5c9f5
commit
9ae6dd9e82
4 changed files with 149 additions and 148 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
119
src/hooks/useEPUBTheme.ts
Normal 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 };
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue