diff --git a/frontend/src/components/shared/FileManager.tsx b/frontend/src/components/shared/FileManager.tsx index 1366a39bc..cc930dff2 100644 --- a/frontend/src/components/shared/FileManager.tsx +++ b/frontend/src/components/shared/FileManager.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useRef, useEffect } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { Modal } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ import { Tool } from '../../types/tool'; import MobileLayout from './fileManager/MobileLayout'; import DesktopLayout from './fileManager/DesktopLayout'; import DragOverlay from './fileManager/DragOverlay'; -import { FileSource } from './fileManager/types'; +import { FileManagerProvider } from './fileManager/FileManagerContext'; interface FileManagerProps { selectedTool?: Tool | null; @@ -18,10 +18,6 @@ interface FileManagerProps { const FileManager: React.FC = ({ selectedTool }) => { const { t } = useTranslation(); const { isFilesModalOpen, closeFilesModal, onFileSelect, onFilesSelect } = useFilesModalContext(); - const fileInputRef = useRef(null); - const [activeSource, setActiveSource] = useState('recent'); - const [searchTerm, setSearchTerm] = useState(''); - const [selectedFileIds, setSelectedFileIds] = useState([]); const [recentFiles, setRecentFiles] = useState([]); const [isDragging, setIsDragging] = useState(false); const [isMobile, setIsMobile] = useState(false); @@ -40,31 +36,21 @@ const FileManager: React.FC = ({ selectedTool }) => { setRecentFiles(files); }, [loadRecentFiles]); - const openFileDialog = useCallback(() => { - fileInputRef.current?.click(); - }, []); - - const handleFileInputChange = useCallback(async (event: React.ChangeEvent) => { - const files = Array.from(event.target.files || []); - if (files.length > 0) { - try { - // Store files in IndexedDB and get FileWithUrl objects - const storedFiles = await Promise.all( - files.map(async (file) => { - await storeFile(file); - return file; - }) - ); - - onFilesSelect(storedFiles); - await refreshRecentFiles(); - } catch (error) { - console.error('Failed to process uploaded files:', error); - } + const handleFilesSelected = useCallback(async (files: FileWithUrl[]) => { + try { + const fileObjects = await Promise.all( + files.map(async (fileWithUrl) => { + if (fileWithUrl.file) { + return fileWithUrl.file; + } + return await convertToFile(fileWithUrl); + }) + ); + onFilesSelect(fileObjects); + } catch (error) { + console.error('Failed to process selected files:', error); } - // Clear the input - event.target.value = ''; - }, [storeFile, onFilesSelect, refreshRecentFiles]); + }, [convertToFile, onFilesSelect]); const handleNewFileUpload = useCallback(async (files: File[]) => { if (files.length > 0) { @@ -79,69 +65,8 @@ const FileManager: React.FC = ({ selectedTool }) => { } }, [storeFile, onFilesSelect, refreshRecentFiles]); - const handleRecentFileSelection = useCallback(async (file: FileWithUrl) => { - try { - const fileObj = await convertToFile(file); - if (onFileSelect) { - onFileSelect(fileObj); - } else { - onFilesSelect([fileObj]); - } - } catch (error) { - console.error('Failed to select recent file:', error); - } - }, [onFileSelect, onFilesSelect, convertToFile]); - - // Selection handlers - const selectionHandlers = { - toggleSelection: (fileId: string) => { - setSelectedFileIds(prev => - prev.includes(fileId) - ? prev.filter(id => id !== fileId) - : [...prev, fileId] - ); - }, - clearSelection: () => setSelectedFileIds([]) - }; - - const selectedFiles = recentFiles.filter(file => - selectedFileIds.includes(file.id || file.name) - ); - - const filteredFiles = recentFiles.filter(file => - file.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); - - const handleOpenFiles = useCallback(() => { - if (selectedFiles.length > 0) { - const filesAsFileObjects = selectedFiles.map(fileWithUrl => { - const file = new File([], fileWithUrl.name, { type: fileWithUrl.type }); - Object.defineProperty(file, 'size', { value: fileWithUrl.size || 0 }); - Object.defineProperty(file, 'lastModified', { value: fileWithUrl.lastModified || Date.now() }); - return file; - }); - onFilesSelect(filesAsFileObjects); - selectionHandlers.clearSelection(); - } - }, [selectedFiles, onFilesSelect, selectionHandlers]); - - const handleFileSelect = useCallback((file: FileWithUrl) => { - selectionHandlers.toggleSelection(file.id || file.name); - }, [selectionHandlers]); - - const handleFileDoubleClick = useCallback(async (file: FileWithUrl) => { - try { - const fileObj = await convertToFile(file); - onFilesSelect([fileObj]); - } catch (error) { - console.error('Failed to load file on double-click:', error); - } - }, [convertToFile, onFilesSelect]); - const handleRemoveFileByIndex = useCallback(async (index: number) => { await handleRemoveFile(index, recentFiles, setRecentFiles); - const file = recentFiles[index]; - setSelectedFileIds(prev => prev.filter(id => id !== (file.id || file.name))); }, [handleRemoveFile, recentFiles]); useEffect(() => { @@ -156,13 +81,22 @@ const FileManager: React.FC = ({ selectedTool }) => { refreshRecentFiles(); } else { // Reset state when modal is closed - setActiveSource('recent'); - setSearchTerm(''); - setSelectedFileIds([]); setIsDragging(false); } }, [isFilesModalOpen, refreshRecentFiles]); + // Cleanup any blob URLs when component unmounts + useEffect(() => { + return () => { + // Clean up blob URLs from recent files + recentFiles.forEach(file => { + if (file.url && file.url.startsWith('blob:')) { + URL.revokeObjectURL(file.url); + } + }); + }; + }, [recentFiles]); + // Modal size constants for consistent scaling const modalHeight = '80vh'; const modalWidth = isMobile ? '100%' : '60vw'; @@ -217,47 +151,17 @@ const FileManager: React.FC = ({ selectedTool }) => { inner: { pointerEvents: 'all' } }} > - {isMobile ? ( - - ) : ( - - )} + + {isMobile ? : } + diff --git a/frontend/src/components/shared/fileManager/DesktopLayout.tsx b/frontend/src/components/shared/fileManager/DesktopLayout.tsx index 004367f8a..946164b4b 100644 --- a/frontend/src/components/shared/fileManager/DesktopLayout.tsx +++ b/frontend/src/components/shared/fileManager/DesktopLayout.tsx @@ -1,54 +1,18 @@ import React from 'react'; -import { Grid, Center, Stack } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { FileWithUrl } from '../../../types/file'; +import { Grid } from '@mantine/core'; import FileSourceButtons from './FileSourceButtons'; import FileDetails from './FileDetails'; import SearchInput from './SearchInput'; import FileListArea from './FileListArea'; import HiddenFileInput from './HiddenFileInput'; -import { FileSource } from './types'; +import { useFileManagerContext } from './FileManagerContext'; -interface DesktopLayoutProps { - activeSource: FileSource; - onSourceChange: (source: FileSource) => void; - onLocalFileClick: () => void; - selectedFiles: FileWithUrl[]; - onOpenFiles: () => void; - searchTerm: string; - onSearchChange: (value: string) => void; - recentFiles: FileWithUrl[]; - filteredFiles: FileWithUrl[]; - selectedFileIds: string[]; - onFileSelect: (file: FileWithUrl) => void; - onFileRemove: (index: number) => void; - onFileDoubleClick: (file: FileWithUrl) => void; - isFileSupported: (fileName: string) => boolean; - fileInputRef: React.RefObject; - onFileInputChange: (event: React.ChangeEvent) => void; - modalHeight: string; -} - -const DesktopLayout: React.FC = ({ - activeSource, - onSourceChange, - onLocalFileClick, - selectedFiles, - onOpenFiles, - searchTerm, - onSearchChange, - recentFiles, - filteredFiles, - selectedFileIds, - onFileSelect, - onFileRemove, - onFileDoubleClick, - isFileSupported, - fileInputRef, - onFileInputChange, - modalHeight, -}) => { - const { t } = useTranslation(); +const DesktopLayout: React.FC = () => { + const { + activeSource, + recentFiles, + modalHeight, + } = useFileManagerContext(); return ( @@ -59,33 +23,17 @@ const DesktopLayout: React.FC = ({ flexShrink: 0, height: '100%', }}> - + {/* Column 2: File List */} {activeSource === 'recent' && ( - + )}
0 ? `calc(${modalHeight} - 6rem)` : '100%' @@ -97,19 +45,12 @@ const DesktopLayout: React.FC = ({ {/* Column 3: File Details */}
- +
{/* Hidden file input for local file selection */} - + ); }; diff --git a/frontend/src/components/shared/fileManager/FileDetails.tsx b/frontend/src/components/shared/fileManager/FileDetails.tsx index 8e5b3e7b9..03d8c38f0 100644 --- a/frontend/src/components/shared/fileManager/FileDetails.tsx +++ b/frontend/src/components/shared/fileManager/FileDetails.tsx @@ -6,77 +6,31 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import { useTranslation } from 'react-i18next'; import { detectFileExtension, getFileSize } from '../../../utils/fileUtils'; import { useIndexedDBThumbnail } from '../../../hooks/useIndexedDBThumbnail'; -import { FileWithUrl } from '../../../types/file'; -import { FileDetailsProps } from './types'; +import { useFileManagerContext } from './FileManagerContext'; -const FileDetails: React.FC = ({ selectedFiles, onOpenFiles, compact = false, modalHeight = '80vh' }) => { +interface FileDetailsProps { + compact?: boolean; +} + +const FileDetails: React.FC = ({ + compact = false +}) => { + const { selectedFiles, onOpenFiles, modalHeight } = useFileManagerContext(); const { t } = useTranslation(); const [currentFileIndex, setCurrentFileIndex] = useState(0); - const [thumbnailCache, setThumbnailCache] = useState>({}); - const [loadingFile, setLoadingFile] = useState(null); const [isAnimating, setIsAnimating] = useState(false); // Get the currently displayed file const currentFile = selectedFiles.length > 0 ? selectedFiles[currentFileIndex] : null; const hasSelection = selectedFiles.length > 0; const hasMultipleFiles = selectedFiles.length > 1; + + // Use IndexedDB hook for the current file + const { thumbnail: currentThumbnail } = useIndexedDBThumbnail(currentFile); - // Only load thumbnail for files not in cache - const shouldLoadThumbnail = loadingFile && !thumbnailCache[loadingFile.id || loadingFile.name]; - const { thumbnail } = useIndexedDBThumbnail(shouldLoadThumbnail ? loadingFile : ({} as FileWithUrl)); - - // Load thumbnails for all selected files - useEffect(() => { - // Start loading thumbnails for uncached files - const uncachedFiles = selectedFiles.filter(file => !thumbnailCache[file.id || file.name]); - if (uncachedFiles.length > 0 && !loadingFile) { - setLoadingFile(uncachedFiles[0]); - } - }, [selectedFiles, thumbnailCache, loadingFile]); - - // Cache thumbnail when it loads and move to next uncached file - useEffect(() => { - if (loadingFile && thumbnail) { - const fileId = loadingFile.id || loadingFile.name; - setThumbnailCache(prev => ({ - ...prev, - [fileId]: thumbnail - })); - - // Find next uncached file to load - const uncachedFiles = selectedFiles.filter(file => - !thumbnailCache[file.id || file.name] && - (file.id || file.name) !== fileId - ); - - if (uncachedFiles.length > 0) { - setLoadingFile(uncachedFiles[0]); - } else { - setLoadingFile(null); - } - } - }, [loadingFile, thumbnail, selectedFiles, thumbnailCache]); - - // Clear cache when selection changes completely - useEffect(() => { - const selectedFileIds = selectedFiles.map(f => f.id || f.name); - setThumbnailCache(prev => { - const newCache: Record = {}; - selectedFileIds.forEach(id => { - if (prev[id]) { - newCache[id] = prev[id]; - } - }); - return newCache; - }); - setLoadingFile(null); - }, [selectedFiles]); - - // Get thumbnail from cache only + // Get thumbnail for current file const getCurrentThumbnail = () => { - if (!currentFile) return null; - const fileId = currentFile.id || currentFile.name; - return thumbnailCache[fileId]; + return currentThumbnail; }; const handlePrevious = () => { @@ -372,7 +326,6 @@ const FileDetails: React.FC - {/* Section 3: Action Button */}