diff --git a/.gitignore b/.gitignore index 80e1a99..6c0e4a9 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# documents +/docstore diff --git a/src/app/api/documents/route.ts b/src/app/api/documents/route.ts new file mode 100644 index 0000000..46c7c76 --- /dev/null +++ b/src/app/api/documents/route.ts @@ -0,0 +1,83 @@ +import { writeFile, readFile, readdir, mkdir } from 'fs/promises'; +import { NextRequest, NextResponse } from 'next/server'; +import path from 'path'; + +const DOCS_DIR = path.join(process.cwd(), 'docstore'); + +// Ensure documents directory exists +async function ensureDocsDir() { + try { + await mkdir(DOCS_DIR, { recursive: true }); + } catch (error) { + console.error('Error creating documents directory:', error); + } +} + +export async function POST(req: NextRequest) { + try { + await ensureDocsDir(); + const data = await req.json(); + + // Save document metadata and content + for (const doc of data.documents) { + const docPath = path.join(DOCS_DIR, `${doc.id}.json`); + const contentPath = path.join(DOCS_DIR, `${doc.id}.${doc.type}`); + + // Save metadata (excluding binary data) + const metadata = { + id: doc.id, + name: doc.name, + size: doc.size, + lastModified: doc.lastModified, + type: doc.type + }; + + await writeFile(docPath, JSON.stringify(metadata)); + + // Save content as raw binary file with proper handling for both PDF and EPUB + const content = Buffer.from(new Uint8Array(doc.data)); + await writeFile(contentPath, content); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error saving documents:', error); + return NextResponse.json({ error: 'Failed to save documents' }, { status: 500 }); + } +} + +export async function GET() { + try { + await ensureDocsDir(); + const documents = []; + + const files = await readdir(DOCS_DIR); + const jsonFiles = files.filter(file => file.endsWith('.json')); + + for (const file of jsonFiles) { + const docPath = path.join(DOCS_DIR, file); + + try { + const metadata = JSON.parse(await readFile(docPath, 'utf8')); + const contentPath = path.join(DOCS_DIR, `${metadata.id}.${metadata.type}`); + const content = await readFile(contentPath); + + // Ensure consistent array format for both PDF and EPUB + const uint8Array = new Uint8Array(content); + + documents.push({ + ...metadata, + data: Array.from(uint8Array) + }); + } catch (error) { + console.error(`Error processing file ${file}:`, error); + continue; + } + } + + return NextResponse.json({ documents }); + } catch (error) { + console.error('Error loading documents:', error); + return NextResponse.json({ error: 'Failed to load documents' }, { status: 500 }); + } +} diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 8e1cd61..bdc4517 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -5,6 +5,10 @@ import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Listbox, import { useTheme } from '@/contexts/ThemeContext'; import { useConfig } from '@/contexts/ConfigContext'; import { ChevronUpDownIcon, CheckIcon } from './icons/Icons'; +import { indexedDBService } from '@/utils/indexedDB'; +import { useDocuments } from '@/contexts/DocumentContext'; + +const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null; interface SettingsModalProps { isOpen: boolean; @@ -20,8 +24,11 @@ const themes = [ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) { const { theme, setTheme } = useTheme(); const { apiKey, baseUrl, updateConfig } = useConfig(); + const { refreshPDFs, refreshEPUBs } = useDocuments(); const [localApiKey, setLocalApiKey] = useState(apiKey); const [localBaseUrl, setLocalBaseUrl] = useState(baseUrl); + const [isSyncing, setIsSyncing] = useState(false); + const [isLoading, setIsLoading] = useState(false); const selectedTheme = themes.find(t => t.id === theme) || themes[0]; useEffect(() => { @@ -29,6 +36,29 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) { setLocalBaseUrl(baseUrl); }, [apiKey, baseUrl]); + const handleSync = async () => { + try { + setIsSyncing(true); + await indexedDBService.syncToServer(); + } catch (error) { + console.error('Sync failed:', error); + } finally { + setIsSyncing(false); + } + }; + + const handleLoad = async () => { + try { + setIsLoading(true); + await indexedDBService.loadFromServer(); + await Promise.all([refreshPDFs(), refreshEPUBs()]); + } catch (error) { + console.error('Load failed:', error); + } finally { + setIsLoading(false); + } + }; + return ( setIsOpen(false)}> @@ -130,6 +160,38 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) { className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent" /> + + {isDev &&
+ +
+
+
+ + +
+
+
+
} diff --git a/src/contexts/DocumentContext.tsx b/src/contexts/DocumentContext.tsx index 0096ebb..2c12951 100644 --- a/src/contexts/DocumentContext.tsx +++ b/src/contexts/DocumentContext.tsx @@ -17,6 +17,9 @@ interface DocumentContextType { addEPUBDocument: (file: File) => Promise; removeEPUBDocument: (id: string) => Promise; isEPUBLoading: boolean; + + refreshPDFs: () => Promise; + refreshEPUBs: () => Promise; } const DocumentContext = createContext(undefined); @@ -26,14 +29,16 @@ export function DocumentProvider({ children }: { children: ReactNode }) { documents: pdfDocs, addDocument: addPDFDocument, removeDocument: removePDFDocument, - isLoading: isPDFLoading + isLoading: isPDFLoading, + refresh: refreshPDFs } = usePDFDocuments(); const { documents: epubDocs, addDocument: addEPUBDocument, removeDocument: removeEPUBDocument, - isLoading: isEPUBLoading + isLoading: isEPUBLoading, + refresh: refreshEPUBs } = useEPUBDocuments(); return ( @@ -45,7 +50,9 @@ export function DocumentProvider({ children }: { children: ReactNode }) { epubDocs, addEPUBDocument, removeEPUBDocument, - isEPUBLoading + isEPUBLoading, + refreshPDFs, + refreshEPUBs }}> {children} diff --git a/src/hooks/useEPUBDocuments.ts b/src/hooks/useEPUBDocuments.ts index c2aa1b0..85c318d 100644 --- a/src/hooks/useEPUBDocuments.ts +++ b/src/hooks/useEPUBDocuments.ts @@ -62,10 +62,25 @@ export function useEPUBDocuments() { } }, []); + const refresh = useCallback(async () => { + if (isDBReady) { + setIsLoading(true); + try { + const docs = await indexedDBService.getAllEPUBDocuments(); + setDocuments(docs); + } catch (error) { + console.error('Failed to refresh documents:', error); + } finally { + setIsLoading(false); + } + } + }, [isDBReady]); + return { documents, isLoading, addDocument, removeDocument, + refresh, }; } diff --git a/src/hooks/usePDFDocuments.ts b/src/hooks/usePDFDocuments.ts index 2491aff..58ecc0d 100644 --- a/src/hooks/usePDFDocuments.ts +++ b/src/hooks/usePDFDocuments.ts @@ -71,10 +71,25 @@ export function usePDFDocuments() { } }, []); + const refresh = useCallback(async () => { + if (isDBReady) { + setIsLoading(true); + try { + const docs = await indexedDBService.getAllDocuments(); + setDocuments(docs); + } catch (error) { + console.error('Failed to refresh documents:', error); + } finally { + setIsLoading(false); + } + } + }, [isDBReady]); + return { documents, isLoading, addDocument, removeDocument, + refresh, // Add refresh to return value }; } diff --git a/src/utils/indexedDB.ts b/src/utils/indexedDB.ts index 4f500f1..77199eb 100644 --- a/src/utils/indexedDB.ts +++ b/src/utils/indexedDB.ts @@ -487,6 +487,81 @@ class IndexedDBService { } }); } + + async syncToServer(): Promise<{ lastSync: number }> { + const pdfDocs = await this.getAllDocuments(); + const epubDocs = await this.getAllEPUBDocuments(); + + const documents = []; + + // Process PDF documents - store the raw PDF data + for (const doc of pdfDocs) { + const arrayBuffer = await doc.data.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); + documents.push({ + ...doc, + type: 'pdf', + data: Array.from(uint8Array) // Convert to regular array for JSON serialization + }); + } + + // Process EPUB documents + for (const doc of epubDocs) { + documents.push({ + ...doc, + type: 'epub', + data: Array.from(new Uint8Array(doc.data)) // Convert to regular array for JSON serialization + }); + } + + const response = await fetch('/api/documents', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ documents }) + }); + + if (!response.ok) { + throw new Error('Failed to sync documents to server'); + } + + return { lastSync: Date.now() }; + } + + async loadFromServer(): Promise<{ lastSync: number }> { + const response = await fetch('/api/documents'); + if (!response.ok) { + throw new Error('Failed to fetch documents from server'); + } + + const { documents } = await response.json(); + + // Process each document + for (const doc of documents) { + if (doc.type === 'pdf') { + // Create a Blob from the raw binary data + const blob = new Blob([new Uint8Array(doc.data)], { type: 'application/pdf' }); + await this.addDocument({ + id: doc.id, + name: doc.name, + size: doc.size, + lastModified: doc.lastModified, + data: blob + }); + } else if (doc.type === 'epub') { + // Convert the numeric array back to ArrayBuffer for EPUB + const uint8Array = new Uint8Array(doc.data); + await this.addEPUBDocument({ + id: doc.id, + name: doc.name, + size: doc.size, + lastModified: doc.lastModified, + data: uint8Array.buffer + }); + } + } + + return { lastSync: Date.now() }; + } } // Make sure we export a singleton instance