feat(desktop): unify folder drag-and-drop across runtimes
This commit is contained in:
158
packages/ui/src/lib/hooks/use-folder-drop.ts
Normal file
158
packages/ui/src/lib/hooks/use-folder-drop.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
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<boolean>
|
||||
onDrop: (paths: string[]) => void | Promise<void>
|
||||
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<boolean>
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user