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