Skip to content

Commit

Permalink
Setting for themed epubs
Browse files Browse the repository at this point in the history
  • Loading branch information
richardr1126 committed Feb 14, 2025
1 parent 09188ec commit 1dcf1b0
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 14 deletions.
7 changes: 4 additions & 3 deletions src/app/epub/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 (
Expand Down
18 changes: 17 additions & 1 deletion src/components/DocumentSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -124,6 +124,22 @@ export function DocumentSettings({ isOpen, setIsOpen, epub }: DocViewSettingsPro
Automatically skip pages with no text content
</p>
</div>
{epub && (
<div className="space-y-2">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={epubTheme}
onChange={(e) => updateConfigKey('epubTheme', e.target.checked)}
className="form-checkbox h-4 w-4 text-accent rounded border-muted"
/>
<span className="text-sm font-medium text-foreground">Use theme (experimental)</span>
</label>
<p className="text-sm text-muted pl-6">
Apply the current app theme to the EPUB viewer
</p>
</div>
)}
</div>
</div>

Expand Down
127 changes: 120 additions & 7 deletions src/components/EPUBViewer.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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: () => <DocumentSkeleton />
});

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;
}
Expand All @@ -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<Book | null>(null);
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 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();
Expand Down Expand Up @@ -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);
Expand All @@ -87,13 +196,17 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
</div>
<div className="flex-1 -mt-16 pt-16">
<ReactReader
key={reloadKey} // Add this line to force remount
location={locationRef.current}
locationChanged={handleLocationChanged}
url={currDocData}
title={currDocName}
tocChanged={(_toc) => (toc.current = _toc)}
showToc={true}
readerStyles={epubTheme && getThemeStyles() || undefined}
getRendition={(_rendition: Rendition) => {
updateTheme(_rendition);

bookRef.current = _rendition.book;
rendition.current = _rendition;
}}
Expand Down
2 changes: 1 addition & 1 deletion src/components/player/TTSPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function TTSPlayer({ currentPage, numPages }: {

return (
<div className={`fixed bottom-4 left-1/2 transform -translate-x-1/2 z-49 transition-opacity duration-300`}>
<div className="bg-base dark:bg-base rounded-full shadow-lg px-3 sm:px-4 py-0.5 sm:py-1 flex items-center space-x-0.5 sm:space-x-1 relative scale-90 sm:scale-100">
<div className="bg-base dark:bg-base rounded-full shadow-lg px-3 sm:px-4 py-0.5 sm:py-1 flex items-center space-x-0.5 sm:space-x-1 relative scale-90 sm:scale-100 border border-offbase">
{/* Speed control */}
<SpeedControl setSpeedAndRestart={setSpeedAndRestart} />

Expand Down
13 changes: 13 additions & 0 deletions src/contexts/ConfigContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
updateConfigKey: <K extends keyof ConfigValues>(key: K, value: ConfigValues[K]) => Promise<void>;
isLoading: boolean;
Expand All @@ -25,6 +26,7 @@ type ConfigValues = {
voiceSpeed: number;
voice: string;
skipBlank: boolean;
epubTheme: boolean; // Add this line
};

const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
Expand All @@ -37,6 +39,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
const [voiceSpeed, setVoiceSpeed] = useState<number>(1);
const [voice, setVoice] = useState<string>('af_sarah');
const [skipBlank, setSkipBlank] = useState<boolean>(true);
const [epubTheme, setEpubTheme] = useState<boolean>(false);

const [isLoading, setIsLoading] = useState(true);
const [isDBReady, setIsDBReady] = useState(false);
Expand All @@ -55,13 +58,15 @@ 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);
if (cachedViewType) console.log('Cached view type found:', cachedViewType);
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';
Expand All @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -152,6 +164,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
voiceSpeed,
voice,
skipBlank,
epubTheme,
updateConfig,
updateConfigKey,
isLoading,
Expand Down
4 changes: 2 additions & 2 deletions src/contexts/EPUBContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
*/
const extractPageText = useCallback(async (book: Book, rendition: Rendition): Promise<string> => {
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);

Expand Down

0 comments on commit 1dcf1b0

Please # to comment.