diff --git a/frontend/src/components/FileManager.tsx b/frontend/src/components/FileManager.tsx index 65f933ca9..02f9af5e4 100644 --- a/frontend/src/components/FileManager.tsx +++ b/frontend/src/components/FileManager.tsx @@ -1,7 +1,6 @@ import React, { useState, useCallback, useEffect } from 'react'; import { Modal } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; -import { useTranslation } from 'react-i18next'; import { FileWithUrl } from '../types/file'; import { useFileManager } from '../hooks/useFileManager'; import { useFilesModalContext } from '../contexts/FilesModalContext'; @@ -16,13 +15,12 @@ interface FileManagerProps { } const FileManager: React.FC = ({ selectedTool }) => { - const { t } = useTranslation(); - const { isFilesModalOpen, closeFilesModal, onFileSelect, onFilesSelect } = useFilesModalContext(); + const { isFilesModalOpen, closeFilesModal, onFilesSelect } = useFilesModalContext(); const [recentFiles, setRecentFiles] = useState([]); const [isDragging, setIsDragging] = useState(false); const [isMobile, setIsMobile] = useState(false); - const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile, touchFile } = useFileManager(); + const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile } = useFileManager(); // File management handlers const isFileSupported = useCallback((fileName: string) => { @@ -40,9 +38,6 @@ const FileManager: React.FC = ({ selectedTool }) => { try { const fileObjects = await Promise.all( files.map(async (fileWithUrl) => { - if (fileWithUrl.file) { - return fileWithUrl.file; - } return await convertToFile(fileWithUrl); }) ); @@ -55,15 +50,14 @@ const FileManager: React.FC = ({ selectedTool }) => { const handleNewFileUpload = useCallback(async (files: File[]) => { if (files.length > 0) { try { - // Store files and refresh recent files - await Promise.all(files.map(file => storeFile(file))); + // Files will get IDs assigned through onFilesSelect -> FileContext addFiles onFilesSelect(files); await refreshRecentFiles(); } catch (error) { console.error('Failed to process dropped files:', error); } } - }, [storeFile, onFilesSelect, refreshRecentFiles]); + }, [onFilesSelect, refreshRecentFiles]); const handleRemoveFileByIndex = useCallback(async (index: number) => { await handleRemoveFile(index, recentFiles, setRecentFiles); diff --git a/frontend/src/components/shared/FileUploadModal.tsx b/frontend/src/components/shared/FileUploadModal.tsx deleted file mode 100644 index a83e96e62..000000000 --- a/frontend/src/components/shared/FileUploadModal.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { Modal } from '@mantine/core'; -import FileUploadSelector from './FileUploadSelector'; -import { useFilesModalContext } from '../../contexts/FilesModalContext'; -import { Tool } from '../../types/tool'; - -interface FileUploadModalProps { - selectedTool?: Tool | null; -} - -const FileUploadModal: React.FC = ({ selectedTool }) => { - const { isFilesModalOpen, closeFilesModal, onFileSelect, onFilesSelect } = useFilesModalContext(); - - - return ( - - - - ); -}; - -export default FileUploadModal; \ No newline at end of file diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx index 6e8a42fab..f84d2ec8b 100644 --- a/frontend/src/contexts/FileContext.tsx +++ b/frontend/src/contexts/FileContext.tsx @@ -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 => { 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]); diff --git a/frontend/src/contexts/FileManagerContext.tsx b/frontend/src/contexts/FileManagerContext.tsx index 33a6e86da..c7f924e8e 100644 --- a/frontend/src/contexts/FileManagerContext.tsx +++ b/frontend/src/contexts/FileManagerContext.tsx @@ -1,5 +1,6 @@ import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react'; import { FileWithUrl } from '../types/file'; +import { StoredFile } from '../services/fileStorage'; // Type for the context value - now contains everything directly interface FileManagerContextValue { @@ -40,7 +41,7 @@ interface FileManagerProviderProps { isOpen: boolean; onFileRemove: (index: number) => void; modalHeight: string; - storeFile: (file: File) => Promise; + storeFile: (file: File) => Promise; refreshRecentFiles: () => Promise; } @@ -65,7 +66,7 @@ export const FileManagerProvider: React.FC = ({ const createdBlobUrls = useRef>(new Set()); // Computed values (with null safety) - const selectedFiles = (recentFiles || []).filter(file => selectedFileIds.includes(file.id)); + const selectedFiles = (recentFiles || []).filter(file => selectedFileIds.includes(file.id || file.name)); const filteredFiles = (recentFiles || []).filter(file => file.name.toLowerCase().includes(searchTerm.toLowerCase()) ); @@ -122,14 +123,13 @@ export const FileManagerProvider: React.FC = ({ const files = Array.from(event.target.files || []); if (files.length > 0) { try { - // Store files and refresh recent files (same as drag-and-drop) - await Promise.all(files.map(file => storeFile(file))); - + // Create FileWithUrl objects - FileContext will handle storage and ID assignment const fileWithUrls = files.map(file => { const url = URL.createObjectURL(file); createdBlobUrls.current.add(url); + return { - id: `local-${Date.now()}-${Math.random()}`, + // No ID assigned here - FileContext will handle storage and ID assignment name: file.name, file, url, diff --git a/frontend/src/contexts/FilesModalContext.tsx b/frontend/src/contexts/FilesModalContext.tsx index 6940ab9e7..788db77bd 100644 --- a/frontend/src/contexts/FilesModalContext.tsx +++ b/frontend/src/contexts/FilesModalContext.tsx @@ -1,21 +1,58 @@ -import React, { createContext, useContext } from 'react'; -import { useFilesModal, UseFilesModalReturn } from '../hooks/useFilesModal'; +import React, { createContext, useContext, useState, useCallback } from 'react'; import { useFileHandler } from '../hooks/useFileHandler'; -interface FilesModalContextType extends UseFilesModalReturn {} +interface FilesModalContextType { + isFilesModalOpen: boolean; + openFilesModal: () => void; + closeFilesModal: () => void; + onFileSelect: (file: File) => void; + onFilesSelect: (files: File[]) => void; + onModalClose: () => void; + setOnModalClose: (callback: () => void) => void; +} const FilesModalContext = createContext(null); export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { addToActiveFiles, addMultipleFiles } = useFileHandler(); - - const filesModal = useFilesModal({ - onFileSelect: addToActiveFiles, - onFilesSelect: addMultipleFiles, - }); + const [isFilesModalOpen, setIsFilesModalOpen] = useState(false); + const [onModalClose, setOnModalClose] = useState<(() => void) | undefined>(); + + const openFilesModal = useCallback(() => { + setIsFilesModalOpen(true); + }, []); + + const closeFilesModal = useCallback(() => { + setIsFilesModalOpen(false); + onModalClose?.(); + }, [onModalClose]); + + const handleFileSelect = useCallback((file: File) => { + addToActiveFiles(file); + closeFilesModal(); + }, [addToActiveFiles, closeFilesModal]); + + const handleFilesSelect = useCallback((files: File[]) => { + addMultipleFiles(files); + closeFilesModal(); + }, [addMultipleFiles, closeFilesModal]); + + const setModalCloseCallback = useCallback((callback: () => void) => { + setOnModalClose(() => callback); + }, []); + + const contextValue: FilesModalContextType = { + isFilesModalOpen, + openFilesModal, + closeFilesModal, + onFileSelect: handleFileSelect, + onFilesSelect: handleFilesSelect, + onModalClose, + setOnModalClose: setModalCloseCallback, + }; return ( - + {children} ); diff --git a/frontend/src/hooks/useFilesModal.ts b/frontend/src/hooks/useFilesModal.ts deleted file mode 100644 index 49e9f2c5e..000000000 --- a/frontend/src/hooks/useFilesModal.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useState, useCallback } from 'react'; - -export interface UseFilesModalReturn { - isFilesModalOpen: boolean; - openFilesModal: () => void; - closeFilesModal: () => void; - onFileSelect?: (file: File) => void; - onFilesSelect?: (files: File[]) => void; - onModalClose?: () => void; - setOnModalClose: (callback: () => void) => void; -} - -interface UseFilesModalProps { - onFileSelect?: (file: File) => void; - onFilesSelect?: (files: File[]) => void; -} - -export const useFilesModal = ({ - onFileSelect, - onFilesSelect -}: UseFilesModalProps = {}): UseFilesModalReturn => { - const [isFilesModalOpen, setIsFilesModalOpen] = useState(false); - const [onModalClose, setOnModalClose] = useState<(() => void) | undefined>(); - - const openFilesModal = useCallback(() => { - setIsFilesModalOpen(true); - }, []); - - const closeFilesModal = useCallback(() => { - setIsFilesModalOpen(false); - onModalClose?.(); - }, [onModalClose]); - - const handleFileSelect = useCallback((file: File) => { - onFileSelect?.(file); - closeFilesModal(); - }, [onFileSelect, closeFilesModal]); - - const handleFilesSelect = useCallback((files: File[]) => { - onFilesSelect?.(files); - closeFilesModal(); - }, [onFilesSelect, closeFilesModal]); - - const setModalCloseCallback = useCallback((callback: () => void) => { - setOnModalClose(() => callback); - }, []); - - return { - isFilesModalOpen, - openFilesModal, - closeFilesModal, - onFileSelect: handleFileSelect, - onFilesSelect: handleFilesSelect, - onModalClose, - setOnModalClose: setModalCloseCallback, - }; -}; \ No newline at end of file diff --git a/frontend/src/utils/fileUtils.ts b/frontend/src/utils/fileUtils.ts index b42d2f646..682cd9f3c 100644 --- a/frontend/src/utils/fileUtils.ts +++ b/frontend/src/utils/fileUtils.ts @@ -1,8 +1,8 @@ import { FileWithUrl } from "../types/file"; import { StoredFile, fileStorage } from "../services/fileStorage"; -export function getFileId(file: File): string { - return (file as File & { id?: string }).id || file.name; +export function getFileId(file: File): string | null { + return (file as File & { id?: string }).id || null; } /**