import { Accessor, createEffect, createSignal, onCleanup, onMount } from "solid-js" import { containsFileDrop, extractDroppedDirectoryPaths, listenForNativeFolderDrops, listenForNativeFolderDropState, normalizeDroppedDirectoryPaths, supportsDesktopFolderDrop, } from "../native/desktop-file-drop" import { runtimeEnv } from "../runtime-env" interface UseFolderDropOptions { enabled: Accessor onDrop: (paths: string[]) => void | Promise onInvalidDrop?: () => void } interface FolderDropBindings { onDragEnter: (event: DragEvent) => void onDragOver: (event: DragEvent) => void onDragLeave: (event: DragEvent) => void onDrop: (event: DragEvent) => void } export function useFolderDrop(options: UseFolderDropOptions): { isActive: Accessor isSupported: boolean bind: FolderDropBindings } { const [isActive, setIsActive] = createSignal(false) const [dragDepth, setDragDepth] = createSignal(0) const isSupported = supportsDesktopFolderDrop() function reset() { setDragDepth(0) setIsActive(false) } async function handleResolvedPaths(paths: string[]) { reset() if (!options.enabled()) { return } const directoryPaths = await normalizeDroppedDirectoryPaths(paths) if (directoryPaths.length === 0) { options.onInvalidDrop?.() return } await options.onDrop(directoryPaths) } createEffect(() => { if (!options.enabled()) { reset() } }) onMount(() => { if (!isSupported) { return } let disposeNativeDrop = () => {} let disposeNativeState = () => {} void listenForNativeFolderDrops((paths) => { if (!options.enabled()) { return } void handleResolvedPaths(paths) }).then((dispose) => { disposeNativeDrop = dispose }) void listenForNativeFolderDropState((state) => { if (!options.enabled()) { reset() return } if (state === "enter") { setIsActive(true) return } reset() }).then((dispose) => { disposeNativeState = dispose }) onCleanup(() => { disposeNativeDrop() disposeNativeState() }) }) const bind: FolderDropBindings = { onDragEnter(event) { if (!isSupported || runtimeEnv.host === "tauri" || !options.enabled() || !containsFileDrop(event)) { return } event.preventDefault() setDragDepth((prev) => prev + 1) setIsActive(true) }, onDragOver(event) { if (!isSupported || runtimeEnv.host === "tauri" || !options.enabled() || !containsFileDrop(event)) { return } event.preventDefault() if (event.dataTransfer) { event.dataTransfer.dropEffect = "copy" } setIsActive(true) }, onDragLeave(event) { if (!isSupported || runtimeEnv.host === "tauri" || !containsFileDrop(event)) { return } event.preventDefault() const nextDepth = Math.max(0, dragDepth() - 1) setDragDepth(nextDepth) if (nextDepth === 0) { setIsActive(false) } }, onDrop(event) { if (!isSupported) { return } event.preventDefault() event.stopPropagation() if (!options.enabled()) { reset() return } if (runtimeEnv.host === "tauri") { reset() return } const paths = extractDroppedDirectoryPaths(event) if (paths.length === 0) { reset() options.onInvalidDrop?.() return } void handleResolvedPaths(paths) }, } return { isActive, isSupported, bind, } }