Refactors to fix blank sections

This commit is contained in:
Richard Roberson 2025-02-19 15:10:11 -07:00
parent 8acf3f6ba6
commit d447132859
11 changed files with 157 additions and 99 deletions

View file

@ -1,6 +1,6 @@
'use client';
import { useEffect, useRef, useCallback, useMemo } from 'react';
import { useEffect, useRef, useCallback } from 'react';
import { useParams } from 'next/navigation';
import dynamic from 'next/dynamic';
import { useEPUB } from '@/contexts/EPUBContext';
@ -10,7 +10,8 @@ import { DocumentSkeleton } from '@/components/DocumentSkeleton';
import TTSPlayer from '@/components/player/TTSPlayer';
import { setLastDocumentLocation } from '@/utils/indexedDB';
import type { Rendition, Book, NavItem } from 'epubjs';
import { useEPUBTheme, getThemeStyles } from '@/hooks/useEPUBTheme';
import { useEPUBTheme, getThemeStyles } from '@/hooks/epub/useEPUBTheme';
import { useEPUBResize } from '@/hooks/epub/useEPUBResize';
const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), {
ssr: false,
@ -31,8 +32,13 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
const toc = useRef<NavItem[]>([]);
const locationRef = useRef<string | number>(currDocPage);
const { updateTheme } = useEPUBTheme(epubTheme, rendition.current);
const containerRef = useRef<HTMLDivElement>(null);
const isEPUBSetOnce = useRef(false);
const isResizing = useRef(false);
useEPUBResize(containerRef, isResizing);
const handleLocationChanged = useCallback((location: string | number) => {
// Set the EPUB flag once the location changes
if (!isEPUBSetOnce.current) {
@ -62,47 +68,23 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
setLastDocumentLocation(id as string, location.toString());
}
skipToLocation(location);
if (isResizing.current) {
skipToLocation(location, false);
isResizing.current = false;
} else {
skipToLocation(location, true);
}
locationRef.current = location;
extractPageText(bookRef.current, rendition.current);
}, [id, skipToLocation, extractPageText, setIsEPUB]);
// Replace the debounced text extraction with a proper implementation using useMemo
const debouncedExtractText = useMemo(() => {
let timeout: NodeJS.Timeout;
return (book: Book, rendition: Rendition) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
extractPageText(book, rendition);
}, 150);
};
}, [extractPageText]);
// Load the initial location and setup resize handler
// Load the initial location
useEffect(() => {
if (!bookRef.current || !rendition.current || isEPUBSetOnce.current) return;
extractPageText(bookRef.current, rendition.current);
// Add resize observer
const resizeObserver = new ResizeObserver(() => {
if (bookRef.current && rendition.current) {
debouncedExtractText(bookRef.current, rendition.current);
}
});
// Observe the container element
const container = document.querySelector('.epub-container');
if (container) {
resizeObserver.observe(container);
}
return () => {
resizeObserver.disconnect();
};
}, [extractPageText, debouncedExtractText]);
}, [extractPageText]);
// Register the location change handler
useEffect(() => {
@ -114,7 +96,7 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
}
return (
<div className={`h-screen flex flex-col ${className}`}>
<div className={`h-screen flex flex-col ${className}`} ref={containerRef}>
<div className="z-10">
<TTSPlayer />
</div>

View file

@ -1,5 +1,5 @@
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { GithubIcon } from './icons/Icons'
import { GithubIcon } from '@/components/icons/Icons'
export function Footer() {
return (

View file

@ -9,16 +9,16 @@ import { useTTS } from '@/contexts/TTSContext';
import { usePDF } from '@/contexts/PDFContext';
import TTSPlayer from '@/components/player/TTSPlayer';
import { useConfig } from '@/contexts/ConfigContext';
import { debounce } from '@/utils/pdf';
import { usePDFResize } from '@/hooks/pdf/usePDFResize';
interface PDFViewerProps {
zoomLevel: number;
}
export function PDFViewer({ zoomLevel }: PDFViewerProps) {
const [containerWidth, setContainerWidth] = useState<number>(0);
const containerRef = useRef<HTMLDivElement>(null);
const scaleRef = useRef<number>(1);
const { containerWidth } = usePDFResize(containerRef);
// Config context
const { viewType } = useConfig();
@ -158,27 +158,6 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
return scaleRef.current;
}, [calculateScale]);
// Modify resize observer effect to use debouncing
useEffect(() => {
if (!containerRef.current) return;
const debouncedResize = debounce((width: unknown) => {
setContainerWidth(Number(width));
}, 150); // 150ms debounce
const observer = new ResizeObserver(entries => {
const width = entries[0]?.contentRect.width;
if (width) {
debouncedResize(width);
}
});
observer.observe(containerRef.current);
return () => {
observer.disconnect();
};
}, []);
return (
<div ref={containerRef} className="flex flex-col items-center overflow-auto max-h-[calc(100vh-100px)] w-full px-6">
<Document

View file

@ -1,8 +1,8 @@
'use client';
import { createContext, useContext, ReactNode } from 'react';
import { usePDFDocuments } from '@/hooks/usePDFDocuments';
import { useEPUBDocuments } from '@/hooks/useEPUBDocuments';
import { usePDFDocuments } from '@/hooks/pdf/usePDFDocuments';
import { useEPUBDocuments } from '@/hooks/epub/useEPUBDocuments';
import { PDFDocument, EPUBDocument } from '@/types/documents';
interface DocumentContextType {

View file

@ -93,7 +93,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
const rangeCfi = createRangeCfi(start.cfi, end.cfi);
const range = await book.getRange(rangeCfi);
const textContent = range.toString();
const textContent = range.toString().trim();
setTTSText(textContent);
setCurrDocText(textContent);

View file

@ -71,7 +71,7 @@ interface TTSContextType {
setCurrDocPages: (num: number | undefined) => void;
setSpeedAndRestart: (speed: number) => void;
setVoiceAndRestart: (voice: string) => void;
skipToLocation: (location: string | number) => void;
skipToLocation: (location: string | number, keepPlaying?: boolean) => void;
registerLocationChangeHandler: (handler: (location: string | number) => void) => void; // EPUB-only: Handles chapter navigation
setIsEPUB: (isEPUB: boolean) => void;
}
@ -160,6 +160,10 @@ export function TTSProvider({ children }: { children: ReactNode }) {
* @returns {Promise<string[]>} Array of processed sentences
*/
const processTextToSentences = useCallback(async (text: string): Promise<string[]> => {
if (text.length === 0) {
return [];
}
const response = await fetch('/api/nlp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -174,14 +178,45 @@ export function TTSProvider({ children }: { children: ReactNode }) {
return sentences;
}, []);
/**
* Stops the current audio playback and clears the active Howl instance
*/
const abortAudio = useCallback(() => {
if (activeHowl) {
activeHowl.stop();
setActiveHowl(null);
}
}, [activeHowl]);
/**
* Navigates to a specific location in the document
* Works for both PDF pages and EPUB locations
*
* @param {string | number} location - The target location to navigate to
*/
const skipToLocation = useCallback((location: string | number, keepPlaying = false) => {
setNextPageLoading(true);
// Reset state for new content
abortAudio();
if (!keepPlaying) {
setIsPlaying(false);
}
setCurrentIndex(0);
setSentences([]);
// Update current page/location
setCurrDocPage(location);
}, [abortAudio]);
/**
* Handles blank text sections based on document type
*
* @param {string[]} sentences - Array of processed sentences
* @returns {boolean} - True if blank section was handled
*/
const handleBlankSection = useCallback((sentences: string[]): boolean => {
if (!isPlaying || !skipBlank || sentences.length > 0) {
const handleBlankSection = useCallback((text: string): boolean => {
if (!isPlaying || !skipBlank || text.length > 0) {
return false;
}
@ -205,7 +240,8 @@ export function TTSProvider({ children }: { children: ReactNode }) {
}
if (currDocPageNumber < currDocPages!) {
incrementPage();
// Pass true to keep playing when skipping blank pages
skipToLocation(currDocPageNumber + 1, true);
toast.success(`Skipping blank page ${currDocPageNumber}`, {
id: `page-${currDocPageNumber}`,
@ -224,7 +260,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
}
return false;
}, [isPlaying, skipBlank, isEPUB, currDocPageNumber, currDocPages, incrementPage]);
}, [isPlaying, skipBlank, isEPUB, currDocPageNumber, currDocPages, skipToLocation]);
/**
* Sets the current text and splits it into sentences
@ -233,18 +269,21 @@ export function TTSProvider({ children }: { children: ReactNode }) {
*/
const setText = useCallback((text: string) => {
console.log('Setting page text:', text);
if (handleBlankSection(text)) return;
processTextToSentences(text)
.then(newSentences => {
if (handleBlankSection(newSentences)) {
if (newSentences.length === 0) {
console.warn('No sentences found in text');
return;
}
setSentences(newSentences);
setNextPageLoading(false);
})
.catch(error => {
console.error('Error processing text:', error);
console.warn('Error processing text:', error);
toast.error('Failed to process text', {
style: {
background: 'var(--background)',
@ -255,16 +294,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
});
}, [processTextToSentences, handleBlankSection]);
/**
* Stops the current audio playback and clears the active Howl instance
*/
const abortAudio = useCallback(() => {
if (activeHowl) {
activeHowl.stop();
setActiveHowl(null);
}
}, [activeHowl]);
/**
* Toggles the playback state between playing and paused
*/
@ -279,25 +308,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
});
}, [abortAudio]);
/**
* Navigates to a specific location in the document
* Works for both PDF pages and EPUB locations
*
* @param {string | number} location - The target location to navigate to
*/
const skipToLocation = useCallback((location: string | number) => {
setNextPageLoading(true);
// Reset state for new content
abortAudio();
setIsPlaying(false);
setCurrentIndex(0);
setSentences([]);
// Update current page/location
setCurrDocPage(location);
}, [abortAudio]);
/**
* Moves to the next or previous sentence
*

View file

@ -0,0 +1,52 @@
import { useEffect, RefObject } from 'react';
export function useEPUBResize(
containerRef: RefObject<HTMLDivElement | null>,
isResizing: RefObject<boolean>
) {
useEffect(() => {
let resizeTimeout: NodeJS.Timeout;
const resizeObserver = new ResizeObserver((entries) => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
console.log('Resizing detected (debounced)', entries[0].contentRect);
isResizing.current = true;
}, 250);
});
const mutationObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length) {
const container = containerRef.current?.querySelector('.epub-container');
if (container) {
console.log('Observer attached to epub-container');
resizeObserver.observe(container);
mutationObserver.disconnect();
break;
}
}
}
});
if (containerRef.current) {
mutationObserver.observe(containerRef.current, {
childList: true,
subtree: true
});
const container = containerRef.current.querySelector('.epub-container');
if (container) {
console.log('Container already exists, attaching observer');
resizeObserver.observe(container);
mutationObserver.disconnect();
}
}
return () => {
clearTimeout(resizeTimeout);
mutationObserver.disconnect();
resizeObserver.disconnect();
};
}, [containerRef, isResizing]);
}

View file

@ -0,0 +1,35 @@
import { RefObject, useState, useEffect } from 'react';
import { debounce } from '@/utils/pdf';
interface UsePDFResizeResult {
containerWidth: number;
setContainerWidth: (width: number) => void;
}
export function usePDFResize(
containerRef: RefObject<HTMLDivElement | null>
): UsePDFResizeResult {
const [containerWidth, setContainerWidth] = useState<number>(0);
useEffect(() => {
if (!containerRef.current) return;
const debouncedResize = debounce((width: unknown) => {
setContainerWidth(Number(width));
}, 150);
const observer = new ResizeObserver(entries => {
const width = entries[0]?.contentRect.width;
if (width) {
debouncedResize(width);
}
});
observer.observe(containerRef.current);
return () => {
observer.disconnect();
};
}, [containerRef]);
return { containerWidth, setContainerWidth };
}