import React, { useState, useEffect } from "react"; import { Box, Flex, Text, Notification } from "@mantine/core"; import { Dropzone, MIME_TYPES } from "@mantine/dropzone"; import { useTranslation } from "react-i18next"; import { GlobalWorkerOptions } from "pdfjs-dist"; import { StorageStats } from "../services/fileStorage"; import { FileWithUrl, defaultStorageConfig } from "../types/file"; // Refactored imports import { fileOperationsService } from "../services/fileOperationsService"; import { checkStorageWarnings } from "../utils/storageUtils"; import StorageStatsCard from "./StorageStatsCard"; import FileCard from "./FileCard.standalone"; GlobalWorkerOptions.workerSrc = "/pdf.worker.js"; interface FileManagerProps { files: FileWithUrl[]; setFiles: React.Dispatch>; allowMultiple?: boolean; setPdfFile?: (fileObj: { file: File; url: string }) => void; setCurrentView?: (view: string) => void; } const FileManager: React.FC = ({ files = [], setFiles, allowMultiple = true, setPdfFile, setCurrentView, }) => { const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [storageStats, setStorageStats] = useState(null); const [notification, setNotification] = useState(null); const [filesLoaded, setFilesLoaded] = useState(false); // Extract operations from service for cleaner code const { loadStorageStats, forceReloadFiles, loadExistingFiles, uploadFiles, removeFile, clearAllFiles, createBlobUrlForFile, checkForPurge, updateStorageStatsIncremental } = fileOperationsService; // Add CSS for spinner animation useEffect(() => { if (!document.querySelector('#spinner-animation')) { const style = document.createElement('style'); style.id = 'spinner-animation'; style.textContent = ` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); } }, []); // Load existing files from IndexedDB on mount useEffect(() => { if (!filesLoaded) { handleLoadExistingFiles(); } }, [filesLoaded]); // Load storage stats and set up periodic updates useEffect(() => { handleLoadStorageStats(); const interval = setInterval(async () => { await handleLoadStorageStats(); await handleCheckForPurge(); }, 10000); // Update every 10 seconds return () => clearInterval(interval); }, []); // Sync UI with IndexedDB whenever storage stats change useEffect(() => { const syncWithStorage = async () => { if (storageStats && filesLoaded) { // If file counts don't match, force reload if (storageStats.fileCount !== files.length) { console.warn('File count mismatch: storage has', storageStats.fileCount, 'but UI shows', files.length, '- forcing reload'); const reloadedFiles = await forceReloadFiles(); setFiles(reloadedFiles); } } }; syncWithStorage(); }, [storageStats, filesLoaded, files.length]); // Handlers using extracted operations const handleLoadStorageStats = async () => { const stats = await loadStorageStats(); if (stats) { setStorageStats(stats); // Check for storage warnings const warning = checkStorageWarnings(stats); if (warning) { setNotification(warning); } } }; const handleLoadExistingFiles = async () => { try { const loadedFiles = await loadExistingFiles(filesLoaded, files); setFiles(loadedFiles); setFilesLoaded(true); } catch (error) { console.error('Failed to load existing files:', error); setFilesLoaded(true); } }; const handleCheckForPurge = async () => { try { const isPurged = await checkForPurge(files); if (isPurged) { console.warn('IndexedDB purge detected - forcing UI reload'); setNotification('Browser cleared storage. Files have been removed. Please re-upload.'); const reloadedFiles = await forceReloadFiles(); setFiles(reloadedFiles); setFilesLoaded(true); } } catch (error) { console.error('Error checking for purge:', error); } }; const handleDrop = async (uploadedFiles: File[]) => { setLoading(true); try { const newFiles = await uploadFiles(uploadedFiles, defaultStorageConfig.useIndexedDB); // Update files state setFiles((prevFiles) => (allowMultiple ? [...prevFiles, ...newFiles] : newFiles)); // Update storage stats incrementally if (storageStats) { const updatedStats = updateStorageStatsIncremental(storageStats, 'add', newFiles); setStorageStats(updatedStats); // Check for storage warnings const warning = checkStorageWarnings(updatedStats); if (warning) { setNotification(warning); } } } catch (error) { console.error('Error handling file drop:', error); setNotification(t("fileManager.uploadError", "Failed to upload some files.")); } finally { setLoading(false); } }; const handleRemoveFile = async (index: number) => { const file = files[index]; try { await removeFile(file); // Update storage stats incrementally if (storageStats) { const updatedStats = updateStorageStatsIncremental(storageStats, 'remove', [file]); setStorageStats(updatedStats); } setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index)); } catch (error) { console.error('Failed to remove file:', error); } }; const handleClearAll = async () => { try { await clearAllFiles(files); // Reset storage stats if (storageStats) { const clearedStats = updateStorageStatsIncremental(storageStats, 'clear'); setStorageStats(clearedStats); } setFiles([]); } catch (error) { console.error('Failed to clear all files:', error); } }; const handleReloadFiles = () => { setFilesLoaded(false); setFiles([]); }; const handleFileDoubleClick = async (file: FileWithUrl) => { if (setPdfFile) { try { const url = await createBlobUrlForFile(file); setPdfFile({ file: file, url: url }); setCurrentView && setCurrentView("viewer"); } catch (error) { console.error('Failed to create blob URL for file:', error); setNotification('Failed to open file. It may have been removed from storage.'); } } }; return (
{/* File Upload Dropzone */} {t("fileChooser.dragAndDropPDF", "Drag PDF files here or click to select")} {/* Storage Stats Card */} {/* Files Display */} {files.length === 0 ? ( {t("noFileSelected", "No files uploaded yet.")} ) : ( {files.map((file, idx) => ( handleRemoveFile(idx)} onDoubleClick={() => handleFileDoubleClick(file)} as FileWithUrl /> ))} )} {/* Notifications */} {notification && ( setNotification(null)} style={{ position: "fixed", bottom: 20, right: 20, zIndex: 1000 }} > {notification} )}
); }; export default FileManager;