refactor(web): align gallery-viewer viewport naming and tunables

Change-Id: I5d9d3b510c2611eeb5f9f147aebb03fa6a6a6964
pull/28743/head
Min Idzelis 2026-06-01 13:18:10 +00:00
parent d120444a87
commit fdd3a1a3a2
2 changed files with 27 additions and 25 deletions

View File

@ -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)}
<div class="absolute" style:overflow="clip" style={getStyle(i)}>
<div class="absolute" style:overflow="clip" style={getStyle(index)}>
<Thumbnail
readonly={disableAssetSelect}
onClick={() => {
@ -382,8 +384,8 @@
asset={currentAsset}
selected={assetInteraction.hasSelectedAsset(currentAsset.id)}
selectionCandidate={assetInteraction.hasSelectionCandidate(currentAsset.id)}
thumbnailWidth={geometry.getWidth(i)}
thumbnailHeight={geometry.getHeight(i)}
thumbnailWidth={geometry.getWidth(index)}
thumbnailHeight={geometry.getHeight(index)}
/>
{#if showAssetName && !isTimelineAsset(asset)}
<div

View File

@ -309,7 +309,7 @@
<GalleryViewer
assets={searchResultAssets}
assetInteraction={assetMultiSelectManager}
onIntersected={loadNextPage}
onEndReached={loadNextPage}
showArchiveIcon={true}
{viewport}
onReload={onSearchQueryUpdate}