Refactors to fix blank sections
This commit is contained in:
parent
8acf3f6ba6
commit
d447132859
11 changed files with 157 additions and 99 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
52
src/hooks/epub/useEPUBResize.ts
Normal file
52
src/hooks/epub/useEPUBResize.ts
Normal 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]);
|
||||
}
|
||||
35
src/hooks/pdf/usePDFResize.ts
Normal file
35
src/hooks/pdf/usePDFResize.ts
Normal 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 };
|
||||
}
|
||||
Loading…
Reference in a new issue