Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,50 @@ onMounted(() => {
setup()
})

async function drop(event: DragEvent) {
function drop(event: DragEvent) {
const items = event.dataTransfer?.items

if (!items) return

const promises: Promise<FileSystemHandle | null>[] = []
// DataTransferItem entries are only valid synchronously inside the event handler, so collect everything
// before awaiting. Chromium/web exposes the File System Access API; WebKitGTK (the native build) does not,
// so we fall back to plain File objects there.
const handlePromises: Promise<FileSystemHandle | null>[] = []
const files: File[] = []

for (const item of items) {
if (item.kind === 'file') {
promises.push(<any>item.getAsFileSystemHandle())
if (item.kind !== 'file') continue

if (typeof (item as any).getAsFileSystemHandle === 'function') {
handlePromises.push((item as any).getAsFileSystemHandle())
} else {
const file = item.getAsFile()

if (file) files.push(file)
}
}

const handles = (await Promise.all(promises)).filter((file) => file !== null)
importDropped(handlePromises, files)
}

async function importDropped(handlePromises: Promise<FileSystemHandle | null>[], files: File[]) {
const handles = (await Promise.all(handlePromises)).filter((handle) => handle !== null)

for (const handle of handles) {
if (handle.kind === 'file') {
await ImporterManager.importFile(await ImportedFileEntry.fromHandle(handle as FileSystemFileHandle))
} else {
const entry = await ImportedDirectoryEntry.fromHandle(handle as FileSystemDirectoryHandle)

if (!entry) return
if (!entry) continue

await ImporterManager.importDirectory(entry)
}
}

for (const file of files) {
await ImporterManager.importFile(new ImportedFileEntry('/' + file.name, await file.arrayBuffer()))
}
}
</script>

Expand Down
14 changes: 11 additions & 3 deletions src/components/FileExplorer/Directory.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,13 @@ const draggingOver = computed(() => draggingCount.value > 0)
const draggingLocation: Ref<'inside' | 'above' | 'below'> = ref('inside')
let expandTimeout: number | null = null

function dragStart() {
function dragStart(event: DragEvent) {
// WebKitGTK (and Firefox) cancel a drag immediately unless data is set on the transfer in dragstart.
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData('text/plain', props.path)
}

requestAnimationFrame(() => {
FileExplorer.draggedItem.value = new BaseEntry(props.path, 'directory')
})
Expand Down Expand Up @@ -143,7 +149,8 @@ function dragOver(event: DragEvent) {

if (y < minY + height / 2) {
draggingLocation.value = 'above'
} else if (y > minY + height / 2 && y < containerMinY + cotnainerHeight - 20) {
} else if (entries.value.length === 0 || y < containerMinY + cotnainerHeight - 20) {
// Empty folders have no children rows, so the "inside" zone would otherwise collapse to nothing.
draggingLocation.value = 'inside'
} else {
draggingLocation.value = 'below'
Expand Down Expand Up @@ -190,6 +197,7 @@ function drop(event: DragEvent) {
class="flex items-center gap-2 cursor-pointer transition-colors duration-100 ease-out rounded pl-1"
:class="{
'hover:bg-background-tertiary': !FileExplorer.draggedItem.value,
'opacity-50': preview,
}"
@click="expanded = !expanded"
@contextmenu.prevent.stop="contextMenu?.open"
Expand All @@ -211,7 +219,7 @@ function drop(event: DragEvent) {
'pl-3': get('fileExplorerIndentation') === 'large',
'pl-6': get('fileExplorerIndentation') === 'x-large',
}"
v-if="expanded && entries.length > 0"
v-if="(expanded && entries.length > 0) || (draggingOver && draggingLocation === 'inside')"
>
<div v-for="entry in orderedEntries" :key="entry.path">
<File
Expand Down
9 changes: 8 additions & 1 deletion src/components/FileExplorer/File.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ const props = defineProps({
},
})

function dragStart() {
function dragStart(event: DragEvent) {
// WebKitGTK (and Firefox) cancel a drag immediately unless data is set on the transfer in dragstart.
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData('text/plain', props.path)
}

requestAnimationFrame(() => {
FileExplorer.draggedItem.value = new BaseEntry(props.path, 'file')
})
Expand Down Expand Up @@ -71,6 +77,7 @@ onMounted(() => {
class="flex items-center gap-2 cursor-pointer transition-colors duration-100 ease-out rounded pl-1"
:class="{
'hover:bg-background-tertiary': !FileExplorer.draggedItem.value,
'opacity-50': preview,
}"
@click="click"
@contextmenu.prevent.stop="contextMenu?.open"
Expand Down
1 change: 1 addition & 0 deletions src/components/FileExplorer/FileExplorer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ function drop(event: DragEvent) {
@contextmenu.prevent="contextMenu?.open"
@dragenter="dragEnter"
@dragleave="dragLeave"
@dragover.prevent
@drop="drop"
>
<div v-for="entry in orderedEntries" :key="entry.path">
Expand Down
29 changes: 29 additions & 0 deletions src/libs/tauri/DragDrop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { readFile, stat } from '@tauri-apps/plugin-fs'
import { basename } from 'pathe'
import { ImporterManager } from '@/libs/import/ImporterManager'
import { ImportedDirectoryEntry, ImportedFileEntry } from '@/libs/fileSystem/FileSystem'
import { TauriFileSystem } from '@/libs/fileSystem/TauriFileSystem'

// Wires up native file drag and drop for the Tauri build.
// With `dragDropEnabled: true` the webview no longer receives HTML5 file drop events; instead Tauri emits a drag-drop event carrying the absolute paths of the dropped items.
export async function setupTauriDragDrop() {
await getCurrentWebview().onDragDropEvent(async (event) => {
if (event.payload.type !== 'drop') return

for (const path of event.payload.paths) {
const info = await stat(path)

if (info.isDirectory) {
const directoryFileSystem = new TauriFileSystem()
directoryFileSystem.setBasePath(path)

await ImporterManager.importDirectory(new ImportedDirectoryEntry('/' + basename(path), directoryFileSystem))
} else {
const data = await readFile(path)

await ImporterManager.importFile(new ImportedFileEntry('/' + basename(path), data.buffer as ArrayBuffer))
}
}
})
}