import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react'; import { FileWithUrl } from '../types/file'; import { StoredFile, fileStorage } from '../services/fileStorage'; // Type for the context value - now contains everything directly interface FileManagerContextValue { // State activeSource: 'recent' | 'local' | 'drive'; selectedFileIds: string[]; searchTerm: string; selectedFiles: FileWithUrl[]; filteredFiles: FileWithUrl[]; fileInputRef: React.RefObject; // Handlers onSourceChange: (source: 'recent' | 'local' | 'drive') => void; onLocalFileClick: () => void; onFileSelect: (file: FileWithUrl) => void; onFileRemove: (index: number) => void; onFileDoubleClick: (file: FileWithUrl) => void; onOpenFiles: () => void; onSearchChange: (value: string) => void; onFileInputChange: (event: React.ChangeEvent) => void; onSelectAll: () => void; onDeleteSelected: () => void; onDownloadSelected: () => void; onDownloadSingle: (file: FileWithUrl) => void; // External props recentFiles: FileWithUrl[]; isFileSupported: (fileName: string) => boolean; modalHeight: string; } // Create the context const FileManagerContext = createContext(null); // Provider component props interface FileManagerProviderProps { children: React.ReactNode; recentFiles: FileWithUrl[]; onFilesSelected: (files: FileWithUrl[]) => void; onClose: () => void; isFileSupported: (fileName: string) => boolean; isOpen: boolean; onFileRemove: (index: number) => void; modalHeight: string; storeFile: (file: File) => Promise; refreshRecentFiles: () => Promise; } export const FileManagerProvider: React.FC = ({ children, recentFiles, onFilesSelected, onClose, isFileSupported, isOpen, onFileRemove, modalHeight, storeFile, refreshRecentFiles, }) => { const [activeSource, setActiveSource] = useState<'recent' | 'local' | 'drive'>('recent'); const [selectedFileIds, setSelectedFileIds] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const fileInputRef = useRef(null); // Track blob URLs for cleanup const createdBlobUrls = useRef>(new Set()); // Computed values (with null safety) const selectedFiles = (recentFiles || []).filter(file => selectedFileIds.includes(file.id || file.name)); const filteredFiles = (recentFiles || []).filter(file => file.name.toLowerCase().includes(searchTerm.toLowerCase()) ); const handleSourceChange = useCallback((source: 'recent' | 'local' | 'drive') => { setActiveSource(source); if (source !== 'recent') { setSelectedFileIds([]); setSearchTerm(''); } }, []); const handleLocalFileClick = useCallback(() => { fileInputRef.current?.click(); }, []); const handleFileSelect = useCallback((file: FileWithUrl) => { setSelectedFileIds(prev => { if (file.id) { if (prev.includes(file.id)) { return prev.filter(id => id !== file.id); } else { return [...prev, file.id]; } } else { return prev; } }); }, []); const handleFileRemove = useCallback((index: number) => { const fileToRemove = filteredFiles[index]; if (fileToRemove) { setSelectedFileIds(prev => prev.filter(id => id !== fileToRemove.id)); } onFileRemove(index); }, [filteredFiles, onFileRemove]); const handleFileDoubleClick = useCallback((file: FileWithUrl) => { if (isFileSupported(file.name)) { onFilesSelected([file]); onClose(); } }, [isFileSupported, onFilesSelected, onClose]); const handleOpenFiles = useCallback(() => { if (selectedFiles.length > 0) { onFilesSelected(selectedFiles); onClose(); } }, [selectedFiles, onFilesSelected, onClose]); const handleSearchChange = useCallback((value: string) => { setSearchTerm(value); }, []); const handleFileInputChange = useCallback(async (event: React.ChangeEvent) => { const files = Array.from(event.target.files || []); if (files.length > 0) { try { // 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 { // No ID assigned here - FileContext will handle storage and ID assignment name: file.name, file, url, size: file.size, lastModified: file.lastModified, }; }); onFilesSelected(fileWithUrls as any /* FIX ME */); await refreshRecentFiles(); onClose(); } catch (error) { console.error('Failed to process selected files:', error); } } event.target.value = ''; }, [storeFile, onFilesSelected, refreshRecentFiles, onClose]); const handleSelectAll = useCallback(() => { const allFilesSelected = filteredFiles.length > 0 && selectedFileIds.length === filteredFiles.length; if (allFilesSelected) { // Deselect all setSelectedFileIds([]); } else { // Select all filtered files setSelectedFileIds(filteredFiles.map(file => file.id || file.name)); } }, [filteredFiles, selectedFileIds]); const handleDeleteSelected = useCallback(async () => { if (selectedFileIds.length === 0) return; try { // Get files to delete based on current filtered view const filesToDelete = filteredFiles.filter(file => selectedFileIds.includes(file.id || file.name) ); // Delete files from storage for (const file of filesToDelete) { const lookupKey = file.id || file.name; await fileStorage.deleteFile(lookupKey); } // Clear selection setSelectedFileIds([]); // Refresh the file list await refreshRecentFiles(); } catch (error) { console.error('Failed to delete selected files:', error); } }, [selectedFileIds, filteredFiles, refreshRecentFiles]); const handleDownloadSelected = useCallback(async () => { if (selectedFileIds.length === 0) return; try { // Get selected files const selectedFilesToDownload = filteredFiles.filter(file => selectedFileIds.includes(file.id || file.name) ); if (selectedFilesToDownload.length === 1) { // Single file download const fileWithUrl = selectedFilesToDownload[0]; const lookupKey = fileWithUrl.id || fileWithUrl.name; const storedFile = await fileStorage.getFile(lookupKey); if (storedFile) { const blob = new Blob([storedFile.data], { type: storedFile.type }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = storedFile.name; document.body.appendChild(link); link.click(); document.body.removeChild(link); // Clean up the blob URL URL.revokeObjectURL(url); } } else if (selectedFilesToDownload.length > 1) { // Multiple files - create ZIP download const { zipFileService } = await import('../services/zipFileService'); // Convert stored files to File objects const files: File[] = []; for (const fileWithUrl of selectedFilesToDownload) { const lookupKey = fileWithUrl.id || fileWithUrl.name; const storedFile = await fileStorage.getFile(lookupKey); if (storedFile) { const file = new File([storedFile.data], storedFile.name, { type: storedFile.type, lastModified: storedFile.lastModified }); files.push(file); } } if (files.length > 0) { // Create ZIP file const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, ''); const zipFilename = `selected-files-${timestamp}.zip`; const { zipFile } = await zipFileService.createZipFromFiles(files, zipFilename); // Download the ZIP file const url = URL.createObjectURL(zipFile); const link = document.createElement('a'); link.href = url; link.download = zipFilename; document.body.appendChild(link); link.click(); document.body.removeChild(link); // Clean up the blob URL URL.revokeObjectURL(url); } } } catch (error) { console.error('Failed to download selected files:', error); } }, [selectedFileIds, filteredFiles]); const handleDownloadSingle = useCallback(async (file: FileWithUrl) => { try { const lookupKey = file.id || file.name; const storedFile = await fileStorage.getFile(lookupKey); if (storedFile) { const blob = new Blob([storedFile.data], { type: storedFile.type }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = storedFile.name; document.body.appendChild(link); link.click(); document.body.removeChild(link); // Clean up the blob URL URL.revokeObjectURL(url); } } catch (error) { console.error('Failed to download file:', error); } }, []); // Cleanup blob URLs when component unmounts useEffect(() => { return () => { // Clean up all created blob URLs createdBlobUrls.current.forEach(url => { URL.revokeObjectURL(url); }); createdBlobUrls.current.clear(); }; }, []); // Reset state when modal closes useEffect(() => { if (!isOpen) { setActiveSource('recent'); setSelectedFileIds([]); setSearchTerm(''); } }, [isOpen]); const contextValue: FileManagerContextValue = { // State activeSource, selectedFileIds, searchTerm, selectedFiles, filteredFiles, fileInputRef, // Handlers onSourceChange: handleSourceChange, onLocalFileClick: handleLocalFileClick, onFileSelect: handleFileSelect, onFileRemove: handleFileRemove, onFileDoubleClick: handleFileDoubleClick, onOpenFiles: handleOpenFiles, onSearchChange: handleSearchChange, onFileInputChange: handleFileInputChange, onSelectAll: handleSelectAll, onDeleteSelected: handleDeleteSelected, onDownloadSelected: handleDownloadSelected, onDownloadSingle: handleDownloadSingle, // External props recentFiles, isFileSupported, modalHeight, }; return ( {children} ); }; // Custom hook to use the context export const useFileManagerContext = (): FileManagerContextValue => { const context = useContext(FileManagerContext); if (!context) { throw new Error( 'useFileManagerContext must be used within a FileManagerProvider. ' + 'Make sure you wrap your component with .' ); } return context; }; // Export the context for advanced use cases export { FileManagerContext };