Files

266 lines
12 KiB
Vue

<script setup lang="ts">
import { useFloating, offset, flip, shift, autoUpdate, hide } from '@floating-ui/vue';
import { resolveFileUrl } from '~~/utils/url';
const { open: sidebarOpen, openSidebar } = useSidebar();
const {
filteredFiles,
selectedIds,
selectedCount,
hasSelection,
isAllSelected,
toggleSelect,
selectAll,
clearSelection,
deleteFile,
deleteSelected,
copyLink,
downloadFile,
getFileIcon,
isImage,
formatSize,
formatDate,
} = await useResources();
const scrollRef = ref<HTMLElement | null>(null);
const dropdownOpen = ref(false);
const dropdownTrigger = ref<HTMLElement | null>(null);
const dropdownContent = ref(null);
const activeMenuFileId = ref<string | null>(null);
const previewSrc = ref<string | null>(null);
const previewOrigin = ref<DOMRect | null>(null);
const { floatingStyles, placement, middlewareData } = useFloating(dropdownTrigger, dropdownContent, {
placement: 'bottom-end',
whileElementsMounted: autoUpdate,
middleware: [offset(6), flip(), shift({ padding: 10 }), hide()],
transform: false,
});
const transformOrigin = computed(() =>
placement.value.startsWith('top')
? 'transform-origin-bottom-center'
: 'transform-origin-top-center'
);
const closeDropdown = () => {
dropdownOpen.value = false;
activeMenuFileId.value = null;
dropdownTrigger.value = null;
};
const menuFile = computed(() =>
filteredFiles.value.find(f => f.id === activeMenuFileId.value)
);
const handleRowClick = (e: MouseEvent) => {
const trigger = (e.target as HTMLElement).closest('[data-action]') as HTMLElement | null;
if (!trigger) return;
const fileId = trigger.dataset.fileId || (trigger.closest('[data-file-id]') as HTMLElement | null)?.dataset.fileId;
if (!fileId) return;
const action = trigger.dataset.action;
switch (action) {
case 'toggle-dropdown':
e.preventDefault();
e.stopPropagation();
if (activeMenuFileId.value === fileId && dropdownOpen.value) {
closeDropdown();
return;
}
activeMenuFileId.value = fileId;
dropdownTrigger.value = trigger;
dropdownOpen.value = true;
break;
case 'toggle-select':
e.preventDefault();
e.stopPropagation();
toggleSelect(fileId);
break;
case 'open':
e.preventDefault();
e.stopPropagation();
const file = filteredFiles.value.find(f => f.id === fileId);
if (file && isImage(file.mimeType)) {
const row = trigger.closest('[data-file-id]') as HTMLElement;
previewOrigin.value = row?.getBoundingClientRect() ?? null;
previewSrc.value = resolveFileUrl(file.url);
}
break;
}
};
const handleSelectAll = () => {
if (isAllSelected.value) {
clearSelection();
} else {
selectAll();
}
};
const handleDeleteSelected = async () => {
await deleteSelected();
};
const closePreview = () => {
previewSrc.value = null;
previewOrigin.value = null;
};
</script>
<template>
<div class="h-full w-full flex flex-col">
<div class="h-14 flex items-center justify-between px-4 shrink-0">
<div class="flex items-center gap-2">
<button v-if="!sidebarOpen" @click="openSidebar"
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors">
<span class="i-mynaui-panel-left-open text-5"></span>
</button>
<h1 class="text-lg font-semibold text-[var(--text-primary)]">Resources</h1>
</div>
</div>
<div ref="scrollRef" class="flex-1 overflow-auto pb-4">
<div v-if="filteredFiles.length === 0"
class="flex flex-col items-center justify-center h-full gap-2 text-[var(--text-tertiary)]">
<span class="i-tabler-books text-10"></span>
<span class="text-sm">No files found</span>
</div>
<div v-else @click="handleRowClick" class="flex flex-col">
<div
class="sticky top-0 z-5 bg-[var(--bg-surface)] flex items-center gap-3 px-3 h-10 shrink-0 text-[var(--text-tertiary)] text-xs font-medium uppercase tracking-wider border-b border-[var(--color-border)]">
<button @click="handleSelectAll"
class="flex items-center justify-center h-4 w-4 rounded border transition-colors shrink-0"
:class="isAllSelected
? 'bg-[var(--color-accent)] border-[var(--color-accent)]'
: 'border-[var(--color-border)] @hover:border-[var(--color-border-active)]'">
<span v-if="isAllSelected" class="i-mynaui-check text-3 text-[var(--color-accent-text)]"></span>
</button>
<span class="flex-1 min-w-0">Name</span>
<span class="w-20 text-right shrink-0 hidden md:block">Size</span>
<span class="w-24 text-right shrink-0 hidden md:block">Date</span>
<div class="w-8 shrink-0"></div>
</div>
<RowVirtualizerFixed :scroll-element="scrollRef" key-field="id" :items="filteredFiles" :item-size="48"
:overscan="20" :prerender="50">
<template v-slot="{ item: file }">
<div :data-file-id="file.id" class="group flex items-center gap-3 px-3 cursor-default h-12"
:class="selectedIds.has(file.id)
? 'bg-[var(--color-active)]'
: '@hover:bg-[var(--color-hover)]'">
<button data-action="toggle-select" :data-file-id="file.id"
class="flex items-center justify-center h-4 w-4 rounded border shrink-0 transition-colors"
:class="selectedIds.has(file.id)
? 'bg-[var(--color-accent)] border-[var(--color-accent)]'
: 'border-[var(--color-border)] @hover:border-[var(--color-border-active)]'">
<span v-if="selectedIds.has(file.id)"
class="i-mynaui-check text-3 text-[var(--color-accent-text)]"></span>
</button>
<div data-action="open" :data-file-id="file.id"
class="flex items-center gap-2 flex-1 min-w-0"
:class="isImage(file.mimeType) ? 'cursor-pointer' : ''">
<span class="text-6 text-[var(--text-tertiary)] shrink-0"
:class="getFileIcon(file.mimeType)"></span>
<span
class="md:text-sm font-medium text-[var(--text-primary)] overflow-hidden text-ellipsis whitespace-nowrap">
{{ file.name }}
</span>
</div>
<span
class="w-20 text-right text-xs text-[var(--text-tertiary)] tabular-nums shrink-0 hidden md:block">
{{ formatSize(file.size) }}
</span>
<span
class="w-24 text-right text-xs text-[var(--text-tertiary)] tabular-nums shrink-0 hidden md:block">
{{ formatDate(file.createdAt) }}
</span>
<div data-action="toggle-dropdown" :data-file-id="file.id"
class="text-[var(--text-secondary)] shrink-0 md:opacity-0 group-hover:opacity-100 p-1 flex items-center justify-center rounded-md @hover:bg-[var(--color-hover)] transition-[opacity,background-color] duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<span class="pointer-events-none h-6 w-6 md:h-4.5 md:w-4.5 i-tabler-dots"></span>
</div>
</div>
</template>
</RowVirtualizerFixed>
</div>
</div>
<Transition
enter-active-class="transition-[opacity,transform] duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
enter-from-class="opacity-0 translate-y-4" enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition-[opacity,transform] duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
leave-from-class="opacity-100 translate-y-0" leave-to-class="opacity-0 translate-y-4">
<div v-if="hasSelection"
class="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 bg-[var(--bg-surface)] border border-[var(--color-border)] rounded-full px-4 py-2 shadow-xl">
<span class="text-sm font-medium text-[var(--text-primary)]">
{{ selectedCount }} selected
</span>
<div class="w-px h-4 bg-[var(--color-border)]"></div>
<button @click="handleDeleteSelected"
class="flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-medium text-red-500 @hover:bg-red-500/10 transition-colors">
<span class="i-mynaui-trash text-4"></span>
Delete
</button>
<button @click="clearSelection"
class="flex items-center justify-center p-1 rounded-full text-[var(--text-tertiary)] @hover:bg-[var(--color-hover)] transition-colors">
<span class="i-mynaui-x text-4"></span>
</button>
</div>
</Transition>
<Teleport to="body">
<Transition
enter-active-class="transition-[opacity,transform] duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
enter-from-class="opacity-0 scale-95 translate-y-1" enter-to-class="opacity-100 scale-100 translate-y-0"
leave-active-class="transition-[opacity,transform] duration-100 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
leave-from-class="opacity-100 scale-100 translate-y-0"
leave-to-class="opacity-0 scale-95 translate-y-1">
<div v-if="dropdownOpen" ref="dropdownContent" :style="{
...floatingStyles,
visibility: middlewareData.hide?.referenceHidden
? 'hidden'
: 'visible',
}" class="fixed z-15" :class="transformOrigin">
<div v-click-outside="closeDropdown"
class="bg-[var(--bg-surface)] border border-[var(--color-border)] rounded-xl p-1.5 shadow-xl flex flex-col gap-1 min-w-40">
<template v-if="menuFile">
<button @click="copyLink(menuFile); closeDropdown()"
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors flex items-center gap-2">
<span class="i-mynaui-copy text-4 text-[var(--text-tertiary)]"></span>
Copy Link
</button>
<button @click="downloadFile(menuFile); closeDropdown()"
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors flex items-center gap-2">
<span class="i-mynaui-download text-4 text-[var(--text-tertiary)]"></span>
Download
</button>
<div class="h-px bg-[var(--color-border)] my-1" />
<button @click="deleteFile(menuFile.id); closeDropdown()"
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors text-red-500 flex items-center gap-2">
<span class="i-mynaui-trash text-4"></span>
Delete
</button>
</template>
</div>
</div>
</Transition>
</Teleport>
<ImageViewer v-if="previewSrc" :src="previewSrc" :origin-rect="previewOrigin" @close="closePreview" />
</div>
</template>