feat: Enhance file management with draft support and improved loading mechanisms

This commit is contained in:
Reece Browne 2025-08-20 00:27:03 +01:00
parent b8cf5fda7e
commit 6aa8e42941
8 changed files with 161 additions and 55 deletions

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Group, Box, Text, ActionIcon, Checkbox, Divider } from '@mantine/core';
import { Group, Box, Text, ActionIcon, Checkbox, Divider, Badge } from '@mantine/core';
import DeleteIcon from '@mui/icons-material/Delete';
import PushPinIcon from '@mui/icons-material/PushPin';
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
@ -64,7 +64,14 @@ const FileListItem: React.FC<FileListItemProps> = ({
</Box>
<Box style={{ flex: 1, minWidth: 0 }}>
<Text size="sm" fw={500} truncate>{file.name}</Text>
<Group gap="xs" align="center">
<Text size="sm" fw={500} truncate style={{ flex: 1 }}>{file.name}</Text>
{file.isDraft && (
<Badge size="xs" variant="light" color="orange">
DRAFT
</Badge>
)}
</Group>
<Text size="xs" c="dimmed">{getFileSize(file)} {getFileDate(file)}</Text>
</Box>

View File

@ -556,10 +556,34 @@ const PageEditor = ({
// Enhanced draft save using centralized IndexedDB manager
const saveDraftToIndexedDB = useCallback(async (doc: PDFDocument) => {
const draftKey = `draft-${doc.id || 'merged'}`;
// Convert PDF document to bytes for storage
const pdfBytes = await doc.save();
const originalFileNames = activeFileIds.map(id => selectors.getFileRecord(id)?.name).filter(Boolean);
// Create a temporary file for thumbnail generation
const tempFile = new File([pdfBytes], `Draft - ${originalFileNames.join(', ') || 'Untitled'}.pdf`, {
type: 'application/pdf',
lastModified: Date.now()
});
// Generate thumbnail for the draft
let thumbnail: string | undefined;
try {
const { generateThumbnailForFile } = await import('../../utils/thumbnailUtils');
thumbnail = await generateThumbnailForFile(tempFile);
} catch (error) {
console.warn('Failed to generate thumbnail for draft:', error);
}
const draftData = {
document: doc,
id: draftKey,
name: `Draft - ${originalFileNames.join(', ') || 'Untitled'}`,
pdfData: pdfBytes,
size: pdfBytes.length,
timestamp: Date.now(),
originalFiles: activeFileIds.map(id => selectors.getFileRecord(id)?.name).filter(Boolean)
thumbnail,
originalFiles: originalFileNames
};
try {

View File

@ -33,7 +33,7 @@ const LandingPage = () => {
{/* White PDF Page Background */}
<Dropzone
onDrop={handleFileDrop}
accept={["*/*"] as any}
accept={["application/pdf", "application/zip", "application/x-zip-compressed"]}
multiple={true}
className="w-4/5 flex items-center justify-center h-[95vh]"
style={{
@ -125,7 +125,7 @@ const LandingPage = () => {
ref={fileInputRef}
type="file"
multiple
accept="*/*"
accept=".pdf,.zip"
onChange={handleFileSelect}
style={{ display: 'none' }}
/>

View File

@ -208,48 +208,12 @@ function FileContextInner({
dispatch
}), [actions]);
// Load files from persistence on mount
useEffect(() => {
if (!enablePersistence || !indexedDB) return;
const loadFromPersistence = async () => {
try {
// Load metadata to populate file list (actual File objects loaded on-demand)
const metadata = await indexedDB.loadAllMetadata();
if (metadata.length === 0) {
if (DEBUG) console.log('📄 No files found in persistence');
return;
}
if (DEBUG) {
console.log(`📄 Loading ${metadata.length} files from persistence`);
}
// Create FileRecords from metadata - File objects loaded when needed
const fileRecords = metadata.map(meta => ({
id: meta.id,
name: meta.name,
size: meta.size,
type: meta.type,
lastModified: meta.lastModified,
thumbnailUrl: meta.thumbnail,
isPinned: false,
createdAt: Date.now()
}));
// Add to state so file manager can show them
dispatch({
type: 'ADD_FILES',
payload: { fileRecords }
});
} catch (error) {
console.error('Failed to load files from persistence:', error);
}
};
loadFromPersistence();
}, [enablePersistence, indexedDB]); // Only run when these change
// Persistence loading disabled - files only loaded on explicit user action
// useEffect(() => {
// if (!enablePersistence || !indexedDB) return;
// const loadFromPersistence = async () => { /* loading logic removed */ };
// loadFromPersistence();
// }, [enablePersistence, indexedDB]);
// Cleanup on unmount
useEffect(() => {

View File

@ -25,6 +25,9 @@ interface IndexedDBContextValue {
// Utilities
getStorageStats: () => Promise<{ used: number; available: number; fileCount: number }>;
// Draft operations
loadAllDraftMetadata: () => Promise<FileMetadata[]>;
}
const IndexedDBContext = createContext<IndexedDBContextValue | null>(null);
@ -56,6 +59,22 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
}, []);
const saveFile = useCallback(async (file: File, fileId: FileId, existingThumbnail?: string): Promise<FileMetadata> => {
// DEBUG: Check original file before saving
if (DEBUG && file.type === 'application/pdf') {
try {
const { getDocument } = await import('pdfjs-dist');
const arrayBuffer = await file.arrayBuffer();
const pdf = await getDocument({ data: arrayBuffer }).promise;
console.log(`🔍 Saving file to IndexedDB:`, {
name: file.name,
size: file.size,
pages: pdf.numPages
});
} catch (error) {
console.error(`🔍 Error validating file before save:`, error);
}
}
// Use existing thumbnail or generate new one
const thumbnail = existingThumbnail || await generateThumbnailForFile(file);
@ -99,6 +118,27 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
lastModified: storedFile.lastModified
});
// DEBUG: Check if file reconstruction is working
if (DEBUG && file.type === 'application/pdf') {
console.log(`🔍 File loaded from IndexedDB:`, {
name: file.name,
originalSize: storedFile.size,
reconstructedSize: file.size,
dataLength: storedFile.data.byteLength,
sizesMatch: storedFile.size === file.size
});
// Quick PDF validation
try {
const { getDocument } = await import('pdfjs-dist');
const arrayBuffer = await file.arrayBuffer();
const pdf = await getDocument({ data: arrayBuffer }).promise;
console.log(`🔍 PDF validation: ${pdf.numPages} pages in reconstructed file`);
} catch (error) {
console.error(`🔍 PDF reconstruction error:`, error);
}
}
// Cache for future use with LRU eviction
fileCache.current.set(fileId, { file, lastAccessed: Date.now() });
evictLRUEntries();
@ -177,6 +217,38 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
return await fileStorage.getStorageStats();
}, []);
const loadAllDraftMetadata = useCallback(async (): Promise<FileMetadata[]> => {
try {
const { indexedDBManager, DATABASE_CONFIGS } = await import('../services/indexedDBManager');
const db = await indexedDBManager.openDatabase(DATABASE_CONFIGS.DRAFTS);
return new Promise((resolve, reject) => {
const transaction = db.transaction(['drafts'], 'readonly');
const store = transaction.objectStore('drafts');
const request = store.getAll();
request.onsuccess = () => {
const drafts = request.result || [];
const draftMetadata: FileMetadata[] = drafts.map((draft: any) => ({
id: draft.id,
name: draft.name || `Draft ${draft.id}`,
type: 'application/pdf',
size: draft.size || 0,
lastModified: draft.timestamp || Date.now(),
thumbnail: draft.thumbnail,
isDraft: true
}));
resolve(draftMetadata);
};
request.onerror = () => reject(request.error);
});
} catch (error) {
console.warn('Failed to load draft metadata:', error);
return [];
}
}, []);
// No periodic cleanup needed - LRU eviction happens on-demand when cache fills
const value: IndexedDBContextValue = {
@ -187,7 +259,8 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
loadAllMetadata,
deleteMultiple,
clearAll,
getStorageStats
getStorageStats,
loadAllDraftMetadata
};
return (

View File

@ -12,7 +12,39 @@ export const useFileManager = () => {
throw new Error('IndexedDB context not available');
}
// Try ID first (preferred)
// Handle drafts differently from regular files
if (fileMetadata.isDraft) {
// Load draft from the drafts database
try {
const { indexedDBManager, DATABASE_CONFIGS } = await import('../services/indexedDBManager');
const db = await indexedDBManager.openDatabase(DATABASE_CONFIGS.DRAFTS);
return new Promise((resolve, reject) => {
const transaction = db.transaction(['drafts'], 'readonly');
const store = transaction.objectStore('drafts');
const request = store.get(fileMetadata.id);
request.onsuccess = () => {
const draft = request.result;
if (draft && draft.pdfData) {
const file = new File([draft.pdfData], fileMetadata.name, {
type: 'application/pdf',
lastModified: fileMetadata.lastModified
});
resolve(file);
} else {
reject(new Error('Draft data not found'));
}
};
request.onerror = () => reject(request.error);
});
} catch (error) {
throw new Error(`Failed to load draft: ${fileMetadata.name} (${error})`);
}
}
// Regular file loading
if (fileMetadata.id) {
const file = await indexedDB.loadFile(fileMetadata.id);
if (file) {
@ -29,11 +61,16 @@ export const useFileManager = () => {
return [];
}
// Get metadata only (no file data) for performance
const storedFileMetadata = await indexedDB.loadAllMetadata();
const sortedFiles = storedFileMetadata.sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0));
// Load both regular files and drafts
const [storedFileMetadata, draftMetadata] = await Promise.all([
indexedDB.loadAllMetadata(),
indexedDB.loadAllDraftMetadata()
]);
// Combine and sort by last modified
const allFiles = [...storedFileMetadata, ...draftMetadata];
const sortedFiles = allFiles.sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0));
// Already in correct FileMetadata format
return sortedFiles;
} catch (error) {
console.error('Failed to load recent files:', error);

View File

@ -156,7 +156,7 @@ class FileStorageService {
}
cursor.continue();
} else {
console.log('Loaded metadata for', files.length, 'files without loading data');
// Metadata loaded efficiently without file data
resolve(files);
}
};

View File

@ -25,6 +25,7 @@ export interface FileMetadata {
size: number;
lastModified: number;
thumbnail?: string;
isDraft?: boolean; // Marks files as draft versions
/** @deprecated Legacy compatibility - will be removed */
storedInIndexedDB?: boolean;
}