diff --git a/src/app/epub/[id]/page.tsx b/src/app/epub/[id]/page.tsx index dae8c58..faebbc1 100644 --- a/src/app/epub/[id]/page.tsx +++ b/src/app/epub/[id]/page.tsx @@ -20,7 +20,6 @@ export default function EPUBPage() { const [isSettingsOpen, setIsSettingsOpen] = useState(false); const loadDocument = useCallback(async () => { - if (!isLoading) return; console.log('Loading new epub (from page.tsx)'); stop(); // Reset TTS when loading new document @@ -36,11 +35,13 @@ export default function EPUBPage() { } finally { setIsLoading(false); } - }, [isLoading, id, setCurrentDocument, stop]); + }, [id, setCurrentDocument, stop]); useEffect(() => { + if (!isLoading) return; + loadDocument(); - }, [loadDocument]); + }, [loadDocument, isLoading]); if (error) { return ( diff --git a/src/components/DocumentSettings.tsx b/src/components/DocumentSettings.tsx index dc15191..3fad0b4 100644 --- a/src/components/DocumentSettings.tsx +++ b/src/components/DocumentSettings.tsx @@ -18,7 +18,7 @@ const viewTypes = [ ]; export function DocumentSettings({ isOpen, setIsOpen, epub }: DocViewSettingsProps) { - const { viewType, skipBlank, updateConfigKey } = useConfig(); + const { viewType, skipBlank, epubTheme, updateConfigKey } = useConfig(); const selectedView = viewTypes.find(v => v.id === viewType) || viewTypes[0]; return ( @@ -124,6 +124,22 @@ export function DocumentSettings({ isOpen, setIsOpen, epub }: DocViewSettingsPro Automatically skip pages with no text content

+ {epub && ( +
+ +

+ Apply the current app theme to the EPUB viewer +

+
+ )} diff --git a/src/components/EPUBViewer.tsx b/src/components/EPUBViewer.tsx index d3f96d5..47f0e81 100644 --- a/src/components/EPUBViewer.tsx +++ b/src/components/EPUBViewer.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useRef, useCallback } from 'react'; +import { useEffect, useRef, useCallback, useState } from 'react'; import { useParams } from 'next/navigation'; import dynamic from 'next/dynamic'; import { useEPUB } from '@/contexts/EPUBContext'; @@ -9,12 +9,80 @@ 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'; const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), { ssr: false, loading: () => }); +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'), +}; + +const getThemeStyles = (): IReactReaderStyle => { + const baseStyle = { + ...ReactReaderStyle, + readerArea: { + ...ReactReaderStyle.readerArea, + transition: undefined, + } + }; + + 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; } @@ -23,13 +91,16 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) { const { id } = useParams(); const { currDocData, currDocName, currDocPage, extractPageText } = useEPUB(); const { setEPUBPageInChapter, registerLocationChangeHandler } = useTTS(); + const { epubTheme } = useConfig(); const bookRef = useRef(null); const rendition = useRef(undefined); const toc = useRef([]); const locationRef = useRef(currDocPage); - + const [reloadKey, setReloadKey] = useState(0); + const [initialPrevLocLoad, setInitialPrevLocLoad] = useState(false); const handleLocationChanged = useCallback((location: string | number, initial = false) => { + if (!bookRef.current?.isOpen) return; // Handle special 'next' and 'prev' cases, which if (location === 'next' && rendition.current) { rendition.current.next(); @@ -60,17 +131,55 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) { // Add a small delay for initial load to ensure rendition is ready if (initial) { - setTimeout(() => { - if (bookRef.current && rendition.current) { - extractPageText(bookRef.current, rendition.current); - } - }, 100); + setInitialPrevLocLoad(true); } else { extractPageText(bookRef.current, rendition.current); } } }, [id, setEPUBPageInChapter, extractPageText]); + // Load the initial location + useEffect(() => { + if (bookRef.current && rendition.current) { + extractPageText(bookRef.current, rendition.current); + } + }, [extractPageText, initialPrevLocLoad]); + + const updateTheme = useCallback((rendition: Rendition) => { + if (!epubTheme) return; // Only apply theme if enabled + + 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); @@ -87,13 +196,17 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
(toc.current = _toc)} showToc={true} + readerStyles={epubTheme && getThemeStyles() || undefined} getRendition={(_rendition: Rendition) => { + updateTheme(_rendition); + bookRef.current = _rendition.book; rendition.current = _rendition; }} diff --git a/src/components/player/TTSPlayer.tsx b/src/components/player/TTSPlayer.tsx index 5909575..2623f8e 100644 --- a/src/components/player/TTSPlayer.tsx +++ b/src/components/player/TTSPlayer.tsx @@ -31,7 +31,7 @@ export default function TTSPlayer({ currentPage, numPages }: { return (
-
+
{/* Speed control */} diff --git a/src/contexts/ConfigContext.tsx b/src/contexts/ConfigContext.tsx index 5ff6744..75b67f2 100644 --- a/src/contexts/ConfigContext.tsx +++ b/src/contexts/ConfigContext.tsx @@ -11,6 +11,7 @@ interface ConfigContextType { voiceSpeed: number; voice: string; skipBlank: boolean; + epubTheme: boolean; // Add this line updateConfig: (newConfig: Partial<{ apiKey: string; baseUrl: string; viewType: ViewType }>) => Promise; updateConfigKey: (key: K, value: ConfigValues[K]) => Promise; isLoading: boolean; @@ -25,6 +26,7 @@ type ConfigValues = { voiceSpeed: number; voice: string; skipBlank: boolean; + epubTheme: boolean; // Add this line }; const ConfigContext = createContext(undefined); @@ -37,6 +39,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) { const [voiceSpeed, setVoiceSpeed] = useState(1); const [voice, setVoice] = useState('af_sarah'); const [skipBlank, setSkipBlank] = useState(true); + const [epubTheme, setEpubTheme] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isDBReady, setIsDBReady] = useState(false); @@ -55,6 +58,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) { const cachedVoiceSpeed = await getItem('voiceSpeed'); const cachedVoice = await getItem('voice'); const cachedSkipBlank = await getItem('skipBlank'); + const cachedEpubTheme = await getItem('epubTheme'); if (cachedApiKey) console.log('Cached API key found:', cachedApiKey); if (cachedBaseUrl) console.log('Cached base URL found:', cachedBaseUrl); @@ -62,6 +66,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) { if (cachedVoiceSpeed) console.log('Cached voice speed found:', cachedVoiceSpeed); if (cachedVoice) console.log('Cached voice found:', cachedVoice); if (cachedSkipBlank) console.log('Cached skip blank found:', cachedSkipBlank); + if (cachedEpubTheme) console.log('Cached EPUB theme found:', cachedEpubTheme); // If not in cache, use env variables const defaultApiKey = process.env.NEXT_PUBLIC_OPENAI_API_KEY || '1234567890'; @@ -74,6 +79,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) { setVoiceSpeed(parseFloat(cachedVoiceSpeed || '1')); setVoice(cachedVoice || 'af_sarah'); setSkipBlank(cachedSkipBlank === 'false' ? false : true); + setEpubTheme(cachedEpubTheme === 'true'); // If not in cache, save to cache if (!cachedApiKey) { @@ -88,6 +94,9 @@ export function ConfigProvider({ children }: { children: ReactNode }) { if (cachedSkipBlank === null) { await setItem('skipBlank', 'true'); } + if (cachedEpubTheme === null) { + await setItem('epubTheme', 'false'); + } } catch (error) { console.error('Error initializing:', error); @@ -137,6 +146,9 @@ export function ConfigProvider({ children }: { children: ReactNode }) { case 'skipBlank': setSkipBlank(value as boolean); break; + case 'epubTheme': + setEpubTheme(value as boolean); + break; } } catch (error) { console.error(`Error updating config key ${key}:`, error); @@ -152,6 +164,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) { voiceSpeed, voice, skipBlank, + epubTheme, updateConfig, updateConfigKey, isLoading, diff --git a/src/contexts/EPUBContext.tsx b/src/contexts/EPUBContext.tsx index 15fb6b7..e874d30 100644 --- a/src/contexts/EPUBContext.tsx +++ b/src/contexts/EPUBContext.tsx @@ -85,8 +85,8 @@ export function EPUBProvider({ children }: { children: ReactNode }) { */ const extractPageText = useCallback(async (book: Book, rendition: Rendition): Promise => { try { - const { start, end } = rendition.location; - if (!start?.cfi || !end?.cfi) return ''; + const { start, end } = rendition?.location; + if (!start?.cfi || !end?.cfi || !book || !book.isOpen || !rendition) return ''; const rangeCfi = createRangeCfi(start.cfi, end.cfi);