|
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, type CommonJustifiedLayout } from '$lib/utils/layout-utils'; |
| 19 | + import { getJustifiedLayoutFromAssets } 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 |
| - 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, { |
| 65 | + const geometry = $derived( |
| 66 | + getJustifiedLayoutFromAssets(assets, { |
75 | 67 | spacing: 2,
|
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 |
| - }); |
| 68 | + heightTolerance: 0.3, |
| 69 | + rowHeight: Math.floor(viewport.width) < 850 ? 100 : 235, |
| 70 | + rowWidth: Math.floor(viewport.width), |
| 71 | + }), |
| 72 | + ); |
119 | 73 |
|
120 | 74 | let currentViewAssetIndex = 0;
|
121 | 75 | let shiftKeyIsDown = $state(false);
|
122 | 76 | let lastAssetMouseEvent: TimelineAsset | null = $state(null);
|
123 |
| - let slidingWindow = $state({ top: 0, bottom: 0 }); |
| 77 | + let slidingTop = $state(0); |
| 78 | + let slidingBottom = $state(0); |
124 | 79 |
|
125 | 80 | const updateSlidingWindow = () => {
|
126 | 81 | const v = $state.snapshot(viewport);
|
127 | 82 | const top = (document.scrollingElement?.scrollTop || 0) - slidingWindowOffset;
|
128 |
| - const bottom = top + v.height; |
129 |
| - const w = { |
130 |
| - top, |
131 |
| - bottom, |
132 |
| - }; |
133 |
| - slidingWindow = w; |
| 83 | + slidingTop = top; |
| 84 | + slidingBottom = top + v.height; |
134 | 85 | };
|
| 86 | + $effect(updateSlidingWindow); |
135 | 87 | const debouncedOnIntersected = debounce(() => onIntersected?.(), 750, { maxWait: 100, leading: true });
|
136 | 88 |
|
137 | 89 | let lastIntersectedHeight = 0;
|
138 | 90 | $effect(() => {
|
139 | 91 | // notify we got to (near) the end of scroll
|
140 | 92 | const scrollPercentage =
|
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 |
| - } |
| 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; |
150 | 98 | }
|
151 | 99 | });
|
152 | 100 | const viewAssetHandler = async (asset: TimelineAsset) => {
|
|
256 | 204 | isShowDeleteConfirmation = false;
|
257 | 205 | await deleteAssets(
|
258 | 206 | !(isTrashEnabled && !force),
|
259 |
| - (assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id))), |
| 207 | + (assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id)) as TimelineAsset[]), |
260 | 208 | assetInteraction.selectedAssets,
|
261 | 209 | onReload,
|
262 | 210 | );
|
|
269 | 217 | assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive,
|
270 | 218 | );
|
271 | 219 | if (ids) {
|
272 |
| - assets = assets.filter((asset) => !ids.includes(asset.id)); |
| 220 | + assets = assets.filter((asset) => !ids.includes(asset.id)) as TimelineAsset[]; |
273 | 221 | deselectAllAssets();
|
274 | 222 | }
|
275 | 223 | };
|
|
454 | 402 | onkeyup={onKeyUp}
|
455 | 403 | onselectstart={onSelectStart}
|
456 | 404 | use:shortcuts={shortcutList}
|
457 |
| - onscroll={() => updateSlidingWindow()} |
| 405 | + onscroll={updateSlidingWindow} |
458 | 406 | />
|
459 | 407 |
|
460 | 408 | {#if isShowDeleteConfirmation}
|
|
468 | 416 | {#if assets.length > 0}
|
469 | 417 | <div
|
470 | 418 | style:position="relative"
|
471 |
| - style:height={assetLayouts.containerHeight + 'px'} |
472 |
| - style:width={assetLayouts.containerWidth - 1 + 'px'} |
| 419 | + style:height={geometry.containerHeight + 'px'} |
| 420 | + style:width={geometry.containerWidth + 'px'} |
473 | 421 | >
|
474 |
| - {#each assetLayouts.assetLayout as layout, layoutIndex (layout.asset.id + '-' + layoutIndex)} |
475 |
| - {@const currentAsset = layout.asset} |
476 |
| - |
477 |
| - {#if layout.display} |
| 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)} |
478 | 425 | <div
|
479 | 426 | class="absolute"
|
480 | 427 | style:overflow="clip"
|
|
484 | 431 | readonly={disableAssetSelect}
|
485 | 432 | onClick={() => {
|
486 | 433 | if (assetInteraction.selectionActive) {
|
487 |
| - handleSelectAssets(toTimelineAsset(currentAsset)); |
| 434 | + handleSelectAssets(toTimelineAsset(asset)); |
488 | 435 | return;
|
489 | 436 | }
|
490 |
| - void viewAssetHandler(toTimelineAsset(currentAsset)); |
| 437 | + void viewAssetHandler(toTimelineAsset(asset)); |
491 | 438 | }}
|
492 |
| - onSelect={() => handleSelectAssets(toTimelineAsset(currentAsset))} |
493 |
| - onMouseEvent={() => assetMouseEventHandler(toTimelineAsset(currentAsset))} |
| 439 | + onSelect={() => handleSelectAssets(toTimelineAsset(asset))} |
| 440 | + onMouseEvent={() => assetMouseEventHandler(toTimelineAsset(asset))} |
494 | 441 | {showArchiveIcon}
|
495 |
| - asset={toTimelineAsset(currentAsset)} |
496 |
| - selected={assetInteraction.hasSelectedAsset(currentAsset.id)} |
497 |
| - selectionCandidate={assetInteraction.hasSelectionCandidate(currentAsset.id)} |
| 442 | + asset={toTimelineAsset(asset)} |
| 443 | + selected={assetInteraction.hasSelectedAsset(asset.id)} |
| 444 | + selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)} |
498 | 445 | thumbnailWidth={layout.width}
|
499 | 446 | thumbnailHeight={layout.height}
|
500 | 447 | />
|
501 |
| - {#if showAssetName && !isTimelineAsset(currentAsset)} |
| 448 | + {#if showAssetName && !isTimelineAsset(asset)} |
502 | 449 | <div
|
503 | 450 | 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"
|
504 | 451 | >
|
505 |
| - {currentAsset.originalFileName} |
| 452 | + {asset.originalFileName} |
506 | 453 | </div>
|
507 | 454 | {/if}
|
508 | 455 | </div>
|
|
0 commit comments