diff --git a/web/src/lib/components/AdaptiveImage.svelte b/web/src/lib/components/AdaptiveImage.svelte
index fc8f3346b5..92e3fad2d3 100644
--- a/web/src/lib/components/AdaptiveImage.svelte
+++ b/web/src/lib/components/AdaptiveImage.svelte
@@ -128,6 +128,10 @@
};
});
+ $effect(() => {
+ assetViewerManager.imageLoaderStatus = status;
+ });
+
$effect(() => {
if (assetViewerManager.zoom > 1 && status.quality.original !== 'success') {
untrack(() => void adaptiveImageLoader.trigger('original'));
diff --git a/web/src/lib/components/LoadingDots.svelte b/web/src/lib/components/LoadingDots.svelte
new file mode 100644
index 0000000000..3dcfcb8122
--- /dev/null
+++ b/web/src/lib/components/LoadingDots.svelte
@@ -0,0 +1,46 @@
+
+
+
+ {#each [0, 1, 2] as i (i)}
+
+ {/each}
+
+
+
diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
index bb52c71260..3ccadf944f 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
@@ -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 @@
-
+
+ {#if assetViewerManager.isImageLoading}
+
+ {#snippet child({ props })}
+
+
+
+ {/snippet}
+
+ {/if}
diff --git a/web/src/lib/managers/asset-viewer-manager.svelte.ts b/web/src/lib/managers/asset-viewer-manager.svelte.ts
index 36047d4690..c815421e2c 100644
--- a/web/src/lib/managers/asset-viewer-manager.svelte.ts
+++ b/web/src/lib/managers/asset-viewer-manager.svelte.ts
@@ -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
{
#zoomState = $state(createDefaultZoomState());
imgRef = $state();
+ imageLoaderStatus = $state();
+ #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;
}