Skip to content

Commit d447132

Browse files
committed
Refactors to fix blank sections
1 parent 8acf3f6 commit d447132

File tree

11 files changed

+157
-99
lines changed

11 files changed

+157
-99
lines changed

src/components/EPUBViewer.tsx

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

3-
import { useEffect, useRef, useCallback, useMemo } from 'react';
3+
import { useEffect, useRef, useCallback } from 'react';
44
import { useParams } from 'next/navigation';
55
import dynamic from 'next/dynamic';
66
import { useEPUB } from '@/contexts/EPUBContext';
@@ -10,7 +10,8 @@ import { DocumentSkeleton } from '@/components/DocumentSkeleton';
1010
import TTSPlayer from '@/components/player/TTSPlayer';
1111
import { setLastDocumentLocation } from '@/utils/indexedDB';
1212
import type { Rendition, Book, NavItem } from 'epubjs';
13-
import { useEPUBTheme, getThemeStyles } from '@/hooks/useEPUBTheme';
13+
import { useEPUBTheme, getThemeStyles } from '@/hooks/epub/useEPUBTheme';
14+
import { useEPUBResize } from '@/hooks/epub/useEPUBResize';
1415

1516
const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), {
1617
ssr: false,
@@ -31,8 +32,13 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
3132
const toc = useRef<NavItem[]>([]);
3233
const locationRef = useRef<string | number>(currDocPage);
3334
const { updateTheme } = useEPUBTheme(epubTheme, rendition.current);
35+
const containerRef = useRef<HTMLDivElement>(null);
3436

3537
const isEPUBSetOnce = useRef(false);
38+
const isResizing = useRef(false);
39+
40+
useEPUBResize(containerRef, isResizing);
41+
3642
const handleLocationChanged = useCallback((location: string | number) => {
3743
// Set the EPUB flag once the location changes
3844
if (!isEPUBSetOnce.current) {
@@ -62,47 +68,23 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
6268
setLastDocumentLocation(id as string, location.toString());
6369
}
6470

65-
skipToLocation(location);
71+
if (isResizing.current) {
72+
skipToLocation(location, false);
73+
isResizing.current = false;
74+
} else {
75+
skipToLocation(location, true);
76+
}
6677

6778
locationRef.current = location;
6879
extractPageText(bookRef.current, rendition.current);
69-
7080
}, [id, skipToLocation, extractPageText, setIsEPUB]);
7181

72-
// Replace the debounced text extraction with a proper implementation using useMemo
73-
const debouncedExtractText = useMemo(() => {
74-
let timeout: NodeJS.Timeout;
75-
return (book: Book, rendition: Rendition) => {
76-
clearTimeout(timeout);
77-
timeout = setTimeout(() => {
78-
extractPageText(book, rendition);
79-
}, 150);
80-
};
81-
}, [extractPageText]);
82-
83-
// Load the initial location and setup resize handler
82+
// Load the initial location
8483
useEffect(() => {
8584
if (!bookRef.current || !rendition.current || isEPUBSetOnce.current) return;
8685

8786
extractPageText(bookRef.current, rendition.current);
88-
89-
// Add resize observer
90-
const resizeObserver = new ResizeObserver(() => {
91-
if (bookRef.current && rendition.current) {
92-
debouncedExtractText(bookRef.current, rendition.current);
93-
}
94-
});
95-
96-
// Observe the container element
97-
const container = document.querySelector('.epub-container');
98-
if (container) {
99-
resizeObserver.observe(container);
100-
}
101-
102-
return () => {
103-
resizeObserver.disconnect();
104-
};
105-
}, [extractPageText, debouncedExtractText]);
87+
}, [extractPageText]);
10688

