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 @@
@@ -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}