266 lines
12 KiB
Vue
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>
|