diff --git a/web/package-lock.json b/web/package-lock.json index 5670cf2cc9..d5a2747893 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -24,6 +24,7 @@ "lodash-es": "^4.17.21", "luxon": "^3.4.4", "socket.io-client": "^4.7.4", + "svelte-gestures": "^5.0.4", "svelte-i18n": "^4.0.0", "svelte-local-storage-store": "^0.6.4", "svelte-maplibre": "^0.9.0", @@ -7791,6 +7792,12 @@ } } }, + "node_modules/svelte-gestures": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/svelte-gestures/-/svelte-gestures-5.0.4.tgz", + "integrity": "sha512-a6cnR46AfFZ8zZyvA38A1wBLBFI7rYuAWQnmv3yYgSdbaJK/U7JG34rSkjMCePRvf4BETJSDfMNngLs5zEAfbw==", + "license": "MIT" + }, "node_modules/svelte-hmr": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", diff --git a/web/package.json b/web/package.json index 7163b04788..d87b6e6c08 100644 --- a/web/package.json +++ b/web/package.json @@ -80,6 +80,7 @@ "lodash-es": "^4.17.21", "luxon": "^3.4.4", "socket.io-client": "^4.7.4", + "svelte-gestures": "^5.0.4", "svelte-i18n": "^4.0.0", "svelte-local-storage-store": "^0.6.4", "svelte-maplibre": "^0.9.0", diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 4e98546069..69d35b9aa4 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -462,6 +462,8 @@ bind:copyImage asset={previewStackedAsset} {preloadAssets} + onPreviousAsset={() => navigateAsset('previous')} + onNextAsset={() => navigateAsset('next')} on:close={closeViewer} haveFadeTransition={false} {sharedLink} @@ -472,6 +474,8 @@ checksum={previewStackedAsset.checksum} projectionType={previewStackedAsset.exifInfo?.projectionType} loopVideo={true} + onPreviousAsset={() => navigateAsset('previous')} + onNextAsset={() => navigateAsset('next')} on:close={closeViewer} on:onVideoEnded={() => navigateAsset()} on:onVideoStarted={handleVideoStarted} @@ -487,6 +491,8 @@ checksum={asset.checksum} projectionType={asset.exifInfo?.projectionType} loopVideo={$slideshowState !== SlideshowState.PlaySlideshow} + onPreviousAsset={() => navigateAsset('previous')} + onNextAsset={() => navigateAsset('next')} on:close={closeViewer} on:onVideoEnded={() => (shouldPlayMotionPhoto = false)} /> @@ -497,7 +503,16 @@ {:else if isShowEditor && selectedEditType === 'crop'} {:else} - + navigateAsset('previous')} + onNextAsset={() => navigateAsset('next')} + on:close={closeViewer} + {sharedLink} + /> {/if} {:else} navigateAsset('previous')} + onNextAsset={() => navigateAsset('next')} on:close={closeViewer} on:onVideoEnded={() => navigateAsset()} on:onVideoStarted={handleVideoStarted} diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 7589ce130a..6f6af652b9 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -15,6 +15,7 @@ import { canCopyImagesToClipboard, copyImageToClipboard } from 'copy-image-clipboard'; import { onDestroy, onMount } from 'svelte'; import { t } from 'svelte-i18n'; + import { type SwipeCustomEvent, swipe } from 'svelte-gestures'; import { fade } from 'svelte/transition'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; @@ -24,6 +25,8 @@ export let element: HTMLDivElement | undefined = undefined; export let haveFadeTransition = true; export let sharedLink: SharedLinkResponseDto | undefined = undefined; + export let onPreviousAsset: (() => void) | null = null; + export let onNextAsset: (() => void) | null = null; export let copyImage: (() => Promise) | null = null; export let zoomToggle: (() => void) | null = null; @@ -110,6 +113,18 @@ handlePromiseError(copyImage()); }; + const onSwipe = (event: SwipeCustomEvent) => { + if ($photoZoomState.currentZoom > 1) { + return; + } + if (onNextAsset && event.detail.direction === 'left') { + onNextAsset(); + } + if (onPreviousAsset && event.detail.direction === 'right') { + onPreviousAsset(); + } + }; + onMount(() => { const onload = () => { imageLoaded = true; @@ -166,6 +181,8 @@ {$getAltText(asset)} @@ -59,6 +72,8 @@ playsinline controls class="h-full object-contain" + use:swipe + on:swipe={onSwipe} on:canplay={(e) => handleCanPlay(e.currentTarget)} on:ended={() => dispatch('onVideoEnded')} on:volumechange={(e) => { 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 129b6c8be7..5f03784c42 100644 --- a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte @@ -8,10 +8,20 @@ export let projectionType: string | null | undefined; export let checksum: string; export let loopVideo: boolean; + export let onPreviousAsset: () => void; + export let onNextAsset: () => void; {#if projectionType === ProjectionType.EQUIRECTANGULAR} {:else} - + {/if}