wraith/frontend/components/sftp/SftpSidebar.vue

198 lines
6.3 KiB
Vue

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useSftp } from '~/composables/useSftp'
const props = defineProps<{
sessionId: string | null
}>()
const sessionIdRef = computed(() => props.sessionId)
const {
entries, currentPath, fileContent,
connect, disconnect, list, readFile, writeFile, mkdir, rename, remove, download,
} = useSftp(sessionIdRef)
const width = ref(260) // resizable sidebar width in px
const isDragging = ref(false)
const newFolderName = ref('')
const showNewFolderInput = ref(false)
const renameTarget = ref<string | null>(null)
const renameTo = ref('')
onMounted(() => {
connect()
list('/')
})
function navigateTo(path: string) {
list(path)
}
function goUp() {
const parts = currentPath.value.split('/').filter(Boolean)
parts.pop()
list(parts.length ? '/' + parts.join('/') : '/')
}
function handleOpenFile(path: string) {
readFile(path)
}
function handleSave(path: string, content: string) {
writeFile(path, content)
}
function handleDelete(path: string) {
if (confirm(`Delete ${path}?`)) {
remove(path)
list(currentPath.value)
}
}
function handleRename(oldPath: string) {
renameTarget.value = oldPath
renameTo.value = oldPath.split('/').pop() || ''
}
function confirmRename() {
if (!renameTarget.value || !renameTo.value) return
const dir = renameTarget.value.split('/').slice(0, -1).join('/')
rename(renameTarget.value, `${dir}/${renameTo.value}`)
renameTarget.value = null
list(currentPath.value)
}
function createFolder() {
if (!newFolderName.value) return
mkdir(`${currentPath.value === '/' ? '' : currentPath.value}/${newFolderName.value}`)
newFolderName.value = ''
showNewFolderInput.value = false
list(currentPath.value)
}
function startResize(event: MouseEvent) {
isDragging.value = true
const startX = event.clientX
const startWidth = width.value
const onMove = (e: MouseEvent) => {
width.value = Math.max(180, Math.min(480, startWidth + (e.clientX - startX)))
}
const onUp = () => {
isDragging.value = false
window.removeEventListener('mousemove', onMove)
window.removeEventListener('mouseup', onUp)
}
window.addEventListener('mousemove', onMove)
window.addEventListener('mouseup', onUp)
}
// Breadcrumb segments from currentPath
const breadcrumbs = computed(() => {
const parts = currentPath.value.split('/').filter(Boolean)
return [
{ name: '/', path: '/' },
...parts.map((part, i) => ({
name: part,
path: '/' + parts.slice(0, i + 1).join('/'),
})),
]
})
</script>
<template>
<div class="flex h-full" :style="{ width: width + 'px' }">
<div class="flex flex-col h-full bg-gray-900 border-r border-gray-800 overflow-hidden flex-1">
<!-- Debug banner remove once SFTP is working -->
<div class="px-2 py-1 bg-yellow-900/50 text-yellow-300 text-xs border-b border-yellow-800">
SFTP: {{ entries.length }} entries | path: {{ currentPath }} | sid: {{ sessionId?.substring(0, 8) }}
</div>
<!-- Toolbar -->
<div class="px-2 py-1.5 border-b border-gray-800 shrink-0">
<!-- Breadcrumbs -->
<div class="flex items-center gap-1 text-xs text-gray-500 overflow-x-auto mb-1">
<button
v-for="(crumb, i) in breadcrumbs"
:key="crumb.path"
class="hover:text-wraith-400 shrink-0"
@click="navigateTo(crumb.path)"
>{{ crumb.name }}</button>
<template v-if="breadcrumbs.length > 1">
<span v-for="(_, i) in breadcrumbs.slice(1)" :key="i" class="text-gray-700">/</span>
</template>
</div>
<!-- Actions -->
<div class="flex items-center gap-1">
<button
v-if="currentPath !== '/'"
@click="goUp"
class="text-xs text-gray-500 hover:text-gray-300 px-1.5 py-0.5 rounded hover:bg-gray-800"
title="Go up"
>↑ Up</button>
<button
@click="list(currentPath)"
class="text-xs text-gray-500 hover:text-gray-300 px-1.5 py-0.5 rounded hover:bg-gray-800"
title="Refresh"
>⟳</button>
<button
@click="showNewFolderInput = !showNewFolderInput"
class="text-xs text-gray-500 hover:text-wraith-400 px-1.5 py-0.5 rounded hover:bg-gray-800"
title="New folder"
>+ Folder</button>
</div>
<!-- New folder input -->
<div v-if="showNewFolderInput" class="flex gap-1 mt-1">
<input
v-model="newFolderName"
class="flex-1 text-xs bg-gray-800 border border-gray-700 rounded px-1.5 py-0.5 text-white"
placeholder="Folder name"
@keyup.enter="createFolder"
autofocus
/>
<button @click="createFolder" class="text-xs text-wraith-400 hover:text-wraith-300">OK</button>
<button @click="showNewFolderInput = false" class="text-xs text-gray-500">✕</button>
</div>
<!-- Rename input -->
<div v-if="renameTarget" class="flex gap-1 mt-1">
<input
v-model="renameTo"
class="flex-1 text-xs bg-gray-800 border border-gray-700 rounded px-1.5 py-0.5 text-white"
placeholder="New name"
@keyup.enter="confirmRename"
autofocus
/>
<button @click="confirmRename" class="text-xs text-wraith-400 hover:text-wraith-300">OK</button>
<button @click="renameTarget = null" class="text-xs text-gray-500">✕</button>
</div>
</div>
<!-- File tree area — either editor or directory listing -->
<div class="flex-1 min-h-0 overflow-hidden">
<SftpFileEditor
v-if="fileContent"
:file-path="fileContent.path"
:content="fileContent.content"
@save="handleSave"
@close="fileContent = null"
/>
<SftpFileTree
v-else
:entries="entries"
:current-path="currentPath"
@navigate="navigateTo"
@open-file="handleOpenFile"
@download="download"
@delete="handleDelete"
@rename="handleRename"
/>
</div>
</div>
<!-- Resize handle -->
<div
class="w-1 bg-transparent hover:bg-wraith-500 cursor-col-resize transition-colors shrink-0"
@mousedown="startResize"
/>
</div>
</template>