701 lines
24 KiB
Vue
701 lines
24 KiB
Vue
<template>
|
|
<div class="view-container"
|
|
@dragover.prevent="onDragOver" @dragleave.prevent="onDragLeave"
|
|
@drop.prevent="onDrop">
|
|
|
|
<!-- Drop overlay -->
|
|
<div v-if="isDragging" class="drop-overlay">
|
|
<div class="drop-content">
|
|
<i class="pi pi-cloud-upload"></i>
|
|
<p>Dateien oder Ordner hier ablegen</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="view-header">
|
|
<div class="breadcrumb">
|
|
<a @click="navigateTo(null)" class="crumb">Dateien</a>
|
|
<template v-for="item in filesStore.breadcrumb" :key="item.id">
|
|
<i class="pi pi-angle-right crumb-sep"></i>
|
|
<a @click="navigateTo(item.id)" class="crumb">{{ item.name }}</a>
|
|
</template>
|
|
</div>
|
|
<div class="header-actions">
|
|
<Button icon="pi pi-folder-plus" label="Neuer Ordner" size="small" outlined @click="showNewFolder = true" />
|
|
<Button icon="pi pi-upload" label="Dateien" size="small" @click="triggerUpload" />
|
|
<Button icon="pi pi-folder" label="Ordner" size="small" outlined @click="triggerFolderUpload" />
|
|
<input ref="fileInput" type="file" multiple hidden @change="handleUpload" />
|
|
<input ref="folderInput" type="file" hidden webkitdirectory @change="handleFolderUpload" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload progress -->
|
|
<div v-if="uploadQueue.length" class="upload-progress">
|
|
<div class="upload-info">
|
|
<i class="pi pi-spin pi-spinner"></i>
|
|
<span>{{ uploadCurrent }} / {{ uploadTotal }} Dateien hochgeladen</span>
|
|
</div>
|
|
<ProgressBar :value="uploadPercent" />
|
|
</div>
|
|
|
|
<DataTable
|
|
:value="filesStore.files"
|
|
:loading="filesStore.loading"
|
|
@row-dblclick="handleDoubleClick"
|
|
striped-rows
|
|
removable-sort
|
|
class="files-table"
|
|
>
|
|
<template #empty>
|
|
<div class="empty-state">
|
|
<i class="pi pi-folder-open" style="font-size: 2rem; color: var(--p-surface-400)"></i>
|
|
<p>Dieser Ordner ist leer</p>
|
|
</div>
|
|
</template>
|
|
|
|
<Column field="name" header="Name" sortable style="min-width: 300px">
|
|
<template #body="{ data }">
|
|
<div class="file-name" @click="data.is_folder && navigateTo(data.id)">
|
|
<i :class="fileIcon(data)" class="file-icon"></i>
|
|
<span>{{ data.name }}</span>
|
|
<Tag v-if="data.shared" value="Geteilt" severity="info" class="shared-tag" />
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="size" header="Groesse" sortable style="width: 120px">
|
|
<template #body="{ data }">
|
|
{{ data.is_folder ? '' : formatSize(data.size) }}
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="updated_at" header="Geaendert" sortable style="width: 180px">
|
|
<template #body="{ data }">
|
|
{{ formatDate(data.updated_at) }}
|
|
</template>
|
|
</Column>
|
|
|
|
<Column header="" style="width: 140px">
|
|
<template #body="{ data }">
|
|
<div class="row-actions">
|
|
<Button
|
|
v-if="!data.is_folder"
|
|
icon="pi pi-download"
|
|
text rounded size="small"
|
|
@click="downloadFile(data)"
|
|
/>
|
|
<Button
|
|
icon="pi pi-share-alt"
|
|
text rounded size="small"
|
|
@click="openShare(data)"
|
|
/>
|
|
<Button
|
|
icon="pi pi-pencil"
|
|
text rounded size="small"
|
|
@click="openRename(data)"
|
|
/>
|
|
<Button
|
|
icon="pi pi-trash"
|
|
text rounded size="small"
|
|
severity="danger"
|
|
@click="confirmDelete(data)"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
|
|
<!-- New Folder Dialog -->
|
|
<Dialog v-model:visible="showNewFolder" header="Neuer Ordner" modal :style="{ width: '400px' }">
|
|
<div class="field">
|
|
<label>Ordnername</label>
|
|
<InputText v-model="newFolderName" fluid autofocus @keyup.enter="createFolder" />
|
|
</div>
|
|
<template #footer>
|
|
<Button label="Abbrechen" text @click="showNewFolder = false" />
|
|
<Button label="Erstellen" @click="createFolder" />
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- Rename Dialog -->
|
|
<Dialog v-model:visible="showRename" header="Umbenennen" modal :style="{ width: '400px' }">
|
|
<div class="field">
|
|
<label>Neuer Name</label>
|
|
<InputText v-model="renameName" fluid autofocus @keyup.enter="doRename" />
|
|
</div>
|
|
<template #footer>
|
|
<Button label="Abbrechen" text @click="showRename = false" />
|
|
<Button label="Umbenennen" @click="doRename" />
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- Share Dialog -->
|
|
<Dialog v-model:visible="showShare" header="Teilen" modal :style="{ width: '550px' }">
|
|
<div v-if="shareFile" class="share-content">
|
|
<h4>{{ shareFile.name }}</h4>
|
|
|
|
<!-- User Sharing -->
|
|
<div class="share-section">
|
|
<h5>Mit Benutzer teilen</h5>
|
|
<div class="user-share-row">
|
|
<InputText v-model="shareUserQuery" placeholder="Benutzername suchen..." fluid @input="searchUsers" />
|
|
<Select v-model="shareUserPermission" :options="userPermOptions" optionLabel="label" optionValue="value" />
|
|
<Button label="Teilen" size="small" @click="shareWithUser" :disabled="!selectedShareUser" />
|
|
</div>
|
|
<div v-if="userSearchResults.length" class="user-search-results">
|
|
<div v-for="u in userSearchResults" :key="u.id"
|
|
class="user-result" :class="{ selected: selectedShareUser?.id === u.id }"
|
|
@click="selectedShareUser = u; shareUserQuery = u.username; userSearchResults = []">
|
|
<i class="pi pi-user"></i> {{ u.username }}
|
|
</div>
|
|
</div>
|
|
<div v-if="filePermissions.length" class="existing-shares">
|
|
<div v-for="perm in filePermissions" :key="perm.id" class="share-perm-item">
|
|
<i class="pi pi-user"></i>
|
|
<span>{{ perm.username }}</span>
|
|
<Tag :value="permLabel(perm.permission)" size="small" />
|
|
<Button icon="pi pi-trash" text size="small" severity="danger" @click="removeUserShare(perm.id)" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Link Sharing -->
|
|
<div class="share-section">
|
|
<h5>Freigabe-Link erstellen</h5>
|
|
<div class="share-form">
|
|
<div class="field">
|
|
<label>Passwort (optional)</label>
|
|
<Password v-model="sharePassword" :feedback="false" toggle-mask fluid />
|
|
</div>
|
|
<div class="field">
|
|
<label>Ablaufdatum (optional)</label>
|
|
<InputText v-model="shareExpiry" type="date" fluid />
|
|
</div>
|
|
<Button label="Link erstellen" icon="pi pi-link" @click="createShare" :loading="shareLoading" />
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="shareLinks.length" class="share-section">
|
|
<h5>Bestehende Links</h5>
|
|
<div v-for="link in shareLinks" :key="link.id" class="share-link-item">
|
|
<div class="link-info">
|
|
<code>{{ currentOrigin }}/share/{{ link.token }}</code>
|
|
<small>
|
|
{{ link.download_count }} Downloads
|
|
<template v-if="link.expires_at"> | Bis {{ formatDate(link.expires_at) }}</template>
|
|
<template v-if="link.has_password"> | Passwortgeschuetzt</template>
|
|
</small>
|
|
</div>
|
|
<div class="link-actions">
|
|
<Button icon="pi pi-copy" text size="small" @click="copyLink(link.token)" />
|
|
<Button icon="pi pi-trash" text size="small" severity="danger" @click="removeShare(link.token)" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
|
|
<!-- Delete Confirm -->
|
|
<Dialog v-model:visible="showDeleteConfirm" header="Loeschen bestaetigen" modal :style="{ width: '400px' }">
|
|
<p>Moechtest du <strong>{{ deleteTarget?.name }}</strong> wirklich loeschen?</p>
|
|
<p v-if="deleteTarget?.is_folder" class="text-warn">Alle Dateien in diesem Ordner werden ebenfalls geloescht!</p>
|
|
<template #footer>
|
|
<Button label="Abbrechen" text @click="showDeleteConfirm = false" />
|
|
<Button label="Loeschen" severity="danger" @click="doDelete" />
|
|
</template>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch, onMounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useFilesStore } from '../stores/files'
|
|
import { useToast } from 'primevue/usetoast'
|
|
import apiClient from '../api/client'
|
|
import DataTable from 'primevue/datatable'
|
|
import Column from 'primevue/column'
|
|
import Button from 'primevue/button'
|
|
import Dialog from 'primevue/dialog'
|
|
import InputText from 'primevue/inputtext'
|
|
import Password from 'primevue/password'
|
|
import Tag from 'primevue/tag'
|
|
import Select from 'primevue/select'
|
|
import ProgressBar from 'primevue/progressbar'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const filesStore = useFilesStore()
|
|
const toast = useToast()
|
|
|
|
const fileInput = ref(null)
|
|
const folderInput = ref(null)
|
|
const isDragging = ref(false)
|
|
const uploadQueue = ref([])
|
|
const uploadCurrent = ref(0)
|
|
const uploadTotal = ref(0)
|
|
const uploadPercent = ref(0)
|
|
const showNewFolder = ref(false)
|
|
const newFolderName = ref('')
|
|
const showRename = ref(false)
|
|
const renameName = ref('')
|
|
const renameTarget = ref(null)
|
|
const showShare = ref(false)
|
|
const shareFile = ref(null)
|
|
const sharePassword = ref('')
|
|
const shareExpiry = ref('')
|
|
const shareLinks = ref([])
|
|
const filePermissions = ref([])
|
|
const shareUserQuery = ref('')
|
|
const selectedShareUser = ref(null)
|
|
const shareUserPermission = ref('read')
|
|
const userSearchResults = ref([])
|
|
const userPermOptions = [{ label: 'Lesen', value: 'read' }, { label: 'Schreiben', value: 'write' }, { label: 'Admin', value: 'admin' }]
|
|
const currentOrigin = window.location.origin
|
|
const shareLoading = ref(false)
|
|
const showDeleteConfirm = ref(false)
|
|
const deleteTarget = ref(null)
|
|
|
|
function currentParentId() {
|
|
const id = route.params.folderId
|
|
return id ? parseInt(id) : null
|
|
}
|
|
|
|
function navigateTo(folderId) {
|
|
if (folderId) {
|
|
router.push(`/files/${folderId}`)
|
|
} else {
|
|
router.push('/files')
|
|
}
|
|
}
|
|
|
|
function handleDoubleClick(event) {
|
|
const data = event.data
|
|
if (data.is_folder) {
|
|
navigateTo(data.id)
|
|
} else {
|
|
downloadFile(data)
|
|
}
|
|
}
|
|
|
|
function fileIcon(data) {
|
|
if (data.is_folder) return 'pi pi-folder'
|
|
const mime = data.mime_type || ''
|
|
if (mime.startsWith('image/')) return 'pi pi-image'
|
|
if (mime.startsWith('video/')) return 'pi pi-video'
|
|
if (mime.startsWith('audio/')) return 'pi pi-volume-up'
|
|
if (mime.includes('pdf')) return 'pi pi-file-pdf'
|
|
if (mime.includes('word') || mime.includes('document')) return 'pi pi-file-word'
|
|
if (mime.includes('sheet') || mime.includes('excel')) return 'pi pi-file-excel'
|
|
if (mime.includes('presentation') || mime.includes('powerpoint')) return 'pi pi-file'
|
|
return 'pi pi-file'
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (!bytes) return '0 B'
|
|
const units = ['B', 'KB', 'MB', 'GB']
|
|
let i = 0
|
|
let size = bytes
|
|
while (size >= 1024 && i < units.length - 1) {
|
|
size /= 1024
|
|
i++
|
|
}
|
|
return `${size.toFixed(i > 0 ? 1 : 0)} ${units[i]}`
|
|
}
|
|
|
|
function formatDate(iso) {
|
|
if (!iso) return ''
|
|
return new Date(iso).toLocaleString('de-DE', {
|
|
day: '2-digit', month: '2-digit', year: 'numeric',
|
|
hour: '2-digit', minute: '2-digit',
|
|
})
|
|
}
|
|
|
|
function triggerUpload() {
|
|
fileInput.value?.click()
|
|
}
|
|
|
|
function triggerFolderUpload() {
|
|
folderInput.value?.click()
|
|
}
|
|
|
|
async function handleUpload(event) {
|
|
const files = Array.from(event.target.files)
|
|
if (!files.length) return
|
|
await uploadFilesWithProgress(files.map(f => ({ file: f, relativePath: null })))
|
|
event.target.value = ''
|
|
}
|
|
|
|
async function handleFolderUpload(event) {
|
|
const files = Array.from(event.target.files)
|
|
if (!files.length) return
|
|
// webkitRelativePath gives us "FolderName/sub/file.txt"
|
|
const items = files.map(f => ({
|
|
file: f,
|
|
relativePath: f.webkitRelativePath || null,
|
|
}))
|
|
await uploadFilesWithProgress(items)
|
|
event.target.value = ''
|
|
}
|
|
|
|
// --- Drag & Drop ---
|
|
let dragCounter = 0
|
|
|
|
function onDragOver(e) {
|
|
isDragging.value = true
|
|
}
|
|
|
|
function onDragLeave(e) {
|
|
dragCounter--
|
|
if (dragCounter <= 0) {
|
|
isDragging.value = false
|
|
dragCounter = 0
|
|
}
|
|
}
|
|
|
|
async function onDrop(e) {
|
|
isDragging.value = false
|
|
dragCounter = 0
|
|
|
|
const items = []
|
|
|
|
if (e.dataTransfer.items) {
|
|
// Use DataTransferItem API for directory support
|
|
const entries = []
|
|
for (const item of e.dataTransfer.items) {
|
|
if (item.kind === 'file') {
|
|
const entry = item.webkitGetAsEntry?.() || item.getAsEntry?.()
|
|
if (entry) {
|
|
entries.push(entry)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recursively read all entries
|
|
for (const entry of entries) {
|
|
await readEntry(entry, '', items)
|
|
}
|
|
} else {
|
|
// Fallback: flat file list
|
|
for (const file of e.dataTransfer.files) {
|
|
items.push({ file, relativePath: null })
|
|
}
|
|
}
|
|
|
|
if (items.length) {
|
|
await uploadFilesWithProgress(items)
|
|
}
|
|
}
|
|
|
|
async function readEntry(entry, parentPath, items) {
|
|
if (entry.isFile) {
|
|
const file = await new Promise((resolve, reject) => entry.file(resolve, reject))
|
|
const relativePath = parentPath ? `${parentPath}/${entry.name}` : null
|
|
items.push({ file, relativePath })
|
|
} else if (entry.isDirectory) {
|
|
const dirPath = parentPath ? `${parentPath}/${entry.name}` : entry.name
|
|
const reader = entry.createReader()
|
|
// readEntries may need to be called multiple times
|
|
let allEntries = []
|
|
const readBatch = () => new Promise((resolve, reject) => reader.readEntries(resolve, reject))
|
|
let batch
|
|
do {
|
|
batch = await readBatch()
|
|
allEntries = allEntries.concat(batch)
|
|
} while (batch.length > 0)
|
|
|
|
for (const child of allEntries) {
|
|
await readEntry(child, dirPath, items)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Upload with progress + folder creation ---
|
|
async function uploadFilesWithProgress(items) {
|
|
uploadTotal.value = items.length
|
|
uploadCurrent.value = 0
|
|
uploadPercent.value = 0
|
|
uploadQueue.value = items
|
|
|
|
// Group by directory path -> create folders first
|
|
const pathCache = {} // "dir/subdir" -> folder_id
|
|
|
|
let errors = 0
|
|
for (const item of items) {
|
|
try {
|
|
let targetParentId = currentParentId()
|
|
|
|
if (item.relativePath) {
|
|
// Extract directory part: "Folder/Sub/file.txt" -> "Folder/Sub"
|
|
const parts = item.relativePath.split('/')
|
|
parts.pop() // remove filename
|
|
const dirPath = parts.join('/')
|
|
|
|
if (dirPath && !pathCache[dirPath]) {
|
|
const res = await apiClient.post('/files/ensure-path', {
|
|
path: dirPath,
|
|
parent_id: currentParentId(),
|
|
})
|
|
pathCache[dirPath] = res.data.folder_id
|
|
|
|
// Also cache parent paths
|
|
for (let i = 1; i < parts.length; i++) {
|
|
const subPath = parts.slice(0, i).join('/')
|
|
if (!pathCache[subPath]) {
|
|
const subRes = await apiClient.post('/files/ensure-path', {
|
|
path: subPath,
|
|
parent_id: currentParentId(),
|
|
})
|
|
pathCache[subPath] = subRes.data.folder_id
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dirPath) {
|
|
targetParentId = pathCache[dirPath]
|
|
}
|
|
}
|
|
|
|
await filesStore.uploadFile(item.file, targetParentId)
|
|
} catch (err) {
|
|
errors++
|
|
}
|
|
|
|
uploadCurrent.value++
|
|
uploadPercent.value = Math.round((uploadCurrent.value / uploadTotal.value) * 100)
|
|
}
|
|
|
|
uploadQueue.value = []
|
|
|
|
if (errors > 0) {
|
|
toast.add({ severity: 'warn', summary: `${uploadCurrent.value - errors} hochgeladen, ${errors} Fehler`, life: 5000 })
|
|
} else {
|
|
toast.add({ severity: 'success', summary: `${uploadCurrent.value} Dateien hochgeladen`, life: 3000 })
|
|
}
|
|
|
|
await filesStore.loadFiles(currentParentId())
|
|
}
|
|
|
|
async function createFolder() {
|
|
if (!newFolderName.value.trim()) return
|
|
try {
|
|
await filesStore.createFolder(newFolderName.value.trim(), currentParentId())
|
|
showNewFolder.value = false
|
|
newFolderName.value = ''
|
|
} catch (err) {
|
|
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
|
}
|
|
}
|
|
|
|
function downloadFile(data) {
|
|
const url = filesStore.downloadUrl(data.id)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = data.name
|
|
a.click()
|
|
}
|
|
|
|
function openRename(data) {
|
|
renameTarget.value = data
|
|
renameName.value = data.name
|
|
showRename.value = true
|
|
}
|
|
|
|
async function doRename() {
|
|
if (!renameName.value.trim() || !renameTarget.value) return
|
|
try {
|
|
await filesStore.renameFile(renameTarget.value.id, renameName.value.trim())
|
|
showRename.value = false
|
|
} catch (err) {
|
|
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
|
}
|
|
}
|
|
|
|
function permLabel(perm) {
|
|
return { read: 'Lesen', write: 'Schreiben', admin: 'Admin' }[perm] || perm
|
|
}
|
|
|
|
async function openShare(data) {
|
|
shareFile.value = data
|
|
sharePassword.value = ''
|
|
shareExpiry.value = ''
|
|
shareUserQuery.value = ''
|
|
selectedShareUser.value = null
|
|
userSearchResults.value = []
|
|
showShare.value = true
|
|
// Load existing links and permissions in parallel
|
|
try {
|
|
const [links, perms] = await Promise.all([
|
|
filesStore.getShareLinks(data.id),
|
|
apiClient.get(`/files/${data.id}/permissions`).then(r => r.data).catch(() => []),
|
|
])
|
|
shareLinks.value = links
|
|
filePermissions.value = perms
|
|
} catch {
|
|
shareLinks.value = []
|
|
filePermissions.value = []
|
|
}
|
|
}
|
|
|
|
let searchTimeout = null
|
|
function searchUsers() {
|
|
selectedShareUser.value = null
|
|
clearTimeout(searchTimeout)
|
|
if (shareUserQuery.value.length < 2) { userSearchResults.value = []; return }
|
|
searchTimeout = setTimeout(async () => {
|
|
try {
|
|
const res = await apiClient.get('/users/search', { params: { q: shareUserQuery.value } })
|
|
userSearchResults.value = res.data
|
|
} catch { userSearchResults.value = [] }
|
|
}, 300)
|
|
}
|
|
|
|
async function shareWithUser() {
|
|
if (!selectedShareUser.value || !shareFile.value) return
|
|
try {
|
|
await apiClient.post(`/files/${shareFile.value.id}/permissions`, {
|
|
user_id: selectedShareUser.value.id,
|
|
permission: shareUserPermission.value,
|
|
})
|
|
toast.add({ severity: 'success', summary: `Mit ${selectedShareUser.value.username} geteilt`, life: 3000 })
|
|
shareUserQuery.value = ''
|
|
selectedShareUser.value = null
|
|
const res = await apiClient.get(`/files/${shareFile.value.id}/permissions`)
|
|
filePermissions.value = res.data
|
|
} catch (err) {
|
|
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
|
}
|
|
}
|
|
|
|
async function removeUserShare(permId) {
|
|
if (!shareFile.value) return
|
|
try {
|
|
await apiClient.delete(`/files/${shareFile.value.id}/permissions/${permId}`)
|
|
filePermissions.value = filePermissions.value.filter(p => p.id !== permId)
|
|
} catch (err) {
|
|
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
|
}
|
|
}
|
|
|
|
async function createShare() {
|
|
if (!shareFile.value) return
|
|
shareLoading.value = true
|
|
try {
|
|
const opts = {}
|
|
if (sharePassword.value) opts.password = sharePassword.value
|
|
if (shareExpiry.value) opts.expires_at = shareExpiry.value
|
|
await filesStore.createShareLink(shareFile.value.id, opts)
|
|
shareLinks.value = await filesStore.getShareLinks(shareFile.value.id)
|
|
sharePassword.value = ''
|
|
shareExpiry.value = ''
|
|
toast.add({ severity: 'success', summary: 'Link erstellt', life: 3000 })
|
|
} catch (err) {
|
|
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
|
} finally {
|
|
shareLoading.value = false
|
|
}
|
|
}
|
|
|
|
function copyLink(token) {
|
|
navigator.clipboard.writeText(`${window.location.origin}/share/${token}`)
|
|
toast.add({ severity: 'info', summary: 'Link kopiert', life: 2000 })
|
|
}
|
|
|
|
async function removeShare(token) {
|
|
try {
|
|
await filesStore.deleteShareLink(token)
|
|
shareLinks.value = shareLinks.value.filter(l => l.token !== token)
|
|
} catch (err) {
|
|
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
|
}
|
|
}
|
|
|
|
function confirmDelete(data) {
|
|
deleteTarget.value = data
|
|
showDeleteConfirm.value = true
|
|
}
|
|
|
|
async function doDelete() {
|
|
if (!deleteTarget.value) return
|
|
try {
|
|
await filesStore.deleteFile(deleteTarget.value.id)
|
|
showDeleteConfirm.value = false
|
|
toast.add({ severity: 'success', summary: 'Geloescht', life: 3000 })
|
|
} catch (err) {
|
|
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
|
}
|
|
}
|
|
|
|
watch(() => route.params.folderId, () => {
|
|
filesStore.loadFiles(currentParentId())
|
|
})
|
|
|
|
onMounted(() => {
|
|
filesStore.loadFiles(currentParentId())
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.view-container { padding: 1.5rem; position: relative; min-height: 400px; }
|
|
.drop-overlay {
|
|
position: absolute; inset: 0; z-index: 100;
|
|
background: rgba(var(--p-primary-500-rgb, 59, 130, 246), 0.08);
|
|
border: 3px dashed var(--p-primary-color);
|
|
border-radius: 12px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
pointer-events: none;
|
|
}
|
|
.drop-content { text-align: center; color: var(--p-primary-color); }
|
|
.drop-content i { font-size: 3rem; }
|
|
.drop-content p { font-size: 1.1rem; font-weight: 500; margin: 0.5rem 0 0; }
|
|
.upload-progress {
|
|
background: var(--p-surface-0); border-radius: 8px; padding: 0.75rem 1rem;
|
|
margin-bottom: 1rem; border: 1px solid var(--p-surface-200);
|
|
}
|
|
.upload-info { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; font-size: 0.875rem; }
|
|
.view-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 1rem;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
.breadcrumb { display: flex; align-items: center; gap: 0.25rem; }
|
|
.crumb { cursor: pointer; color: var(--p-primary-color); font-weight: 500; }
|
|
.crumb:hover { text-decoration: underline; }
|
|
.crumb-sep { font-size: 0.75rem; color: var(--p-surface-400); }
|
|
.header-actions { display: flex; gap: 0.5rem; }
|
|
.file-name {
|
|
display: flex; align-items: center; gap: 0.5rem; cursor: pointer;
|
|
}
|
|
.file-icon { font-size: 1.125rem; width: 1.25rem; text-align: center; }
|
|
.shared-tag { font-size: 0.7rem; }
|
|
.row-actions { display: flex; gap: 0; }
|
|
.empty-state {
|
|
display: flex; flex-direction: column; align-items: center;
|
|
gap: 0.5rem; padding: 3rem; color: var(--p-text-muted-color);
|
|
}
|
|
.field { margin-bottom: 1rem; }
|
|
.field label { display: block; margin-bottom: 0.5rem; font-weight: 500; font-size: 0.875rem; }
|
|
.share-content h4 { margin: 0 0 1rem; }
|
|
.share-section { margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--p-surface-200); }
|
|
.share-section:last-child { border-bottom: none; }
|
|
.share-section h5 { margin: 0 0 0.75rem; font-size: 0.9rem; }
|
|
.share-form { }
|
|
.user-share-row { display: flex; gap: 0.5rem; align-items: flex-start; }
|
|
.user-search-results { border: 1px solid var(--p-surface-200); border-radius: 6px; margin-top: 0.25rem; max-height: 150px; overflow-y: auto; }
|
|
.user-result { padding: 0.5rem 0.75rem; cursor: pointer; display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; }
|
|
.user-result:hover, .user-result.selected { background: var(--p-primary-50); }
|
|
.existing-shares { margin-top: 0.5rem; }
|
|
.share-perm-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0; font-size: 0.875rem; }
|
|
.share-link-item {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
padding: 0.5rem 0; border-bottom: 1px solid var(--p-surface-100);
|
|
}
|
|
.link-info { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
.link-info code { font-size: 0.8rem; word-break: break-all; }
|
|
.link-info small { color: var(--p-text-muted-color); }
|
|
.link-actions { display: flex; }
|
|
.text-warn { color: var(--p-orange-500); font-size: 0.875rem; }
|
|
</style>
|