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';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useCallback, useMemo } from 'react';
|
import { useEffect, useRef, useCallback } from 'react';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { useEPUB } from '@/contexts/EPUBContext';
|
import { useEPUB } from '@/contexts/EPUBContext';
|
||||||
|
|
@ -10,7 +10,8 @@ import { DocumentSkeleton } from '@/components/DocumentSkeleton';
|
||||||
import TTSPlayer from '@/components/player/TTSPlayer';
|
import TTSPlayer from '@/components/player/TTSPlayer';
|
||||||
import { setLastDocumentLocation } from '@/utils/indexedDB';
|
import { setLastDocumentLocation } from '@/utils/indexedDB';
|
||||||
import type { Rendition, Book, NavItem } from 'epubjs';
|
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), {
|
const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
|
@ -31,8 +32,13 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
|
||||||
const toc = useRef<NavItem[]>([]);
|
const toc = useRef<NavItem[]>([]);
|
||||||
const locationRef = useRef<string | number>(currDocPage);
|
const locationRef = useRef<string | number>(currDocPage);
|
||||||
const { updateTheme } = useEPUBTheme(epubTheme, rendition.current);
|
const { updateTheme } = useEPUBTheme(epubTheme, rendition.current);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const isEPUBSetOnce = useRef(false);
|
const isEPUBSetOnce = useRef(false);
|
||||||
|
const isResizing = useRef(false);
|
||||||
|
|
||||||
|
useEPUBResize(containerRef, isResizing);
|
||||||
|
|
||||||
const handleLocationChanged = useCallback((location: string | number) => {
|
const handleLocationChanged = useCallback((location: string | number) => {
|
||||||
// Set the EPUB flag once the location changes
|
// Set the EPUB flag once the location changes
|
||||||
if (!isEPUBSetOnce.current) {
|
if (!isEPUBSetOnce.current) {
|
||||||
|
|
@ -62,47 +68,23 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
|
||||||
setLastDocumentLocation(id as string, location.toString());
|
setLastDocumentLocation(id as string, location.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
skipToLocation(location);
|
if (isResizing.current) {
|
||||||
|
skipToLocation(location, false);
|
||||||
|
isResizing.current = false;
|
||||||
|
} else {
|
||||||
|
skipToLocation(location, true);
|
||||||
|
}
|
||||||
|
|
||||||
locationRef.current = location;
|
locationRef.current = location;
|
||||||
extractPageText(bookRef.current, rendition.current);
|
extractPageText(bookRef.current, rendition.current);
|
||||||
|
|
||||||
}, [id, skipToLocation, extractPageText, setIsEPUB]);
|
}, [id, skipToLocation, extractPageText, setIsEPUB]);
|
||||||
|
|
||||||
// Replace the debounced text extraction with a proper implementation using useMemo
|
// Load the initial location
|
||||||
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
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bookRef.current || !rendition.current || isEPUBSetOnce.current) return;
|
if (!bookRef.current || !rendition.current || isEPUBSetOnce.current) return;
|
||||||
|
|
||||||
extractPageText(bookRef.current, rendition.current);
|
extractPageText(bookRef.current, rendition.current);
|
||||||
|
}, [extractPageText]);
|
||||||
// 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]);
|
|
||||||
|
|
||||||
// Register the location change handler
|
// Register the location change handler
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -114,7 +96,7 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`h-screen flex flex-col ${className}`}>
|
<div className={`h-screen flex flex-col ${className}`} ref={containerRef}>
|
||||||
<div className="z-10">
|
<div className="z-10">
|
||||||
<TTSPlayer />
|
<TTSPlayer />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
|
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
|
||||||
import { GithubIcon } from './icons/Icons'
|
import { GithubIcon } from '@/components/icons/Icons'
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,16 @@ import { useTTS } from '@/contexts/TTSContext';
|
||||||
import { usePDF } from '@/contexts/PDFContext';
|
import { usePDF } from '@/contexts/PDFContext';
|
||||||
import TTSPlayer from '@/components/player/TTSPlayer';
|
import TTSPlayer from '@/components/player/TTSPlayer';
|
||||||
import { useConfig } from '@/contexts/ConfigContext';
|
import { useConfig } from '@/contexts/ConfigContext';
|
||||||
import { debounce } from '@/utils/pdf';
|
import { usePDFResize } from '@/hooks/pdf/usePDFResize';
|
||||||
|
|
||||||
interface PDFViewerProps {
|
interface PDFViewerProps {
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PDFViewer({ zoomLevel }: PDFViewerProps) {
|
export function PDFViewer({ zoomLevel }: PDFViewerProps) {
|
||||||
const [containerWidth, setContainerWidth] = useState<number>(0);
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const scaleRef = useRef<number>(1);
|
const scaleRef = useRef<number>(1);
|
||||||
|
const { containerWidth } = usePDFResize(containerRef);
|
||||||
|
|
||||||
// Config context
|
// Config context
|
||||||
const { viewType } = useConfig();
|
const { viewType } = useConfig();
|
||||||
|
|
@ -158,27 +158,6 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
|
||||||
return scaleRef.current;
|
return scaleRef.current;
|
||||||
}, [calculateScale]);
|
}, [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 (
|
return (
|
||||||
<div ref={containerRef} className="flex flex-col items-center overflow-auto max-h-[calc(100vh-100px)] w-full px-6">
|
<div ref={containerRef} className="flex flex-col items-center overflow-auto max-h-[calc(100vh-100px)] w-full px-6">
|
||||||
<Document
|
<Document
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useContext, ReactNode } from 'react';
|
import { createContext, useContext, ReactNode } from 'react';
|
||||||
import { usePDFDocuments } from '@/hooks/usePDFDocuments';
|
import { usePDFDocuments } from '@/hooks/pdf/usePDFDocuments';
|
||||||
import { useEPUBDocuments } from '@/hooks/useEPUBDocuments';
|
import { useEPUBDocuments } from '@/hooks/epub/useEPUBDocuments';
|
||||||
import { PDFDocument, EPUBDocument } from '@/types/documents';
|
import { PDFDocument, EPUBDocument } from '@/types/documents';
|
||||||
|
|
||||||
interface DocumentContextType {
|
interface DocumentContextType {
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
|
||||||
const rangeCfi = createRangeCfi(start.cfi, end.cfi);
|
const rangeCfi = createRangeCfi(start.cfi, end.cfi);
|
||||||
|
|
||||||
const range = await book.getRange(rangeCfi);
|
const range = await book.getRange(rangeCfi);
|
||||||
const textContent = range.toString();
|
const textContent = range.toString().trim();
|
||||||
|
|
||||||
setTTSText(textContent);
|
setTTSText(textContent);
|
||||||
setCurrDocText(textContent);
|
setCurrDocText(textContent);
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ interface TTSContextType {
|
||||||
setCurrDocPages: (num: number | undefined) => void;
|
setCurrDocPages: (num: number | undefined) => void;
|
||||||
setSpeedAndRestart: (speed: number) => void;
|
setSpeedAndRestart: (speed: number) => void;
|
||||||
setVoiceAndRestart: (voice: string) => 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
|
registerLocationChangeHandler: (handler: (location: string | number) => void) => void; // EPUB-only: Handles chapter navigation
|
||||||
setIsEPUB: (isEPUB: boolean) => void;
|
setIsEPUB: (isEPUB: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -160,6 +160,10 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
* @returns {Promise<string[]>} Array of processed sentences
|
* @returns {Promise<string[]>} Array of processed sentences
|
||||||
*/
|
*/
|
||||||
const processTextToSentences = useCallback(async (text: string): Promise<string[]> => {
|
const processTextToSentences = useCallback(async (text: string): Promise<string[]> => {
|
||||||
|
if (text.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/nlp', {
|
const response = await fetch('/api/nlp', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|
@ -174,14 +178,45 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
return sentences;
|
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
|
* Handles blank text sections based on document type
|
||||||
*
|
*
|
||||||
* @param {string[]} sentences - Array of processed sentences
|
* @param {string[]} sentences - Array of processed sentences
|
||||||
* @returns {boolean} - True if blank section was handled
|
* @returns {boolean} - True if blank section was handled
|
||||||
*/
|
*/
|
||||||
const handleBlankSection = useCallback((sentences: string[]): boolean => {
|
const handleBlankSection = useCallback((text: string): boolean => {
|
||||||
if (!isPlaying || !skipBlank || sentences.length > 0) {
|
if (!isPlaying || !skipBlank || text.length > 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,7 +240,8 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currDocPageNumber < currDocPages!) {
|
if (currDocPageNumber < currDocPages!) {
|
||||||
incrementPage();
|
// Pass true to keep playing when skipping blank pages
|
||||||
|
skipToLocation(currDocPageNumber + 1, true);
|
||||||
|
|
||||||
toast.success(`Skipping blank page ${currDocPageNumber}`, {
|
toast.success(`Skipping blank page ${currDocPageNumber}`, {
|
||||||
id: `page-${currDocPageNumber}`,
|
id: `page-${currDocPageNumber}`,
|
||||||
|
|
@ -224,7 +260,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}, [isPlaying, skipBlank, isEPUB, currDocPageNumber, currDocPages, incrementPage]);
|
}, [isPlaying, skipBlank, isEPUB, currDocPageNumber, currDocPages, skipToLocation]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the current text and splits it into sentences
|
* Sets the current text and splits it into sentences
|
||||||
|
|
@ -233,18 +269,21 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
*/
|
*/
|
||||||
const setText = useCallback((text: string) => {
|
const setText = useCallback((text: string) => {
|
||||||
console.log('Setting page text:', text);
|
console.log('Setting page text:', text);
|
||||||
|
|
||||||
|
if (handleBlankSection(text)) return;
|
||||||
|
|
||||||
processTextToSentences(text)
|
processTextToSentences(text)
|
||||||
.then(newSentences => {
|
.then(newSentences => {
|
||||||
if (handleBlankSection(newSentences)) {
|
if (newSentences.length === 0) {
|
||||||
|
console.warn('No sentences found in text');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSentences(newSentences);
|
setSentences(newSentences);
|
||||||
setNextPageLoading(false);
|
setNextPageLoading(false);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error processing text:', error);
|
console.warn('Error processing text:', error);
|
||||||
toast.error('Failed to process text', {
|
toast.error('Failed to process text', {
|
||||||
style: {
|
style: {
|
||||||
background: 'var(--background)',
|
background: 'var(--background)',
|
||||||
|
|
@ -255,16 +294,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
});
|
});
|
||||||
}, [processTextToSentences, handleBlankSection]);
|
}, [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
|
* Toggles the playback state between playing and paused
|
||||||
*/
|
*/
|
||||||
|
|
@ -279,25 +308,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
});
|
});
|
||||||
}, [abortAudio]);
|
}, [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
|
* 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