Use indexed db

This commit is contained in:
Richard Roberson 2025-01-18 21:59:02 -07:00
parent 109d43f585
commit 355e4ebe29
9 changed files with 492 additions and 120 deletions

23
package-lock.json generated
View file

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.0",
"@types/string-similarity": "^4.0.2", "@types/string-similarity": "^4.0.2",
"@types/uuid": "^10.0.0",
"compromise": "^14.14.4", "compromise": "^14.14.4",
"lru-cache": "^11.0.2", "lru-cache": "^11.0.2",
"next": "15.1.5", "next": "15.1.5",
@ -19,7 +20,8 @@
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-dropzone": "^14.3.5", "react-dropzone": "^14.3.5",
"react-pdf": "^9.2.1", "react-pdf": "^9.2.1",
"string-similarity": "^4.0.4" "string-similarity": "^4.0.4",
"uuid": "^11.0.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
@ -1165,6 +1167,12 @@
"integrity": "sha512-LkJQ/jsXtCVMK+sKYAmX/8zEq+/46f1PTQw7YtmQwb74jemS1SlNLmARM2Zml9DgdDTWKAtc5L13WorpHPDjDA==", "integrity": "sha512-LkJQ/jsXtCVMK+sKYAmX/8zEq+/46f1PTQw7YtmQwb74jemS1SlNLmARM2Zml9DgdDTWKAtc5L13WorpHPDjDA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.20.0", "version": "8.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
@ -6751,6 +6759,19 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/uuid": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
"integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/warning": { "node_modules/warning": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",

View file

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.0",
"@types/string-similarity": "^4.0.2", "@types/string-similarity": "^4.0.2",
"@types/uuid": "^10.0.0",
"compromise": "^14.14.4", "compromise": "^14.14.4",
"lru-cache": "^11.0.2", "lru-cache": "^11.0.2",
"next": "15.1.5", "next": "15.1.5",
@ -20,7 +21,8 @@
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-dropzone": "^14.3.5", "react-dropzone": "^14.3.5",
"react-pdf": "^9.2.1", "react-pdf": "^9.2.1",
"string-similarity": "^4.0.4" "string-similarity": "^4.0.4",
"uuid": "^11.0.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",

View file

@ -4,13 +4,53 @@ import { PDFViewer } from '@/components/PDFViewer';
import { usePDF } from '@/context/PDFContext'; import { usePDF } from '@/context/PDFContext';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useState } from 'react';
import { PDFSkeleton } from '@/components/PDFSkeleton';
export default function PDFViewerPage() { export default function PDFViewerPage() {
const { id } = useParams(); const { id } = useParams();
const { getDocument } = usePDF(); const { getDocument } = usePDF();
const router = useRouter(); const router = useRouter();
const [document, setDocument] = useState<{ name: string; data: Blob } | null>(null);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const document = getDocument(id as string); useEffect(() => {
async function loadDocument() {
try {
const doc = await getDocument(id as string);
if (!doc) {
setError('Document not found');
return;
}
setDocument(doc);
} catch (err) {
console.error('Error loading document:', err);
setError('Failed to load document');
} finally {
setIsLoading(false);
}
}
loadDocument();
}, [id, getDocument]);
if (error) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<p className="text-red-500 mb-4">{error}</p>
<Link
href="/"
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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to Documents
</Link>
</div>
);
}
return ( return (
<> <>
@ -25,10 +65,18 @@ export default function PDFViewerPage() {
</svg> </svg>
Documents Documents
</Link> </Link>
<h1 className="mr-2 text-xl font-semibold text-foreground">{document?.name}</h1> <h1 className="mr-2 text-xl font-semibold text-foreground">
{isLoading ? 'Loading...' : document?.name}
</h1>
</div> </div>
</div> </div>
<PDFViewer pdfFile={document?.data} /> {isLoading ? (
<div className="p-4">
<PDFSkeleton />
</div>
) : (
<PDFViewer pdfData={document?.data} />
)}
</> </>
); );
} }

View file

