Compare commits

..

2 Commits

Author SHA1 Message Date
midzelis
592874208b feat: add spinner to SearchResults 2025-10-07 14:44:05 +00:00
midzelis
90ab75f968 refactor: push up layout() into PhotostreamSegment 2025-10-07 14:41:57 +00:00
5 changed files with 51 additions and 24 deletions

View File

@@ -9,8 +9,10 @@
import StreamWithViewer from '$lib/components/timeline/StreamWithViewer.svelte';
import Skeleton from '$lib/elements/Skeleton.svelte';
import { SearchResultsManager } from '$lib/managers/searchresults-manager/SearchResultsManager.svelte';
import { SearchResultsSegment } from '$lib/managers/searchresults-manager/SearchResultsSegment.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { LoadingSpinner } from '@immich/ui';
import type { Snippet } from 'svelte';
interface Props {
@@ -92,5 +94,12 @@
{/snippet}
</SelectableSegment>
{/snippet}
{#snippet segmentFooter({ segment })}
{#if (segment as SearchResultsSegment).hasNextPage}
<div class="w-full flex justify-center items-center h-50">
<LoadingSpinner size="giant" />
</div>
{/if}
{/snippet}
</Photostream>
</StreamWithViewer>

View File

@@ -18,6 +18,13 @@
},
]
>;
segmentFooter: Snippet<
[
{
segment: PhotostreamSegment;
},
]
>;
skeleton: Snippet<
[
{
@@ -59,6 +66,8 @@
let {
segment,
segmentFooter,
skeleton,
enableRouting,
timelineManager = $bindable(),
@@ -72,7 +81,7 @@
isShowDeleteConfirmation = $bindable(false),
children,
skeleton,
empty,
header,
handleTimelineScroll = () => {},
@@ -89,7 +98,6 @@
let { gridScrollTarget } = assetViewingStore;
let element: HTMLElement | undefined = $state();
let timelineElement: HTMLElement | undefined = $state();
const maxMd = $derived(mobileDevice.maxMd);
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
@@ -97,6 +105,9 @@
$effect(() => {
const layoutOptions = maxMd ? smallHeaderHeight : largeHeaderHeight;
timelineManager.setLayoutOptions(layoutOptions);
// this next line is important in order to ensure that the reactive signals of ViewerAsset.#intersecting
// are marked as dependencies of timeline.#scrollTop.
updateSlidingWindow();
});
const scrollTo = (top: number) => {
@@ -205,7 +216,6 @@
onscroll={() => (handleTimelineScroll(), updateSlidingWindow(), updateIsScrolling())}
>
<section
bind:this={timelineElement}
id="virtual-timeline"
class:relative={true}
class:invisible={showSkeleton}
@@ -245,6 +255,15 @@
})}
{/if}
</div>
{#if segmentFooter}
<div
style:position="absolute"
style:transform={`translate3d(0,${absoluteHeight + monthGroup.height}px,0)`}
style:width="100%"
>
{@render segmentFooter?.({ segment: monthGroup })}
</div>
{/if}
{/each}
<!-- spacer for lead-out -->
<div

View File

@@ -133,7 +133,7 @@ export abstract class PhotostreamManager {
const changed = value !== this.#viewportWidth;
this.#viewportWidth = value;
this.suspendTransitions = true;
void this.updateViewportGeometry(changed);
this.updateViewportGeometry(changed);
}
get viewportWidth() {
@@ -143,7 +143,7 @@ export abstract class PhotostreamManager {
set viewportHeight(value: number) {
this.#viewportHeight = value;
this.#suspendTransitions = true;
void this.updateViewportGeometry(false);
this.updateViewportGeometry(false);
}
get viewportHeight() {
@@ -151,10 +151,8 @@ export abstract class PhotostreamManager {
}
updateSlidingWindow(scrollTop: number) {
if (this.#scrollTop !== scrollTop) {
this.#scrollTop = scrollTop;
this.updateIntersections();
}
this.#scrollTop = scrollTop;
this.updateIntersections();
}
updateIntersections() {

View File

@@ -7,6 +7,7 @@ import type { PhotostreamManager } from '$lib/managers/photostream-manager/Photo
import { getTestHook } from '$lib/managers/photostream-manager/TestHooks.svelte';
import type { AssetOperation, MoveAsset, TimelineAsset } from '$lib/managers/timeline-manager/types';
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
import { getJustifiedLayoutFromAssets, getPosition } from '$lib/utils/layout-utils';
export type SegmentIdentifier = {
matches(segment: PhotostreamSegment): boolean;
@@ -144,7 +145,16 @@ export abstract class PhotostreamSegment {
this.loader?.cancel();
}
layout(_?: boolean) {}
layout(_?: boolean): void {
const timelineAssets = this.viewerAssets.map((viewerAsset) => viewerAsset.asset);
const layoutOptions = this.timelineManager.layoutOptions;
const geometry = getJustifiedLayoutFromAssets(timelineAssets, layoutOptions);
this.height = timelineAssets.length === 0 ? 0 : geometry.containerHeight + this.timelineManager.headerHeight;
for (let i = 0; i < this.viewerAssets.length; i++) {
const position = getPosition(geometry, i);
this.viewerAssets[i].position = position;
}
}
updateIntersection({ intersecting, actuallyIntersecting }: { intersecting: boolean; actuallyIntersecting: boolean }) {
this.intersecting = intersecting;

View File

@@ -8,7 +8,6 @@ import type {
} from '$lib/managers/searchresults-manager/SearchResultsManager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
import { getJustifiedLayoutFromAssets, getPosition } from '$lib/utils/layout-utils';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import { TUNABLES } from '$lib/utils/tunables';
import { searchAssets, searchSmart } from '@immich/sdk';
@@ -22,7 +21,7 @@ export class SearchResultsSegment extends PhotostreamSegment {
#id: string;
#searchTerms: SearchTerms;
#currentPage: string | null = null;
#nextPage: string | null = null;
#nextPage: string | null = $state(null);
#viewerAssets: ViewerAsset[] = $state([]);
@@ -73,22 +72,14 @@ export class SearchResultsSegment extends PhotostreamSegment {
this.layout();
}
layout(): void {
const timelineAssets = this.#viewerAssets.map((viewerAsset) => viewerAsset.asset);
const layoutOptions = this.timelineManager.layoutOptions;
const geometry = getJustifiedLayoutFromAssets(timelineAssets, layoutOptions);
this.height = timelineAssets.length === 0 ? 0 : geometry.containerHeight + this.timelineManager.headerHeight;
for (let i = 0; i < this.#viewerAssets.length; i++) {
const position = getPosition(geometry, i);
this.#viewerAssets[i].position = position;
}
}
get viewerAssets(): ViewerAsset[] {
return this.#viewerAssets;
}
get hasNextPage() {
return this.#nextPage !== null;
}
findAssetAbsolutePosition(assetId: string) {
const viewerAsset = this.#viewerAssets.find((viewAsset) => viewAsset.id === assetId);
if (viewerAsset) {