Stirling-PDF/frontend/src/hooks/useFileManager.ts
Reece Browne f691e690e4 feat: Enhance file handling and processing capabilities
- Implement thumbnail caching in PageThumbnail component to improve performance.
- Update ConvertSettings to map selected files to their corresponding IDs in FileContext.
- Refactor FileContext to support quick deduplication using a new quickKey mechanism.
- Introduce addStoredFiles action to handle files with preserved IDs for better session management.
- Enhance FilesModalContext to support selection of stored files with metadata.
- Update useFileHandler to include logic for adding stored files.
- Modify useFileManager to support selection of stored files while maintaining backward compatibility.
- Improve file processing service with cancellation capabilities for ongoing operations.
- Centralize IndexedDB management with a new IndexedDBManager to streamline database interactions.
- Refactor file storage service to utilize the centralized IndexedDB manager for better database handling.
- Remove deprecated content hash logic and related fields from file types.
2025-08-14 21:47:02 +01:00

154 lines
5.0 KiB
TypeScript

import { useState, useCallback } from 'react';
import { fileStorage } from '../services/fileStorage';
import { FileWithUrl, FileMetadata } from '../types/file';
import { generateThumbnailForFile } from '../utils/thumbnailUtils';
export const useFileManager = () => {
const [loading, setLoading] = useState(false);
const convertToFile = useCallback(async (fileMetadata: FileMetadata): Promise<File> => {
// Always use ID - no fallback to names to prevent identity drift
if (!fileMetadata.id) {
throw new Error('File ID is required - cannot convert file without stable ID');
}
const storedFile = await fileStorage.getFile(fileMetadata.id);
if (storedFile) {
const file = new File([storedFile.data], storedFile.name, {
type: storedFile.type,
lastModified: storedFile.lastModified
});
// NO FILE MUTATION - Return clean File, let FileContext manage ID
return file;
}
throw new Error('File not found in storage');
}, []);
const loadRecentFiles = useCallback(async (): Promise<FileMetadata[]> => {
setLoading(true);
try {
// Get metadata only (no file data) for performance
const storedFileMetadata = await fileStorage.getAllFileMetadata();
const sortedFiles = storedFileMetadata.sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0));
// Convert StoredFile metadata to FileMetadata format
return sortedFiles.map(stored => ({
id: stored.id, // UUID from FileContext
name: stored.name,
type: stored.type,
size: stored.size,
lastModified: stored.lastModified,
thumbnail: stored.thumbnail,
storedInIndexedDB: true
}));
} catch (error) {
console.error('Failed to load recent files:', error);
return [];
} finally {
setLoading(false);
}
}, []);
const handleRemoveFile = useCallback(async (index: number, files: FileMetadata[], setFiles: (files: FileMetadata[]) => void) => {
const file = files[index];
if (!file.id) {
throw new Error('File ID is required for removal');
}
try {
await fileStorage.deleteFile(file.id);
setFiles(files.filter((_, i) => i !== index));
} catch (error) {
console.error('Failed to remove file:', error);
throw error;
}
}, []);
const storeFile = useCallback(async (file: File, fileId: string) => {
try {
// Generate thumbnail for the file
const thumbnail = await generateThumbnailForFile(file);
// Store file with provided UUID from FileContext
const storedFile = await fileStorage.storeFile(file, fileId, thumbnail);
// NO FILE MUTATION - Return StoredFile, FileContext manages mapping
return storedFile;
} catch (error) {
console.error('Failed to store file:', error);
throw error;
}
}, []);
const createFileSelectionHandlers = useCallback((
selectedFiles: string[],
setSelectedFiles: (files: string[]) => void
) => {
const toggleSelection = (fileId: string) => {
setSelectedFiles(
selectedFiles.includes(fileId)
? selectedFiles.filter(id => id !== fileId)
: [...selectedFiles, fileId]
);
};
const clearSelection = () => {
setSelectedFiles([]);
};
const selectMultipleFiles = async (files: FileMetadata[], onFilesSelect: (files: File[]) => void, onStoredFilesSelect?: (filesWithMetadata: Array<{ file: File; originalId: string; metadata: FileMetadata }>) => void) => {
if (selectedFiles.length === 0) return;
try {
// Filter by UUID and convert to File objects
const selectedFileObjects = files.filter(f => selectedFiles.includes(f.id));
if (onStoredFilesSelect) {
// NEW: Use stored files flow that preserves IDs
const filesWithMetadata = await Promise.all(
selectedFileObjects.map(async (metadata) => ({
file: await convertToFile(metadata),
originalId: metadata.id,
metadata
}))
);
onStoredFilesSelect(filesWithMetadata);
} else {
// LEGACY: Old flow that generates new UUIDs (for backward compatibility)
const filePromises = selectedFileObjects.map(convertToFile);
const convertedFiles = await Promise.all(filePromises);
onFilesSelect(convertedFiles); // FileContext will assign new UUIDs
}
clearSelection();
} catch (error) {
console.error('Failed to load selected files:', error);
throw error;
}
};
return {
toggleSelection,
clearSelection,
selectMultipleFiles
};
}, [convertToFile]);
const touchFile = useCallback(async (id: string) => {
try {
await fileStorage.touchFile(id);
} catch (error) {
console.error('Failed to touch file:', error);
}
}, []);
return {
loading,
convertToFile,
loadRecentFiles,
handleRemoveFile,
storeFile,
touchFile,
createFileSelectionHandlers
};
};