2025-08-05 15:55:35 +01:00
|
|
|
import React, { useState, useCallback, useEffect } from 'react';
|
2025-08-05 14:39:27 +01:00
|
|
|
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';
|
|
|
|
import { Tool } from '../../types/tool';
|
|
|
|
import MobileLayout from './fileManager/MobileLayout';
|
|
|
|
import DesktopLayout from './fileManager/DesktopLayout';
|
|
|
|
import DragOverlay from './fileManager/DragOverlay';
|
2025-08-05 15:55:35 +01:00
|
|
|
import { FileManagerProvider } from './fileManager/FileManagerContext';
|
2025-08-05 14:39:27 +01:00
|
|
|
|
|
|
|
interface FileManagerProps {
|
|
|
|
selectedTool?: Tool | null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|
|
|
const { t } = useTranslation();
|
|
|
|
const { isFilesModalOpen, closeFilesModal, onFileSelect, onFilesSelect } = useFilesModalContext();
|
|
|
|
const [recentFiles, setRecentFiles] = useState<FileWithUrl[]>([]);
|
|
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
|
|
const [isMobile, setIsMobile] = useState(false);
|
|
|
|
|
2025-08-05 17:52:18 +01:00
|
|
|
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile, touchFile } = useFileManager();
|
2025-08-05 14:39:27 +01:00
|
|
|
|
|
|
|
// File management handlers
|
|
|
|
const isFileSupported = useCallback((fileName: string) => {
|
|
|
|
if (!selectedTool?.supportedFormats) return true;
|
|
|
|
const extension = fileName.split('.').pop()?.toLowerCase();
|
|
|
|
return selectedTool.supportedFormats.includes(extension || '');
|
|
|
|
}, [selectedTool?.supportedFormats]);
|
|
|
|
|
|
|
|
const refreshRecentFiles = useCallback(async () => {
|
|
|
|
const files = await loadRecentFiles();
|
|
|
|
setRecentFiles(files);
|
|
|
|
}, [loadRecentFiles]);
|
|
|
|
|
2025-08-05 15:55:35 +01:00
|
|
|
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);
|
2025-08-05 14:39:27 +01:00
|
|
|
}
|
2025-08-05 15:55:35 +01:00
|
|
|
}, [convertToFile, onFilesSelect]);
|
2025-08-05 14:39:27 +01:00
|
|
|
|
|
|
|
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)));
|
|
|
|
onFilesSelect(files);
|
|
|
|
await refreshRecentFiles();
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Failed to process dropped files:', error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [storeFile, onFilesSelect, refreshRecentFiles]);
|
|
|
|
|
|
|
|
const handleRemoveFileByIndex = useCallback(async (index: number) => {
|
|
|
|
await handleRemoveFile(index, recentFiles, setRecentFiles);
|
|
|
|
}, [handleRemoveFile, recentFiles]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-08-05 17:52:18 +01:00
|
|
|
const checkMobile = () => setIsMobile(window.innerWidth < 1030);
|
2025-08-05 14:39:27 +01:00
|
|
|
checkMobile();
|
|
|
|
window.addEventListener('resize', checkMobile);
|
|
|
|
return () => window.removeEventListener('resize', checkMobile);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (isFilesModalOpen) {
|
|
|
|
refreshRecentFiles();
|
|
|
|
} else {
|
|
|
|
// Reset state when modal is closed
|
|
|
|
setIsDragging(false);
|
|
|
|
}
|
|
|
|
}, [isFilesModalOpen, refreshRecentFiles]);
|
|
|
|
|
2025-08-05 15:55:35 +01:00
|
|
|
// 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]);
|
|
|
|
|
2025-08-05 14:39:27 +01:00
|
|
|
// Modal size constants for consistent scaling
|
|
|
|
const modalHeight = '80vh';
|
2025-08-05 17:52:18 +01:00
|
|
|
const modalWidth = isMobile ? '100%' : '80vw';
|
2025-08-05 14:39:27 +01:00
|
|
|
const modalMaxWidth = isMobile ? '100%' : '1200px';
|
|
|
|
const modalMaxHeight = '1200px';
|
2025-08-05 17:52:18 +01:00
|
|
|
const modalMinWidth = isMobile ? '320px' : '800px';
|
2025-08-05 14:39:27 +01:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Modal
|
|
|
|
opened={isFilesModalOpen}
|
|
|
|
onClose={closeFilesModal}
|
|
|
|
size={isMobile ? "100%" : "auto"}
|
|
|
|
centered
|
|
|
|
radius={30}
|
|
|
|
className="overflow-hidden p-0"
|
|
|
|
withCloseButton={false}
|
|
|
|
styles={{
|
|
|
|
content: {
|
|
|
|
position: 'relative',
|
|
|
|
margin: isMobile ? '1rem' : '2rem'
|
|
|
|
},
|
|
|
|
body: { padding: 0 },
|
|
|
|
header: { display: 'none' }
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<div style={{
|
|
|
|
position: 'relative',
|
|
|
|
height: modalHeight,
|
|
|
|
width: modalWidth,
|
|
|
|
maxWidth: modalMaxWidth,
|
|
|
|
maxHeight: modalMaxHeight,
|
|
|
|
minWidth: modalMinWidth,
|
|
|
|
margin: '0 auto',
|
|
|
|
overflow: 'hidden'
|
|
|
|
}}>
|
|
|
|
<Dropzone
|
|
|
|
onDrop={handleNewFileUpload}
|
|
|
|
onDragEnter={() => setIsDragging(true)}
|
|
|
|
onDragLeave={() => setIsDragging(false)}
|
|
|
|
accept={["*/*"]}
|
|
|
|
multiple={true}
|
|
|
|
activateOnClick={false}
|
|
|
|
style={{
|
|
|
|
height: '100%',
|
|
|
|
width: '100%',
|
|
|
|
border: 'none',
|
|
|
|
borderRadius: '30px',
|
2025-08-05 17:52:18 +01:00
|
|
|
backgroundColor: 'var(--bg-file-manager)'
|
2025-08-05 14:39:27 +01:00
|
|
|
}}
|
|
|
|
styles={{
|
|
|
|
inner: { pointerEvents: 'all' }
|
|
|
|
}}
|
|
|
|
>
|
2025-08-05 15:55:35 +01:00
|
|
|
<FileManagerProvider
|
|
|
|
recentFiles={recentFiles}
|
|
|
|
onFilesSelected={handleFilesSelected}
|
|
|
|
onClose={closeFilesModal}
|
|
|
|
isFileSupported={isFileSupported}
|
|
|
|
isOpen={isFilesModalOpen}
|
|
|
|
onFileRemove={handleRemoveFileByIndex}
|
|
|
|
modalHeight={modalHeight}
|
2025-08-05 17:52:18 +01:00
|
|
|
storeFile={storeFile}
|
|
|
|
refreshRecentFiles={refreshRecentFiles}
|
2025-08-05 15:55:35 +01:00
|
|
|
>
|
|
|
|
{isMobile ? <MobileLayout /> : <DesktopLayout />}
|
|
|
|
</FileManagerProvider>
|
2025-08-05 14:39:27 +01:00
|
|
|
</Dropzone>
|
|
|
|
|
|
|
|
<DragOverlay isVisible={isDragging} />
|
|
|
|
</div>
|
|
|
|
</Modal>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default FileManager;
|