10789
// Register the location change handler
10890
useEffect(() => {
@@ -114,7 +96,7 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
11496
}
11597

11698
return (
117-
<div className={`h-screen flex flex-col ${className}`}>
99+
<div className={`h-screen flex flex-col ${className}`} ref={containerRef}>
118100
<div className="z-10">
119101
<TTSPlayer />
120102
</div>

src/components/Footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
2-
import { GithubIcon } from './icons/Icons'
2+
import { GithubIcon } from '@/components/icons/Icons'
33

44
export function Footer() {
55
return (

src/components/PDFViewer.tsx

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ import { useTTS } from '@/contexts/TTSContext';
99
import { usePDF } from '@/contexts/PDFContext';
1010
import TTSPlayer from '@/components/player/TTSPlayer';
1111
import { useConfig } from '@/contexts/ConfigContext';
12-
import { debounce } from '@/utils/pdf';
12+
import { usePDFResize } from '@/hooks/pdf/usePDFResize';
1313

1414
interface PDFViewerProps {
1515
zoomLevel: number;
1616
}
1717

1818
export function PDFViewer({ zoomLevel }: PDFViewerProps) {
19-
const [containerWidth, setContainerWidth] = useState<number>(0);
2019
const containerRef = useRef<HTMLDivElement>(null);
2120
const scaleRef = useRef<number>(1);
21+
const { containerWidth } = usePDFResize(containerRef);
2222

2323
// Config context
2424
const { viewType } = useConfig();
@@ -158,27 +158,6 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
158158
return scaleRef.current;
159159
}, [calculateScale]);
160160

161-
// Modify resize observer effect to use debouncing
162-
useEffect(() => {
163-
if (!containerRef.current) return;
164-
165-
const debouncedResize = debounce((width: unknown) => {
166-
setContainerWidth(Number(width));
167-
}, 150); // 150ms debounce
168-
169-
const observer = new ResizeObserver(entries => {
170-
const width = entries[0]?.contentRect.width;
171-
if (width) {
172-
debouncedResize(width);
173-
}
174-
});
175-
176-
observer.observe(containerRef.current);
177-
return () => {
178-
observer.disconnect();
179-
};
180-
}, []);
181-
182161
return (
183162
<div ref={containerRef} className="flex flex-col items-center overflow-auto max-h-[calc(100vh-100px)] w-full px-6">
184163
<Document

src/contexts/DocumentContext.tsx

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

33
import { createContext, useContext, ReactNode } from 'react';
4-
import { usePDFDocuments } from '@/hooks/usePDFDocuments';
5-
import { useEPUBDocuments } from '@/hooks/useEPUBDocuments';
4+
import { usePDFDocuments } from '@/hooks/pdf/usePDFDocuments';
5+
import { useEPUBDocuments } from '@/hooks/epub/useEPUBDocuments';
66
import { PDFDocument, EPUBDocument } from '@/types/documents';
77

88
interface DocumentContextType {

src/contexts/EPUBContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
9393
const rangeCfi = createRangeCfi(start.cfi, end.cfi);
9494

9595
const range = await book.getRange(rangeCfi);
96-
const textContent = range.toString();
96+
const textContent = range.toString().trim();
9797

9898
setTTSText(textContent);
9999
setCurrDocText(textContent);

src/contexts/TTSContext.tsx

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ interface TTSContextType {
7171
setCurrDocPages: (num: number | undefined) => void;
7272
setSpeedAndRestart: (speed: number) => void;
7373
setVoiceAndRestart: (voice: string) => void;
74-
skipToLocation: (location: string | number) => void;
74+
skipToLocation: (location: string | number, keepPlaying?: boolean) => void;
7575
registerLocationChangeHandler: (handler: (location: string | number) => void) => void; // EPUB-only: Handles chapter navigation
7676
setIsEPUB: (isEPUB: boolean) => void;
7777
}
@@ -160,6 +160,10 @@ export function TTSProvider({ children }: { children: ReactNode }) {
160160
* @returns {Promise<string[]>} Array of processed sentences
161161
*/
162162
const processTextToSentences = useCallback(async (text: string): Promise<string[]> => {
163+
if (text.length === 0) {
164+
return [];
165+
}
166+
163167
const response = await fetch('/api/nlp', {
164168
method: 'POST',
165169
headers: { 'Content-Type': 'application/json' },
@@ -174,14 +178,45 @@ export function TTSProvider({ children }: { children: ReactNode }) {
174178
return sentences;
175179
}, []);
176180

181+
/**
182+
* Stops the current audio playback and clears the active Howl instance
183+
*/
184+
const abortAudio = useCallback(() => {
185+
if (activeHowl) {
186+
activeHowl.stop();
187+
setActiveHowl(null);
188+
}
189+
}, [activeHowl]);
190+
191+
/**
192+
* Navigates to a specific location in the document
193+
* Works for both PDF pages and EPUB locations
194+
*
195+
* @param {string | number} location - The target location to navigate to
196+
*/
197+
const skipToLocation = useCallback((location: string | number, keepPlaying = false) => {
198+
setNextPageLoading(true);
199+
200+
// Reset state for new content
201+
abortAudio();
202+
if (!keepPlaying) {
203+
setIsPlaying(false);
204+
}
205+
setCurrentIndex(0);
206+
setSentences([]);
207+
208+
// Update current page/location
209+
setCurrDocPage(location);
210+
}, [abortAudio]);
211+
177212
/**
178213
* Handles blank text sections based on document type
179214
*
180215
* @param {string[]} sentences - Array of processed sentences
181216
* @returns {boolean} - True if blank section was handled
182217
*/
183-
const handleBlankSection = useCallback((sentences: string[]): boolean => {
184-
if (!isPlaying || !skipBlank || sentences.length > 0) {
218+
const handleBlankSection = useCallback((text: string): boolean => {
219+
if (!isPlaying || !skipBlank || text.length > 0) {
185220
return false;
186221
}
187222

@@ -205,7 +240,8 @@ export function TTSProvider({ children }: { children: ReactNode }) {
205240
}
206241

207242
if (currDocPageNumber < currDocPages!) {
208-
incrementPage();
243+
// Pass true to keep playing when skipping blank pages
244+
skipToLocation(currDocPageNumber + 1, true);
209245

210246
toast.success(`Skipping blank page ${currDocPageNumber}`, {
211247
id: `page-${currDocPageNumber}`,
@@ -224,7 +260,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
224260
}
225261

226262
return false;
227-
}, [isPlaying, skipBlank, isEPUB, currDocPageNumber, currDocPages, incrementPage]);
263+
}, [isPlaying, skipBlank, isEPUB, currDocPageNumber, currDocPages, skipToLocation]);
228264

229265
/**
230266
* Sets the current text and splits it into sentences
@@ -233,18 +269,21 @@ export function TTSProvider({ children }: { children: ReactNode }) {
233269
*/
234270
const setText = useCallback((text: string) => {
235271
console.log('Setting page text:', text);
272+
273+
if (handleBlankSection(text)) return;
236274

237275
processTextToSentences(text)
238276
.then(newSentences => {
239-
if (handleBlankSection(newSentences)) {
277+
if (newSentences.length === 0) {
278+
console.warn('No sentences found in text');
240279
return;
241280
}
242-
281+
243282
setSentences(newSentences);
244283
setNextPageLoading(false);
245284
})
246285
.catch(error => {
247-
console.error('Error processing text:', error);
286+
console.warn('Error processing text:', error);
248287
toast.error('Failed to process text', {
249288
style: {
250289
background: 'var(--background)',
@@ -255,16 +294,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
255294
});
256295
}, [processTextToSentences, handleBlankSection]);
257296

258-
/**
259-
* Stops the current audio playback and clears the active Howl instance
260-
*/
261-
const abortAudio = useCallback(() => {
262-
if (activeHowl) {
263-
activeHowl.stop();
264-
setActiveHowl(null);
265-
}
266-
}, [activeHowl]);
267-
268297
/**
269298
* Toggles the playback state between playing and paused
270299
*/
@@ -279,25 +308,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
279308
});
280309
}, [abortAudio]);
281310

282-
/**
283-
* Navigates to a specific location in the document
284-
* Works for both PDF pages and EPUB locations
285-
*
286-
* @param {string | number} location - The target location to navigate to
287-
*/
288-
const skipToLocation = useCallback((location: string | number) => {
289-
setNextPageLoading(true);
290-
291-
// Reset state for new content
292-
abortAudio();
293-
setIsPlaying(false);
294-
setCurrentIndex(0);
295-
setSentences([]);
296-
297-
// Update current page/location
298-
setCurrDocPage(location);
299-
}, [abortAudio]);
300-
301311
/**
302312
* Moves to the next or previous sentence
303313
*
File renamed without changes.

src/hooks/epub/useEPUBResize.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useEffect, RefObject } from 'react';
2+
3+
export function useEPUBResize(
4+
containerRef: RefObject<HTMLDivElement | null>,
5+
isResizing: RefObject<boolean>
6+
) {
7+
useEffect(() => {
8+
let resizeTimeout: NodeJS.Timeout;
9+
10+
const resizeObserver = new ResizeObserver((entries) => {
11+
clearTimeout(resizeTimeout);
12+
resizeTimeout = setTimeout(() => {
13+
console.log('Resizing detected (debounced)', entries[0].contentRect);
14+
isResizing.current = true;
15+
}, 250);
16+
});
17+
18+
const mutationObserver = new MutationObserver((mutations) => {
19+
for (const mutation of mutations) {
20+
if (mutation.addedNodes.length) {
21+
const container = containerRef.current?.querySelector('.epub-container');
22+
if (container) {
23+
console.log('Observer attached to epub-container');
24+
resizeObserver.observe(container);
25+
mutationObserver.disconnect();
26+
break;
27+
}
28+
}
29+
}
30+
});
31+
32+
if (containerRef.current) {
33+
mutationObserver.observe(containerRef.current, {
34+
childList: true,
35+
subtree: true
36+
});
37+
38+
const container = containerRef.current.querySelector('.epub-container');
39+
if (container) {
40+
console.log('Container already exists, attaching observer');
41+
resizeObserver.observe(container);
42+
mutationObserver.disconnect();
43+
}
44+
}
45+
46+
return () => {
47+
clearTimeout(resizeTimeout);
48+
mutationObserver.disconnect();
49+
resizeObserver.disconnect();
50+
};
51+
}, [containerRef, isResizing]);
52+
}
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)