710e7f35ed
Co-authored-by: Douwe Osinga <douwe@squareup.com>
139 lines
4.1 KiB
TypeScript
139 lines
4.1 KiB
TypeScript
import { useCallback, useState, useRef, useEffect } from 'react';
|
|
import { compressImageDataUrl, errorMessage } from '../utils/conversionUtils';
|
|
|
|
export interface DroppedFile {
|
|
id: string;
|
|
path: string;
|
|
name: string;
|
|
type: string;
|
|
isImage: boolean;
|
|
dataUrl?: string;
|
|
isLoading?: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export const useFileDrop = () => {
|
|
const [droppedFiles, setDroppedFiles] = useState<DroppedFile[]>([]);
|
|
const activeReadersRef = useRef<Set<FileReader>>(new Set());
|
|
|
|
// Cleanup effect to prevent memory leaks
|
|
useEffect(() => {
|
|
return () => {
|
|
// Abort any active FileReaders on unmount
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
const readers = activeReadersRef.current;
|
|
readers.forEach((reader) => {
|
|
try {
|
|
reader.abort();
|
|
} catch {
|
|
// Reader might already be done, ignore errors
|
|
}
|
|
});
|
|
readers.clear();
|
|
};
|
|
}, []);
|
|
|
|
const handleDrop = useCallback(async (e: React.DragEvent<HTMLDivElement>) => {
|
|
e.preventDefault();
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
const droppedFileObjects: DroppedFile[] = [];
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
|
|
let droppedFile: DroppedFile;
|
|
|
|
try {
|
|
const path = window.electron.getPathForFile(file);
|
|
const isImage = file.type.startsWith('image/');
|
|
|
|
droppedFile = {
|
|
id: `dropped-${Date.now()}-${i}`,
|
|
path,
|
|
name: file.name,
|
|
type: file.type,
|
|
isImage,
|
|
isLoading: isImage, // Only images need loading state for preview generation
|
|
};
|
|
} catch (error) {
|
|
console.error('Error processing file:', file.name, error);
|
|
// Create an error file object
|
|
droppedFile = {
|
|
id: `dropped-error-${Date.now()}-${i}`,
|
|
path: '',
|
|
name: file.name,
|
|
type: file.type,
|
|
isImage: false,
|
|
isLoading: false,
|
|
error: `Failed to get file path: ${errorMessage(error, 'Unknown error')}`,
|
|
};
|
|
}
|
|
|
|
droppedFileObjects.push(droppedFile);
|
|
|
|
if (droppedFile.isImage && !droppedFile.error) {
|
|
const reader = new FileReader();
|
|
activeReadersRef.current.add(reader);
|
|
|
|
reader.onload = async (event) => {
|
|
const dataUrl = event.target?.result as string;
|
|
try {
|
|
// Compress the image
|
|
const compressedDataUrl = await compressImageDataUrl(dataUrl);
|
|
setDroppedFiles((prev) =>
|
|
prev.map((f) =>
|
|
f.id === droppedFile.id
|
|
? { ...f, dataUrl: compressedDataUrl, isLoading: false }
|
|
: f
|
|
)
|
|
);
|
|
} catch (compressionError) {
|
|
console.error('Failed to compress image:', file.name, compressionError);
|
|
setDroppedFiles((prev) =>
|
|
prev.map((f) =>
|
|
f.id === droppedFile.id
|
|
? { ...f, error: 'Failed to compress image', isLoading: false }
|
|
: f
|
|
)
|
|
);
|
|
}
|
|
activeReadersRef.current.delete(reader);
|
|
};
|
|
|
|
reader.onerror = () => {
|
|
console.error('Failed to read image:', file.name);
|
|
setDroppedFiles((prev) =>
|
|
prev.map((f) =>
|
|
f.id === droppedFile.id
|
|
? { ...f, error: 'Failed to load image', isLoading: false }
|
|
: f
|
|
)
|
|
);
|
|
activeReadersRef.current.delete(reader);
|
|
};
|
|
|
|
reader.onabort = () => {
|
|
activeReadersRef.current.delete(reader);
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
}
|
|
}
|
|
|
|
setDroppedFiles((prev) => [...prev, ...droppedFileObjects]);
|
|
}
|
|
}, []);
|
|
|
|
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
|
e.preventDefault();
|
|
}, []);
|
|
|
|
return {
|
|
droppedFiles,
|
|
setDroppedFiles,
|
|
handleDrop,
|
|
handleDragOver,
|
|
};
|
|
};
|