feat(share) : page explorateur de fichiers du partage
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('sharedFiles.title') }}</h1>
|
||||
|
||||
<!-- Fil d'Ariane -->
|
||||
<nav class="mt-4 flex flex-wrap items-center gap-1 text-sm text-neutral-500">
|
||||
<button class="hover:text-primary-500" @click="openPath('')">{{ $t('sharedFiles.root') }}</button>
|
||||
<template v-for="crumb in breadcrumb" :key="crumb.path">
|
||||
<span>/</span>
|
||||
<button class="hover:text-primary-500" @click="openPath(crumb.path)">{{ crumb.name }}</button>
|
||||
</template>
|
||||
</nav>
|
||||
|
||||
<!-- Filtre local -->
|
||||
<div class="mt-4 max-w-sm">
|
||||
<MalioInputText
|
||||
v-model="filter"
|
||||
:placeholder="$t('sharedFiles.filterPlaceholder')"
|
||||
input-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- États -->
|
||||
<div v-if="loading" class="mt-10 flex justify-center">
|
||||
<Icon name="heroicons:arrow-path" class="h-6 w-6 animate-spin text-neutral-400" />
|
||||
</div>
|
||||
<p v-else-if="error" class="mt-10 text-sm text-red-600">{{ error }}</p>
|
||||
<p v-else-if="visibleEntries.length === 0" class="mt-10 text-sm text-neutral-400">{{ $t('sharedFiles.empty') }}</p>
|
||||
|
||||
<!-- Tableau -->
|
||||
<table v-else class="mt-6 w-full text-sm">
|
||||
<thead class="border-b border-neutral-200 text-left text-xs uppercase tracking-wider text-neutral-400">
|
||||
<tr>
|
||||
<th class="py-2">{{ $t('sharedFiles.colName') }}</th>
|
||||
<th class="py-2">{{ $t('sharedFiles.colSize') }}</th>
|
||||
<th class="py-2">{{ $t('sharedFiles.colModified') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="entry in visibleEntries"
|
||||
:key="entry.path"
|
||||
class="cursor-pointer border-b border-neutral-100 hover:bg-neutral-50"
|
||||
@click="onEntryClick(entry)"
|
||||
>
|
||||
<td class="flex items-center gap-2 py-2">
|
||||
<Icon :name="entry.isDir ? 'mdi:folder-outline' : iconForMime(entry.mimeType)" class="h-5 w-5 text-neutral-400" />
|
||||
<span class="truncate">{{ entry.name }}</span>
|
||||
</td>
|
||||
<td class="py-2 text-neutral-500">{{ entry.isDir ? '—' : formatFileSize(entry.size) }}</td>
|
||||
<td class="py-2 text-neutral-500">{{ formatDate(entry.modifiedAt) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<SharedFilePreview
|
||||
:entry="previewEntry"
|
||||
:has-prev="previewIndex > 0"
|
||||
:has-next="previewIndex >= 0 && previewIndex < fileEntries.length - 1"
|
||||
@close="previewEntry = null"
|
||||
@prev="stepPreview(-1)"
|
||||
@next="stepPreview(1)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Breadcrumb, FileEntry } from '~/services/dto/share'
|
||||
import { useShareService } from '~/services/share'
|
||||
import { formatFileSize } from '~/utils/format'
|
||||
|
||||
useHead({ title: 'Documents' })
|
||||
|
||||
const { browse } = useShareService()
|
||||
const { enabled, ensureLoaded } = useShareStatus()
|
||||
|
||||
const currentPath = ref('')
|
||||
const breadcrumb = ref<Breadcrumb[]>([])
|
||||
const entries = ref<FileEntry[]>([])
|
||||
const filter = ref('')
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const previewEntry = ref<FileEntry | null>(null)
|
||||
|
||||
const visibleEntries = computed(() => {
|
||||
const f = filter.value.trim().toLowerCase()
|
||||
if (!f) return entries.value
|
||||
return entries.value.filter((e) => e.name.toLowerCase().includes(f))
|
||||
})
|
||||
|
||||
const fileEntries = computed(() => visibleEntries.value.filter((e) => !e.isDir))
|
||||
const previewIndex = computed(() => previewEntry.value ? fileEntries.value.findIndex((e) => e.path === previewEntry.value!.path) : -1)
|
||||
|
||||
async function load(path: string) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const result = await browse(path)
|
||||
currentPath.value = result.path
|
||||
breadcrumb.value = result.breadcrumb
|
||||
entries.value = result.entries
|
||||
} catch (e: unknown) {
|
||||
error.value = (e as Error)?.message ?? 'Erreur'
|
||||
entries.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openPath(path: string) {
|
||||
filter.value = ''
|
||||
load(path)
|
||||
}
|
||||
|
||||
function onEntryClick(entry: FileEntry) {
|
||||
if (entry.isDir) {
|
||||
openPath(entry.path)
|
||||
} else {
|
||||
previewEntry.value = entry
|
||||
}
|
||||
}
|
||||
|
||||
function stepPreview(delta: number) {
|
||||
const idx = previewIndex.value + delta
|
||||
if (idx >= 0 && idx < fileEntries.value.length) {
|
||||
previewEntry.value = fileEntries.value[idx] ?? null
|
||||
}
|
||||
}
|
||||
|
||||
function iconForMime(mime: string): string {
|
||||
if (mime.startsWith('image/')) return 'mdi:file-image-outline'
|
||||
if (mime === 'application/pdf') return 'mdi:file-pdf-box'
|
||||
if (mime.startsWith('text/')) return 'mdi:file-document-outline'
|
||||
return 'mdi:file-outline'
|
||||
}
|
||||
|
||||
function formatDate(ts: number | null): string {
|
||||
if (!ts) return '—'
|
||||
return new Date(ts * 1000).toLocaleString()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await ensureLoaded()
|
||||
if (enabled.value === false) {
|
||||
await navigateTo('/')
|
||||
return
|
||||
}
|
||||
load('')
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user