import { AnimatePresence, motion } from "framer-motion"; import { DragEvent, ReactNode, useCallback, useMemo, useState } from "react"; import { BsmImage } from "./bsm-image.component"; import BeatImpatient from "../../../../assets/images/apngs/beat-impatient.png"; import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; import Color from "color"; import { cn } from "renderer/helpers/css-class.helpers"; import { CloseIcon } from "../svgs/icons/close-icon.component"; import { OpenDialogOptions } from "electron"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import { useService } from "renderer/hooks/use-service.hook"; import { IpcService } from "renderer/services/ipc.service"; import { NotificationService } from "renderer/services/notification.service"; import { lastValueFrom } from "rxjs"; type Props = Readonly<{ children?: ReactNode; className?: string; open?: boolean; text?: string; subtext?: string; onFiles?: (paths: string[]) => void; onClose?: () => void; filters?: OpenDialogOptions["filters"]; dialogOptions?: { text?: string; dialog?: Exclude; } }>; // Supports drop and drop functionality for files. export function Dropzone({ children, className, open, text, subtext, filters, dialogOptions, onFiles, onClose }: Props) { const ipc = useService(IpcService); const notification = useService(NotificationService); const t = useTranslation(); const [dragCount, setDragCount] = useState(0); const [dropZoneDragging, setDropZoneDragging] = useState(false); const themeColor = useThemeColor("first-color"); const color = new Color(themeColor).lighten(.25).saturate(.8).hex(); const supportedExtensions = useMemo(() => { if(!filters?.length) { return []; } return filters.flatMap(filter => filter.extensions ?? []); }, [filters]); const handleDrop = useCallback((event: DragEvent) => { event.preventDefault(); event.stopPropagation(); setDragCount(() => 0); setDropZoneDragging(() => false); if (!onFiles || !event.dataTransfer?.files?.length) { return; } const paths = Array.from(event.dataTransfer.files).reduce((acc, file) => { const path = window.electron.webUtils.getPathForFile(file); if(supportedExtensions.some(ext => path.endsWith(ext))) { acc.push(path); } return acc; }, []); if(!paths.length) { notification.notifyError({ title: t("notifications.shared.errors.titles.file-not-supported"), desc: t("notifications.shared.errors.msg.file-not-supported", { types: supportedExtensions.join(", ") }), duration: 8000 }); return; } onFiles(paths); }, [supportedExtensions]); const onDragOver = useCallback((event: DragEvent) => { event.preventDefault(); event.stopPropagation(); }, []); const onDragEnter = useCallback((event: DragEvent) => { event.preventDefault(); event.stopPropagation(); setDragCount(prev => prev + 1); }, []); const onDragLeave = useCallback((event: DragEvent) => { event.preventDefault(); event.stopPropagation(); setDragCount(prev => prev - 1); }, []); const openDialog = useCallback(async () => { if(!dialogOptions?.dialog) { return; } const res = await lastValueFrom(ipc.sendV2("open-dialog", { ...dialogOptions.dialog, filters })); if(res?.canceled || !res?.filePaths?.length) { return; } onFiles?.(res.filePaths); }, []); return (
{(dragCount || open) ? (
{onClose && ( )}
setDropZoneDragging(true)} onDragLeave={() => setDropZoneDragging(false)} className="relative size-full z-[1] border-dashed border-3 rounded-md flex flex-col gap-1 justify-center items-center" style={{ borderColor: color }} >
{text} {subtext} {dialogOptions?.dialog && ( )}
): undefined} {children}
); }