wraith/frontend/components/sftp/FileTree.vue

115 lines
3.3 KiB
Vue

<script setup lang="ts">
import { ref } from 'vue'
interface FileEntry {
name: string
path: string
size: number
isDirectory: boolean
permissions: string
modified: string
}
const props = defineProps<{
entries: FileEntry[]
currentPath: string
}>()
const emit = defineEmits<{
(e: 'navigate', path: string): void
(e: 'openFile', path: string): void
(e: 'download', path: string): void
(e: 'delete', path: string): void
(e: 'rename', oldPath: string): void
(e: 'mkdir'): void
}>()
const contextMenu = ref<{ visible: boolean; x: number; y: number; entry: FileEntry | null }>({
visible: false,
x: 0,
y: 0,
entry: null,
})
function handleClick(entry: FileEntry) {
if (entry.isDirectory) {
emit('navigate', entry.path)
} else {
emit('openFile', entry.path)
}
}
function showContext(event: MouseEvent, entry: FileEntry) {
event.preventDefault()
contextMenu.value = { visible: true, x: event.clientX, y: event.clientY, entry }
}
function closeContext() {
contextMenu.value.visible = false
}
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
}
</script>
<template>
<div class="h-full overflow-y-auto text-sm" @click="closeContext">
<div
v-for="entry in entries"
:key="entry.path"
class="flex items-center gap-2 px-3 py-1 hover:bg-gray-800 cursor-pointer group"
@click="handleClick(entry)"
@contextmenu="showContext($event, entry)"
>
<!-- Icon -->
<span class="shrink-0 text-base">
{{ entry.isDirectory ? '📁' : '📄' }}
</span>
<!-- Name -->
<span class="flex-1 truncate" :class="entry.isDirectory ? 'text-wraith-300' : 'text-gray-300'">
{{ entry.name }}
</span>
<!-- Size (files only) -->
<span v-if="!entry.isDirectory" class="text-gray-600 text-xs shrink-0">
{{ formatSize(entry.size) }}
</span>
</div>
<p v-if="entries.length === 0" class="text-gray-600 text-center py-4 text-xs">
Empty directory
</p>
</div>
<!-- Context menu -->
<Teleport to="body">
<div
v-if="contextMenu.visible"
class="fixed z-50 bg-gray-800 border border-gray-700 rounded shadow-xl py-1 min-w-[140px] text-sm"
:style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
@click.stop
>
<button
v-if="!contextMenu.entry?.isDirectory"
class="w-full text-left px-3 py-1.5 hover:bg-gray-700 text-gray-300"
@click="emit('openFile', contextMenu.entry!.path); closeContext()"
>Open / Edit</button>
<button
class="w-full text-left px-3 py-1.5 hover:bg-gray-700 text-gray-300"
@click="emit('download', contextMenu.entry!.path); closeContext()"
>Download</button>
<button
class="w-full text-left px-3 py-1.5 hover:bg-gray-700 text-gray-300"
@click="emit('rename', contextMenu.entry!.path); closeContext()"
>Rename</button>
<div class="border-t border-gray-700 my-1" />
<button
class="w-full text-left px-3 py-1.5 hover:bg-gray-700 text-red-400"
@click="emit('delete', contextMenu.entry!.path); closeContext()"
>Delete</button>
</div>
</Teleport>
</template>