mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Download utils, shift click selections
This commit is contained in:
parent
0ba9bb4733
commit
fc9c4fdb7f
@ -1989,7 +1989,14 @@
|
|||||||
"fileSize": "Size",
|
"fileSize": "Size",
|
||||||
"fileVersion": "Version",
|
"fileVersion": "Version",
|
||||||
"totalSelected": "Total Selected",
|
"totalSelected": "Total Selected",
|
||||||
"dropFilesHere": "Drop files here"
|
"dropFilesHere": "Drop files here",
|
||||||
|
"selectAll": "Select All",
|
||||||
|
"deselectAll": "Deselect All",
|
||||||
|
"deleteSelected": "Delete Selected",
|
||||||
|
"downloadSelected": "Download Selected",
|
||||||
|
"selectedCount": "{{count}} selected",
|
||||||
|
"download": "Download",
|
||||||
|
"delete": "Delete"
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically",
|
"temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically",
|
||||||
|
@ -19,7 +19,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
|||||||
activeSource,
|
activeSource,
|
||||||
recentFiles,
|
recentFiles,
|
||||||
filteredFiles,
|
filteredFiles,
|
||||||
selectedFileIds,
|
selectedFilesSet,
|
||||||
onFileSelect,
|
onFileSelect,
|
||||||
onFileRemove,
|
onFileRemove,
|
||||||
onFileDoubleClick,
|
onFileDoubleClick,
|
||||||
@ -54,9 +54,9 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
|||||||
<FileListItem
|
<FileListItem
|
||||||
key={file.id || file.name}
|
key={file.id || file.name}
|
||||||
file={file}
|
file={file}
|
||||||
isSelected={selectedFileIds.includes(file.id || file.name)}
|
isSelected={selectedFilesSet.has(file.id || file.name)}
|
||||||
isSupported={isFileSupported(file.name)}
|
isSupported={isFileSupported(file.name)}
|
||||||
onSelect={() => onFileSelect(file)}
|
onSelect={(shiftKey) => onFileSelect(file, index, shiftKey)}
|
||||||
onRemove={() => onFileRemove(index)}
|
onRemove={() => onFileRemove(index)}
|
||||||
onDownload={() => onDownloadSingle(file)}
|
onDownload={() => onDownloadSingle(file)}
|
||||||
onDoubleClick={() => onFileDoubleClick(file)}
|
onDoubleClick={() => onFileDoubleClick(file)}
|
||||||
|
@ -11,7 +11,7 @@ interface FileListItemProps {
|
|||||||
file: FileWithUrl;
|
file: FileWithUrl;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isSupported: boolean;
|
isSupported: boolean;
|
||||||
onSelect: () => void;
|
onSelect: (shiftKey?: boolean) => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onDownload?: () => void;
|
onDownload?: () => void;
|
||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
@ -42,9 +42,13 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
backgroundColor: isSelected ? 'var(--mantine-color-gray-0)' : (shouldShowHovered ? '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',
|
||||||
|
userSelect: 'none',
|
||||||
|
WebkitUserSelect: 'none',
|
||||||
|
MozUserSelect: 'none',
|
||||||
|
msUserSelect: 'none'
|
||||||
}}
|
}}
|
||||||
onClick={onSelect}
|
onClick={(e) => onSelect(e.shiftKey)}
|
||||||
onDoubleClick={onDoubleClick}
|
onDoubleClick={onDoubleClick}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
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, fileStorage } from '../services/fileStorage';
|
import { StoredFile, fileStorage } from '../services/fileStorage';
|
||||||
|
import { downloadFiles } from '../utils/downloadUtils';
|
||||||
|
|
||||||
// Type for the context value - now contains everything directly
|
// Type for the context value - now contains everything directly
|
||||||
interface FileManagerContextValue {
|
interface FileManagerContextValue {
|
||||||
@ -11,11 +12,12 @@ interface FileManagerContextValue {
|
|||||||
selectedFiles: FileWithUrl[];
|
selectedFiles: FileWithUrl[];
|
||||||
filteredFiles: FileWithUrl[];
|
filteredFiles: FileWithUrl[];
|
||||||
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||||
|
selectedFilesSet: Set<string>;
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
onSourceChange: (source: 'recent' | 'local' | 'drive') => void;
|
onSourceChange: (source: 'recent' | 'local' | 'drive') => void;
|
||||||
onLocalFileClick: () => void;
|
onLocalFileClick: () => void;
|
||||||
onFileSelect: (file: FileWithUrl) => void;
|
onFileSelect: (file: FileWithUrl, index: number, shiftKey?: boolean) => void;
|
||||||
onFileRemove: (index: number) => void;
|
onFileRemove: (index: number) => void;
|
||||||
onFileDoubleClick: (file: FileWithUrl) => void;
|
onFileDoubleClick: (file: FileWithUrl) => void;
|
||||||
onOpenFiles: () => void;
|
onOpenFiles: () => void;
|
||||||
@ -64,22 +66,29 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
const [activeSource, setActiveSource] = useState<'recent' | 'local' | 'drive'>('recent');
|
const [activeSource, setActiveSource] = useState<'recent' | 'local' | 'drive'>('recent');
|
||||||
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
|
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [lastClickedIndex, setLastClickedIndex] = useState<number | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Track blob URLs for cleanup
|
// Track blob URLs for cleanup
|
||||||
const createdBlobUrls = useRef<Set<string>>(new Set());
|
const createdBlobUrls = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
// Computed values (with null safety)
|
// Computed values (with null safety)
|
||||||
const selectedFiles = (recentFiles || []).filter(file => selectedFileIds.includes(file.id || file.name));
|
const selectedFilesSet = new Set(selectedFileIds);
|
||||||
const filteredFiles = (recentFiles || []).filter(file =>
|
|
||||||
file.name.toLowerCase().includes(searchTerm.toLowerCase())
|
const selectedFiles = selectedFileIds.length === 0 ? [] :
|
||||||
);
|
(recentFiles || []).filter(file => selectedFilesSet.has(file.id || file.name));
|
||||||
|
|
||||||
|
const filteredFiles = !searchTerm ? recentFiles || [] :
|
||||||
|
(recentFiles || []).filter(file =>
|
||||||
|
file.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
const handleSourceChange = useCallback((source: 'recent' | 'local' | 'drive') => {
|
const handleSourceChange = useCallback((source: 'recent' | 'local' | 'drive') => {
|
||||||
setActiveSource(source);
|
setActiveSource(source);
|
||||||
if (source !== 'recent') {
|
if (source !== 'recent') {
|
||||||
setSelectedFileIds([]);
|
setSelectedFileIds([]);
|
||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
|
setLastClickedIndex(null);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -87,19 +96,46 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleFileSelect = useCallback((file: FileWithUrl) => {
|
const handleFileSelect = useCallback((file: FileWithUrl, currentIndex: number, shiftKey?: boolean) => {
|
||||||
setSelectedFileIds(prev => {
|
const fileId = file.id || file.name;
|
||||||
if (file.id) {
|
if (!fileId) return;
|
||||||
if (prev.includes(file.id)) {
|
|
||||||
return prev.filter(id => id !== file.id);
|
if (shiftKey && lastClickedIndex !== null) {
|
||||||
} else {
|
// Range selection with shift-click
|
||||||
return [...prev, file.id];
|
const startIndex = Math.min(lastClickedIndex, currentIndex);
|
||||||
|
const endIndex = Math.max(lastClickedIndex, currentIndex);
|
||||||
|
|
||||||
|
setSelectedFileIds(prev => {
|
||||||
|
const selectedSet = new Set(prev);
|
||||||
|
|
||||||
|
// Add all files in the range to selection
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
const rangeFileId = filteredFiles[i]?.id || filteredFiles[i]?.name;
|
||||||
|
if (rangeFileId) {
|
||||||
|
selectedSet.add(rangeFileId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return prev;
|
return Array.from(selectedSet);
|
||||||
}
|
});
|
||||||
});
|
} else {
|
||||||
}, []);
|
// Normal click behavior - optimized with Set for O(1) lookup
|
||||||
|
setSelectedFileIds(prev => {
|
||||||
|
const selectedSet = new Set(prev);
|
||||||
|
|
||||||
|
if (selectedSet.has(fileId)) {
|
||||||
|
selectedSet.delete(fileId);
|
||||||
|
} else {
|
||||||
|
selectedSet.add(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(selectedSet);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update last clicked index for future range selections
|
||||||
|
setLastClickedIndex(currentIndex);
|
||||||
|
}
|
||||||
|
}, [filteredFiles, lastClickedIndex]);
|
||||||
|
|
||||||
const handleFileRemove = useCallback((index: number) => {
|
const handleFileRemove = useCallback((index: number) => {
|
||||||
const fileToRemove = filteredFiles[index];
|
const fileToRemove = filteredFiles[index];
|
||||||
@ -161,9 +197,11 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
if (allFilesSelected) {
|
if (allFilesSelected) {
|
||||||
// Deselect all
|
// Deselect all
|
||||||
setSelectedFileIds([]);
|
setSelectedFileIds([]);
|
||||||
|
setLastClickedIndex(null);
|
||||||
} else {
|
} else {
|
||||||
// Select all filtered files
|
// Select all filtered files
|
||||||
setSelectedFileIds(filteredFiles.map(file => file.id || file.name));
|
setSelectedFileIds(filteredFiles.map(file => file.id || file.name));
|
||||||
|
setLastClickedIndex(null);
|
||||||
}
|
}
|
||||||
}, [filteredFiles, selectedFileIds]);
|
}, [filteredFiles, selectedFileIds]);
|
||||||
|
|
||||||
@ -192,6 +230,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
}
|
}
|
||||||
}, [selectedFileIds, filteredFiles, refreshRecentFiles]);
|
}, [selectedFileIds, filteredFiles, refreshRecentFiles]);
|
||||||
|
|
||||||
|
|
||||||
const handleDownloadSelected = useCallback(async () => {
|
const handleDownloadSelected = useCallback(async () => {
|
||||||
if (selectedFileIds.length === 0) return;
|
if (selectedFileIds.length === 0) return;
|
||||||
|
|
||||||
@ -201,65 +240,10 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
selectedFileIds.includes(file.id || file.name)
|
selectedFileIds.includes(file.id || file.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedFilesToDownload.length === 1) {
|
// Use generic download utility
|
||||||
// Single file download
|
await downloadFiles(selectedFilesToDownload, {
|
||||||
const fileWithUrl = selectedFilesToDownload[0];
|
zipFilename: `selected-files-${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.zip`
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('Failed to download selected files:', error);
|
console.error('Failed to download selected files:', error);
|
||||||
}
|
}
|
||||||
@ -267,23 +251,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
|
|
||||||
const handleDownloadSingle = useCallback(async (file: FileWithUrl) => {
|
const handleDownloadSingle = useCallback(async (file: FileWithUrl) => {
|
||||||
try {
|
try {
|
||||||
const lookupKey = file.id || file.name;
|
await downloadFiles([file]);
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('Failed to download file:', error);
|
console.error('Failed to download file:', error);
|
||||||
}
|
}
|
||||||
@ -307,6 +275,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
setActiveSource('recent');
|
setActiveSource('recent');
|
||||||
setSelectedFileIds([]);
|
setSelectedFileIds([]);
|
||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
|
setLastClickedIndex(null);
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
@ -318,6 +287,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
selectedFiles,
|
selectedFiles,
|
||||||
filteredFiles,
|
filteredFiles,
|
||||||
fileInputRef,
|
fileInputRef,
|
||||||
|
selectedFilesSet,
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
onSourceChange: handleSourceChange,
|
onSourceChange: handleSourceChange,
|
||||||
|
152
frontend/src/utils/downloadUtils.ts
Normal file
152
frontend/src/utils/downloadUtils.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { FileWithUrl } from '../types/file';
|
||||||
|
import { fileStorage } from '../services/fileStorage';
|
||||||
|
import { zipFileService } from '../services/zipFileService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a blob as a file using browser download API
|
||||||
|
* @param blob - The blob to download
|
||||||
|
* @param filename - The filename for the download
|
||||||
|
*/
|
||||||
|
export function downloadBlob(blob: Blob, filename: string): void {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
// Clean up the blob URL
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a single file from IndexedDB storage
|
||||||
|
* @param file - The file object with storage information
|
||||||
|
* @throws Error if file cannot be retrieved from storage
|
||||||
|
*/
|
||||||
|
export async function downloadFileFromStorage(file: FileWithUrl): Promise<void> {
|
||||||
|
const lookupKey = file.id || file.name;
|
||||||
|
const storedFile = await fileStorage.getFile(lookupKey);
|
||||||
|
|
||||||
|
if (!storedFile) {
|
||||||
|
throw new Error(`File "${file.name}" not found in storage`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([storedFile.data], { type: storedFile.type });
|
||||||
|
downloadBlob(blob, storedFile.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads multiple files as individual downloads
|
||||||
|
* @param files - Array of files to download
|
||||||
|
*/
|
||||||
|
export async function downloadMultipleFiles(files: FileWithUrl[]): Promise<void> {
|
||||||
|
for (const file of files) {
|
||||||
|
await downloadFileFromStorage(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads multiple files as a single ZIP archive
|
||||||
|
* @param files - Array of files to include in ZIP
|
||||||
|
* @param zipFilename - Optional custom ZIP filename (defaults to timestamped name)
|
||||||
|
*/
|
||||||
|
export async function downloadFilesAsZip(files: FileWithUrl[], zipFilename?: string): Promise<void> {
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw new Error('No files provided for ZIP download');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert stored files to File objects
|
||||||
|
const fileObjects: File[] = [];
|
||||||
|
for (const fileWithUrl of files) {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
fileObjects.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileObjects.length === 0) {
|
||||||
|
throw new Error('No valid files found in storage for ZIP download');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate default filename if not provided
|
||||||
|
const finalZipFilename = zipFilename ||
|
||||||
|
`files-${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.zip`;
|
||||||
|
|
||||||
|
// Create and download ZIP
|
||||||
|
const { zipFile } = await zipFileService.createZipFromFiles(fileObjects, finalZipFilename);
|
||||||
|
downloadBlob(zipFile, finalZipFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smart download function that handles single or multiple files appropriately
|
||||||
|
* - Single file: Downloads directly
|
||||||
|
* - Multiple files: Downloads as ZIP
|
||||||
|
* @param files - Array of files to download
|
||||||
|
* @param options - Download options
|
||||||
|
*/
|
||||||
|
export async function downloadFiles(
|
||||||
|
files: FileWithUrl[],
|
||||||
|
options: {
|
||||||
|
forceZip?: boolean;
|
||||||
|
zipFilename?: string;
|
||||||
|
multipleAsIndividual?: boolean;
|
||||||
|
} = {}
|
||||||
|
): Promise<void> {
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw new Error('No files provided for download');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 1 && !options.forceZip) {
|
||||||
|
// Single file download
|
||||||
|
await downloadFileFromStorage(files[0]);
|
||||||
|
} else if (options.multipleAsIndividual) {
|
||||||
|
// Multiple individual downloads
|
||||||
|
await downloadMultipleFiles(files);
|
||||||
|
} else {
|
||||||
|
// ZIP download (default for multiple files)
|
||||||
|
await downloadFilesAsZip(files, options.zipFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a File object directly (for files already in memory)
|
||||||
|
* @param file - The File object to download
|
||||||
|
* @param filename - Optional custom filename
|
||||||
|
*/
|
||||||
|
export function downloadFileObject(file: File, filename?: string): void {
|
||||||
|
downloadBlob(file, filename || file.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads text content as a file
|
||||||
|
* @param content - Text content to download
|
||||||
|
* @param filename - Filename for the download
|
||||||
|
* @param mimeType - MIME type (defaults to text/plain)
|
||||||
|
*/
|
||||||
|
export function downloadTextAsFile(
|
||||||
|
content: string,
|
||||||
|
filename: string,
|
||||||
|
mimeType: string = 'text/plain'
|
||||||
|
): void {
|
||||||
|
const blob = new Blob([content], { type: mimeType });
|
||||||
|
downloadBlob(blob, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads JSON data as a file
|
||||||
|
* @param data - Data to serialize and download
|
||||||
|
* @param filename - Filename for the download
|
||||||
|
*/
|
||||||
|
export function downloadJsonAsFile(data: any, filename: string): void {
|
||||||
|
const content = JSON.stringify(data, null, 2);
|
||||||
|
downloadTextAsFile(content, filename, 'application/json');
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user