|
16 | 16 | import { archiveAssets, cancelMultiselect } from '$lib/utils/asset-utils';
|
17 | 17 | import { moveFocus } from '$lib/utils/focus-util';
|
18 | 18 | import { handleError } from '$lib/utils/handle-error';
|
19 |
| - import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils'; |
| 19 | + import { getJustifiedLayoutFromAssets, type CommonJustifiedLayout } from '$lib/utils/layout-utils'; |
20 | 20 | import { navigate } from '$lib/utils/navigation';
|
21 | 21 | import { isTimelineAsset, toTimelineAsset } from '$lib/utils/timeline-util';
|
22 | 22 | import { AssetVisibility, type AssetResponseDto } from '@immich/sdk';
|
|
27 | 27 | import Portal from '../portal/portal.svelte';
|
28 | 28 |
|
29 | 29 | interface Props {
|
30 |
| - assets: TimelineAsset[] | AssetResponseDto[]; |
| 30 | + assets: (TimelineAsset | AssetResponseDto)[]; |
31 | 31 | assetInteraction: AssetInteraction;
|
32 | 32 | disableAssetSelect?: boolean;
|
33 | 33 | showArchiveIcon?: boolean;
|
|
62 | 62 |
|
63 | 63 | let { isViewing: isViewerOpen, asset: viewingAsset, setAssetId } = assetViewingStore;
|
64 | 64 |
|
65 |
| - const geometry = $derived( |
66 |
| - getJustifiedLayoutFromAssets(assets, { |
| 65 | + let geometry: CommonJustifiedLayout | undefined = $state(); |
| 66 | +
|
| 67 | + $effect(() => { |
| 68 | + const _assets = assets; |
| 69 | + updateSlidingWindow(); |
| 70 | +
|
| 71 | + const rowWidth = Math.floor(viewport.width); |
| 72 | + const rowHeight = rowWidth < 850 ? 100 : 235; |
| 73 | +
|
| 74 | + geometry = getJustifiedLayoutFromAssets(_assets, { |
67 | 75 | spacing: 2,
|
68 |
| - heightTolerance: 0.3, |
69 |
| - rowHeight: Math.floor(viewport.width) < 850 ? 100 : 235, |
70 |
| - rowWidth: Math.floor(viewport.width), |
71 |
| - }), |
72 |
| - ); |
| 76 | + heightTolerance: 0.15, |
| 77 | + rowHeight, |
| 78 | + rowWidth, |
| 79 | + }); |
| 80 | + }); |
| 81 | +
|
| 82 | + let assetLayouts = $derived.by(() => { |
| 83 | + const assetLayout = []; |
| 84 | + let containerHeight = 0; |
| 85 | + let containerWidth = 0; |
| 86 | + if (geometry) { |
| 87 | + containerHeight = geometry.containerHeight; |
| 88 | + containerWidth = geometry.containerWidth; |
| 89 | + for (const [index, asset] of assets.entries()) { |
| 90 | + const top = geometry.getTop(index); |
| 91 | + const left = geometry.getLeft(index); |
| 92 | + const width = geometry.getWidth(index); |
| 93 | + const height = geometry.getHeight(index); |
| 94 | +
|
| 95 | + const layoutTopWithOffset = top + pageHeaderOffset; |
| 96 | + const layoutBottom = layoutTopWithOffset + height; |
| 97 | +
|
| 98 | + const display = layoutTopWithOffset < slidingWindow.bottom && layoutBottom > slidingWindow.top; |
| 99 | +
|
| 100 | + const layout = { |
| 101 | + asset, |
| 102 | + top, |
| 103 | + left, |
| 104 | + width, |
| 105 | + height, |
| 106 | + display, |
| 107 | + }; |
| 108 | +
|
| 109 | + assetLayout.push(layout); |
| 110 | + } |
| 111 | + } |
| 112 | +
|
| 113 | + return { |
| 114 | + assetLayout, |
| 115 | + containerHeight, |
| 116 | + containerWidth, |
| 117 | + }; |
| 118 | + }); |
73 | 119 |
|
74 | 120 | let currentViewAssetIndex = 0;
|
75 | 121 | let shiftKeyIsDown = $state(false);
|
76 | 122 | let lastAssetMouseEvent: TimelineAsset | null = $state(null);
|
77 |
| - let slidingTop = $state(0); |
78 |
| - let slidingBottom = $state(0); |
| 123 | + let slidingWindow = $state({ top: 0, bottom: 0 }); |
79 | 124 |
|
80 | 125 | const updateSlidingWindow = () => {
|
81 | 126 | const v = $state.snapshot(viewport);
|
82 | 127 | const top = (document.scrollingElement?.scrollTop || 0) - slidingWindowOffset;
|
83 |
| - slidingTop = top; |
84 |
| - slidingBottom = top + v.height; |
| 128 | + const bottom = top + v.height; |
| 129 | + const w = { |
| 130 | + top, |
| 131 | + bottom, |
| 132 | + }; |
| 133 | + slidingWindow = w; |
85 | 134 | };
|
86 |
| - $effect(updateSlidingWindow); |
87 | 135 | const debouncedOnIntersected = debounce(() => onIntersected?.(), 750, { maxWait: 100, leading: true });
|
88 | 136 |
|
89 | 137 | let lastIntersectedHeight = 0;
|
90 | 138 | $effect(() => {
|
91 | 139 | // notify we got to (near) the end of scroll
|
92 | 140 | const scrollPercentage =
|
93 |
| - (slidingBottom - viewport.height) / (viewport.height - (document.scrollingElement?.clientHeight || 0)); |
94 |
| -
|
95 |
| - if (scrollPercentage > 0.9 && lastIntersectedHeight !== geometry.containerHeight) { |
96 |
| - debouncedOnIntersected(); |
97 |
| - lastIntersectedHeight = geometry.containerHeight; |
| 141 | + ((slidingWindow.bottom - viewport.height) / (viewport.height - (document.scrollingElement?.clientHeight || 0))) * |
| 142 | + 100; |
| 143 | +
|
| 144 | + if (scrollPercentage > 90) { |
| 145 | + const intersectedHeight = geometry?.containerHeight || 0; |
| 146 | + if (lastIntersectedHeight !== intersectedHeight) { |
| 147 | + debouncedOnIntersected(); |
| 148 | + lastIntersectedHeight = intersectedHeight; |
| 149 | + } |
98 | 150 | }
|
99 | 151 | });
|
100 | 152 | const viewAssetHandler = async (asset: TimelineAsset) => {
|
|
204 | 256 | isShowDeleteConfirmation = false;
|
205 | 257 | await deleteAssets(
|
206 | 258 | !(isTrashEnabled && !force),
|
207 |
| - (assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id)) as TimelineAsset[]), |
| 259 | + (assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id))), |
208 | 260 | assetInteraction.selectedAssets,
|
209 | 261 | onReload,
|
210 | 262 | );
|
|
217 | 269 | assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive,
|
218 | 270 | );
|
219 | 271 | if (ids) {
|
220 |
| - assets = assets.filter((asset) => !ids.includes(asset.id)) as TimelineAsset[]; |
| 272 | + assets = assets.filter((asset) => !ids.includes(asset.id)); |
221 | 273 | deselectAllAssets();
|
222 | 274 | }
|
223 | 275 | };
|
|
402 | 454 | onkeyup={onKeyUp}
|
403 | 455 | onselectstart={onSelectStart}
|
404 | 456 | use:shortcuts={shortcutList}
|
405 |
| - onscroll={updateSlidingWindow} |
| 457 | + onscroll={() => updateSlidingWindow()} |
406 | 458 | />
|
407 | 459 |
|
408 | 460 | {#if isShowDeleteConfirmation}
|
|
416 | 468 | {#if assets.length > 0}
|
417 | 469 | <div
|
418 | 470 | style:position="relative"
|
419 |
| - style:height={geometry.containerHeight + 'px'} |
420 |
| - style:width={geometry.containerWidth + 'px'} |
| 471 | + style:height={assetLayouts.containerHeight + 'px'} |
| 472 | + style:width={assetLayouts.containerWidth - 1 + 'px'} |
421 | 473 | >
|
422 |
| - {#each assets as asset, i (asset.id + i)} |
423 |
| - {#if geometry.getTop(i) + pageHeaderOffset < slidingBottom && geometry.getTop(i) + pageHeaderOffset + geometry.getHeight(i) > slidingTop} |
424 |
| - {@const layout = geometry.getPosition(i)} |
| 474 | + {#each assetLayouts.assetLayout as layout, layoutIndex (layout.asset.id + '-' + layoutIndex)} |
| 475 | + {@const currentAsset = layout.asset} |
| 476 | + |
| 477 | + {#if layout.display} |
425 | 478 | <div
|
426 | 479 | class="absolute"
|
427 | 480 | style:overflow="clip"
|
|
431 | 484 | readonly={disableAssetSelect}
|
432 | 485 | onClick={() => {
|
433 | 486 | if (assetInteraction.selectionActive) {
|
434 |
| - handleSelectAssets(toTimelineAsset(asset)); |
| 487 | + handleSelectAssets(toTimelineAsset(currentAsset)); |
435 | 488 | return;
|
436 | 489 | }
|
437 |
| - void viewAssetHandler(toTimelineAsset(asset)); |
| 490 | + void viewAssetHandler(toTimelineAsset(currentAsset)); |
438 | 491 | }}
|
439 |
| - onSelect={() => handleSelectAssets(toTimelineAsset(asset))} |
440 |
| - onMouseEvent={() => assetMouseEventHandler(toTimelineAsset(asset))} |
| 492 | + onSelect={() => handleSelectAssets(toTimelineAsset(currentAsset))} |
| 493 | + onMouseEvent={() => assetMouseEventHandler(toTimelineAsset(currentAsset))} |
441 | 494 | {showArchiveIcon}
|
442 |
| - asset={toTimelineAsset(asset)} |
443 |
| - selected={assetInteraction.hasSelectedAsset(asset.id)} |
444 |
| - selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)} |
| 495 | + asset={toTimelineAsset(currentAsset)} |
| 496 | + selected={assetInteraction.hasSelectedAsset(currentAsset.id)} |
| 497 | + selectionCandidate={assetInteraction.hasSelectionCandidate(currentAsset.id)} |
445 | 498 | thumbnailWidth={layout.width}
|
446 | 499 | thumbnailHeight={layout.height}
|
447 | 500 | />
|
448 |
| - {#if showAssetName && !isTimelineAsset(asset)} |
| 501 | + {#if showAssetName && !isTimelineAsset(currentAsset)} |
449 | 502 | <div
|
450 | 503 | class="absolute text-center p-1 text-xs font-mono font-semibold w-full bottom-0 bg-linear-to-t bg-slate-50/75 dark:bg-slate-800/75 overflow-clip text-ellipsis whitespace-pre-wrap"
|
451 | 504 | >
|
452 |
| - {asset.originalFileName} |
| 505 | + {currentAsset.originalFileName} |
453 | 506 | </div>
|
454 | 507 | {/if}
|
455 | 508 | </div>
|
|
0 commit comments