feat: add spinner to SearchResults

This commit is contained in:
midzelis 2025-09-19 01:54:14 +00:00
parent 90ab75f968
commit 592874208b
4 changed files with 40 additions and 10 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,11 +151,9 @@ export abstract class PhotostreamManager {
}
updateSlidingWindow(scrollTop: number) {
if (this.#scrollTop !== scrollTop) {
this.#scrollTop = scrollTop;
this.updateIntersections();
}
}
updateIntersections() {
if (this.#updatingIntersections || !this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) {

View file

@ -21,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([]);
@ -76,6 +76,10 @@ export class SearchResultsSegment extends PhotostreamSegment {
return this.#viewerAssets;
}
get hasNextPage() {
return this.#nextPage !== null;
}
findAssetAbsolutePosition(assetId: string) {
const viewerAsset = this.#viewerAssets.find((viewAsset) => viewAsset.id === assetId);
if (viewerAsset) {