Files
CodeNomad/packages/ui/src/lib/hooks/use-folder-drop.ts
Shantur Rathore 3b411e2e73 fix(ui): gate desktop privileges by host and window context (#347)
Don't let remote server windows use local features like local file browser etc
2026-04-20 20:28:11 +01:00

159 lines
3.5 KiB
TypeScript

import { Accessor, createEffect, createSignal, onCleanup, onMount } from "solid-js"
import {
containsFileDrop,
extractDroppedDirectoryPaths,
listenForNativeFolderDrops,
listenForNativeFolderDropState,
normalizeDroppedDirectoryPaths,
supportsDesktopFolderDrop,
} from "../native/desktop-file-drop"
import { isTauriHost } 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 || isTauriHost() || !options.enabled() || !containsFileDrop(event)) {
return
}
event.preventDefault()
setDragDepth((prev) => prev + 1)
setIsActive(true)
},
onDragOver(event) {
if (!isSupported || isTauriHost() || !options.enabled() || !containsFileDrop(event)) {
return
}
event.preventDefault()
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "copy"
}
setIsActive(true)
},
onDragLeave(event) {
if (!isSupported || isTauriHost() || !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 (isTauriHost()) {
reset()
return
}
const paths = extractDroppedDirectoryPaths(event)
if (paths.length === 0) {
reset()
options.onInvalidDrop?.()
return
}
void handleResolvedPaths(paths)
},
}
return {
isActive,
isSupported,
bind,
}
}