mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Select all, delete, download
This commit is contained in:
parent
d1cb3f0b30
commit
0ba9bb4733
@ -4,6 +4,7 @@ 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 FileActions from './FileActions';
|
||||||
import HiddenFileInput from './HiddenFileInput';
|
import HiddenFileInput from './HiddenFileInput';
|
||||||
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
||||||
|
|
||||||
@ -45,12 +46,20 @@ const DesktopLayout: React.FC = () => {
|
|||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}>
|
}}>
|
||||||
{activeSource === 'recent' && (
|
{activeSource === 'recent' && (
|
||||||
<div style={{
|
<>
|
||||||
flexShrink: 0,
|
<div style={{
|
||||||
borderBottom: '1px solid var(--mantine-color-gray-3)'
|
flexShrink: 0,
|
||||||
}}>
|
borderBottom: '1px solid var(--mantine-color-gray-3)'
|
||||||
<SearchInput />
|
}}>
|
||||||
</div>
|
<SearchInput />
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
borderBottom: '1px solid var(--mantine-color-gray-3)'
|
||||||
|
}}>
|
||||||
|
<FileActions />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ flex: 1, minHeight: 0 }}>
|
<div style={{ flex: 1, minHeight: 0 }}>
|
||||||
|
120
frontend/src/components/fileManager/FileActions.tsx
Normal file
120
frontend/src/components/fileManager/FileActions.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Group, Button, Text, Tooltip, ActionIcon } from '@mantine/core';
|
||||||
|
import SelectAllIcon from '@mui/icons-material/SelectAll';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
||||||
|
|
||||||
|
const FileActions: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
recentFiles,
|
||||||
|
selectedFileIds,
|
||||||
|
filteredFiles,
|
||||||
|
onSelectAll,
|
||||||
|
onDeleteSelected,
|
||||||
|
onDownloadSelected,
|
||||||
|
} = useFileManagerContext();
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
onSelectAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteSelected = () => {
|
||||||
|
if (selectedFileIds.length > 0) {
|
||||||
|
onDeleteSelected();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadSelected = () => {
|
||||||
|
if (selectedFileIds.length > 0) {
|
||||||
|
onDownloadSelected();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only show actions if there are files
|
||||||
|
if (recentFiles.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allFilesSelected = filteredFiles.length > 0 && selectedFileIds.length === filteredFiles.length;
|
||||||
|
const hasSelection = selectedFileIds.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
padding: '0.75rem 1rem',
|
||||||
|
backgroundColor: 'var(--mantine-color-gray-0)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
minHeight: '3rem',
|
||||||
|
position: 'relative',
|
||||||
|
}}>
|
||||||
|
{/* Left: Select All */}
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
label={allFilesSelected
|
||||||
|
? t('fileManager.deselectAll', 'Deselect All')
|
||||||
|
: t('fileManager.selectAll', 'Select All')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
color='dimmed'
|
||||||
|
onClick={handleSelectAll}
|
||||||
|
disabled={filteredFiles.length === 0}
|
||||||
|
radius="sm"
|
||||||
|
>
|
||||||
|
<SelectAllIcon style={{ fontSize: '1rem' }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Center: Selected count */}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
}}>
|
||||||
|
{hasSelection && (
|
||||||
|
<Text size="sm" c="dimmed" fw={500}>
|
||||||
|
{t('fileManager.selectedCount', '{{count}} selected', { count: selectedFileIds.length })}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Delete and Download */}
|
||||||
|
<Group gap="xs">
|
||||||
|
<Tooltip label={t('fileManager.deleteSelected', 'Delete Selected')}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
color="dimmed"
|
||||||
|
onClick={handleDeleteSelected}
|
||||||
|
disabled={!hasSelection}
|
||||||
|
radius="sm"
|
||||||
|
>
|
||||||
|
<DeleteIcon style={{ fontSize: '1rem' }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label={t('fileManager.downloadSelected', 'Download Selected')}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
color="dimmed"
|
||||||
|
onClick={handleDownloadSelected}
|
||||||
|
disabled={!hasSelection}
|
||||||
|
radius="sm"
|
||||||
|
>
|
||||||
|
<DownloadIcon style={{ fontSize: '1rem' }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileActions;
|
@ -23,6 +23,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
|||||||
onFileSelect,
|
onFileSelect,
|
||||||
onFileRemove,
|
onFileRemove,
|
||||||
onFileDoubleClick,
|
onFileDoubleClick,
|
||||||
|
onDownloadSingle,
|
||||||
isFileSupported,
|
isFileSupported,
|
||||||
} = useFileManagerContext();
|
} = useFileManagerContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -57,6 +58,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
|||||||
isSupported={isFileSupported(file.name)}
|
isSupported={isFileSupported(file.name)}
|
||||||
onSelect={() => onFileSelect(file)}
|
onSelect={() => onFileSelect(file)}
|
||||||
onRemove={() => onFileRemove(index)}
|
onRemove={() => onFileRemove(index)}
|
||||||
|
onDownload={() => onDownloadSingle(file)}
|
||||||
onDoubleClick={() => onFileDoubleClick(file)}
|
onDoubleClick={() => onFileDoubleClick(file)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Group, Box, Text, ActionIcon, Checkbox, Divider } from '@mantine/core';
|
import { Group, Box, Text, ActionIcon, Checkbox, Divider, Menu } from '@mantine/core';
|
||||||
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { getFileSize, getFileDate } from '../../utils/fileUtils';
|
import { getFileSize, getFileDate } from '../../utils/fileUtils';
|
||||||
import { FileWithUrl } from '../../types/file';
|
import { FileWithUrl } from '../../types/file';
|
||||||
|
|
||||||
@ -10,6 +13,7 @@ interface FileListItemProps {
|
|||||||
isSupported: boolean;
|
isSupported: boolean;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
|
onDownload?: () => void;
|
||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
}
|
}
|
||||||
@ -19,10 +23,16 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
|||||||
isSelected,
|
isSelected,
|
||||||
isSupported,
|
isSupported,
|
||||||
onSelect,
|
onSelect,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
onDownload,
|
||||||
onDoubleClick
|
onDoubleClick
|
||||||
}) => {
|
}) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// Keep item in hovered state if menu is open
|
||||||
|
const shouldShowHovered = isHovered || isMenuOpen;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -30,7 +40,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
|||||||
p="sm"
|
p="sm"
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
backgroundColor: isSelected ? 'var(--mantine-color-gray-0)' : (isHovered ? 'var(--mantine-color-gray-0)' : 'var(--bg-file-list)'),
|
backgroundColor: isSelected ? 'var(--mantine-color-gray-0)' : (shouldShowHovered ? 'var(--mantine-color-gray-0)' : 'var(--bg-file-list)'),
|
||||||
opacity: isSupported ? 1 : 0.5,
|
opacity: isSupported ? 1 : 0.5,
|
||||||
transition: 'background-color 0.15s ease'
|
transition: 'background-color 0.15s ease'
|
||||||
}}
|
}}
|
||||||
@ -59,21 +69,54 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
|||||||
<Text size="sm" fw={500} truncate>{file.name}</Text>
|
<Text size="sm" fw={500} truncate>{file.name}</Text>
|
||||||
<Text size="xs" c="dimmed">{getFileSize(file)} • {getFileDate(file)}</Text>
|
<Text size="xs" c="dimmed">{getFileSize(file)} • {getFileDate(file)}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
{/* Delete button - fades in/out on hover */}
|
|
||||||
<ActionIcon
|
{/* Three dots menu - fades in/out on hover */}
|
||||||
variant="subtle"
|
<Menu
|
||||||
c="dimmed"
|
position="bottom-end"
|
||||||
size="md"
|
withinPortal
|
||||||
onClick={(e) => { e.stopPropagation(); onRemove(); }}
|
onOpen={() => setIsMenuOpen(true)}
|
||||||
style={{
|
onClose={() => setIsMenuOpen(false)}
|
||||||
opacity: isHovered ? 1 : 0,
|
|
||||||
transform: isHovered ? 'scale(1)' : 'scale(0.8)',
|
|
||||||
transition: 'opacity 0.3s ease, transform 0.3s ease',
|
|
||||||
pointerEvents: isHovered ? 'auto' : 'none'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DeleteIcon style={{ fontSize: 20 }} />
|
<Menu.Target>
|
||||||
</ActionIcon>
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
c="dimmed"
|
||||||
|
size="md"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
opacity: shouldShowHovered ? 1 : 0,
|
||||||
|
transform: shouldShowHovered ? 'scale(1)' : 'scale(0.8)',
|
||||||
|
transition: 'opacity 0.3s ease, transform 0.3s ease',
|
||||||
|
pointerEvents: shouldShowHovered ? 'auto' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoreVertIcon style={{ fontSize: 20 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
|
||||||
|
<Menu.Dropdown>
|
||||||
|
{onDownload && (
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<DownloadIcon style={{ fontSize: 16 }} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDownload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('fileManager.download', 'Download')}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<DeleteIcon style={{ fontSize: 16 }} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemove();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('fileManager.delete', 'Delete')}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
{ <Divider color="var(--mantine-color-gray-3)" />}
|
{ <Divider color="var(--mantine-color-gray-3)" />}
|
||||||
|
@ -4,6 +4,7 @@ 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 FileActions from './FileActions';
|
||||||
import HiddenFileInput from './HiddenFileInput';
|
import HiddenFileInput from './HiddenFileInput';
|
||||||
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
||||||
|
|
||||||
@ -22,10 +23,11 @@ const MobileLayout: React.FC = () => {
|
|||||||
// Estimate heights of fixed components
|
// Estimate heights of fixed components
|
||||||
const fileSourceHeight = '3rem'; // FileSourceButtons height
|
const fileSourceHeight = '3rem'; // FileSourceButtons height
|
||||||
const fileDetailsHeight = selectedFiles.length > 0 ? '10rem' : '8rem'; // FileDetails compact height
|
const fileDetailsHeight = selectedFiles.length > 0 ? '10rem' : '8rem'; // FileDetails compact height
|
||||||
|
const fileActionsHeight = activeSource === 'recent' ? '3rem' : '0rem'; // FileActions height (now at bottom)
|
||||||
const searchHeight = activeSource === 'recent' ? '3rem' : '0rem'; // SearchInput height
|
const searchHeight = activeSource === 'recent' ? '3rem' : '0rem'; // SearchInput height
|
||||||
const gapHeight = activeSource === 'recent' ? '3rem' : '2rem'; // Stack gaps
|
const gapHeight = activeSource === 'recent' ? '3.75rem' : '2rem'; // Stack gaps
|
||||||
|
|
||||||
return `calc(${baseHeight} - ${fileSourceHeight} - ${fileDetailsHeight} - ${searchHeight} - ${gapHeight})`;
|
return `calc(${baseHeight} - ${fileSourceHeight} - ${fileDetailsHeight} - ${fileActionsHeight} - ${searchHeight} - ${gapHeight})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -51,12 +53,20 @@ const MobileLayout: React.FC = () => {
|
|||||||
minHeight: 0
|
minHeight: 0
|
||||||
}}>
|
}}>
|
||||||
{activeSource === 'recent' && (
|
{activeSource === 'recent' && (
|
||||||
<Box style={{
|
<>
|
||||||
flexShrink: 0,
|
<Box style={{
|
||||||
borderBottom: '1px solid var(--mantine-color-gray-2)'
|
flexShrink: 0,
|
||||||
}}>
|
borderBottom: '1px solid var(--mantine-color-gray-2)'
|
||||||
<SearchInput />
|
}}>
|
||||||
</Box>
|
<SearchInput />
|
||||||
|
</Box>
|
||||||
|
<Box style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
borderBottom: '1px solid var(--mantine-color-gray-2)'
|
||||||
|
}}>
|
||||||
|
<FileActions />
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box style={{ flex: 1, minHeight: 0 }}>
|
<Box style={{ flex: 1, minHeight: 0 }}>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react';
|
||||||
import { FileWithUrl } from '../types/file';
|
import { FileWithUrl } from '../types/file';
|
||||||
import { StoredFile } from '../services/fileStorage';
|
import { StoredFile, fileStorage } from '../services/fileStorage';
|
||||||
|
|
||||||
// Type for the context value - now contains everything directly
|
// Type for the context value - now contains everything directly
|
||||||
interface FileManagerContextValue {
|
interface FileManagerContextValue {
|
||||||
@ -21,6 +21,10 @@ interface FileManagerContextValue {
|
|||||||
onOpenFiles: () => void;
|
onOpenFiles: () => void;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
onFileInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
onFileInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
onSelectAll: () => void;
|
||||||
|
onDeleteSelected: () => void;
|
||||||
|
onDownloadSelected: () => void;
|
||||||
|
onDownloadSingle: (file: FileWithUrl) => void;
|
||||||
|
|
||||||
// External props
|
// External props
|
||||||
recentFiles: FileWithUrl[];
|
recentFiles: FileWithUrl[];
|
||||||
@ -152,6 +156,140 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
}, [storeFile, onFilesSelected, refreshRecentFiles, onClose]);
|
}, [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
|
// Cleanup blob URLs when component unmounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@ -190,6 +328,10 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
onOpenFiles: handleOpenFiles,
|
onOpenFiles: handleOpenFiles,
|
||||||
onSearchChange: handleSearchChange,
|
onSearchChange: handleSearchChange,
|
||||||
onFileInputChange: handleFileInputChange,
|
onFileInputChange: handleFileInputChange,
|
||||||
|
onSelectAll: handleSelectAll,
|
||||||
|
onDeleteSelected: handleDeleteSelected,
|
||||||
|
onDownloadSelected: handleDownloadSelected,
|
||||||
|
onDownloadSingle: handleDownloadSingle,
|
||||||
|
|
||||||
// External props
|
// External props
|
||||||
recentFiles,
|
recentFiles,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user