mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-20 13:47:46 +02:00
Changed to provider pattern to reduce prop drilling
This commit is contained in:
parent
af1643a5ae
commit
7e18cc275b
@ -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 { Modal } from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -9,7 +9,7 @@ import { Tool } from '../../types/tool';
|
|||||||
import MobileLayout from './fileManager/MobileLayout';
|
import MobileLayout from './fileManager/MobileLayout';
|
||||||
import DesktopLayout from './fileManager/DesktopLayout';
|
import DesktopLayout from './fileManager/DesktopLayout';
|
||||||
import DragOverlay from './fileManager/DragOverlay';
|
import DragOverlay from './fileManager/DragOverlay';
|
||||||
import { FileSource } from './fileManager/types';
|
import { FileManagerProvider } from './fileManager/FileManagerContext';
|
||||||
|
|
||||||
interface FileManagerProps {
|
interface FileManagerProps {
|
||||||
selectedTool?: Tool | null;
|
selectedTool?: Tool | null;
|
||||||
@ -18,10 +18,6 @@ interface FileManagerProps {
|
|||||||
const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isFilesModalOpen, closeFilesModal, onFileSelect, onFilesSelect } = useFilesModalContext();
|
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 [recentFiles, setRecentFiles] = useState<FileWithUrl[]>([]);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
@ -40,31 +36,21 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
setRecentFiles(files);
|
setRecentFiles(files);
|
||||||
}, [loadRecentFiles]);
|
}, [loadRecentFiles]);
|
||||||
|
|
||||||
const openFileDialog = useCallback(() => {
|
const handleFilesSelected = useCallback(async (files: FileWithUrl[]) => {
|
||||||
fileInputRef.current?.click();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleFileInputChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const files = Array.from(event.target.files || []);
|
|
||||||
if (files.length > 0) {
|
|
||||||
try {
|
try {
|
||||||
// Store files in IndexedDB and get FileWithUrl objects
|
const fileObjects = await Promise.all(
|
||||||
const storedFiles = await Promise.all(
|
files.map(async (fileWithUrl) => {
|
||||||
files.map(async (file) => {
|
if (fileWithUrl.file) {
|
||||||
await storeFile(file);
|
return fileWithUrl.file;
|
||||||
return file;
|
}
|
||||||
|
return await convertToFile(fileWithUrl);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
onFilesSelect(fileObjects);
|
||||||
onFilesSelect(storedFiles);
|
|
||||||
await refreshRecentFiles();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to process uploaded files:', error);
|
console.error('Failed to process selected files:', error);
|
||||||
}
|
}
|
||||||
}
|
}, [convertToFile, onFilesSelect]);
|
||||||
// Clear the input
|
|
||||||
event.target.value = '';
|
|
||||||
}, [storeFile, onFilesSelect, refreshRecentFiles]);
|
|
||||||
|
|
||||||
const handleNewFileUpload = useCallback(async (files: File[]) => {
|
const handleNewFileUpload = useCallback(async (files: File[]) => {
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
@ -79,69 +65,8 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
}
|
}
|
||||||
}, [storeFile, onFilesSelect, refreshRecentFiles]);
|
}, [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) => {
|
const handleRemoveFileByIndex = useCallback(async (index: number) => {
|
||||||
await handleRemoveFile(index, recentFiles, setRecentFiles);
|
await handleRemoveFile(index, recentFiles, setRecentFiles);
|
||||||
const file = recentFiles[index];
|
|
||||||
setSelectedFileIds(prev => prev.filter(id => id !== (file.id || file.name)));
|
|
||||||
}, [handleRemoveFile, recentFiles]);
|
}, [handleRemoveFile, recentFiles]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -156,13 +81,22 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
refreshRecentFiles();
|
refreshRecentFiles();
|
||||||
} else {
|
} else {
|
||||||
// Reset state when modal is closed
|
// Reset state when modal is closed
|
||||||
setActiveSource('recent');
|
|
||||||
setSearchTerm('');
|
|
||||||
setSelectedFileIds([]);
|
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
}
|
}
|
||||||
}, [isFilesModalOpen, refreshRecentFiles]);
|
}, [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
|
// Modal size constants for consistent scaling
|
||||||
const modalHeight = '80vh';
|
const modalHeight = '80vh';
|
||||||
const modalWidth = isMobile ? '100%' : '60vw';
|
const modalWidth = isMobile ? '100%' : '60vw';
|
||||||
@ -217,47 +151,17 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
inner: { pointerEvents: 'all' }
|
inner: { pointerEvents: 'all' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isMobile ? (
|
<FileManagerProvider
|
||||||
<MobileLayout
|
|
||||||
activeSource={activeSource}
|
|
||||||
onSourceChange={setActiveSource}
|
|
||||||
onLocalFileClick={openFileDialog}
|
|
||||||
selectedFiles={selectedFiles}
|
|
||||||
onOpenFiles={handleOpenFiles}
|
|
||||||
searchTerm={searchTerm}
|
|
||||||
onSearchChange={setSearchTerm}
|
|
||||||
recentFiles={recentFiles}
|
recentFiles={recentFiles}
|
||||||
filteredFiles={filteredFiles}
|
onFilesSelected={handleFilesSelected}
|
||||||
selectedFileIds={selectedFileIds}
|
onClose={closeFilesModal}
|
||||||
onFileSelect={handleFileSelect}
|
|
||||||
onFileRemove={handleRemoveFileByIndex}
|
|
||||||
onFileDoubleClick={handleFileDoubleClick}
|
|
||||||
isFileSupported={isFileSupported}
|
isFileSupported={isFileSupported}
|
||||||
modalHeight={modalHeight}
|
isOpen={isFilesModalOpen}
|
||||||
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}
|
onFileRemove={handleRemoveFileByIndex}
|
||||||
onFileDoubleClick={handleFileDoubleClick}
|
|
||||||
isFileSupported={isFileSupported}
|
|
||||||
fileInputRef={fileInputRef}
|
|
||||||
onFileInputChange={handleFileInputChange}
|
|
||||||
modalHeight={modalHeight}
|
modalHeight={modalHeight}
|
||||||
/>
|
>
|
||||||
)}
|
{isMobile ? <MobileLayout /> : <DesktopLayout />}
|
||||||
|
</FileManagerProvider>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
<DragOverlay isVisible={isDragging} />
|
<DragOverlay isVisible={isDragging} />
|
||||||
|
@ -1,54 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Grid, Center, Stack } from '@mantine/core';
|
import { Grid } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { FileWithUrl } from '../../../types/file';
|
|
||||||
import FileSourceButtons from './FileSourceButtons';
|
import FileSourceButtons from './FileSourceButtons';
|
||||||
import FileDetails from './FileDetails';
|
import FileDetails from './FileDetails';
|
||||||
import SearchInput from './SearchInput';
|
import SearchInput from './SearchInput';
|
||||||
import FileListArea from './FileListArea';
|
import FileListArea from './FileListArea';
|
||||||
import HiddenFileInput from './HiddenFileInput';
|
import HiddenFileInput from './HiddenFileInput';
|
||||||
import { FileSource } from './types';
|
import { useFileManagerContext } from './FileManagerContext';
|
||||||
|
|
||||||
interface DesktopLayoutProps {
|
const DesktopLayout: React.FC = () => {
|
||||||
activeSource: FileSource;
|
const {
|
||||||
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,
|
activeSource,
|
||||||
onSourceChange,
|
|
||||||
onLocalFileClick,
|
|
||||||
selectedFiles,
|
|
||||||
onOpenFiles,
|
|
||||||
searchTerm,
|
|
||||||
onSearchChange,
|
|
||||||
recentFiles,
|
recentFiles,
|
||||||
filteredFiles,
|
|
||||||
selectedFileIds,
|
|
||||||
onFileSelect,
|
|
||||||
onFileRemove,
|
|
||||||
onFileDoubleClick,
|
|
||||||
isFileSupported,
|
|
||||||
fileInputRef,
|
|
||||||
onFileInputChange,
|
|
||||||
modalHeight,
|
modalHeight,
|
||||||
}) => {
|
} = useFileManagerContext();
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid gutter="md" h="100%" grow={false} style={{ flexWrap: 'nowrap' }}>
|
<Grid gutter="md" h="100%" grow={false} style={{ flexWrap: 'nowrap' }}>
|
||||||
@ -59,33 +23,17 @@ const DesktopLayout: React.FC<DesktopLayoutProps> = ({
|
|||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
}}>
|
}}>
|
||||||
<FileSourceButtons
|
<FileSourceButtons />
|
||||||
activeSource={activeSource}
|
|
||||||
onSourceChange={onSourceChange}
|
|
||||||
onLocalFileClick={onLocalFileClick}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
{/* Column 2: File List */}
|
{/* Column 2: File List */}
|
||||||
<Grid.Col span="auto" style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>
|
<Grid.Col span="auto" style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>
|
||||||
{activeSource === 'recent' && (
|
{activeSource === 'recent' && (
|
||||||
<SearchInput
|
<SearchInput style={{ marginBottom: '1rem', flexShrink: 0 }} />
|
||||||
value={searchTerm}
|
|
||||||
onChange={onSearchChange}
|
|
||||||
style={{ marginBottom: '1rem', flexShrink: 0 }}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ flex: 1, minHeight: 0 }}>
|
<div style={{ flex: 1, minHeight: 0 }}>
|
||||||
<FileListArea
|
<FileListArea
|
||||||
activeSource={activeSource}
|
|
||||||
recentFiles={recentFiles}
|
|
||||||
filteredFiles={filteredFiles}
|
|
||||||
selectedFileIds={selectedFileIds}
|
|
||||||
onFileSelect={onFileSelect}
|
|
||||||
onFileRemove={onFileRemove}
|
|
||||||
onFileDoubleClick={onFileDoubleClick}
|
|
||||||
isFileSupported={isFileSupported}
|
|
||||||
scrollAreaHeight={`calc(${modalHeight} - 6rem)`}
|
scrollAreaHeight={`calc(${modalHeight} - 6rem)`}
|
||||||
scrollAreaStyle={{
|
scrollAreaStyle={{
|
||||||
height: activeSource === 'recent' && recentFiles.length > 0 ? `calc(${modalHeight} - 6rem)` : '100%'
|
height: activeSource === 'recent' && recentFiles.length > 0 ? `calc(${modalHeight} - 6rem)` : '100%'
|
||||||
@ -97,19 +45,12 @@ const DesktopLayout: React.FC<DesktopLayoutProps> = ({
|
|||||||
{/* Column 3: File Details */}
|
{/* Column 3: File Details */}
|
||||||
<Grid.Col span="content" style={{ minWidth: '20rem', width: '20rem', flexShrink: 0, height: '100%' }}>
|
<Grid.Col span="content" style={{ minWidth: '20rem', width: '20rem', flexShrink: 0, height: '100%' }}>
|
||||||
<div style={{ height: '100%' }}>
|
<div style={{ height: '100%' }}>
|
||||||
<FileDetails
|
<FileDetails />
|
||||||
selectedFiles={selectedFiles}
|
|
||||||
onOpenFiles={onOpenFiles}
|
|
||||||
modalHeight={modalHeight}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
{/* Hidden file input for local file selection */}
|
{/* Hidden file input for local file selection */}
|
||||||
<HiddenFileInput
|
<HiddenFileInput />
|
||||||
fileInputRef={fileInputRef}
|
|
||||||
onFileInputChange={onFileInputChange}
|
|
||||||
/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,14 +6,18 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { detectFileExtension, getFileSize } from '../../../utils/fileUtils';
|
import { detectFileExtension, getFileSize } from '../../../utils/fileUtils';
|
||||||
import { useIndexedDBThumbnail } from '../../../hooks/useIndexedDBThumbnail';
|
import { useIndexedDBThumbnail } from '../../../hooks/useIndexedDBThumbnail';
|
||||||
import { FileWithUrl } from '../../../types/file';
|
import { useFileManagerContext } from './FileManagerContext';
|
||||||
import { FileDetailsProps } from './types';
|
|
||||||
|
|
||||||
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 { t } = useTranslation();
|
||||||
const [currentFileIndex, setCurrentFileIndex] = useState(0);
|
const [currentFileIndex, setCurrentFileIndex] = useState(0);
|
||||||
const [thumbnailCache, setThumbnailCache] = useState<Record<string, string>>({});
|
|
||||||
const [loadingFile, setLoadingFile] = useState<FileWithUrl | null>(null);
|
|
||||||
const [isAnimating, setIsAnimating] = useState(false);
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
|
||||||
// Get the currently displayed file
|
// Get the currently displayed file
|
||||||
@ -21,62 +25,12 @@ const FileDetails: React.FC<FileDetailsProps & { compact?: boolean; modalHeight?
|
|||||||
const hasSelection = selectedFiles.length > 0;
|
const hasSelection = selectedFiles.length > 0;
|
||||||
const hasMultipleFiles = selectedFiles.length > 1;
|
const hasMultipleFiles = selectedFiles.length > 1;
|
||||||
|
|
||||||
// Only load thumbnail for files not in cache
|
// Use IndexedDB hook for the current file
|
||||||
const shouldLoadThumbnail = loadingFile && !thumbnailCache[loadingFile.id || loadingFile.name];
|
const { thumbnail: currentThumbnail } = useIndexedDBThumbnail(currentFile);
|
||||||
const { thumbnail } = useIndexedDBThumbnail(shouldLoadThumbnail ? loadingFile : ({} as FileWithUrl));
|
|
||||||
|
|
||||||
// Load thumbnails for all selected files
|
// Get thumbnail for current file
|
||||||
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
|
|
||||||
const getCurrentThumbnail = () => {
|
const getCurrentThumbnail = () => {
|
||||||
if (!currentFile) return null;
|
return currentThumbnail;
|
||||||
const fileId = currentFile.id || currentFile.name;
|
|
||||||
return thumbnailCache[fileId];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrevious = () => {
|
const handlePrevious = () => {
|
||||||
@ -372,7 +326,6 @@ const FileDetails: React.FC<FileDetailsProps & { compact?: boolean; modalHeight?
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Section 3: Action Button */}
|
|
||||||
<Button
|
<Button
|
||||||
size="md"
|
size="md"
|
||||||
onClick={onOpenFiles}
|
onClick={onOpenFiles}
|
||||||
|
@ -3,24 +3,19 @@ import { Center, ScrollArea, Text, Stack } from '@mantine/core';
|
|||||||
import CloudIcon from '@mui/icons-material/Cloud';
|
import CloudIcon from '@mui/icons-material/Cloud';
|
||||||
import HistoryIcon from '@mui/icons-material/History';
|
import HistoryIcon from '@mui/icons-material/History';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FileWithUrl } from '../../../types/file';
|
|
||||||
import FileListItem from './FileListItem';
|
import FileListItem from './FileListItem';
|
||||||
import { FileSource } from './types';
|
import { useFileManagerContext } from './FileManagerContext';
|
||||||
|
|
||||||
interface FileListAreaProps {
|
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;
|
scrollAreaHeight: string;
|
||||||
scrollAreaStyle?: React.CSSProperties;
|
scrollAreaStyle?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileListArea: React.FC<FileListAreaProps> = ({
|
const FileListArea: React.FC<FileListAreaProps> = ({
|
||||||
|
scrollAreaHeight,
|
||||||
|
scrollAreaStyle = {},
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
activeSource,
|
activeSource,
|
||||||
recentFiles,
|
recentFiles,
|
||||||
filteredFiles,
|
filteredFiles,
|
||||||
@ -29,9 +24,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
|||||||
onFileRemove,
|
onFileRemove,
|
||||||
onFileDoubleClick,
|
onFileDoubleClick,
|
||||||
isFileSupported,
|
isFileSupported,
|
||||||
scrollAreaHeight,
|
} = useFileManagerContext();
|
||||||
scrollAreaStyle = {},
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (activeSource === 'recent') {
|
if (activeSource === 'recent') {
|
||||||
|
@ -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 };
|
@ -4,14 +4,16 @@ import HistoryIcon from '@mui/icons-material/History';
|
|||||||
import FolderIcon from '@mui/icons-material/Folder';
|
import FolderIcon from '@mui/icons-material/Folder';
|
||||||
import CloudIcon from '@mui/icons-material/Cloud';
|
import CloudIcon from '@mui/icons-material/Cloud';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FileSourceButtonsProps } from './types';
|
import { useFileManagerContext } from './FileManagerContext';
|
||||||
|
|
||||||
const FileSourceButtons: React.FC<FileSourceButtonsProps & { horizontal?: boolean }> = ({
|
interface FileSourceButtonsProps {
|
||||||
activeSource,
|
horizontal?: boolean;
|
||||||
onSourceChange,
|
}
|
||||||
onLocalFileClick,
|
|
||||||
|
const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
|
||||||
horizontal = false
|
horizontal = false
|
||||||
}) => {
|
}) => {
|
||||||
|
const { activeSource, onSourceChange, onLocalFileClick } = useFileManagerContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const buttonProps = {
|
const buttonProps = {
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useFileManagerContext } from './FileManagerContext';
|
||||||
|
|
||||||
interface HiddenFileInputProps {
|
const HiddenFileInput: React.FC = () => {
|
||||||
fileInputRef: React.RefObject<HTMLInputElement>;
|
const { fileInputRef, onFileInputChange } = useFileManagerContext();
|
||||||
onFileInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HiddenFileInput: React.FC<HiddenFileInputProps> = ({ fileInputRef, onFileInputChange }) => {
|
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
multiple={true}
|
multiple={true}
|
||||||
accept={["*/*"].join(',')}
|
accept="*/*"
|
||||||
onChange={onFileInputChange}
|
onChange={onFileInputChange}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
data-testid="file-input"
|
data-testid="file-input"
|
||||||
|
@ -1,107 +1,47 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Stack, Box } from '@mantine/core';
|
import { Stack, Box } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { FileWithUrl } from '../../../types/file';
|
|
||||||
import FileSourceButtons from './FileSourceButtons';
|
import FileSourceButtons from './FileSourceButtons';
|
||||||
import FileDetails from './FileDetails';
|
import FileDetails from './FileDetails';
|
||||||
import SearchInput from './SearchInput';
|
import SearchInput from './SearchInput';
|
||||||
import FileListArea from './FileListArea';
|
import FileListArea from './FileListArea';
|
||||||
import HiddenFileInput from './HiddenFileInput';
|
import HiddenFileInput from './HiddenFileInput';
|
||||||
import { FileSource } from './types';
|
import { useFileManagerContext } from './FileManagerContext';
|
||||||
|
|
||||||
interface MobileLayoutProps {
|
const MobileLayout: React.FC = () => {
|
||||||
activeSource: FileSource;
|
const {
|
||||||
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,
|
activeSource,
|
||||||
onSourceChange,
|
|
||||||
onLocalFileClick,
|
|
||||||
selectedFiles,
|
selectedFiles,
|
||||||
onOpenFiles,
|
|
||||||
searchTerm,
|
|
||||||
onSearchChange,
|
|
||||||
recentFiles,
|
|
||||||
filteredFiles,
|
|
||||||
selectedFileIds,
|
|
||||||
onFileSelect,
|
|
||||||
onFileRemove,
|
|
||||||
onFileDoubleClick,
|
|
||||||
isFileSupported,
|
|
||||||
modalHeight,
|
modalHeight,
|
||||||
fileInputRef,
|
} = useFileManagerContext();
|
||||||
onFileInputChange,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack h="100%" gap="sm" p="sm">
|
<Stack h="100%" gap="sm" p="sm">
|
||||||
{/* Section 1: File Sources - Fixed at top */}
|
{/* Section 1: File Sources - Fixed at top */}
|
||||||
<Box style={{ flexShrink: 0 }}>
|
<Box style={{ flexShrink: 0 }}>
|
||||||
<FileSourceButtons
|
<FileSourceButtons horizontal={true} />
|
||||||
activeSource={activeSource}
|
|
||||||
onSourceChange={onSourceChange}
|
|
||||||
onLocalFileClick={onLocalFileClick}
|
|
||||||
horizontal={true}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box style={{ flexShrink: 0 }}>
|
<Box style={{ flexShrink: 0 }}>
|
||||||
<FileDetails
|
<FileDetails compact={true} />
|
||||||
selectedFiles={selectedFiles}
|
|
||||||
onOpenFiles={onOpenFiles}
|
|
||||||
compact={true}
|
|
||||||
modalHeight={modalHeight}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Section 3: Search Bar - Fixed above file list */}
|
{/* Section 3: Search Bar - Fixed above file list */}
|
||||||
{activeSource === 'recent' && (
|
{activeSource === 'recent' && (
|
||||||
<Box style={{ flexShrink: 0 }}>
|
<Box style={{ flexShrink: 0 }}>
|
||||||
<SearchInput
|
<SearchInput />
|
||||||
value={searchTerm}
|
|
||||||
onChange={onSearchChange}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Section 4: File List - Fixed height scrollable area */}
|
{/* Section 4: File List - Fixed height scrollable area */}
|
||||||
<Box style={{ flexShrink: 0 }}>
|
<Box style={{ flexShrink: 0 }}>
|
||||||
<FileListArea
|
<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'})`}
|
scrollAreaHeight={`calc(${modalHeight} - ${selectedFiles.length > 0 ? '300px' : '200px'})`}
|
||||||
scrollAreaStyle={{ maxHeight: '400px', minHeight: '150px' }}
|
scrollAreaStyle={{ maxHeight: '400px', minHeight: '150px' }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Hidden file input for local file selection */}
|
{/* Hidden file input for local file selection */}
|
||||||
<HiddenFileInput
|
<HiddenFileInput />
|
||||||
fileInputRef={fileInputRef}
|
|
||||||
onFileInputChange={onFileInputChange}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,22 +2,22 @@ import React from 'react';
|
|||||||
import { TextInput } from '@mantine/core';
|
import { TextInput } from '@mantine/core';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useFileManagerContext } from './FileManagerContext';
|
||||||
|
|
||||||
interface SearchInputProps {
|
interface SearchInputProps {
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchInput: React.FC<SearchInputProps> = ({ value, onChange, style }) => {
|
const SearchInput: React.FC<SearchInputProps> = ({ style }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { searchTerm, onSearchChange } = useFileManagerContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder={t('fileManager.searchFiles', 'Search files...')}
|
placeholder={t('fileManager.searchFiles', 'Search files...')}
|
||||||
leftSection={<SearchIcon />}
|
leftSection={<SearchIcon />}
|
||||||
value={value}
|
value={searchTerm}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onSearchChange(e.target.value)}
|
||||||
style={style}
|
style={style}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -11,13 +11,3 @@ export interface FileListItemProps {
|
|||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileDetailsProps {
|
|
||||||
selectedFiles: FileWithUrl[];
|
|
||||||
onOpenFiles: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileSourceButtonsProps {
|
|
||||||
activeSource: FileSource;
|
|
||||||
onSourceChange: (source: FileSource) => void;
|
|
||||||
onLocalFileClick: () => void;
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user