@ -2,7 +2,23 @@ import { usePDF } from '@/context/PDFContext';
import Link from 'next/link'; import Link from 'next/link';
export function DocumentList() { export function DocumentList() {
const { documents, removeDocument } = usePDF(); const { documents, removeDocument, isLoading, error } = usePDF();
if (isLoading) {
return (
<div className="w-full text-center text-muted">
Loading documents...
</div>
);
}
if (error) {
return (
<div className="w-full text-center text-red-500">
{error}
</div>
);
}
if (documents.length === 0) { if (documents.length === 0) {
return ( return (
@ -30,17 +46,23 @@ export function DocumentList() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg> </svg>
</div> </div>
<div className="min-w-0"> <div className="flex-1 min-w-0">
<h3 className="font-medium text-foreground truncate">{doc.name}</h3> <p className="text-foreground font-medium truncate">{doc.name}</p>
<p className="text-sm text-muted"> <p className="text-sm text-muted truncate">
{(doc.size / 1024 / 1024).toFixed(2)} MB {(doc.size / 1024 / 1024).toFixed(2)} MB
</p> </p>
</div> </div>
</Link> </Link>
<button <button
onClick={() => removeDocument(doc.id)} onClick={async () => {
className="p-2 text-muted hover:text-accent transition-colors flex-shrink-0 ml-2" try {
title="Remove document" await removeDocument(doc.id);
} catch (err) {
console.error('Failed to remove document:', err);
}
}}
className="ml-4 p-2 text-muted hover:text-red-500 rounded-lg hover:bg-red-50 transition-colors"
aria-label="Delete document"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />

View file

@ -9,22 +9,33 @@ interface PDFUploaderProps {
} }
export function PDFUploader({ className = '' }: PDFUploaderProps) { export function PDFUploader({ className = '' }: PDFUploaderProps) {
const { addDocument } = usePDF(); const { addDocument, error: contextError } = usePDF();
const [isDragging, setIsDragging] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const onDrop = useCallback(async (acceptedFiles: File[]) => { const onDrop = useCallback(async (acceptedFiles: File[]) => {
const file = acceptedFiles[0]; const file = acceptedFiles[0];
if (file && file.type === 'application/pdf') { if (file && file.type === 'application/pdf') {
await addDocument(file); setIsUploading(true);
setError(null);
try {
await addDocument(file);
} catch (err) {
setError(contextError || 'Failed to upload PDF. Please try again.');
console.error('Upload error:', err);
} finally {
setIsUploading(false);
}
} }
}, [addDocument]); }, [addDocument, contextError]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop, onDrop,
accept: { accept: {
'application/pdf': ['.pdf'] 'application/pdf': ['.pdf']
}, },
multiple: false multiple: false,
disabled: isUploading
}); });
return ( return (
@ -33,8 +44,8 @@ export function PDFUploader({ className = '' }: PDFUploaderProps) {
className={` className={`
w-full p-8 border-2 border-dashed rounded-lg w-full p-8 border-2 border-dashed rounded-lg
${isDragActive ? 'border-accent bg-base' : 'border-muted'} ${isDragActive ? 'border-accent bg-base' : 'border-muted'}
transition-colors duration-200 ease-in-out cursor-pointer transition-colors duration-200 ease-in-out
hover:border-accent hover:bg-base ${isUploading ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:border-accent hover:bg-base'}
${className} ${className}
`} `}
> >
@ -55,12 +66,21 @@ export function PDFUploader({ className = '' }: PDFUploaderProps) {
/> />
</svg> </svg>
<p className="mb-2 text-lg font-semibold text-foreground"> {isUploading ? (
Drop your PDF here, or click to select <p className="text-lg font-semibold text-foreground">
</p> Uploading PDF...
<p className="text-sm text-muted"> </p>
Only PDF files are accepted ) : (
</p> <>
<p className="mb-2 text-lg font-semibold text-foreground">
{isDragActive ? 'Drop your PDF here' : 'Drop your PDF here, or click to select'}
</p>
<p className="text-sm text-muted">
Only PDF files are accepted
</p>
{error && <p className="mt-2 text-sm text-red-500">{error}</p>}
</>
)}
</div> </div>
</div> </div>
); );

View file

@ -3,7 +3,7 @@
import { Document, Page, pdfjs } from 'react-pdf'; import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css'; import 'react-pdf/dist/Page/TextLayer.css';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useMemo } from 'react';
import { PDFSkeleton } from './PDFSkeleton'; import { PDFSkeleton } from './PDFSkeleton';
import { useTTS } from '@/context/TTSContext'; import { useTTS } from '@/context/TTSContext';
import stringSimilarity from 'string-similarity'; import stringSimilarity from 'string-similarity';
@ -12,7 +12,7 @@ import stringSimilarity from 'string-similarity';
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs'; pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs';
interface PDFViewerProps { interface PDFViewerProps {
pdfFile: string | undefined; pdfData: Blob | undefined;
} }
interface TextHighlight { interface TextHighlight {
@ -23,11 +23,98 @@ interface TextHighlight {
}; };
} }
export function PDFViewer({ pdfFile }: PDFViewerProps) { export function PDFViewer({ pdfData }: PDFViewerProps) {
const [numPages, setNumPages] = useState<number>(); const [numPages, setNumPages] = useState<number>();
const { setText, currentSentence } = useTTS(); const { setText, currentSentence } = useTTS();
const [pdfText, setPdfText] = useState(''); const [pdfText, setPdfText] = useState('');
const [highlights, setHighlights] = useState<TextHighlight[]>([]); const [highlights, setHighlights] = useState<TextHighlight[]>([]);
const [pdfDataUrl, setPdfDataUrl] = useState<string>();
const [loadingError, setLoadingError] = useState<string>();
// Convert Blob to data URL when pdfData changes
useEffect(() => {
if (!pdfData) return;
const reader = new FileReader();
reader.onload = () => {
setPdfDataUrl(reader.result as string);
};
reader.onerror = () => {
console.error('Error reading file:', reader.error);
setLoadingError('Failed to load PDF');
};
reader.readAsDataURL(pdfData);
return () => {
setPdfDataUrl(undefined);
};
}, [pdfData]);
// Load PDF text content
useEffect(() => {
if (!pdfDataUrl) return;
let isCurrentPdf = true;
let currentLoadingTask: any = null;
setLoadingError(undefined);
const loadPdfText = async () => {
try {
// Create a typed array from the base64 data
const base64Data = pdfDataUrl.split(',')[1];
const binaryData = atob(base64Data);
const length = binaryData.length;
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = binaryData.charCodeAt(i);
}
const loadingTask = pdfjs.getDocument({
data: bytes,
disableAutoFetch: true,
disableStream: false,
});
currentLoadingTask = loadingTask;
const pdf = await loadingTask.promise;
if (!isCurrentPdf) return;
let fullText = '';
for (let i = 1; i <= pdf.numPages; i++) {
if (!isCurrentPdf) break;
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
const pageText = textContent.items
.map((item: any) => item.str)
.join(' ');
fullText += pageText + ' ';
}
if (!isCurrentPdf) return;
console.log('Loaded PDF text sample:', fullText.substring(0, 100));
setPdfText(fullText);
setText(fullText);
} catch (error) {
if (!isCurrentPdf) return;
console.error('Error loading PDF text:', error);
setLoadingError('Failed to extract PDF text');
}
};
loadPdfText();
return () => {
isCurrentPdf = false;
if (currentLoadingTask) {
currentLoadingTask.destroy();
}
setPdfText('');
};
}, [pdfDataUrl, setText]);
// Function to clear all highlights // Function to clear all highlights
const clearHighlights = useCallback(() => { const clearHighlights = useCallback(() => {
@ -137,12 +224,12 @@ export function PDFViewer({ pdfFile }: PDFViewerProps) {
const lengthPenalty = lengthDiff / patternLength; // Normalized length difference const lengthPenalty = lengthDiff / patternLength; // Normalized length difference
const adjustedRating = similarity * (1 - lengthPenalty * 0.5); // Reduce score based on length difference const adjustedRating = similarity * (1 - lengthPenalty * 0.5); // Reduce score based on length difference
console.log('Comparing:', { // console.log('Comparing:', {
text: combinedText, // text: combinedText,
similarity, // similarity,
lengthDiff, // lengthDiff,
adjustedRating // adjustedRating
}); // });
// Update best match if we have better adjusted rating // Update best match if we have better adjusted rating
if (adjustedRating > bestMatch.rating) { if (adjustedRating > bestMatch.rating) {
@ -181,37 +268,6 @@ export function PDFViewer({ pdfFile }: PDFViewerProps) {
} }
}, [clearHighlights]); }, [clearHighlights]);
useEffect(() => {
if (pdfFile) {
const loadPdfText = async () => {
try {
const pdf = await pdfjs.getDocument(pdfFile).promise;
let fullText = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
const pageText = textContent.items
.map((item: any) => item.str)
.join(' ');
fullText += pageText + ' ';
}
console.log('Loaded PDF text sample:', fullText.substring(0, 100));
setPdfText(fullText);
setText(fullText);
} catch (error) {
console.error('Error loading PDF text:', error);
}
};
loadPdfText();
}
// Clear highlights when PDF changes
return () => clearHighlights();
}, [pdfFile, setText, clearHighlights]);
// Update highlights when current sentence changes // Update highlights when current sentence changes
useEffect(() => { useEffect(() => {
console.log('Current sentence changed:', currentSentence); console.log('Current sentence changed:', currentSentence);
@ -229,11 +285,14 @@ export function PDFViewer({ pdfFile }: PDFViewerProps) {
return ( return (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
{loadingError ? (
<div className="text-red-500 mb-4">{loadingError}</div>
) : null}
<Document <Document
file={pdfFile}
onLoadSuccess={onDocumentLoadSuccess}
loading={<PDFSkeleton />} loading={<PDFSkeleton />}
noData={<PDFSkeleton />} noData={<PDFSkeleton />}
file={pdfDataUrl}
onLoadSuccess={onDocumentLoadSuccess}
className="flex flex-col items-center" className="flex flex-col items-center"
> >
{Array.from( {Array.from(

View file

@ -1,72 +1,90 @@
'use client'; 'use client';
import { createContext, useContext, useState, ReactNode, useEffect } from 'react'; import { createContext, useContext, useState, ReactNode, useEffect } from 'react';
import { indexedDBService, type PDFDocument } from '@/services/indexedDB';
interface PDFDocument { import { v4 as uuidv4 } from 'uuid';
id: string;
name: string;
size: number;
lastModified: number;
data: string; // Base64 encoded file data
}
interface PDFContextType { interface PDFContextType {
documents: PDFDocument[]; documents: PDFDocument[];
addDocument: (file: File) => Promise<string>; addDocument: (file: File) => Promise<string>;
getDocument: (id: string) => PDFDocument | undefined; getDocument: (id: string) => Promise<PDFDocument | undefined>;
removeDocument: (id: string) => void; removeDocument: (id: string) => Promise<void>;
isLoading: boolean;
error: string | null;
} }
const PDFContext = createContext<PDFContextType | undefined>(undefined); const PDFContext = createContext<PDFContextType | undefined>(undefined);
const LOCAL_STORAGE_KEY = 'pdf-documents';
export function PDFProvider({ children }: { children: ReactNode }) { export function PDFProvider({ children }: { children: ReactNode }) {
const [documents, setDocuments] = useState<PDFDocument[]>([]); const [documents, setDocuments] = useState<PDFDocument[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Load documents from local storage on mount
useEffect(() => { useEffect(() => {
const storedDocs = localStorage.getItem(LOCAL_STORAGE_KEY); const loadDocuments = async () => {
if (storedDocs) { try {
setDocuments(JSON.parse(storedDocs)); setError(null);
} await indexedDBService.init();
const docs = await indexedDBService.getAllDocuments();
setDocuments(docs);
} catch (error) {
console.error('Failed to load documents:', error);
setError('Failed to initialize document storage. Please check if your browser supports IndexedDB.');
} finally {
setIsLoading(false);
}
};
loadDocuments();
}, []); }, []);
// Save documents to local storage whenever they change
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(documents));
}, [documents]);
const addDocument = async (file: File): Promise<string> => { const addDocument = async (file: File): Promise<string> => {
return new Promise((resolve) => { setError(null);
const reader = new FileReader(); const id = uuidv4();
reader.onloadend = () => { const newDoc: PDFDocument = {
const base64Data = reader.result as string; id,
const newDoc: PDFDocument = { name: file.name,
id: `${file.name.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-')}-${Date.now()}`, size: file.size,
name: file.name, lastModified: file.lastModified,
size: file.size, data: new Blob([file], { type: file.type })
lastModified: file.lastModified, };
data: base64Data
}; try {
await indexedDBService.addDocument(newDoc);
setDocuments(prev => [...prev, newDoc]); setDocuments(prev => [...prev, newDoc]);
resolve(newDoc.id); return id;
}; } catch (error) {
reader.readAsDataURL(file); console.error('Failed to add document:', error);
}); setError('Failed to save the document. Please try again.');
throw error;
}
}; };
const getDocument = (id: string) => { const getDocument = async (id: string): Promise<PDFDocument | undefined> => {
return documents.find(doc => doc.id === id); setError(null);
try {
return await indexedDBService.getDocument(id);
} catch (error) {
console.error('Failed to get document:', error);
setError('Failed to retrieve the document. Please try again.');
return undefined;
}
}; };
const removeDocument = (id: string) => { const removeDocument = async (id: string): Promise<void> => {
setDocuments(prev => prev.filter(doc => doc.id !== id)); setError(null);
try {
await indexedDBService.removeDocument(id);
setDocuments(prev => prev.filter(doc => doc.id !== id));
} catch (error) {
console.error('Failed to remove document:', error);
setError('Failed to remove the document. Please try again.');
throw error;
}
}; };
return ( return (
<PDFContext.Provider value={{ documents, addDocument, getDocument, removeDocument }}> <PDFContext.Provider value={{ documents, addDocument, getDocument, removeDocument, isLoading, error }}>
{children} {children}
</PDFContext.Provider> </PDFContext.Provider>
); );

View file

@ -49,6 +49,8 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
const [currentAudioIndex, setCurrentAudioIndex] = useState(0); const [currentAudioIndex, setCurrentAudioIndex] = useState(0);
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const skipTriggeredRef = useRef(false); const skipTriggeredRef = useRef(false);
const skipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isPausingRef = useRef(false);
// Create OpenAI instance // Create OpenAI instance
const [openai] = useState( const [openai] = useState(
@ -151,9 +153,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
source.onended = () => { source.onended = () => {
setActiveSource(null); setActiveSource(null);
setIsProcessing(false); setIsProcessing(false);
if (isPlaying && !skipTriggeredRef.current) { // Only advance if we're playing and not pausing or skipping
if (isPlaying && !skipTriggeredRef.current && !isPausingRef.current) {
processNextSentence(); processNextSentence();
} }
isPausingRef.current = false; // Reset pause flag
}; };
source.start(0); source.start(0);
@ -173,19 +177,24 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
const togglePlay = useCallback(() => { const togglePlay = useCallback(() => {
setIsPlaying((prev) => { setIsPlaying((prev) => {
if (!prev) { if (!prev) {
return true; // Start playing isPausingRef.current = false;
return true;
} else { } else {
// Pause playback
if (activeSource) { if (activeSource) {
activeSource.stop(); // Stop the audio source isPausingRef.current = true;
setActiveSource(null); // Clear the active source activeSource.stop();
setActiveSource(null);
} }
return false; // Set isPlaying to false return false;
} }
}); });
}, [activeSource]); }, [activeSource]);
const skipForward = useCallback(() => { const skipForward = useCallback(() => {
if (skipTimeoutRef.current) {
clearTimeout(skipTimeoutRef.current);
}
skipTriggeredRef.current = true; skipTriggeredRef.current = true;
// Cancel any ongoing request // Cancel any ongoing request
if (currentRequestRef.current) { if (currentRequestRef.current) {
@ -206,12 +215,16 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
}); });
// Reset skip flag after a short delay // Reset skip flag after a short delay
setTimeout(() => { skipTimeoutRef.current = setTimeout(() => {
skipTriggeredRef.current = false; skipTriggeredRef.current = false;
}, 100); }, 100);
}, [sentences, activeSource]); }, [sentences, activeSource]);
const skipBackward = useCallback(() => { const skipBackward = useCallback(() => {
if (skipTimeoutRef.current) {
clearTimeout(skipTimeoutRef.current);
}
skipTriggeredRef.current = true; skipTriggeredRef.current = true;
// Cancel any ongoing request // Cancel any ongoing request
if (currentRequestRef.current) { if (currentRequestRef.current) {
@ -232,7 +245,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
}); });
// Reset skip flag after a short delay // Reset skip flag after a short delay
setTimeout(() => { skipTimeoutRef.current = setTimeout(() => {
skipTriggeredRef.current = false; skipTriggeredRef.current = false;
}, 100); }, 100);
}, [sentences, activeSource]); }, [sentences, activeSource]);

169
src/services/indexedDB.ts Normal file
View file

@ -0,0 +1,169 @@
const DB_NAME = 'openreader-db';
const DB_VERSION = 1;
const STORE_NAME = 'pdf-documents';
export interface PDFDocument {
id: string;
name: string;
size: number;
lastModified: number;
data: Blob;
}
class IndexedDBService {
private db: IDBDatabase | null = null;
async init(): Promise<void> {
if (!window.indexedDB) {
throw new Error('IndexedDB is not supported in this browser');
}
return new Promise((resolve, reject) => {
console.log('Initializing IndexedDB...');
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = (event) => {
const error = (event.target as IDBRequest).error;
console.error('IndexedDB initialization error:', error);
reject(error);
};
request.onsuccess = (event) => {
console.log('IndexedDB initialized successfully');
this.db = (event.target as IDBOpenDBRequest).result;
resolve();
};
request.onupgradeneeded = (event) => {
console.log('Upgrading IndexedDB schema...');
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
console.log('Creating PDF documents store...');
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
}
};
});
}
async addDocument(document: PDFDocument): Promise<void> {
if (!this.db) {
console.log('Database not initialized, initializing now...');
await this.init();
}
return new Promise((resolve, reject) => {
try {
console.log('Adding document to IndexedDB:', document.name);
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.put(document);
request.onerror = (event) => {
const error = (event.target as IDBRequest).error;
console.error('Error adding document:', error);
reject(error);
};
transaction.oncomplete = () => {
console.log('Document added successfully:', document.name);
resolve();
};
} catch (error) {
console.error('Error in addDocument transaction:', error);
reject(error);
}
});
}
async getDocument(id: string): Promise<PDFDocument | undefined> {
if (!this.db) {
console.log('Database not initialized, initializing now...');
await this.init();
}
return new Promise((resolve, reject) => {
try {
console.log('Fetching document:', id);
const transaction = this.db!.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(id);
request.onerror = (event) => {
const error = (event.target as IDBRequest).error;
console.error('Error fetching document:', error);
reject(error);
};
request.onsuccess = () => {
console.log('Document fetch result:', request.result ? 'found' : 'not found');
resolve(request.result);
};
} catch (error) {
console.error('Error in getDocument transaction:', error);
reject(error);
}
});
}
async getAllDocuments(): Promise<PDFDocument[]> {
if (!this.db) {
console.log('Database not initialized, initializing now...');
await this.init();
}
return new Promise((resolve, reject) => {
try {
console.log('Fetching all documents');
const transaction = this.db!.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onerror = (event) => {
const error = (event.target as IDBRequest).error;
console.error('Error fetching all documents:', error);
reject(error);
};
request.onsuccess = () => {
console.log('Retrieved documents count:', request.result?.length || 0);
resolve(request.result || []);
};
} catch (error) {
console.error('Error in getAllDocuments transaction:', error);
reject(error);
}
});
}
async removeDocument(id: string): Promise<void> {
if (!this.db) {
console.log('Database not initialized, initializing now...');
await this.init();
}
return new Promise((resolve, reject) => {
try {
console.log('Removing document:', id);
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.delete(id);
request.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();
};
} catch (error) {
console.error('Error in removeDocument transaction:', error);
reject(error);
}
});
}
}
export const indexedDBService = new IndexedDBService();