add passive loading indicator to asset-viewer

This commit is contained in:
midzelis
2026-03-08 22:13:17 +00:00
parent 4e4a856a00
commit 6fdf99ca16
4 changed files with 78 additions and 2 deletions

View File

@@ -128,6 +128,10 @@
};
});
$effect(() => {
assetViewerManager.imageLoaderStatus = status;
});
$effect(() => {
if (assetViewerManager.zoom > 1 && status.quality.original !== 'success') {
untrack(() => void adaptiveImageLoader.trigger('original'));

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import type { ClassValue } from 'svelte/elements';
interface Props {
class?: ClassValue;
}
let { class: className }: Props = $props();
</script>
<div class="delayed inline-flex items-center gap-1 {className}">
{#each [0, 1, 2] as i (i)}
<span class="dot block size-1.5 rounded-full bg-white shadow-[0_0_3px_rgba(0,0,0,0.6)]" style:--delay="{i * 0.25}s"
></span>
{/each}
</div>
<style>
.delayed {
visibility: hidden;
animation: delayed-visibility 0s linear 0.4s forwards;
}
@keyframes delayed-visibility {
to {
visibility: visible;
}
}
.dot {
animation: dot-stream 1.6s var(--delay, 0s) ease-in-out infinite;
}
@keyframes dot-stream {
0%,
80%,
100% {
opacity: 0.3;
transform: scale(0.8);
}
40% {
opacity: 1;
transform: scale(1.15);
}
}
</style>

View File

@@ -34,7 +34,9 @@
type PersonResponseDto,
type StackResponseDto,
} from '@immich/sdk';
import { ActionButton, CommandPaletteDefaultProvider, type ActionItem } from '@immich/ui';
import { ActionButton, CommandPaletteDefaultProvider, Tooltip, type ActionItem } from '@immich/ui';
import LoadingDots from '$lib/components/LoadingDots.svelte';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import {
mdiArrowLeft,
mdiArrowRight,
@@ -104,7 +106,16 @@
<ActionButton action={Close} />
</div>
<div class="flex gap-2 overflow-x-auto dark" data-testid="asset-viewer-navbar-actions">
<div class="flex items-center gap-2 overflow-x-auto dark" data-testid="asset-viewer-navbar-actions">
{#if assetViewerManager.isImageLoading}
<Tooltip text={$t('loading')}>
{#snippet child({ props })}
<div {...props} role="status" aria-label={$t('loading')}>
<LoadingDots class="me-1" />
</div>
{/snippet}
</Tooltip>
{/if}
<ActionButton action={Cast} />
<ActionButton action={Actions.Share} />
<ActionButton action={Actions.Offline} />

View File

@@ -1,3 +1,4 @@
import type { ImageLoaderStatus } from '$lib/utils/adaptive-image-loader.svelte';
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
import { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
import { PersistedLocalStorage } from '$lib/utils/persisted';
@@ -23,10 +24,24 @@ export class AssetViewerManager extends BaseEventManager<Events> {
#zoomState = $state(createDefaultZoomState());
imgRef = $state<HTMLImageElement | undefined>();
imageLoaderStatus = $state<ImageLoaderStatus | undefined>();
#isImageLoading = $derived.by(() => {
const quality = this.imageLoaderStatus?.quality;
if (!quality) {
return false;
}
const previewOrOriginalReady = quality.preview === 'success' || quality.original === 'success';
const loadingOriginal = this.zoom > 1 && quality.original !== 'success';
return !previewOrOriginalReady || loadingOriginal;
});
isShowActivityPanel = $state(false);
isPlayingMotionPhoto = $state(false);
isShowEditor = $state(false);
get isImageLoading() {
return this.#isImageLoading;
}
get isShowDetailPanel() {
return isShowDetailPanel.current;
}