diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 684a2a44bf..9a1b346c3e 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -37,7 +37,6 @@ } from '@immich/sdk'; import { CommandPaletteDefaultProvider } from '@immich/ui'; import { onDestroy, onMount, untrack } from 'svelte'; - import type { SwipeCustomEvent } from 'svelte-gestures'; import { t } from 'svelte-i18n'; import { fly } from 'svelte/transition'; import Thumbnail from '../assets/thumbnail/thumbnail.svelte'; @@ -186,53 +185,57 @@ assetViewerManager.closeEditor(); }; - const tracker = new InvocationTracker(); - const navigateAsset = (order?: 'previous' | 'next') => { - if (!order) { - if ($slideshowState === SlideshowState.PlaySlideshow) { - order = $slideshowNavigation === SlideshowNavigation.AscendingOrder ? 'previous' : 'next'; - } else { - return; + const getNavigationTarget = () => { + if ($slideshowState === SlideshowState.PlaySlideshow) { + return $slideshowNavigation === SlideshowNavigation.AscendingOrder ? 'previous' : 'next'; + } else { + return 'skip'; + } + }; + + const completeNavigation = async (target: 'previous' | 'next') => { + preloadManager.cancelBeforeNavigation(target); + let hasNext: boolean; + + if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) { + hasNext = target === 'previous' ? slideshowHistory.previous() : slideshowHistory.next(); + if (!hasNext) { + const asset = await onRandom?.(); + if (asset) { + slideshowHistory.queue(asset); + hasNext = true; + } } + } else { + hasNext = + target === 'previous' ? await navigateToAsset(cursor.previousAsset) : await navigateToAsset(cursor.nextAsset); } - preloadManager.cancelBeforeNavigation(order); - - if (tracker.isActive()) { + if ($slideshowState !== SlideshowState.PlaySlideshow) { return; } - void tracker.invoke(async () => { - let hasNext: boolean; + if (hasNext) { + $restartSlideshowProgress = true; + } else if ($slideshowRepeat && slideshowStartAssetId) { + await setAssetId(slideshowStartAssetId); + $restartSlideshowProgress = true; + } else { + await handleStopSlideshow(); + } + }; - 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; - } - } - } else { - hasNext = - order === 'previous' ? await navigateToAsset(cursor.previousAsset) : await navigateToAsset(cursor.nextAsset); - } + const tracker = new InvocationTracker(); + const navigateAsset = (target: 'previous' | 'next' | 'skip') => { + if (target === 'skip' || tracker.isActive()) { + return; + } - if ($slideshowState !== SlideshowState.PlaySlideshow) { - return; - } - - if (hasNext) { - $restartSlideshowProgress = true; - } else if ($slideshowRepeat && slideshowStartAssetId) { - // Loop back to starting asset - await setAssetId(slideshowStartAssetId); - $restartSlideshowProgress = true; - } else { - await handleStopSlideshow(); - } - }, $t('error_while_navigating')); + void tracker.invoke( + () => completeNavigation(target), + (error: unknown) => handleError(error, $t('error_while_navigating')), + () => eventManager.emit('ViewerFinishNavigate'), + ); }; /** @@ -419,24 +422,6 @@ assetViewerManager.isShowDetailPanel && !assetViewerManager.isShowEditor, ); - - const onSwipe = (event: SwipeCustomEvent) => { - if (assetViewerManager.zoom > 1) { - return; - } - - if (ocrManager.showOverlay) { - return; - } - - if (event.detail.direction === 'left') { - navigateAsset('next'); - } - - if (event.detail.direction === 'right') { - navigateAsset('previous'); - } - }; @@ -492,26 +477,26 @@
{#if viewerKind === 'StackVideoViewer'} navigateAsset('previous')} - onNextAsset={() => navigateAsset('next')} + onSwipe={(direction) => navigateAsset(direction === 'left' ? 'next' : 'previous')} onClose={closeViewer} - onVideoEnded={() => navigateAsset()} + onVideoEnded={() => navigateAsset(getNavigationTarget())} onVideoStarted={handleVideoStarted} {playOriginalVideo} /> {:else if viewerKind === 'LiveVideoViewer'} navigateAsset('previous')} - onNextAsset={() => navigateAsset('next')} + onSwipe={(direction) => navigateAsset(direction === 'left' ? 'next' : 'previous')} onVideoEnded={() => (assetViewerManager.isPlayingMotionPhoto = false)} {playOriginalVideo} /> @@ -520,17 +505,21 @@ {:else if viewerKind === 'CropArea'} {:else if viewerKind === 'PhotoViewer'} - + navigateAsset(direction === 'left' ? 'next' : 'previous')} + /> {:else if viewerKind === 'VideoViewer'} navigateAsset('previous')} - onNextAsset={() => navigateAsset('next')} + onSwipe={(direction) => navigateAsset(direction === 'left' ? 'next' : 'previous')} onClose={closeViewer} - onVideoEnded={() => navigateAsset()} + onVideoEnded={() => navigateAsset(getNavigationTarget())} onVideoStarted={handleVideoStarted} {playOriginalVideo} /> diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 2cb1b6c78d..bd309450f7 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -5,6 +5,7 @@ import AdaptiveImage from '$lib/components/AdaptiveImage.svelte'; import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte'; import OcrBoundingBox from '$lib/components/asset-viewer/ocr-bounding-box.svelte'; + import SwipeFeedback from '$lib/components/asset-viewer/swipe-feedback.svelte'; import AssetViewerEvents from '$lib/components/AssetViewerEvents.svelte'; import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { castManager } from '$lib/managers/cast-manager.svelte'; @@ -21,7 +22,7 @@ import { type SharedLinkResponseDto } from '@immich/sdk'; import { toastManager } from '@immich/ui'; import { onDestroy, untrack } from 'svelte'; - import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures'; + import { fromAction } from 'svelte/attachments'; import { t } from 'svelte-i18n'; import type { AssetCursor } from './asset-viewer.svelte'; @@ -31,7 +32,7 @@ sharedLink?: SharedLinkResponseDto; onReady?: () => void; onError?: () => void; - onSwipe?: (event: SwipeCustomEvent) => void; + onSwipe?: (direction: 'left' | 'right') => void; } let { cursor, element = $bindable(), sharedLink, onReady, onError, onSwipe }: Props = $props(); @@ -163,6 +164,13 @@ }); const faces = $derived(Array.from(faceToNameMap.keys())); + + let swipeFeedbackReset = $state<(() => void) | undefined>(); + $effect(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + asset.id; + untrack(() => swipeFeedbackReset?.()); + }); @@ -176,14 +184,17 @@ ]} /> - + + {#snippet leftPreview()} + {#if cursor.previousAsset} + + {/if} + {/snippet} + + {#snippet rightPreview()} + {#if cursor.nextAsset} + + {/if} + {/snippet} + diff --git a/web/src/lib/components/asset-viewer/swipe-feedback.svelte b/web/src/lib/components/asset-viewer/swipe-feedback.svelte new file mode 100644 index 0000000000..9cae46139b --- /dev/null +++ b/web/src/lib/components/asset-viewer/swipe-feedback.svelte @@ -0,0 +1,393 @@ + + + + + +
+ {#if leftPreview} + +
+ {@render leftPreview()} +
+ {/if} + + {#if rightPreview} + +
+ {@render rightPreview()} +
+ {/if} + +
+ {@render children()} +
+
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 78fdc3a1ba..276b3cb6ba 100644 --- a/web/src/lib/components/asset-viewer/video-native-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-native-viewer.svelte @@ -1,5 +1,8 @@ -{#if showVideo} -
- {#if castManager.isCasting} -
- -
- {:else} - + + {#if showVideo} +
+ {#if castManager.isCasting} +
+ +
+ {:else} +
+ - {#if isLoading} -
- + {#if isLoading} +
+ +
+ {/if} + + {#if isFaceEditMode.value} + + {/if}
{/if} - - {#if isFaceEditMode.value} - - {/if} +
+ {/if} + {#snippet leftPreview()} + {#if previousAsset} + {/if} -
-{/if} + {/snippet} + + {#snippet rightPreview()} + {#if nextAsset} + + {/if} + {/snippet} +
+ + diff --git a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte index 57d8acd78a..291c5a14eb 100644 --- a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte @@ -1,50 +1,50 @@ {#if projectionType === ProjectionType.EQUIRECTANGULAR} - + {:else} (invocable: () => Promise, localizedMessage: string) { + async invoke(invocable: () => Promise, catchCallback?: (error: unknown) => void, finallyCallback?: () => void) { const invocation = this.startInvocation(); try { return await invocable(); } catch (error: unknown) { - handleError(error, localizedMessage); + if (catchCallback) { + catchCallback(error); + } else { + console.error(error); + } } finally { invocation.endInvocation(); + finallyCallback?.(); } } }