mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
add passive loading indicator to asset-viewer
This commit is contained in:
@@ -128,6 +128,10 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
assetViewerManager.imageLoaderStatus = status;
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (assetViewerManager.zoom > 1 && status.quality.original !== 'success') {
|
if (assetViewerManager.zoom > 1 && status.quality.original !== 'success') {
|
||||||
untrack(() => void adaptiveImageLoader.trigger('original'));
|
untrack(() => void adaptiveImageLoader.trigger('original'));
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -34,7 +34,9 @@
|
|||||||
type PersonResponseDto,
|
type PersonResponseDto,
|
||||||
type StackResponseDto,
|
type StackResponseDto,
|
||||||
} from '@immich/sdk';
|
} 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 {
|
import {
|
||||||
mdiArrowLeft,
|
mdiArrowLeft,
|
||||||
mdiArrowRight,
|
mdiArrowRight,
|
||||||
@@ -104,7 +106,16 @@
|
|||||||
<ActionButton action={Close} />
|
<ActionButton action={Close} />
|
||||||
</div>
|
</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={Cast} />
|
||||||
<ActionButton action={Actions.Share} />
|
<ActionButton action={Actions.Share} />
|
||||||
<ActionButton action={Actions.Offline} />
|
<ActionButton action={Actions.Offline} />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ImageLoaderStatus } from '$lib/utils/adaptive-image-loader.svelte';
|
||||||
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
||||||
import { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
|
import { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
|
||||||
import { PersistedLocalStorage } from '$lib/utils/persisted';
|
import { PersistedLocalStorage } from '$lib/utils/persisted';
|
||||||
@@ -23,10 +24,24 @@ export class AssetViewerManager extends BaseEventManager<Events> {
|
|||||||
#zoomState = $state(createDefaultZoomState());
|
#zoomState = $state(createDefaultZoomState());
|
||||||
|
|
||||||
imgRef = $state<HTMLImageElement | undefined>();
|
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);
|
isShowActivityPanel = $state(false);
|
||||||
isPlayingMotionPhoto = $state(false);
|
isPlayingMotionPhoto = $state(false);
|
||||||
isShowEditor = $state(false);
|
isShowEditor = $state(false);
|
||||||
|
|
||||||
|
get isImageLoading() {
|
||||||
|
return this.#isImageLoading;
|
||||||
|
}
|
||||||
|
|
||||||
get isShowDetailPanel() {
|
get isShowDetailPanel() {
|
||||||
return isShowDetailPanel.current;
|
return isShowDetailPanel.current;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user