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

pull/28768/head
Min Idzelis 2026-06-02 08:54:44 -04:00 committed by GitHub
parent 942d3c648c
commit 65d8b35f8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 27 additions and 25 deletions

View File

@ -22,11 +22,16 @@
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils'; import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
import { navigate } from '$lib/utils/navigation'; import { navigate } from '$lib/utils/navigation';
import { isTimelineAsset, toTimelineAsset } from '$lib/utils/timeline-util'; import { isTimelineAsset, toTimelineAsset } from '$lib/utils/timeline-util';
import { TUNABLES } from '$lib/utils/tunables';
import { AssetVisibility, type AssetResponseDto } from '@immich/sdk'; import { AssetVisibility, type AssetResponseDto } from '@immich/sdk';
import { modalManager } from '@immich/ui'; import { modalManager } from '@immich/ui';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
const {
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
} = TUNABLES;
type Props = { type Props = {
assets: AssetResponseDto[]; assets: AssetResponseDto[];
viewerAssets?: AssetResponseDto[]; viewerAssets?: AssetResponseDto[];
@ -34,7 +39,7 @@
disableAssetSelect?: boolean; disableAssetSelect?: boolean;
showArchiveIcon?: boolean; showArchiveIcon?: boolean;
viewport: Viewport; viewport: Viewport;
onIntersected?: (() => void) | undefined; onEndReached?: (() => void) | undefined;
showAssetName?: boolean; showAssetName?: boolean;
onReload?: (() => void) | undefined; onReload?: (() => void) | undefined;
pageHeaderOffset?: number; pageHeaderOffset?: number;
@ -50,7 +55,7 @@
disableAssetSelect = false, disableAssetSelect = false,
showArchiveIcon = false, showArchiveIcon = false,
viewport, viewport,
onIntersected = undefined, onEndReached = undefined,
showAssetName = false, showAssetName = false,
onReload = undefined, onReload = undefined,
slidingWindowOffset = 0, slidingWindowOffset = 0,
@ -70,24 +75,23 @@
}), }),
); );
const getStyle = (i: number) => { const getStyle = (index: number) => {
const geo = geometry; return `top: ${geometry.getTop(index)}px; left: ${geometry.getLeft(index)}px; width: ${geometry.getWidth(index)}px; height: ${geometry.getHeight(index)}px;`;
return `top: ${geo.getTop(i)}px; left: ${geo.getLeft(i)}px; width: ${geo.getWidth(i)}px; height: ${geo.getHeight(i)}px;`;
}; };
const isIntersecting = (i: number) => { const isInOrNearViewport = (index: number) => {
const geo = geometry;
const window = slidingWindow; const window = slidingWindow;
const top = geo.getTop(i); const top = geometry.getTop(index);
return top + pageHeaderOffset < window.bottom && top + geo.getHeight(i) > window.top; return top + pageHeaderOffset < window.bottom && top + geometry.getHeight(index) > window.top;
}; };
let shiftKeyIsDown = $state(false); let shiftKeyIsDown = $state(false);
let lastAssetMouseEvent: TimelineAsset | null = $state(null); let lastAssetMouseEvent: TimelineAsset | null = $state(null);
let scrollTop = $state(0); let scrollTop = $state(0);
let slidingWindow = $derived.by(() => { let slidingWindow = $derived.by(() => {
const top = (scrollTop || 0) - slidingWindowOffset; const top = (scrollTop || 0) - slidingWindowOffset - INTERSECTION_EXPAND_TOP;
const bottom = top + viewport.height + slidingWindowOffset; const bottom = top + viewport.height + slidingWindowOffset + INTERSECTION_EXPAND_BOTTOM;
return { return {
top, top,
bottom, bottom,
@ -101,17 +105,15 @@
const updateSlidingWindow = () => (scrollTop = document.scrollingElement?.scrollTop ?? 0); 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(() => { $effect(() => {
// Intersect if there's only one viewport worth of assets left to scroll.
if (geometry.containerHeight - slidingWindow.bottom <= viewport.height) { if (geometry.containerHeight - slidingWindow.bottom <= viewport.height) {
// Notify we got to (near) the end of scroll. const contentHeight = geometry.containerHeight;
const intersectedHeight = geometry.containerHeight; if (lastEndReachedHeight !== contentHeight) {
if (lastIntersectedHeight !== intersectedHeight) { debouncedOnEndReached();
debouncedOnIntersected(); lastEndReachedHeight = contentHeight;
lastIntersectedHeight = intersectedHeight;
} }
} }
}); });
@ -362,10 +364,10 @@
style:height={geometry.containerHeight + 'px'} style:height={geometry.containerHeight + 'px'}
style:width={geometry.containerWidth + 'px'} style:width={geometry.containerWidth + 'px'}
> >
{#each assets as asset, i (asset.id + '-' + i)} {#each assets as asset, index (asset.id + '-' + index)}
{#if isIntersecting(i)} {#if isInOrNearViewport(index)}
{@const currentAsset = toTimelineAsset(asset)} {@const currentAsset = toTimelineAsset(asset)}
<div class="absolute" style:overflow="clip" style={getStyle(i)}> <div class="absolute" style:overflow="clip" style={getStyle(index)}>
<Thumbnail <Thumbnail
readonly={disableAssetSelect} readonly={disableAssetSelect}
onClick={() => { onClick={() => {
@ -382,8 +384,8 @@
asset={currentAsset} asset={currentAsset}
selected={assetInteraction.hasSelectedAsset(currentAsset.id)} selected={assetInteraction.hasSelectedAsset(currentAsset.id)}
selectionCandidate={assetInteraction.hasSelectionCandidate(currentAsset.id)} selectionCandidate={assetInteraction.hasSelectionCandidate(currentAsset.id)}
thumbnailWidth={geometry.getWidth(i)} thumbnailWidth={geometry.getWidth(index)}
thumbnailHeight={geometry.getHeight(i)} thumbnailHeight={geometry.getHeight(index)}
/> />
{#if showAssetName && !isTimelineAsset(asset)} {#if showAssetName && !isTimelineAsset(asset)}
<div <div

View File

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