diff --git a/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte index fc2131310a..ef1c2bda76 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte @@ -22,11 +22,16 @@ import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils'; import { navigate } from '$lib/utils/navigation'; import { isTimelineAsset, toTimelineAsset } from '$lib/utils/timeline-util'; + import { TUNABLES } from '$lib/utils/tunables'; import { AssetVisibility, type AssetResponseDto } from '@immich/sdk'; import { modalManager } from '@immich/ui'; import { debounce } from 'lodash-es'; import { t } from 'svelte-i18n'; + const { + TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM }, + } = TUNABLES; + type Props = { assets: AssetResponseDto[]; viewerAssets?: AssetResponseDto[]; @@ -34,7 +39,7 @@ disableAssetSelect?: boolean; showArchiveIcon?: boolean; viewport: Viewport; - onIntersected?: (() => void) | undefined; + onEndReached?: (() => void) | undefined; showAssetName?: boolean; onReload?: (() => void) | undefined; pageHeaderOffset?: number; @@ -50,7 +55,7 @@ disableAssetSelect = false, showArchiveIcon = false, viewport, - onIntersected = undefined, + onEndReached = undefined, showAssetName = false, onReload = undefined, slidingWindowOffset = 0, @@ -70,24 +75,23 @@ }), ); - const getStyle = (i: number) => { - const geo = geometry; - return `top: ${geo.getTop(i)}px; left: ${geo.getLeft(i)}px; width: ${geo.getWidth(i)}px; height: ${geo.getHeight(i)}px;`; + const getStyle = (index: number) => { + return `top: ${geometry.getTop(index)}px; left: ${geometry.getLeft(index)}px; width: ${geometry.getWidth(index)}px; height: ${geometry.getHeight(index)}px;`; }; - const isIntersecting = (i: number) => { - const geo = geometry; + const isInOrNearViewport = (index: number) => { const window = slidingWindow; - const top = geo.getTop(i); - return top + pageHeaderOffset < window.bottom && top + geo.getHeight(i) > window.top; + const top = geometry.getTop(index); + return top + pageHeaderOffset < window.bottom && top + geometry.getHeight(index) > window.top; }; let shiftKeyIsDown = $state(false); let lastAssetMouseEvent: TimelineAsset | null = $state(null); let scrollTop = $state(0); + let slidingWindow = $derived.by(() => { - const top = (scrollTop || 0) - slidingWindowOffset; - const bottom = top + viewport.height + slidingWindowOffset; + const top = (scrollTop || 0) - slidingWindowOffset - INTERSECTION_EXPAND_TOP; + const bottom = top + viewport.height + slidingWindowOffset + INTERSECTION_EXPAND_BOTTOM; return { top, bottom, @@ -101,17 +105,15 @@ const updateSlidingWindow = () => (scrollTop = document.scrollingElement?.scrollTop ?? 0); - const debouncedOnIntersected = debounce(() => onIntersected?.(), 750, { maxWait: 100, leading: true }); + const debouncedOnEndReached = debounce(() => onEndReached?.(), 750, { maxWait: 100, leading: true }); - let lastIntersectedHeight = 0; + let lastEndReachedHeight = 0; $effect(() => { - // Intersect if there's only one viewport worth of assets left to scroll. if (geometry.containerHeight - slidingWindow.bottom <= viewport.height) { - // Notify we got to (near) the end of scroll. - const intersectedHeight = geometry.containerHeight; - if (lastIntersectedHeight !== intersectedHeight) { - debouncedOnIntersected(); - lastIntersectedHeight = intersectedHeight; + const contentHeight = geometry.containerHeight; + if (lastEndReachedHeight !== contentHeight) { + debouncedOnEndReached(); + lastEndReachedHeight = contentHeight; } } }); @@ -362,10 +364,10 @@ style:height={geometry.containerHeight + 'px'} style:width={geometry.containerWidth + 'px'} > - {#each assets as asset, i (asset.id + '-' + i)} - {#if isIntersecting(i)} + {#each assets as asset, index (asset.id + '-' + index)} + {#if isInOrNearViewport(index)} {@const currentAsset = toTimelineAsset(asset)} -