Changed to provider pattern to reduce prop drilling

This commit is contained in:
Connor Yoh 2025-08-05 15:55:35 +01:00
parent af1643a5ae
commit 7e18cc275b
10 changed files with 309 additions and 382 deletions

View File

@ -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<FileManagerProps> = ({ selectedTool }) => {
const { t } = useTranslation();
const { isFilesModalOpen, closeFilesModal, onFileSelect, onFilesSelect } = useFilesModalContext();
const fileInputRef = useRef<HTMLInputElement>(null);
const [activeSource, setActiveSource] = useState<FileSource>('recent');
const [searchTerm, setSearchTerm] = useState('');
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
const [recentFiles, setRecentFiles] = useState<FileWithUrl[]>([]);
const [isDragging, setIsDragging] = useState(false);
const [isMobile, setIsMobile] = useState(false);
@ -40,31 +36,21 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
setRecentFiles(files);
}, [loadRecentFiles]);
const openFileDialog = useCallback(() => {
fileInputRef.current?.click();
}, []);
const handleFileInputChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
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<FileManagerProps> = ({ 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<FileManagerProps> = ({ 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<FileManagerProps> = ({ selectedTool }) => {
inner: { pointerEvents: 'all' }
}}
>
{isMobile ? (
<MobileLayout
activeSource={activeSource}
onSourceChange={setActiveSource}
onLocalFileClick={openFileDialog}
selectedFiles={selectedFiles}
onOpenFiles={handleOpenFiles}
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
recentFiles={recentFiles}
filteredFiles={filteredFiles}
selectedFileIds={selectedFileIds}
onFileSelect={handleFileSelect}
onFileRemove={handleRemoveFileByIndex}
onFileDoubleClick={handleFileDoubleClick}
isFileSupported={isFileSupported}
modalHeight={modalHeight}
fileInputRef={fileInputRef}
onFileInputChange={handleFileInputChange}
/>
) : (
<DesktopLayout
activeSource={activeSource}
onSourceChange={setActiveSource}
onLocalFileClick={openFileDialog}
selectedFiles={selectedFiles}
onOpenFiles={handleOpenFiles}
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
recentFiles={recentFiles}
filteredFiles={filteredFiles}
selectedFileIds={selectedFileIds}
onFileSelect={handleFileSelect}
onFileRemove={handleRemoveFileByIndex}
onFileDoubleClick={handleFileDoubleClick}
isFileSupported={isFileSupported}
fileInputRef={fileInputRef}
onFileInputChange={handleFileInputChange}
modalHeight={modalHeight}
/>
)}
<FileManagerProvider
recentFiles={recentFiles}
onFilesSelected={handleFilesSelected}
onClose={closeFilesModal}
isFileSupported={isFileSupported}
isOpen={isFilesModalOpen}
onFileRemove={handleRemoveFileByIndex}
modalHeight={modalHeight}
>
{isMobile ? <MobileLayout /> : <DesktopLayout />}
</FileManagerProvider>
</Dropzone>
<DragOverlay isVisible={isDragging} />

View File

@ -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<HTMLInputElement>;
onFileInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
modalHeight: string;
}
const DesktopLayout: React.FC<DesktopLayoutProps> = ({
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 (
<Grid gutter="md" h="100%" grow={false} style={{ flexWrap: 'nowrap' }}>
@ -59,33 +23,17 @@ const DesktopLayout: React.FC<DesktopLayoutProps> = ({
flexShrink: 0,
height: '100%',
}}>
<FileSourceButtons
activeSource={activeSource}
onSourceChange={onSourceChange}
onLocalFileClick={onLocalFileClick}
/>
<FileSourceButtons />
</Grid.Col>
{/* Column 2: File List */}
<Grid.Col span="auto" style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>
{activeSource === 'recent' && (
<SearchInput
value={searchTerm}
onChange={onSearchChange}
style={{ marginBottom: '1rem', flexShrink: 0 }}
/>
<SearchInput style={{ marginBottom: '1rem', flexShrink: 0 }} />
)}
<div style={{ flex: 1, minHeight: 0 }}>
<FileListArea
activeSource={activeSource}
recentFiles={recentFiles}
filteredFiles={filteredFiles}
selectedFileIds={selectedFileIds}
onFileSelect={onFileSelect}
onFileRemove={onFileRemove}
onFileDoubleClick={onFileDoubleClick}
isFileSupported={isFileSupported}
scrollAreaHeight={`calc(${modalHeight} - 6rem)`}
scrollAreaStyle={{
height: activeSource === 'recent' && recentFiles.length > 0 ? `calc(${modalHeight} - 6rem)` : '100%'
@ -97,19 +45,12 @@ const DesktopLayout: React.FC<DesktopLayoutProps> = ({
{/* Column 3: File Details */}
<Grid.Col span="content" style={{ minWidth: '20rem', width: '20rem', flexShrink: 0, height: '100%' }}>
<div style={{ height: '100%' }}>
<FileDetails
selectedFiles={selectedFiles}
onOpenFiles={onOpenFiles}
modalHeight={modalHeight}
/>
<FileDetails />
</div>
</Grid.Col>
{/* Hidden file input for local file selection */}
<HiddenFileInput
fileInputRef={fileInputRef}
onFileInputChange={onFileInputChange}
/>
<HiddenFileInput />
</Grid>
);
};

View File

@ -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<FileDetailsProps & { compact?: boolean; modalHeight?: string }> = ({ selectedFiles, onOpenFiles, compact = false, modalHeight = '80vh' }) => {
interface FileDetailsProps {
compact?: boolean;
}
const FileDetails: React.FC<FileDetailsProps> = ({
compact = false
}) => {
const { selectedFiles, onOpenFiles, modalHeight } = useFileManagerContext();
const { t } = useTranslation();
const [currentFileIndex, setCurrentFileIndex] = useState(0);
const [thumbnailCache, setThumbnailCache] = useState<Record<string, string>>({});
const [loadingFile, setLoadingFile] = useState<FileWithUrl | null>(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<string, string> = {};
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<FileDetailsProps & { compact?: boolean; modalHeight?
</ScrollArea>
</Card>
{/* Section 3: Action Button */}
<Button
size="md"
onClick={onOpenFiles}

View File

@ -3,35 +3,28 @@ import { Center, ScrollArea, Text, Stack } from '@mantine/core';
import CloudIcon from '@mui/icons-material/Cloud';
import HistoryIcon from '@mui/icons-material/History';
import { useTranslation } from 'react-i18next';
import { FileWithUrl } from '../../../types/file';
import FileListItem from './FileListItem';
import { FileSource } from './types';
import { useFileManagerContext } from './FileManagerContext';
interface FileListAreaProps {
activeSource: FileSource;
recentFiles: FileWithUrl[];
filteredFiles: FileWithUrl[];
selectedFileIds: string[];
onFileSelect: (file: FileWithUrl) => void;
onFileRemove: (index: number) => void;
onFileDoubleClick: (file: FileWithUrl) => void;
isFileSupported: (fileName: string) => boolean;
scrollAreaHeight: string;
scrollAreaStyle?: React.CSSProperties;
}
const FileListArea: React.FC<FileListAreaProps> = ({
activeSource,
recentFiles,
filteredFiles,
selectedFileIds,
onFileSelect,
onFileRemove,
onFileDoubleClick,
isFileSupported,
scrollAreaHeight,
scrollAreaStyle = {},
}) => {
const {
activeSource,
recentFiles,
filteredFiles,
selectedFileIds,
onFileSelect,
onFileRemove,
onFileDoubleClick,
isFileSupported,
} = useFileManagerContext();
const { t } = useTranslation();
if (activeSource === 'recent') {

View File

@ -0,0 +1,206 @@
import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react';
import { FileWithUrl } from '../../../types/file';
import { FileSource } from './types';
// Type for the context value - now contains everything directly
interface FileManagerContextValue {
// State
activeSource: FileSource;
selectedFileIds: string[];
searchTerm: string;
selectedFiles: FileWithUrl[];
filteredFiles: FileWithUrl[];
fileInputRef: React.RefObject<HTMLInputElement>;
// Handlers
onSourceChange: (source: FileSource) => 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<HTMLInputElement>) => void;
// External props
recentFiles: FileWithUrl[];
isFileSupported: (fileName: string) => boolean;
modalHeight: string;
}
// Create the context
const FileManagerContext = createContext<FileManagerContextValue | null>(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;
}
export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
children,
recentFiles,
onFilesSelected,
onClose,
isFileSupported,
isOpen,
onFileRemove,
modalHeight,
}) => {
const [activeSource, setActiveSource] = useState<FileSource>('recent');
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const fileInputRef = useRef<HTMLInputElement>(null);
// Track blob URLs for cleanup
const createdBlobUrls = useRef<Set<string>>(new Set());
// Computed values (with null safety)
const selectedFiles = (recentFiles || []).filter(file => selectedFileIds.includes(file.id));
const filteredFiles = (recentFiles || []).filter(file =>
file.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleSourceChange = useCallback((source: FileSource) => {
setActiveSource(source);
if (source !== 'recent') {
setSelectedFileIds([]);
setSearchTerm('');
}
}, []);
const handleLocalFileClick = useCallback(() => {
fileInputRef.current?.click();
}, []);
const handleFileSelect = useCallback((file: FileWithUrl) => {
setSelectedFileIds(prev => {
if (prev.includes(file.id)) {
return prev.filter(id => id !== file.id);
} else {
return [...prev, file.id];
}
});
}, []);
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((event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []);
if (files.length > 0) {
const fileWithUrls = files.map(file => {
const url = URL.createObjectURL(file);
createdBlobUrls.current.add(url);
return {
id: `local-${Date.now()}-${Math.random()}`,
name: file.name,
file,
url,
size: file.size,
lastModified: file.lastModified,
};
});
onFilesSelected(fileWithUrls);
onClose();
}
event.target.value = '';
}, [onFilesSelected, onClose]);
// 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,
// External props
recentFiles,
isFileSupported,
modalHeight,
};
return (
<FileManagerContext.Provider value={contextValue}>
{children}
</FileManagerContext.Provider>
);
};
// 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 <FileManagerProvider>.'
);
}
return context;
};
// Export the context for advanced use cases
export { FileManagerContext };

View File

@ -4,14 +4,16 @@ import HistoryIcon from '@mui/icons-material/History';
import FolderIcon from '@mui/icons-material/Folder';
import CloudIcon from '@mui/icons-material/Cloud';
import { useTranslation } from 'react-i18next';
import { FileSourceButtonsProps } from './types';
import { useFileManagerContext } from './FileManagerContext';
const FileSourceButtons: React.FC<FileSourceButtonsProps & { horizontal?: boolean }> = ({
activeSource,
onSourceChange,
onLocalFileClick,
interface FileSourceButtonsProps {
horizontal?: boolean;
}
const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
horizontal = false
}) => {
const { activeSource, onSourceChange, onLocalFileClick } = useFileManagerContext();
const { t } = useTranslation();
const buttonProps = {

View File

@ -1,17 +1,15 @@
import React from 'react';
import { useFileManagerContext } from './FileManagerContext';
interface HiddenFileInputProps {
fileInputRef: React.RefObject<HTMLInputElement>;
onFileInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
const HiddenFileInput: React.FC = () => {
const { fileInputRef, onFileInputChange } = useFileManagerContext();
const HiddenFileInput: React.FC<HiddenFileInputProps> = ({ fileInputRef, onFileInputChange }) => {
return (
<input
ref={fileInputRef}
type="file"
multiple={true}
accept={["*/*"].join(',')}
accept="*/*"
onChange={onFileInputChange}
style={{ display: 'none' }}
data-testid="file-input"

View File

@ -1,107 +1,47 @@
import React from 'react';
import { Stack, Box } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { FileWithUrl } from '../../../types/file';
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 MobileLayoutProps {
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;
modalHeight: string;
fileInputRef: React.RefObject<HTMLInputElement>;
onFileInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
const MobileLayout: React.FC<MobileLayoutProps> = ({
activeSource,
onSourceChange,
onLocalFileClick,
selectedFiles,
onOpenFiles,
searchTerm,
onSearchChange,
recentFiles,
filteredFiles,
selectedFileIds,
onFileSelect,
onFileRemove,
onFileDoubleClick,
isFileSupported,
modalHeight,
fileInputRef,
onFileInputChange,
}) => {
const { t } = useTranslation();
const MobileLayout: React.FC = () => {
const {
activeSource,
selectedFiles,
modalHeight,
} = useFileManagerContext();
return (
<Stack h="100%" gap="sm" p="sm">
{/* Section 1: File Sources - Fixed at top */}
<Box style={{ flexShrink: 0 }}>
<FileSourceButtons
activeSource={activeSource}
onSourceChange={onSourceChange}
onLocalFileClick={onLocalFileClick}
horizontal={true}
/>
<FileSourceButtons horizontal={true} />
</Box>
<Box style={{ flexShrink: 0 }}>
<FileDetails
selectedFiles={selectedFiles}
onOpenFiles={onOpenFiles}
compact={true}
modalHeight={modalHeight}
/>
<FileDetails compact={true} />
</Box>
{/* Section 3: Search Bar - Fixed above file list */}
{activeSource === 'recent' && (
<Box style={{ flexShrink: 0 }}>
<SearchInput
value={searchTerm}
onChange={onSearchChange}
/>
<SearchInput />
</Box>
)}
{/* Section 4: File List - Fixed height scrollable area */}
<Box style={{ flexShrink: 0 }}>
<FileListArea
activeSource={activeSource}
recentFiles={recentFiles}
filteredFiles={filteredFiles}
selectedFileIds={selectedFileIds}
onFileSelect={onFileSelect}
onFileRemove={onFileRemove}
onFileDoubleClick={onFileDoubleClick}
isFileSupported={isFileSupported}
scrollAreaHeight={`calc(${modalHeight} - ${selectedFiles.length > 0 ? '300px' : '200px'})`}
scrollAreaStyle={{ maxHeight: '400px', minHeight: '150px' }}
/>
</Box>
{/* Hidden file input for local file selection */}
<HiddenFileInput
fileInputRef={fileInputRef}
onFileInputChange={onFileInputChange}
/>
<HiddenFileInput />
</Stack>
);
};

View File

@ -2,22 +2,22 @@ import React from 'react';
import { TextInput } from '@mantine/core';
import SearchIcon from '@mui/icons-material/Search';
import { useTranslation } from 'react-i18next';
import { useFileManagerContext } from './FileManagerContext';
interface SearchInputProps {
value: string;
onChange: (value: string) => void;
style?: React.CSSProperties;
}
const SearchInput: React.FC<SearchInputProps> = ({ value, onChange, style }) => {
const SearchInput: React.FC<SearchInputProps> = ({ style }) => {
const { t } = useTranslation();
const { searchTerm, onSearchChange } = useFileManagerContext();
return (
<TextInput
placeholder={t('fileManager.searchFiles', 'Search files...')}
leftSection={<SearchIcon />}
value={value}
onChange={(e) => onChange(e.target.value)}
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
style={style}
/>
);

View File

@ -11,13 +11,3 @@ export interface FileListItemProps {
onDoubleClick?: () => void;
}
export interface FileDetailsProps {
selectedFiles: FileWithUrl[];
onOpenFiles: () => void;
}
export interface FileSourceButtonsProps {
activeSource: FileSource;
onSourceChange: (source: FileSource) => void;
onLocalFileClick: () => void;
}