Skip to content

Commit 1dcf1b0

Browse files
committed
Setting for themed epubs
1 parent 09188ec commit 1dcf1b0

File tree

6 files changed

+157
-14
lines changed

6 files changed

+157
-14
lines changed

src/app/epub/[id]/page.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export default function EPUBPage() {
2020
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
2121

2222
const loadDocument = useCallback(async () => {
23-
if (!isLoading) return;
2423
console.log('Loading new epub (from page.tsx)');
2524
stop(); // Reset TTS when loading new document
2625

@@ -36,11 +35,13 @@ export default function EPUBPage() {
3635
} finally {
3736
setIsLoading(false);
3837
}
39-
}, [isLoading, id, setCurrentDocument, stop]);
38+
}, [id, setCurrentDocument, stop]);
4039

4140
useEffect(() => {
41+
if (!isLoading) return;
42+
4243
loadDocument();
43-
}, [loadDocument]);
44+
}, [loadDocument, isLoading]);
4445

4546
if (error) {
4647
return (

src/components/DocumentSettings.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const viewTypes = [
1818
];
1919

2020
export function DocumentSettings({ isOpen, setIsOpen, epub }: DocViewSettingsProps) {
21-
const { viewType, skipBlank, updateConfigKey } = useConfig();
21+
const { viewType, skipBlank, epubTheme, updateConfigKey } = useConfig();
2222
const selectedView = viewTypes.find(v => v.id === viewType) || viewTypes[0];
2323

2424
return (
@@ -124,6 +124,22 @@ export function DocumentSettings({ isOpen, setIsOpen, epub }: DocViewSettingsPro
124124
Automatically skip pages with no text content
125125
</p>
126126
</div>
127+
{epub && (
128+
<div className="space-y-2">
129+
<label className="flex items-center space-x-2">
130+
<input
131+
type="checkbox"
132+
checked={epubTheme}
133+
onChange={(e) => updateConfigKey('epubTheme', e.target.checked)}
134+
className="form-checkbox h-4 w-4 text-accent rounded border-muted"
135+
/>
136+
<span className="text-sm font-medium text-foreground">Use theme (experimental)</span>
137+
</label>
138+
<p className="text-sm text-muted pl-6">
139+
Apply the current app theme to the EPUB viewer
140+
</p>
141+
</div>
142+
)}
127143
</div>
128144
</div>
129145

src/components/EPUBViewer.tsx

Lines changed: 120 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useEffect, useRef, useCallback } from 'react';
3+
import { useEffect, useRef, useCallback, useState } from 'react';
44
import { useParams } from 'next/navigation';
55
import dynamic from 'next/dynamic';
66
import { useEPUB } from '@/contexts/EPUBContext';
@@ -9,12 +9,80 @@ import { DocumentSkeleton } from '@/components/DocumentSkeleton';
99
import TTSPlayer from '@/components/player/TTSPlayer';
1010
import { setLastDocumentLocation } from '@/utils/indexedDB';
1111
import type { Rendition, Book, NavItem } from 'epubjs';
12+
import { ReactReaderStyle, type IReactReaderStyle } from 'react-reader';
13+
import { useConfig } from '@/contexts/ConfigContext';
1214

1315
const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), {
1416
ssr: false,
1517
loading: () => <DocumentSkeleton />
1618
});
1719

20+
const colors = {
21+
background: getComputedStyle(document.documentElement).getPropertyValue('--background'),
22+
foreground: getComputedStyle(document.documentElement).getPropertyValue('--foreground'),
23+
base: getComputedStyle(document.documentElement).getPropertyValue('--base'),
24+
offbase: getComputedStyle(document.documentElement).getPropertyValue('--offbase'),
25+
muted: getComputedStyle(document.documentElement).getPropertyValue('--muted'),
26+
};
27+
28+
const getThemeStyles = (): IReactReaderStyle => {
29+
const baseStyle = {
30+
...ReactReaderStyle,
31+
readerArea: {
32+
...ReactReaderStyle.readerArea,
33+
transition: undefined,
34+
}
35+
};
36+
37+
return {
38+
...baseStyle,
39+
arrow: {
40+
...baseStyle.arrow,
41+
color: colors.foreground,
42+
},
43+
arrowHover: {
44+
...baseStyle.arrowHover,
45+
color: colors.muted,
46+
},
47+
readerArea: {
48+
...baseStyle.readerArea,
49+
backgroundColor: colors.base,
50+
},
51+
titleArea: {
52+
...baseStyle.titleArea,
53+
color: colors.foreground,
54+
display: 'none',
55+
},
56+
tocArea: {
57+
...baseStyle.tocArea,
58+
background: colors.base,
59+
},
60+
tocButtonExpanded: {
61+
...baseStyle.tocButtonExpanded,
62+
background: colors.offbase,
63+
},
64+
tocButtonBar: {
65+
...baseStyle.tocButtonBar,
66+
background: colors.muted,
67+
},
68+
tocButton: {
69+
...baseStyle.tocButton,
70+
color: colors.muted,
71+
},
72+
tocAreaButton: {
73+
...baseStyle.tocAreaButton,
74+
color: colors.muted,
75+
backgroundColor: colors.offbase,
76+
padding: '0.25rem',
77+
paddingLeft: '0.5rem',
78+
paddingRight: '0.5rem',
79+
marginBottom: '0.25rem',
80+
borderRadius: '0.25rem',
81+
borderColor: 'transparent',
82+
},
83+
};
84+
};
85+
1886
interface EPUBViewerProps {
1987
className?: string;
2088
}
@@ -23,13 +91,16 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
2391
const { id } = useParams();
2492
const { currDocData, currDocName, currDocPage, extractPageText } = useEPUB();
2593
const { setEPUBPageInChapter, registerLocationChangeHandler } = useTTS();
94+
const { epubTheme } = useConfig();
2695
const bookRef = useRef<Book | null>(null);
2796
const rendition = useRef<Rendition | undefined>(undefined);
2897
const toc = useRef<NavItem[]>([]);
2998
const locationRef = useRef<string | number>(currDocPage);
30-
99+
const [reloadKey, setReloadKey] = useState(0);
100+
const [initialPrevLocLoad, setInitialPrevLocLoad] = useState(false);
31101

32102
const handleLocationChanged = useCallback((location: string | number, initial = false) => {
103+
if (!bookRef.current?.isOpen) return;
33104
// Handle special 'next' and 'prev' cases, which
34105
if (location === 'next' && rendition.current) {
35106
rendition.current.next();
@@ -60,17 +131,55 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
60131

61132
// Add a small delay for initial load to ensure rendition is ready
62133
if (initial) {
63-
setTimeout(() => {
64-
if (bookRef.current && rendition.current) {
65-
extractPageText(bookRef.current, rendition.current);
66-
}
67-
}, 100);
134+
setInitialPrevLocLoad(true);
68135
} else {
69136
extractPageText(bookRef.current, rendition.current);
70137
}
71138
}
72139
}, [id, setEPUBPageInChapter, extractPageText]);
73140

141+
// Load the initial location
142+
useEffect(() => {
143+
if (bookRef.current && rendition.current) {
144+
extractPageText(bookRef.current, rendition.current);
145+
}
146+
}, [extractPageText, initialPrevLocLoad]);
147+
148+
const updateTheme = useCallback((rendition: Rendition) => {
149+
if (!epubTheme) return; // Only apply theme if enabled
150+
151+
rendition.themes.override('color', colors.foreground);
152+
rendition.themes.override('background', colors.base);
153+
}, [epubTheme]);
154+
155+
// Watch for theme changes
156+
useEffect(() => {
157+
if (!epubTheme || !bookRef.current?.isOpen || !rendition.current) return;
158+
159+
const observer = new MutationObserver((mutations) => {
160+
mutations.forEach((mutation) => {
161+
if (mutation.attributeName === 'class') {
162+
if (epubTheme) {
163+
setReloadKey(prev => prev + 1);
164+
}
165+
}
166+
});
167+
});
168+
169+
observer.observe(document.documentElement, {
170+
attributes: true,
171+
attributeFilter: ['class']
172+
});
173+
174+
return () => observer.disconnect();
175+
}, [epubTheme]);
176+
177+
// Watch for epubTheme changes
178+
useEffect(() => {
179+
if (!epubTheme || !bookRef.current?.isOpen || !rendition.current) return;
180+
setReloadKey(prev => prev + 1);
181+
}, [epubTheme]);
182+
74183
// Register the location change handler
75184
useEffect(() => {
76185
registerLocationChangeHandler(handleLocationChanged);
@@ -87,13 +196,17 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
87196
</div>
88197
<div className="flex-1 -mt-16 pt-16">
89198
<ReactReader
199+
key={reloadKey} // Add this line to force remount
90200
location={locationRef.current}
91201
locationChanged={handleLocationChanged}
92202
url={currDocData}
93203
title={currDocName}
94204
tocChanged={(_toc) => (toc.current = _toc)}
95205
showToc={true}
206+
readerStyles={epubTheme && getThemeStyles() || undefined}
96207
getRendition={(_rendition: Rendition) => {
208+
updateTheme(_rendition);
209+
97210
bookRef.current = _rendition.book;
98211
rendition.current = _rendition;
99212
}}

src/components/player/TTSPlayer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
3131

3232
return (
3333
<div className={`fixed bottom-4 left-1/2 transform -translate-x-1/2 z-49 transition-opacity duration-300`}>
34-
<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">
34+
<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">
3535
{/* Speed control */}
3636
<SpeedControl setSpeedAndRestart={setSpeedAndRestart} />
3737

src/contexts/ConfigContext.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface ConfigContextType {
1111
voiceSpeed: number;
1212
voice: string;
1313
skipBlank: boolean;
14+
epubTheme: boolean; // Add this line
1415
updateConfig: (newConfig: Partial<{ apiKey: string; baseUrl: string; viewType: ViewType }>) => Promise<void>;
1516
updateConfigKey: <K extends keyof ConfigValues>(key: K, value: ConfigValues[K]) => Promise<void>;
1617
isLoading: boolean;
@@ -25,6 +26,7 @@ type ConfigValues = {
2526
voiceSpeed: number;
2627
voice: string;
2728
skipBlank: boolean;
29+
epubTheme: boolean; // Add this line
2830
};
2931

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

4144
const [isLoading, setIsLoading] = useState(true);
4245
const [isDBReady, setIsDBReady] = useState(false);
@@ -55,13 +58,15 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
5558
const cachedVoiceSpeed = await getItem('voiceSpeed');
5659
const cachedVoice = await getItem('voice');
5760
const cachedSkipBlank = await getItem('skipBlank');
61+
const cachedEpubTheme = await getItem('epubTheme');
5862

5963
if (cachedApiKey) console.log('Cached API key found:', cachedApiKey);
6064
if (cachedBaseUrl) console.log('Cached base URL found:', cachedBaseUrl);
6165
if (cachedViewType) console.log('Cached view type found:', cachedViewType);
6266
if (cachedVoiceSpeed) console.log('Cached voice speed found:', cachedVoiceSpeed);
6367
if (cachedVoice) console.log('Cached voice found:', cachedVoice);
6468
if (cachedSkipBlank) console.log('Cached skip blank found:', cachedSkipBlank);
69+
if (cachedEpubTheme) console.log('Cached EPUB theme found:', cachedEpubTheme);
6570

6671
// If not in cache, use env variables
6772
const defaultApiKey = process.env.NEXT_PUBLIC_OPENAI_API_KEY || '1234567890';
@@ -74,6 +79,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
7479
setVoiceSpeed(parseFloat(cachedVoiceSpeed || '1'));
7580
setVoice(cachedVoice || 'af_sarah');
7681
setSkipBlank(cachedSkipBlank === 'false' ? false : true);
82+
setEpubTheme(cachedEpubTheme === 'true');
7783

7884
// If not in cache, save to cache
7985
if (!cachedApiKey) {
@@ -88,6 +94,9 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
8894
if (cachedSkipBlank === null) {
8995
await setItem('skipBlank', 'true');
9096
}
97+
if (cachedEpubTheme === null) {
98+
await setItem('epubTheme', 'false');
99+
}
91100

92101
} catch (error) {
93102
console.error('Error initializing:', error);
@@ -137,6 +146,9 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
137146
case 'skipBlank':
138147
setSkipBlank(value as boolean);
139148
break;
149+
case 'epubTheme':
150+
setEpubTheme(value as boolean);
151+
break;
140152
}
141153
} catch (error) {
142154
console.error(`Error updating config key ${key}:`, error);
@@ -152,6 +164,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
152164
voiceSpeed,
153165
voice,
154166
skipBlank,
167+
epubTheme,
155168
updateConfig,
156169
updateConfigKey,
157170
isLoading,

src/contexts/EPUBContext.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
8585
*/
8686
const extractPageText = useCallback(async (book: Book, rendition: Rendition): Promise<string> => {
8787
try {
88-
const { start, end } = rendition.location;
89-
if (!start?.cfi || !end?.cfi) return '';
88+
const { start, end } = rendition?.location;
89+
if (!start?.cfi || !end?.cfi || !book || !book.isOpen || !rendition) return '';
9090

9191
const rangeCfi = createRangeCfi(start.cfi, end.cfi);
9292

0 commit comments

Comments
 (0)