diff --git a/frontend/src/components/assets/AssetCard.vue b/frontend/src/components/assets/AssetCard.vue index a80a0ce..21669fe 100644 --- a/frontend/src/components/assets/AssetCard.vue +++ b/frontend/src/components/assets/AssetCard.vue @@ -12,6 +12,8 @@ :src="asset.thumbnail_url" alt="" draggable="false" + loading="lazy" + decoding="async" class="size-full object-cover" />
diff --git a/frontend/src/components/common/FileTypeIcon.vue b/frontend/src/components/common/FileTypeIcon.vue index 5e94c65..6f4e720 100644 --- a/frontend/src/components/common/FileTypeIcon.vue +++ b/frontend/src/components/common/FileTypeIcon.vue @@ -7,7 +7,14 @@ 'grid place-items-center', ]" > - +
diff --git a/frontend/src/components/projects/useProjectBrowser.ts b/frontend/src/components/projects/useProjectBrowser.ts index 457e8a2..b54fb9e 100644 --- a/frontend/src/components/projects/useProjectBrowser.ts +++ b/frontend/src/components/projects/useProjectBrowser.ts @@ -40,6 +40,8 @@ export function useProjectBrowser( const tag = ref(null) const sort = ref({ field: 'creation', order: 'desc' }) const limit = ref(PAGE_SIZE) + const loadingMore = ref(false) + const reachedEnd = ref(false) const selection = ref([]) const preview = ref<{ asset: Asset; url: string } | null>(null) const view = ref<'grid' | 'list'>(storedView()) @@ -141,13 +143,21 @@ export function useProjectBrowser( } return counts }) - const hasMore = computed(() => assets.value.length < total.value) + const hasMore = computed(() => !reachedEnd.value && assets.value.length < total.value) const hasProcessing = computed(() => assets.value.some((asset) => asset.status === 'Processing')) watch(searchInput, (value) => { clearTimeout(debounceTimer) debounceTimer = setTimeout(() => (search.value = value.trim()), 250) }) + watch( + () => assetsCall.loading, + (loading) => { + if (loading) return + loadingMore.value = false + reachedEnd.value = assets.value.length < limit.value + }, + ) watch([currentProject, currentFolder, category, tag, search, sort], resetList, { deep: true }) watch(view, (value) => localStorage.setItem('vms_asset_view', value)) watch(hasProcessing, configurePolling, { immediate: true }) @@ -303,9 +313,17 @@ export function useProjectBrowser( function resetList() { limit.value = PAGE_SIZE + loadingMore.value = false + reachedEnd.value = false selection.value = [] } + function loadMore() { + if (assetsCall.loading || !hasMore.value) return + loadingMore.value = true + limit.value += PAGE_SIZE + } + return { project, folders, @@ -322,6 +340,7 @@ export function useProjectBrowser( folderCounts, selectedAssets, hasMore, + loadingMore, searchInput, category, tag, @@ -333,14 +352,13 @@ export function useProjectBrowser( hasNextPreview, showPreviousPreview: () => stepPreview(-1), showNextPreview: () => stepPreview(1), - limit, openAsset, moveAssets, moveFolder, deleteFolder, reloadAssets, reloadAll, - loadMore: () => (limit.value += PAGE_SIZE), + loadMore, } } diff --git a/frontend/src/composables/useInfiniteScroll.ts b/frontend/src/composables/useInfiniteScroll.ts new file mode 100644 index 0000000..fdc737f --- /dev/null +++ b/frontend/src/composables/useInfiniteScroll.ts @@ -0,0 +1,40 @@ +import { onScopeDispose, watch, type Ref } from 'vue' + +export function useInfiniteScroll( + target: Ref, + busy: Ref, + canLoad: () => boolean, + onLoadMore: () => void, +) { + let observer: IntersectionObserver | undefined + let intersecting = false + + function maybeLoad() { + if (intersecting && !busy.value && canLoad()) onLoadMore() + } + + watch( + target, + (el) => { + observer?.disconnect() + observer = undefined + intersecting = false + if (!el) return + observer = new IntersectionObserver( + (entries) => { + intersecting = entries[entries.length - 1]?.isIntersecting ?? false + maybeLoad() + }, + { rootMargin: '600px 0px' }, + ) + observer.observe(el) + }, + { immediate: true }, + ) + + watch(busy, (now, before) => { + if (before && !now) maybeLoad() + }) + + onScopeDispose(() => observer?.disconnect()) +} diff --git a/frontend/src/pages/ProjectDetailPage.vue b/frontend/src/pages/ProjectDetailPage.vue index c909643..ca0407d 100644 --- a/frontend/src/pages/ProjectDetailPage.vue +++ b/frontend/src/pages/ProjectDetailPage.vue @@ -174,13 +174,9 @@ @drop-assets="(names, target) => moveAssets(names, target)" @drop-folder="(name, target) => moveFolder(name, target)" /> -
-