Feature/v2/filemanager (#4121)

FileManager Component Overview

Purpose: Modal component for selecting and managing PDF files with
preview capabilities

  Architecture:
- Responsive Layouts: MobileLayout.tsx (stacked) vs DesktopLayout.tsx
(3-column)
- Central State: FileManagerContext handles file operations, selection,
and modal state
  - File Storage: IndexedDB persistence with thumbnail caching

  Key Components:
  - FileSourceButtons: Switch between Recent/Local/Drive sources
  - FileListArea: Scrollable file grid with search functionality
- FilePreview: PDF thumbnails with dynamic shadow stacking (1-2 shadow
pages based on file count)
  - FileDetails: File info card with metadata
  - CompactFileDetails: Mobile-optimized file info layout

  File Flow:
1. Users select source → browse/search files → select multiple files →
preview with navigation → open in
  tools
  2. Files persist across tool switches via FileContext integration
  3. Memory management handles large PDFs (up to 100GB+)

 ```mermaid
 graph TD
      FM[FileManager] --> ML[MobileLayout]
      FM --> DL[DesktopLayout]

      ML --> FSB[FileSourceButtons<br/>Recent/Local/Drive]
      ML --> FLA[FileListArea]
      ML --> FD[FileDetails]

      DL --> FSB
      DL --> FLA
      DL --> FD

      FLA --> FLI[FileListItem]
      FD --> FP[FilePreview]
      FD --> CFD[CompactFileDetails]

  ```

---------

Co-authored-by: Connor Yoh <[email protected]>
This commit is contained in:
ConnorYoh
2025-08-08 15:15:09 +01:00
committed by GitHub
co-authored by Connor Yoh
parent 9861332040
commit 7e3321ee16
33 changed files with 1818 additions and 506 deletions
+20 -8
View File
@@ -100,7 +100,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
case 'REMOVE_FILES':
const remainingFiles = state.activeFiles.filter(file => {
const fileId = getFileId(file);
return !action.payload.includes(fileId);
return !fileId || !action.payload.includes(fileId);
});
const safeSelectedFileIds = Array.isArray(state.selectedFileIds) ? state.selectedFileIds : [];
return {
@@ -491,26 +491,38 @@ export function FileContextProvider({
}, [cleanupFile]);
// Action implementations
const addFiles = useCallback(async (files: File[]) => {
const addFiles = useCallback(async (files: File[]): Promise<File[]> => {
dispatch({ type: 'ADD_FILES', payload: files });
// Auto-save to IndexedDB if persistence enabled
if (enablePersistence) {
for (const file of files) {
try {
// Check if file already has an ID (already in IndexedDB)
// Check if file already has an explicit ID property (already in IndexedDB)
const fileId = getFileId(file);
if (!fileId) {
// File doesn't have ID, store it and get the ID
const storedFile = await fileStorage.storeFile(file);
// Add the ID to the file object
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
// File doesn't have explicit ID, store it with thumbnail
try {
// Generate thumbnail for better recent files experience
const thumbnail = await thumbnailGenerationService.generateThumbnail(file);
const storedFile = await fileStorage.storeFile(file, thumbnail);
// Add the ID to the file object
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
} catch (thumbnailError) {
// If thumbnail generation fails, store without thumbnail
console.warn('Failed to generate thumbnail, storing without:', thumbnailError);
const storedFile = await fileStorage.storeFile(file);
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
}
}
} catch (error) {
console.error('Failed to store file:', error);
}
}
}
// Return files with their IDs assigned
return files;
}, [enablePersistence]);
const removeFiles = useCallback((fileIds: string[], deleteFromStorage: boolean = true) => {
@@ -682,7 +694,7 @@ export function FileContextProvider({
const getFileById = useCallback((fileId: string): File | undefined => {
return state.activeFiles.find(file => {
const actualFileId = getFileId(file);
return actualFileId === fileId;
return actualFileId && actualFileId === fileId;
});
}, [state.activeFiles]);