diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index a8e96709ff..015015a751 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -148,6 +148,7 @@ export class MediaRepository { quality: options.quality, // this is default in libvips (except the threshold is 90), but we need to set it manually in sharp chromaSubsampling: options.quality >= 80 ? '4:4:4' : '4:2:0', + progressive: true, }) .toFile(output); } diff --git a/web/src/app.css b/web/src/app.css index bf7601f63b..dd04c422dd 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -74,6 +74,9 @@ --immich-dark-bg: 10 10 10; --immich-dark-fg: 229 231 235; --immich-dark-gray: 33 33 33; + + /* transitions */ + --immich-split-viewer-nav: enabled; } button:not(:disabled), @@ -171,3 +174,258 @@ @apply bg-subtle rounded-lg; } } + +@layer base { + ::view-transition { + background: black; + animation-duration: 250ms; + } + + ::view-transition-old(*), + ::view-transition-new(*) { + mix-blend-mode: normal; + animation-duration: inherit; + } + + ::view-transition-old(*) { + animation-name: fadeOut; + animation-fill-mode: forwards; + } + ::view-transition-new(*) { + animation-name: fadeIn; + animation-fill-mode: forwards; + } + + ::view-transition-old(root) { + animation: 250ms 0s fadeOut forwards; + } + ::view-transition-new(root) { + animation: 250ms 0s fadeIn forwards; + } + html:active-view-transition-type(slideshow) { + &::view-transition-old(root) { + animation: 1s 0s fadeOut forwards; + } + &::view-transition-new(root) { + animation: 1s 0s fadeIn forwards; + } + } + html:active-view-transition-type(viewer-nav) { + &::view-transition-old(root) { + animation: 350ms 0s fadeOut forwards; + } + &::view-transition-new(root) { + animation: 350ms 0s fadeIn forwards; + } + } + ::view-transition-old(info) { + animation: 250ms 0s flyOutRight forwards; + } + ::view-transition-new(info) { + animation: 250ms 0s flyInRight forwards; + } + + ::view-transition-group(detail-panel) { + z-index: 1; + } + ::view-transition-old(detail-panel), + ::view-transition-new(detail-panel) { + animation: none; + } + ::view-transition-group(letterbox-left), + ::view-transition-group(letterbox-right), + ::view-transition-group(letterbox-top), + ::view-transition-group(letterbox-bottom) { + z-index: 4; + } + + ::view-transition-old(letterbox-left), + ::view-transition-old(letterbox-right), + ::view-transition-old(letterbox-top), + ::view-transition-old(letterbox-bottom) { + background-color: black; + } + + ::view-transition-new(letterbox-left), + ::view-transition-new(letterbox-right) { + height: 100dvh; + } + + ::view-transition-new(letterbox-left), + ::view-transition-new(letterbox-right), + ::view-transition-new(letterbox-top), + ::view-transition-new(letterbox-bottom) { + background-color: black; + opacity: 1 !important; + } + + ::view-transition-group(exclude-leftbutton), + ::view-transition-group(exclude-rightbutton), + ::view-transition-group(exclude) { + animation: none; + z-index: 5; + } + ::view-transition-old(exclude-leftbutton), + ::view-transition-old(exclude-rightbutton), + ::view-transition-old(exclude) { + visibility: hidden; + } + ::view-transition-new(exclude-leftbutton), + ::view-transition-new(exclude-rightbutton), + ::view-transition-new(exclude) { + animation: none; + z-index: 5; + } + + ::view-transition-old(hero) { + animation: 350ms fadeOut forwards; + align-content: center; + } + ::view-transition-new(hero) { + animation: 350ms fadeIn forwards; + align-content: center; + } + ::view-transition-old(next), + ::view-transition-old(next-old) { + animation: 250ms cubic-bezier(0.25, 0.46, 0.45, 0.94) flyOutLeft forwards; + overflow: hidden; + } + + ::view-transition-new(next), + ::view-transition-new(next-new) { + animation: 250ms cubic-bezier(0.25, 0.46, 0.45, 0.94) flyInRight forwards; + overflow: hidden; + } + + ::view-transition-old(previous) { + animation: 250ms cubic-bezier(0.25, 0.46, 0.45, 0.94) flyOutRight forwards; + } + ::view-transition-old(previous-old) { + animation: 250ms cubic-bezier(0.25, 0.46, 0.45, 0.94) flyOutRight forwards; + overflow: hidden; + z-index: -1; + } + + ::view-transition-new(previous) { + animation: 250ms cubic-bezier(0.25, 0.46, 0.45, 0.94) flyInLeft forwards; + } + + ::view-transition-new(previous-new) { + animation: 250ms cubic-bezier(0.25, 0.46, 0.45, 0.94) flyInLeft forwards; + overflow: hidden; + } + + @keyframes flyInLeft { + from { + /* object-position: -25dvw; */ + transform: translateX(-15%); + opacity: 0.1; + filter: blur(4px); + } + 50% { + opacity: 0.4; + filter: blur(2px); + } + to { + opacity: 1; + filter: blur(0); + } + } + + @keyframes flyOutLeft { + from { + opacity: 1; + filter: blur(0); + } + 50% { + opacity: 0.4; + filter: blur(2px); + } + to { + /* object-position: -25dvw; */ + transform: translateX(-15%); + opacity: 0.1; + filter: blur(4px); + } + } + + @keyframes flyInRight { + from { + /* object-position: 25dvw; */ + transform: translateX(15%); + opacity: 0.1; + filter: blur(4px); + } + 50% { + opacity: 0.4; + filter: blur(2px); + } + to { + opacity: 1; + filter: blur(0); + } + } + + /* Fly out to right */ + @keyframes flyOutRight { + from { + opacity: 1; + filter: blur(0); + } + 50% { + opacity: 0.4; + filter: blur(2px); + } + to { + /* object-position: 50dvw 0px; */ + transform: translateX(15%); + opacity: 0.1; + filter: blur(4px); + } + } + + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + @keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } + } + + @media (prefers-reduced-motion) { + ::view-transition-group(previous), + ::view-transition-group(next) { + width: 100% !important; + height: 100% !important; + transform: none !important; + } + + ::view-transition-old(previous), + ::view-transition-old(next) { + animation: 250ms fadeOut forwards; + transform-origin: center; + height: 100%; + width: 100%; + object-fit: contain; + overflow: hidden; + } + + ::view-transition-new(previous), + ::view-transition-new(next) { + animation: 250ms fadeIn forwards; + transform-origin: center; + height: 100%; + width: 100%; + object-fit: contain; + } + } +} diff --git a/web/src/lib/actions/thumbhash.ts b/web/src/lib/actions/thumbhash.ts index e49f04dbee..d738f567a4 100644 --- a/web/src/lib/actions/thumbhash.ts +++ b/web/src/lib/actions/thumbhash.ts @@ -7,13 +7,23 @@ import { thumbHashToRGBA } from 'thumbhash'; * @param param1 object containing the base64 encoded hash (base64Thumbhash: yourString) */ export function thumbhash(canvas: HTMLCanvasElement, { base64ThumbHash }: { base64ThumbHash: string }) { - const ctx = canvas.getContext('2d'); - if (ctx) { - const { w, h, rgba } = thumbHashToRGBA(decodeBase64(base64ThumbHash)); - const pixels = ctx.createImageData(w, h); - canvas.width = w; - canvas.height = h; - pixels.data.set(rgba); - ctx.putImageData(pixels, 0, 0); - } + const render = (hash: string) => { + const ctx = canvas.getContext('2d'); + if (ctx) { + const { w, h, rgba } = thumbHashToRGBA(decodeBase64(hash)); + const pixels = ctx.createImageData(w, h); + canvas.width = w; + canvas.height = h; + pixels.data.set(rgba); + ctx.putImageData(pixels, 0, 0); + } + }; + + render(base64ThumbHash); + + return { + update({ base64ThumbHash: newHash }: { base64ThumbHash: string }) { + render(newHash); + }, + }; } diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index fe0c6b3edf..2f39ccf395 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -12,12 +12,14 @@ import { authManager } from '$lib/managers/auth-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte'; import { preloadManager } from '$lib/managers/PreloadManager.svelte'; + import { viewTransitionManager } from '$lib/managers/ViewTransitionManager.svelte'; import { closeEditorCofirm } from '$lib/stores/asset-editor.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { ocrManager } from '$lib/stores/ocr.svelte'; import { alwaysLoadOriginalVideo } from '$lib/stores/preferences.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { user } from '$lib/stores/user.store'; + import { resetZoomState } from '$lib/stores/zoom-image.store'; import { getAssetJobMessage, getAssetUrl, getSharedLink, handlePromiseError } from '$lib/utils'; import type { OnUndoDelete } from '$lib/utils/actions'; import { handleError } from '$lib/utils/handle-error'; @@ -28,7 +30,6 @@ import { AssetJobName, AssetTypeEnum, - getAllAlbums, getAssetInfo, getStack, runAssetJobs, @@ -38,9 +39,9 @@ type StackResponseDto, } from '@immich/sdk'; import { toastManager } from '@immich/ui'; - import { onDestroy, onMount, untrack } from 'svelte'; + import { onDestroy, onMount, tick, untrack } from 'svelte'; import { t } from 'svelte-i18n'; - import { fly } from 'svelte/transition'; + import { fly, slide } from 'svelte/transition'; import Thumbnail from '../assets/thumbnail/thumbnail.svelte'; import ActivityStatus from './activity-status.svelte'; import ActivityViewer from './activity-viewer.svelte'; @@ -91,7 +92,7 @@ copyImage = $bindable(), }: Props = $props(); - const { setAssetId } = assetViewingStore; + const { setAssetId, invisible } = assetViewingStore; const { restartProgress: restartSlideshowProgress, stopProgress: stopSlideshowProgress, @@ -104,7 +105,6 @@ let asset = $derived(cursor.current); let nextAsset = $derived(cursor.nextAsset); let previousAsset = $derived(cursor.previousAsset); - let appearsInAlbums: AlbumResponseDto[] = $state([]); let sharedLink = getSharedLink(); let previewStackedAsset: AssetResponseDto | undefined = $state(); let isShowEditor = $state(false); @@ -113,9 +113,15 @@ let selectedEditType: string = $state(''); let stack: StackResponseDto | null = $state(null); + let slideShowPlaying = $derived($slideshowState === SlideshowState.PlaySlideshow); + let slideShowAscending = $derived($slideshowNavigation === SlideshowNavigation.AscendingOrder); + let slideShowShuffle = $derived($slideshowNavigation === SlideshowNavigation.Shuffle); + let zoomToggle = $state(() => void 0); let playOriginalVideo = $state($alwaysLoadOriginalVideo); + let refreshAlbumsSignal = $state(0); + const setPlayOriginalVideo = (value: boolean) => { playOriginalVideo = value; }; @@ -150,7 +156,26 @@ } }; - onMount(async () => { + let transitionName = $state('hero'); + let equirectangularTransitionName = $state('hero'); + let detailPanelTransitionName = $state(null); + + let addInfoTransition; + let finished; + onMount(() => { + addInfoTransition = () => { + detailPanelTransitionName = 'info'; + transitionName = 'hero'; + equirectangularTransitionName = 'hero'; + }; + eventManager.on('TransitionToAssetViewer', addInfoTransition); + eventManager.on('TransitionToTimeline', addInfoTransition); + finished = () => { + detailPanelTransitionName = null; + transitionName = null; + }; + eventManager.on('Finished', finished); + unsubscribes.push( slideshowState.subscribe((value) => { if (value === SlideshowState.PlaySlideshow) { @@ -168,10 +193,6 @@ } }), ); - - if (!sharedLink) { - await handleGetAllAlbums(); - } }); onDestroy(() => { @@ -180,21 +201,13 @@ } activityManager.reset(); + eventManager.off('TransitionToAssetViewer', addInfoTransition!); + eventManager.off('TransitionToTimeline', addInfoTransition!); + eventManager.off('Finished', finished!); }); - const handleGetAllAlbums = async () => { - if (authManager.isSharedLink) { - return; - } - - try { - appearsInAlbums = await getAllAlbums({ assetId: asset.id }); - } catch (error) { - console.error('Error getting album that asset belong to', error); - } - }; - const closeViewer = () => { + transitionName = 'hero'; onClose?.(asset); }; @@ -204,45 +217,98 @@ }); }; + const startTransition = async ( + types: string[], + targetTransition: string | null, + targetAsset: AssetResponseDto | null, + navigateFn: () => Promise, + ) => { + transitionName = viewTransitionManager.getTransitionName('old', targetTransition); + equirectangularTransitionName = viewTransitionManager.getTransitionName('old', targetTransition); + detailPanelTransitionName = 'detail-panel'; + await tick(); + const navigationResult = new Promise((navigationResolve) => { + viewTransitionManager.startTransition( + new Promise((resolve) => { + eventManager.once('StartViewTransition', async () => { + transitionName = viewTransitionManager.getTransitionName('new', targetTransition); + if (targetAsset && isEquirectangular(asset) && !isEquirectangular(targetAsset)) { + equirectangularTransitionName = null; + } + await tick(); + navigationResolve(await navigateFn()); + }); + eventManager.once('AssetViewerFree', () => tick().then(resolve)); + }), + types, + ); + }); + return navigationResult; + }; + const tracker = new InvocationTracker(); - const navigateAsset = (order?: 'previous' | 'next', e?: Event) => { + const navigateAsset = (order?: 'previous' | 'next', skipTransition: boolean = false) => { if (!order) { - if ($slideshowState === SlideshowState.PlaySlideshow) { - order = $slideshowNavigation === SlideshowNavigation.AscendingOrder ? 'previous' : 'next'; + if (slideShowPlaying) { + order = slideShowAscending ? 'previous' : 'next'; } else { return; } } - e?.stopPropagation(); preloadManager.cancel(asset); if (tracker.isActive()) { return; } + let skipped = false; + if (viewTransitionManager.skipTransitions()) { + skipped = true; + } + void tracker.invoke(async () => { let hasNext = false; - - if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) { - hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next(); - if (!hasNext) { - const asset = await onRandom?.(); - if (asset) { - slideshowHistory.queue(asset); - hasNext = true; + if (slideShowPlaying && slideShowShuffle) { + const navigate = async () => { + let next = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next(); + if (!next) { + const asset = await onRandom?.(); + if (asset) { + slideshowHistory.queue(asset); + next = true; + } } + return next; + }; + // eslint-disable-next-line unicorn/prefer-ternary + if (viewTransitionManager.isSupported() && !skipped && !skipTransition) { + hasNext = await startTransition(['slideshow'], null, null, navigate); + } else { + hasNext = await navigate(); } } else if (onNavigateToAsset) { - hasNext = - order === 'previous' - ? await onNavigateToAsset(cursor.previousAsset) - : await onNavigateToAsset(cursor.nextAsset); + // only transition if the target is already preloaded, and is in a secure context + const targetAsset = order === 'previous' ? previousAsset : nextAsset; + const navigate = async () => + order === 'previous' ? await onNavigateToAsset(previousAsset) : await onNavigateToAsset(nextAsset); + if (viewTransitionManager.isSupported() && !skipped && !skipTransition && !!targetAsset) { + const targetTransition = slideShowPlaying ? null : order; + hasNext = await startTransition( + slideShowPlaying ? ['slideshow'] : ['viewer-nav'], + targetTransition, + targetAsset, + navigate, + ); + } else { + hasNext = await navigate(); + } + resetZoomState(); } else { hasNext = false; } - if ($slideshowState === SlideshowState.PlaySlideshow) { + if (slideShowPlaying) { if (hasNext) { $restartSlideshowProgress = true; } else { @@ -310,7 +376,7 @@ const handleAction = async (action: Action) => { switch (action.type) { case AssetAction.ADD_TO_ALBUM: { - await handleGetAllAlbums(); + refreshAlbumsSignal++; break; } case AssetAction.REMOVE_ASSET_FROM_STACK: { @@ -369,7 +435,6 @@ const refresh = async () => { await refreshStack(); - await handleGetAllAlbums(); ocrManager.clear(); if (!sharedLink) { if (previewStackedAsset) { @@ -403,6 +468,13 @@ } }); + const isEquirectangular = (asset: AssetResponseDto) => { + return ( + asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || + (asset.originalPath && asset.originalPath.toLowerCase().endsWith('.insp')) + ); + }; + const viewerKind = $derived.by(() => { if (previewStackedAsset) { return asset.type === AssetTypeEnum.Image ? 'StackPhotoViewer' : 'StackVideoViewer'; @@ -410,10 +482,7 @@ if (asset.type === AssetTypeEnum.Image) { if (assetViewerManager.isPlayingMotionPhoto && asset.livePhotoVideoId) { return 'LiveVideoViewer'; - } else if ( - asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || - (asset.originalPath && asset.originalPath.toLowerCase().endsWith('.insp')) - ) { + } else if (isEquirectangular(asset)) { return 'ImagePanaramaViewer'; } else if (isShowEditor && selectedEditType === 'crop') { return 'CropArea'; @@ -431,12 +500,16 @@
{#if $slideshowState === SlideshowState.None && !isShowEditor} -
+
+
navigateAsset('previous')} />
{/if} -
+
{#if viewerKind === 'StackPhotoViewer'} navigateAsset('previous')} - onNextAsset={() => navigateAsset('next')} + onPreviousAsset={() => navigateAsset('previous', true)} + onNextAsset={() => navigateAsset('next', true)} {sharedLink} /> {:else if viewerKind === 'StackVideoViewer'} {:else if viewerKind === 'LiveVideoViewer'} {:else if viewerKind === 'ImagePanaramaViewer'} - + {:else if viewerKind === 'CropArea'} {:else if viewerKind === 'PhotoViewer'} navigateAsset('previous')} - onNextAsset={() => navigateAsset('next')} + onPreviousAsset={() => navigateAsset('previous', true)} + onNextAsset={() => navigateAsset('next', true)} {sharedLink} - onFree={() => eventManager.emit('AssetViewerFree')} + onReady={() => eventManager.emit('AssetViewerFree')} /> {:else if viewerKind === 'VideoViewer'} {#if $slideshowState === SlideshowState.None && showNavigation && !isShowEditor && nextAsset} -
+
navigateAsset('next')} />
{/if} {#if asset.hasMetadata && $slideshowState === SlideshowState.None && assetViewerManager.isShowDetailPanel && !isShowEditor}
- +
{/if} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 0328025594..044f774ebf 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -14,13 +14,19 @@ import { boundingBoxesArray } from '$lib/stores/people.store'; import { locale } from '$lib/stores/preferences.store'; import { preferences, user } from '$lib/stores/user.store'; - import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils'; + import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils'; import { delay, getDimensions } from '$lib/utils/asset-utils'; import { getByteUnitString } from '$lib/utils/byte-units'; import { getMetadataSearchQuery } from '$lib/utils/metadata-search'; import { fromISODateTime, fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util'; import { getParentPath } from '$lib/utils/tree-utils'; - import { AssetMediaSize, getAssetInfo, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk'; + import { + AssetMediaSize, + getAllAlbums, + getAssetInfo, + type AlbumResponseDto, + type AssetResponseDto, + } from '@immich/sdk'; import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui'; import { mdiCalendar, @@ -44,11 +50,11 @@ interface Props { asset: AssetResponseDto; - albums?: AlbumResponseDto[]; currentAlbum?: AlbumResponseDto | null; + refreshAlbumsSignal?: number; } - let { asset, albums = [], currentAlbum = null }: Props = $props(); + let { asset, refreshAlbumsSignal = 0, currentAlbum = null }: Props = $props(); let showAssetPath = $state(false); let showEditFaces = $state(false); @@ -74,6 +80,17 @@ ); let previousId: string | undefined = $state(); + let albums = $state([]); + + $effect(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + refreshAlbumsSignal; + if (authManager.isSharedLink) { + return; + } + handlePromiseError(getAllAlbums({ assetId: asset.id }).then((response) => (albums = response))); + }); + $effect(() => { if (!previousId) { previousId = asset.id; diff --git a/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte b/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte index 01b2982efb..4925b93517 100644 --- a/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte +++ b/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte @@ -11,7 +11,7 @@ import { t } from 'svelte-i18n'; interface Props { - htmlElement: HTMLImageElement | HTMLVideoElement; + htmlElement: HTMLImageElement | HTMLVideoElement | undefined | null; containerWidth: number; containerHeight: number; assetId: string; @@ -78,6 +78,9 @@ }); $effect(() => { + if (!htmlElement) { + return; + } const { actualWidth, actualHeight } = getContainedSize(htmlElement); const offsetArea = { width: (containerWidth - actualWidth) / 2, diff --git a/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte b/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte index 08ba43526d..40abab429f 100644 --- a/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte +++ b/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte @@ -8,11 +8,12 @@ import { fade } from 'svelte/transition'; type Props = { + transitionName?: string | null; asset: AssetResponseDto; zoomToggle?: (() => void) | null; }; - let { asset, zoomToggle = $bindable() }: Props = $props(); + let { transitionName, asset, zoomToggle = $bindable() }: Props = $props(); const loadAssetData = async (id: string) => { const data = await viewAsset({ ...authManager.params, id, size: AssetMediaSize.Preview }); @@ -20,11 +21,12 @@ }; -
+
{#await Promise.all([loadAssetData(asset.id), import('./photo-sphere-viewer-adapter.svelte')])} {:then [data, { default: PhotoSphereViewer }]} -
+
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 727e46f33c..cee41fccae 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -1,6 +1,7 @@ @@ -251,12 +338,7 @@ { shortcut: { key: 'z' }, onShortcut: zoomToggle, preventDefault: false }, ]} /> -{#if imageError} -
- -
-{/if} - +
-
- {#if !imageLoaded} -
+ {#if blurredSlideshow} + + {/if} +
+
+
+
+
+ {#if asset.thumbhash} + + {#if thumbnailPreloaded} + {$getAltText(toTimelineAsset(asset))} + {/if} + {/if} + {#if !imageLoaded && !asset.thumbhash && !imageError} +
- {:else if !imageError} - {#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground} + {/if} + {#if imageError} +
+ +
+ {/if} + {#key imageLoaderUrl} +
- {/if} -
- {$getAltText(toTimelineAsset(asset))} @@ -307,10 +452,9 @@ {/each}
- - {#if isFaceEditMode.value} - - {/if} + {/key} + {#if isFaceEditMode.value} + {/if}
@@ -321,9 +465,13 @@ visibility: visible; } } - #broken-asset, + #spinner { visibility: hidden; animation: 0s linear 0.4s forwards delayedVisibility; } + [data-blur] { + visibility: hidden; + animation: 0s linear 0.1s forwards delayedVisibility; + } diff --git a/web/src/lib/components/asset-viewer/video-native-viewer.svelte b/web/src/lib/components/asset-viewer/video-native-viewer.svelte index 8383333324..e100c3a068 100644 --- a/web/src/lib/components/asset-viewer/video-native-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-native-viewer.svelte @@ -4,6 +4,7 @@ import VideoRemoteViewer from '$lib/components/asset-viewer/video-remote-viewer.svelte'; import { assetViewerFadeDuration } from '$lib/constants'; import { assetCacheManager } from '$lib/managers/AssetCacheManager.svelte'; + import { authManager } from '$lib/managers/auth-manager.svelte'; import { castManager } from '$lib/managers/cast-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; @@ -22,6 +23,8 @@ import { fade } from 'svelte/transition'; interface Props { + transitionName?: string | null; + asset: AssetResponseDto; assetId: string; previousAsset?: AssetResponseDto; nextAsset?: AssetResponseDto; @@ -37,6 +40,8 @@ } let { + transitionName, + asset, assetId, previousAsset, nextAsset, @@ -51,8 +56,6 @@ onClose = () => {}, }: Props = $props(); - let asset = $state(null); - let videoPlayer: HTMLVideoElement | undefined = $state(); let isLoading = $state(true); let assetFileUrl = $derived( @@ -83,7 +86,7 @@ $effect( () => - void assetCacheManager.getAsset({ key: cacheKey ?? assetId, id: assetId }).then((assetDto) => (asset = assetDto)), + void assetCacheManager.getAsset({ ...authManager.params, id: assetId }).then((assetDto) => (asset = assetDto)), ); $effect(() => { @@ -193,8 +196,9 @@ />
{:else} -
+