(null);
const onDrop = useCallback(async (acceptedFiles: File[]) => {
const file = acceptedFiles[0];
- if (file && file.type === 'application/pdf') {
- setIsUploading(true);
- setError(null);
- try {
- await addDocument(file);
- } catch (err) {
- setError('Failed to upload PDF. Please try again.');
- console.error('Upload error:', err);
- } finally {
- setIsUploading(false);
+ if (!file) return;
+
+ setIsUploading(true);
+ setError(null);
+
+ try {
+ if (file.type === 'application/pdf') {
+ await addPDF(file);
+ } else if (file.type === 'application/epub+zip') {
+ await addEPUB(file);
}
+ } catch (err) {
+ setError('Failed to upload file. Please try again.');
+ console.error('Upload error:', err);
+ } finally {
+ setIsUploading(false);
}
- }, [addDocument]);
+ }, [addPDF, addEPUB]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
- 'application/pdf': ['.pdf']
+ 'application/pdf': ['.pdf'],
+ 'application/epub+zip': ['.epub']
},
multiple: false,
disabled: isUploading
@@ -56,15 +64,15 @@ export function PDFUploader({ className = '' }: PDFUploaderProps) {
{isUploading ? (
- Uploading PDF...
+ Uploading file...
) : (
<>
- {isDragActive ? 'Drop your PDF here' : 'Drop your PDF here, or click to select'}
+ {isDragActive ? 'Drop your file here' : 'Drop your file here, or click to select'}
- Only PDF files are currently accepted
+ PDF and EPUB files are accepted
{error && {error}
}
>
diff --git a/src/components/PDFViewer.tsx b/src/components/PDFViewer.tsx
index eee29d9..f615905 100644
--- a/src/components/PDFViewer.tsx
+++ b/src/components/PDFViewer.tsx
@@ -4,7 +4,7 @@ import { RefObject, useCallback, useState, useEffect, useRef } from 'react';
import { Document, Page } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
-import { PDFSkeleton } from './PDFSkeleton';
+import { DocumentSkeleton } from '@/components/DocumentSkeleton';
import { useTTS } from '@/contexts/TTSContext';
import { usePDF } from '@/contexts/PDFContext';
import TTSPlayer from '@/components/player/TTSPlayer';
@@ -165,8 +165,8 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
return (
}
- noData={
}
+ loading={
}
+ noData={
}
file={currDocURL}
onLoadSuccess={(pdf) => {
onDocumentLoadSuccess(pdf);
diff --git a/src/hooks/epub/useEpubDocuments.ts b/src/hooks/epub/useEpubDocuments.ts
new file mode 100644
index 0000000..94ef087
--- /dev/null
+++ b/src/hooks/epub/useEpubDocuments.ts
@@ -0,0 +1,66 @@
+'use client';
+
+import { useState, useCallback, useEffect } from 'react';
+import { v4 as uuidv4 } from 'uuid';
+import { indexedDBService, type EPUBDocument } from '@/utils/indexedDB';
+import { useConfig } from '@/contexts/ConfigContext';
+
+export function useEpubDocuments() {
+ const { isDBReady } = useConfig();
+ const [documents, setDocuments] = useState
([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const loadDocuments = useCallback(async () => {
+ if (isDBReady) {
+ try {
+ const docs = await indexedDBService.getAllEpubDocuments();
+ setDocuments(docs);
+ } catch (error) {
+ console.error('Failed to load EPUB documents:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ }, [isDBReady]);
+
+ useEffect(() => {
+ loadDocuments();
+ }, [loadDocuments]);
+
+ const addDocument = useCallback(async (file: File): Promise => {
+ const id = uuidv4();
+ const newDoc: EPUBDocument = {
+ id,
+ name: file.name,
+ size: file.size,
+ lastModified: file.lastModified,
+ data: new Blob([file], { type: file.type }),
+ };
+
+ try {
+ await indexedDBService.addEpubDocument(newDoc);
+ setDocuments((prev) => [...prev, newDoc]);
+ return id;
+ } catch (error) {
+ console.error('Failed to add EPUB document:', error);
+ throw error;
+ }
+ }, []);
+
+ const removeDocument = useCallback(async (id: string): Promise => {
+ try {
+ await indexedDBService.removeEpubDocument(id);
+ setDocuments((prev) => prev.filter((doc) => doc.id !== id));
+ } catch (error) {
+ console.error('Failed to remove EPUB document:', error);
+ throw error;
+ }
+ }, []);
+
+ return {
+ documents,
+ isLoading,
+ addDocument,
+ removeDocument,
+ };
+}
diff --git a/src/utils/indexedDB.ts b/src/utils/indexedDB.ts
index cdee46a..6dd10b5 100644
--- a/src/utils/indexedDB.ts
+++ b/src/utils/indexedDB.ts
@@ -1,6 +1,7 @@
const DB_NAME = 'openreader-db';
-const DB_VERSION = 1;
+const DB_VERSION = 2; // Increased version number
const PDF_STORE_NAME = 'pdf-documents';
+const EPUB_STORE_NAME = 'epub-documents';
const CONFIG_STORE_NAME = 'config';
export interface PDFDocument {
@@ -11,6 +12,14 @@ export interface PDFDocument {
data: Blob;
}
+export interface EPUBDocument {
+ id: string;
+ name: string;
+ size: number;
+ lastModified: number;
+ data: Blob;
+}
+
export interface Config {
key: string;
value: string;
@@ -54,6 +63,11 @@ class IndexedDBService {
db.createObjectStore(PDF_STORE_NAME, { keyPath: 'id' });
}
+ if (!db.objectStoreNames.contains(EPUB_STORE_NAME)) {
+ console.log('Creating EPUB documents store...');
+ db.createObjectStore(EPUB_STORE_NAME, { keyPath: 'id' });
+ }
+
if (!db.objectStoreNames.contains(CONFIG_STORE_NAME)) {
console.log('Creating config store...');
db.createObjectStore(CONFIG_STORE_NAME, { keyPath: 'key' });
@@ -181,6 +195,103 @@ class IndexedDBService {
});
}
+ // Add EPUB Document Methods
+ async addEpubDocument(document: EPUBDocument): Promise {
+ if (!this.db) {
+ await this.init();
+ }
+
+ 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.put(document);
+
+ request.onerror = (event) => {
+ reject((event.target as IDBRequest).error);
+ };
+
+ transaction.oncomplete = () => {
+ resolve();
+ };
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+
+ async getEpubDocument(id: string): Promise {
+ if (!this.db) {
+ await this.init();
+ }
+
+ return new Promise((resolve, reject) => {
+ try {
+ const transaction = this.db!.transaction([EPUB_STORE_NAME], 'readonly');
+ const store = transaction.objectStore(EPUB_STORE_NAME);
+ const request = store.get(id);
+
+ request.onerror = (event) => {
+ reject((event.target as IDBRequest).error);
+ };
+
+ request.onsuccess = () => {
+ resolve(request.result);
+ };
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+
+ async getAllEpubDocuments(): Promise {
+ if (!this.db) {
+ await this.init();
+ }
+
+ return new Promise((resolve, reject) => {
+ try {
+ const transaction = this.db!.transaction([EPUB_STORE_NAME], 'readonly');
+ const store = transaction.objectStore(EPUB_STORE_NAME);
+ const request = store.getAll();
+
+ request.onerror = (event) => {
+ reject((event.target as IDBRequest).error);
+ };
+
+ request.onsuccess = () => {
+ resolve(request.result || []);
+ };
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+
+ async removeEpubDocument(id: string): Promise {
+ if (!this.db) {
+ await this.init();
+ }
+
+ 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);
+
+ request.onerror = (event) => {
+ reject((event.target as IDBRequest).error);
+ };
+
+ transaction.oncomplete = () => {
+ resolve();
+ };
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+
// Config Methods
async setConfigItem(key: string, value: string): Promise {
if (!this.db) {