Drag and drop files between folders

This commit is contained in:
2026-05-22 20:08:09 +02:00
parent a3e4a08281
commit 88eebf6944
5 changed files with 85 additions and 7 deletions
+16 -2
View File
@@ -18,8 +18,8 @@
<button :class="{active: view === 'prefs'}" @click="view = 'prefs'"> Preferences</button>
<button v-if="isAdmin" :class="{active: view === 'admin'}" @click="view = 'admin'">👤 Admin</button>
</div>
<FileTree v-if="view === 'files'" :files="filteredFiles" :selected="currentFile" @select="openFile" @delete="deleteItem" />
<FileTree v-if="view === 'shared'" :files="sharedFiles" :selected="currentFile" @select="openFile" @delete="deleteItem" />
<FileTree v-if="view === 'files'" :files="filteredFiles" :selected="currentFile" @select="openFile" @delete="deleteItem" @move="moveFile" />
<FileTree v-if="view === 'shared'" :files="sharedFiles" :selected="currentFile" @select="openFile" @delete="deleteItem" @move="moveFile" />
</aside>
<main class="editor-area" v-if="view === 'files' || view === 'shared'">
<div class="toolbar">
@@ -361,6 +361,20 @@ async function deleteItem(item) {
await loadFiles()
}
async function moveFile({ from, to }) {
const filename = from.split('/').pop()
const newPath = to + '/' + filename
try {
await api('/api/files/move', { from, to: newPath })
if (currentFile.value === from) {
currentFile.value = newPath
}
await loadFiles()
} catch (e) {
alert('Move failed')
}
}
// ─── Shared ──────────────────────────────────────────────────────────────────
async function loadShared() {
+37 -5
View File
@@ -3,36 +3,67 @@
<div v-for="item in files" :key="item.path" class="tree-item">
<div
class="tree-node"
:class="{ selected: item.path === selected, folder: item.isDir }"
:class="{ selected: item.path === selected, folder: item.isDir, 'drop-target': dropTarget === item.path }"
@click="item.isDir ? toggleFolder(item) : $emit('select', item.path)"
@contextmenu.prevent="showContext($event, item)"
@contextmenu.prevent="null"
draggable="true"
@dragstart="onDragStart($event, item)"
@dragover.prevent="onDragOver($event, item)"
@dragleave="onDragLeave"
@drop="onDrop($event, item)"
>
<span class="icon">{{ item.isDir ? (expanded[item.path] ? '📂' : '📁') : '📄' }}</span>
<span class="name">{{ item.name }}</span>
<button class="delete-btn" @click.stop="$emit('delete', item)" title="Delete">×</button>
</div>
<div v-if="item.isDir && expanded[item.path] && item.children" class="children">
<FileTree :files="item.children" :selected="selected" @select="$emit('select', $event)" @delete="$emit('delete', $event)" />
<FileTree :files="item.children" :selected="selected" @select="$emit('select', $event)" @delete="$emit('delete', $event)" @move="$emit('move', $event)" />
</div>
</div>
</div>
</template>
<script setup>
import { reactive } from 'vue'
import { reactive, ref } from 'vue'
defineProps({
files: { type: Array, default: () => [] },
selected: { type: String, default: '' },
})
defineEmits(['select', 'delete'])
const emit = defineEmits(['select', 'delete', 'move'])
const expanded = reactive({})
const dropTarget = ref('')
function toggleFolder(item) {
expanded[item.path] = !expanded[item.path]
}
function onDragStart(e, item) {
e.dataTransfer.setData('text/plain', item.path)
e.dataTransfer.effectAllowed = 'move'
}
function onDragOver(e, item) {
if (item.isDir) {
e.dataTransfer.dropEffect = 'move'
dropTarget.value = item.path
}
}
function onDragLeave() {
dropTarget.value = ''
}
function onDrop(e, targetItem) {
dropTarget.value = ''
const sourcePath = e.dataTransfer.getData('text/plain')
if (!sourcePath || !targetItem.isDir || sourcePath === targetItem.path) return
// Don't drop into itself
if (targetItem.path.startsWith(sourcePath + '/')) return
emit('move', { from: sourcePath, to: targetItem.path })
}
</script>
<style scoped>
@@ -50,6 +81,7 @@ function toggleFolder(item) {
}
.tree-node:hover { background: var(--hover-bg, #313244); }
.tree-node.selected { background: var(--selected-bg, #45475a); }
.tree-node.drop-target { background: var(--accent, #89b4fa); opacity: 0.7; }
.icon { font-size: 14px; }
.name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.delete-btn {