Save last EPUB location
This commit is contained in:
parent
02595147fa
commit
e8008b68ab
7 changed files with 150 additions and 40 deletions
|
|
@ -9,16 +9,21 @@ import { EPUBViewer } from '@/components/EPUBViewer';
|
|||
import { Button } from '@headlessui/react';
|
||||
import { DocumentSettings } from '@/components/DocumentSettings';
|
||||
import { SettingsIcon } from '@/components/icons/Icons';
|
||||
import { useTTS } from "@/contexts/TTSContext";
|
||||
|
||||
export default function EPUBPage() {
|
||||
const { id } = useParams();
|
||||
const { setCurrentDocument, currDocName, clearCurrDoc } = useEPUB();
|
||||
const { stop } = useTTS();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
|
||||
const loadDocument = useCallback(async () => {
|
||||
if (!isLoading) return;
|
||||
console.log('Loading new epub (from page.tsx)');
|
||||
stop(); // Reset TTS when loading new document
|
||||
|
||||
try {
|
||||
if (!id) {
|
||||
setError('Document not found');
|
||||
|
|
@ -31,7 +36,7 @@ export default function EPUBPage() {
|
|||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isLoading, id, setCurrentDocument]);
|
||||
}, [isLoading, id, setCurrentDocument, stop]);
|
||||
|
||||
useEffect(() => {
|
||||
loadDocument();
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export default function PDFViewerPage() {
|
|||
const loadDocument = useCallback(async () => {
|
||||
if (!isLoading) return; // Prevent calls when not loading new doc
|
||||
console.log('Loading new document (from page.tsx)');
|
||||
stop(); // Reset TTS when loading new document
|
||||
try {
|
||||
if (!id) {
|
||||
setError('Document not found');
|
||||
|
|
@ -44,7 +45,7 @@ export default function PDFViewerPage() {
|
|||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isLoading, id, setCurrentDocument]);
|
||||
}, [isLoading, id, setCurrentDocument, stop]);
|
||||
|
||||
useEffect(() => {
|
||||
loadDocument();
|
||||
|
|
@ -59,7 +60,7 @@ export default function PDFViewerPage() {
|
|||
<p className="text-red-500 mb-4">{error}</p>
|
||||
<Link
|
||||
href="/"
|
||||
onClick={() => {clearCurrDoc(); stop();}}
|
||||
onClick={() => {clearCurrDoc();}}
|
||||
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2 text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -78,7 +79,7 @@ export default function PDFViewerPage() {
|
|||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href="/"
|
||||
onClick={() => {clearCurrDoc(); stop();}}
|
||||
onClick={() => {clearCurrDoc();}}
|
||||
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transform transition-transform duration-200 ease-in-out hover:scale-[1.02]"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="currentColor" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useEPUB } from '@/contexts/EPUBContext';
|
||||
import { useTTS } from '@/contexts/TTSContext';
|
||||
import { DocumentSkeleton } from '@/components/DocumentSkeleton';
|
||||
import TTSPlayer from '@/components/player/TTSPlayer';
|
||||
import { setLastDocumentLocation } from '@/utils/indexedDB';
|
||||
import type { Rendition, Book, NavItem } from 'epubjs';
|
||||
|
||||
const ReactReader = dynamic(() => import('react-reader').then(mod => mod.ReactReader), {
|
||||
|
|
@ -18,6 +20,7 @@ interface EPUBViewerProps {
|
|||
}
|
||||
|
||||
export function EPUBViewer({ className = '' }: EPUBViewerProps) {
|
||||
const { id } = useParams();
|
||||
const { currDocData, currDocName, currDocPage, extractPageText } = useEPUB();
|
||||
const { setEPUBPageInChapter, registerLocationChangeHandler } = useTTS();
|
||||
const bookRef = useRef<Book | null>(null);
|
||||
|
|
@ -25,6 +28,19 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
|
|||
const toc = useRef<NavItem[]>([]);
|
||||
const locationRef = useRef<string | number>(currDocPage);
|
||||
|
||||
// Load the last location when component mounts
|
||||
// useEffect(() => {
|
||||
// const loadLastLocation = async () => {
|
||||
// if (id) {
|
||||
// const lastLocation = await getLastDocumentLocation(id as string);
|
||||
// if (lastLocation) {
|
||||
// locationRef.current = lastLocation;
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// loadLastLocation();
|
||||
// }, [id]);
|
||||
|
||||
const handleLocationChanged = useCallback((location: string | number) => {
|
||||
// Handle special 'next' and 'prev' cases
|
||||
if (location === 'next' && rendition.current) {
|
||||
|
|
@ -42,12 +58,21 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
|
|||
|
||||
console.log('Displayed:', displayed, 'Chapter:', chapter);
|
||||
|
||||
locationRef.current = location;
|
||||
setEPUBPageInChapter(displayed.page, displayed.total, chapter?.label || '');
|
||||
|
||||
if (locationRef.current !== 1) {
|
||||
// Save the location to IndexedDB
|
||||
if (id) {
|
||||
console.log('Saving location:', location);
|
||||
setLastDocumentLocation(id as string, location.toString());
|
||||
}
|
||||
}
|
||||
|
||||
locationRef.current = location;
|
||||
|
||||
setEPUBPageInChapter(displayed.page, displayed.total, chapter?.label || '');
|
||||
extractPageText(bookRef.current, rendition.current);
|
||||
}
|
||||
}, [setEPUBPageInChapter, extractPageText]);
|
||||
}, [id, setEPUBPageInChapter, extractPageText]);
|
||||
|
||||
// Register the location change handler
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ interface EPUBContextType {
|
|||
currDocData: ArrayBuffer | undefined;
|
||||
currDocName: string | undefined;
|
||||
currDocPages: number | undefined;
|
||||
currDocPage: number;
|
||||
currDocPage: number | string;
|
||||
currDocText: string | undefined;
|
||||
setCurrentDocument: (id: string) => Promise<void>;
|
||||
clearCurrDoc: () => void;
|
||||
|
|
@ -28,7 +28,7 @@ interface EPUBContextType {
|
|||
const EPUBContext = createContext<EPUBContextType | undefined>(undefined);
|
||||
|
||||
export function EPUBProvider({ children }: { children: ReactNode }) {
|
||||
const { setText: setTTSText, stop, currDocPage, currDocPages, setCurrDocPages } = useTTS();
|
||||
const { setText: setTTSText, currDocPage, currDocPages, setCurrDocPages } = useTTS();
|
||||
|
||||
// Current document state
|
||||
const [currDocData, setCurrDocData] = useState<ArrayBuffer>();
|
||||
|
|
@ -51,8 +51,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
|
|||
setCurrDocName(undefined);
|
||||
setCurrDocText(undefined);
|
||||
setCurrDocPages(undefined);
|
||||
stop();
|
||||
}, [setCurrDocPages, stop]);
|
||||
}, [setCurrDocPages]);
|
||||
|
||||
/**
|
||||
* Sets the current document based on its ID
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const PDFContext = createContext<PDFContextType | undefined>(undefined);
|
|||
* Handles document loading, text processing, and integration with TTS.
|
||||
*/
|
||||
export function PDFProvider({ children }: { children: ReactNode }) {
|
||||
const { setText: setTTSText, currDocPage, currDocPages, setCurrDocPages } = useTTS();
|
||||
const { setText: setTTSText, stop, currDocPageNumber: currDocPage, currDocPages, setCurrDocPages } = useTTS();
|
||||
|
||||
// Current document state
|
||||
const [currDocURL, setCurrDocURL] = useState<string>();
|
||||
|
|
@ -136,8 +136,8 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
setCurrDocURL(undefined);
|
||||
setCurrDocText(undefined);
|
||||
setCurrDocPages(undefined);
|
||||
setTTSText('');
|
||||
}, [setCurrDocPages, setTTSText]);
|
||||
stop();
|
||||
}, [setCurrDocPages, stop]);
|
||||
|
||||
// Context value memoization
|
||||
const contextValue = useMemo(
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import React, {
|
|||
import OpenAI from 'openai';
|
||||
import { Howl } from 'howler';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
import { useConfig } from '@/contexts/ConfigContext';
|
||||
import { splitIntoSentences, preprocessSentenceForAudio } from '@/utils/nlp';
|
||||
|
|
@ -34,6 +35,7 @@ import { useAudioCache } from '@/hooks/audio/useAudioCache';
|
|||
import { useVoiceManagement } from '@/hooks/audio/useVoiceManagement';
|
||||
import { useMediaSession } from '@/hooks/audio/useMediaSession';
|
||||
import { useAudioContext } from '@/hooks/audio/useAudioContext';
|
||||
import { getLastDocumentLocation } from '@/utils/indexedDB';
|
||||
|
||||
// Media globals
|
||||
declare global {
|
||||
|
|
@ -52,7 +54,8 @@ interface TTSContextType {
|
|||
currentSentence: string;
|
||||
|
||||
// Navigation
|
||||
currDocPage: number;
|
||||
currDocPage: string | number; // Change this to allow both types
|
||||
currDocPageNumber: number; // For PDF
|
||||
currDocPages: number | undefined;
|
||||
|
||||
// Voice settings
|
||||
|
|
@ -110,6 +113,9 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
locationChangeHandlerRef.current = handler;
|
||||
}, []);
|
||||
|
||||
// Get document ID from URL params
|
||||
const { id } = useParams();
|
||||
|
||||
/**
|
||||
* State Management
|
||||
*/
|
||||
|
|
@ -120,13 +126,15 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [speed, setSpeed] = useState(voiceSpeed);
|
||||
const [voice, setVoice] = useState(configVoice);
|
||||
const [currDocPage, setCurrDocPage] = useState<number>(1);
|
||||
const [currDocPage, setCurrDocPage] = useState<string | number>(1);
|
||||
const [currDocPages, setCurrDocPages] = useState<number>();
|
||||
const [nextPageLoading, setNextPageLoading] = useState(false);
|
||||
|
||||
// Add this state to track if we're in EPUB mode
|
||||
const [isEPUB, setIsEPUB] = useState(false);
|
||||
|
||||
const currDocPageNumber = (!isEPUB ? parseInt(currDocPage.toString()) : 1);
|
||||
|
||||
console.log('page:', currDocPage, 'pages:', currDocPages);
|
||||
|
||||
/**
|
||||
|
|
@ -137,8 +145,8 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
*/
|
||||
const incrementPage = useCallback((num = 1) => {
|
||||
setNextPageLoading(true);
|
||||
setCurrDocPage(currDocPage + num);
|
||||
}, [currDocPage]);
|
||||
setCurrDocPage(currDocPageNumber + num);
|
||||
}, [currDocPageNumber]);
|
||||
|
||||
/**
|
||||
* Sets the current text and splits it into sentences
|
||||
|
|
@ -167,12 +175,12 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
position: 'top-center',
|
||||
});
|
||||
return;
|
||||
} else if (currDocPage < currDocPages!) {
|
||||
} else if (currDocPageNumber < currDocPages!) {
|
||||
// For PDF, increment the page
|
||||
incrementPage();
|
||||
|
||||
toast.success(`Skipping blank page ${currDocPage}`, {
|
||||
id: `page-${currDocPage}`,
|
||||
toast.success(`Skipping blank page ${currDocPageNumber}`, {
|
||||
id: `page-${currDocPageNumber}`,
|
||||
iconTheme: {
|
||||
primary: 'var(--accent)',
|
||||
secondary: 'var(--background)',
|
||||
|
|
@ -190,7 +198,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
|
||||
setSentences(newSentences);
|
||||
setNextPageLoading(false);
|
||||
}, [isPlaying, skipBlank, currDocPage, currDocPages, incrementPage, isEPUB]);
|
||||
}, [isPlaying, skipBlank, currDocPageNumber, currDocPages, incrementPage, isEPUB]);
|
||||
|
||||
/**
|
||||
* Stops the current audio playback and clears the active Howl instance
|
||||
|
|
@ -299,8 +307,8 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
// For PDFs and other documents, check page bounds
|
||||
if (!isEPUB) {
|
||||
// Handle next/previous page transitions
|
||||
if ((nextIndex >= sentences.length && currDocPage < currDocPages!) ||
|
||||
(nextIndex < 0 && currDocPage > 1)) {
|
||||
if ((nextIndex >= sentences.length && currDocPageNumber < currDocPages!) ||
|
||||
(nextIndex < 0 && currDocPageNumber > 1)) {
|
||||
console.log('PDF: Advancing to next/prev page');
|
||||
setCurrentIndex(0);
|
||||
setSentences([]);
|
||||
|
|
@ -309,12 +317,12 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
|
||||
// Handle end of document (PDF only)
|
||||
if (nextIndex >= sentences.length && currDocPage >= currDocPages!) {
|
||||
if (nextIndex >= sentences.length && currDocPageNumber >= currDocPages!) {
|
||||
console.log('PDF: Reached end of document');
|
||||
setIsPlaying(false);
|
||||
}
|
||||
}
|
||||
}, [currentIndex, incrementPage, sentences, currDocPage, currDocPages, isEPUB]);
|
||||
}, [currentIndex, incrementPage, sentences, currDocPageNumber, currDocPages, isEPUB]);
|
||||
|
||||
/**
|
||||
* Moves forward one sentence in the text
|
||||
|
|
@ -637,6 +645,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
isProcessing,
|
||||
currentSentence: sentences[currentIndex] || '',
|
||||
currDocPage,
|
||||
currDocPageNumber,
|
||||
currDocPages,
|
||||
availableVoices,
|
||||
togglePlay,
|
||||
|
|
@ -657,6 +666,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
sentences,
|
||||
currentIndex,
|
||||
currDocPage,
|
||||
currDocPageNumber,
|
||||
currDocPages,
|
||||
availableVoices,
|
||||
togglePlay,
|
||||
|
|
@ -680,6 +690,20 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
skipBackward,
|
||||
});
|
||||
|
||||
// Load last location on mount for EPUB only
|
||||
useEffect(() => {
|
||||
if (id && isEPUB) {
|
||||
getLastDocumentLocation(id as string).then(lastLocation => {
|
||||
if (lastLocation) {
|
||||
setCurrDocPage(lastLocation);
|
||||
if (locationChangeHandlerRef.current) {
|
||||
locationChangeHandlerRef.current(lastLocation);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [id, isEPUB]);
|
||||
|
||||
/**
|
||||
* Renders the TTS context provider with its children
|
||||
*
|
||||
|
|
|
|||
|
|
@ -174,20 +174,40 @@ class IndexedDBService {
|
|||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
console.log('Removing document:', id);
|
||||
const transaction = this.db!.transaction([PDF_STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(PDF_STORE_NAME);
|
||||
const request = store.delete(id);
|
||||
const locationKey = `lastLocation_${id}`;
|
||||
|
||||
request.onerror = (event) => {
|
||||
// Create two transactions - one for document deletion, one for location cleanup
|
||||
const docTransaction = this.db!.transaction([PDF_STORE_NAME], 'readwrite');
|
||||
const configTransaction = this.db!.transaction([CONFIG_STORE_NAME], 'readwrite');
|
||||
|
||||
const docStore = docTransaction.objectStore(PDF_STORE_NAME);
|
||||
const configStore = configTransaction.objectStore(CONFIG_STORE_NAME);
|
||||
|
||||
// Remove the document
|
||||
const docRequest = docStore.delete(id);
|
||||
// Remove any saved location
|
||||
const locationRequest = configStore.delete(locationKey);
|
||||
|
||||
docRequest.onerror = (event) => {
|
||||
const error = (event.target as IDBRequest).error;
|
||||
console.error('Error removing document:', error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
console.log('Document removed successfully:', id);
|
||||
resolve();
|
||||
locationRequest.onerror = (event) => {
|
||||
console.warn('Error cleaning up location:', (event.target as IDBRequest).error);
|
||||
// Don't reject here, as document deletion is the primary operation
|
||||
};
|
||||
|
||||
// Wait for both transactions to complete
|
||||
Promise.all([
|
||||
new Promise<void>((res) => { docTransaction.oncomplete = () => res(); }),
|
||||
new Promise<void>((res) => { configTransaction.oncomplete = () => res(); })
|
||||
]).then(() => {
|
||||
console.log('Document and location data removed successfully:', id);
|
||||
resolve();
|
||||
}).catch(reject);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in removeDocument transaction:', error);
|
||||
reject(error);
|
||||
|
|
@ -303,18 +323,43 @@ class IndexedDBService {
|
|||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const transaction = this.db!.transaction([EPUB_STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(EPUB_STORE_NAME);
|
||||
const request = store.delete(id);
|
||||
console.log('Removing EPUB document:', id);
|
||||
const locationKey = `lastLocation_${id}`;
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject((event.target as IDBRequest).error);
|
||||
// Create two transactions - one for document deletion, one for location cleanup
|
||||
const docTransaction = this.db!.transaction([EPUB_STORE_NAME], 'readwrite');
|
||||
const configTransaction = this.db!.transaction([CONFIG_STORE_NAME], 'readwrite');
|
||||
|
||||
const docStore = docTransaction.objectStore(EPUB_STORE_NAME);
|
||||
const configStore = configTransaction.objectStore(CONFIG_STORE_NAME);
|
||||
|
||||
// Remove the document
|
||||
const docRequest = docStore.delete(id);
|
||||
// Remove any saved location
|
||||
const locationRequest = configStore.delete(locationKey);
|
||||
|
||||
docRequest.onerror = (event) => {
|
||||
const error = (event.target as IDBRequest).error;
|
||||
console.error('Error removing EPUB document:', error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
locationRequest.onerror = (event) => {
|
||||
console.warn('Error cleaning up location:', (event.target as IDBRequest).error);
|
||||
// Don't reject here, as document deletion is the primary operation
|
||||
};
|
||||
|
||||
// Wait for both transactions to complete
|
||||
Promise.all([
|
||||
new Promise<void>((res) => { docTransaction.oncomplete = () => res(); }),
|
||||
new Promise<void>((res) => { configTransaction.oncomplete = () => res(); })
|
||||
]).then(() => {
|
||||
console.log('EPUB document and location data removed successfully:', id);
|
||||
resolve();
|
||||
};
|
||||
}).catch(reject);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in removeEPUBDocument transaction:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
|
@ -455,4 +500,15 @@ export async function getItem(key: string): Promise<string | null> {
|
|||
|
||||
export async function setItem(key: string, value: string): Promise<void> {
|
||||
return indexedDBService.setConfigItem(key, value);
|
||||
}
|
||||
|
||||
// Add these helper functions before the final export
|
||||
export async function getLastDocumentLocation(docId: string): Promise<string | null> {
|
||||
const key = `lastLocation_${docId}`;
|
||||
return indexedDBService.getConfigItem(key);
|
||||
}
|
||||
|
||||
export async function setLastDocumentLocation(docId: string, location: string): Promise<void> {
|
||||
const key = `lastLocation_${docId}`;
|
||||
return indexedDBService.setConfigItem(key, location);
|
||||
}
|
||||
Loading…
Reference in a new issue