mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
feat: Enhance file management with draft support and improved loading mechanisms
This commit is contained in:
parent
b8cf5fda7e
commit
6aa8e42941
@ -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>
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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' }}
|
||||
/>
|
||||
|
@ -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(() => {
|
||||
|
@ -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 (
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user