115 lines
3.3 KiB
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="flex-1 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>
|