mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 22:51:59 +00:00
feat(web): navigate assets with gestures (next/prev) (#11888)
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
f3e176e192
commit
c008feca63
6 changed files with 69 additions and 2 deletions
7
web/package-lock.json
generated
7
web/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'}
|
||||
<CropArea {asset} />
|
||||
{:else}
|
||||
<PhotoViewer bind:zoomToggle bind:copyImage {asset} {preloadAssets} on:close={closeViewer} {sharedLink} />
|
||||
<PhotoViewer
|
||||
bind:zoomToggle
|
||||
bind:copyImage
|
||||
{asset}
|
||||
{preloadAssets}
|
||||
onPreviousAsset={() => navigateAsset('previous')}
|
||||
onNextAsset={() => navigateAsset('next')}
|
||||
on:close={closeViewer}
|
||||
{sharedLink}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<VideoViewer
|
||||
|
@ -505,6 +520,8 @@
|
|||
checksum={asset.checksum}
|
||||
projectionType={asset.exifInfo?.projectionType}
|
||||
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
||||
onPreviousAsset={() => navigateAsset('previous')}
|
||||
onNextAsset={() => navigateAsset('next')}
|
||||
on:close={closeViewer}
|
||||
on:onVideoEnded={() => navigateAsset()}
|
||||
on:onVideoStarted={handleVideoStarted}
|
||||
|
|
|
@ -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<void>) | 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 @@
|
|||
<img
|
||||
bind:this={$photoViewer}
|
||||
src={assetFileUrl}
|
||||
use:swipe
|
||||
on:swipe={onSwipe}
|
||||
alt={$getAltText(asset)}
|
||||
class="h-full w-full {$slideshowState === SlideshowState.None
|
||||
? 'object-contain'
|
||||
|
|
|
@ -5,12 +5,16 @@
|
|||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AssetMediaSize } from '@immich/sdk';
|
||||
import { createEventDispatcher, tick } from 'svelte';
|
||||
import { swipe } from 'svelte-gestures';
|
||||
import type { SwipeCustomEvent } from 'svelte-gestures';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let assetId: string;
|
||||
export let loopVideo: boolean;
|
||||
export let checksum: string;
|
||||
export let onPreviousAsset: () => void;
|
||||
export let onNextAsset: () => void;
|
||||
|
||||
let element: HTMLVideoElement | undefined = undefined;
|
||||
let isVideoLoading = true;
|
||||
|
@ -49,6 +53,15 @@
|
|||
handleError(error, $t('errors.unable_to_play_video'));
|
||||
}
|
||||
};
|
||||
|
||||
const onSwipe = (event: SwipeCustomEvent) => {
|
||||
if (event.detail.direction === 'left') {
|
||||
onNextAsset();
|
||||
}
|
||||
if (event.detail.direction === 'right') {
|
||||
onPreviousAsset();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
|
||||
|
@ -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) => {
|
||||
|
|
|
@ -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;
|
||||
</script>
|
||||
|
||||
{#if projectionType === ProjectionType.EQUIRECTANGULAR}
|
||||
<PanoramaViewer asset={{ id: assetId, type: AssetTypeEnum.Video }} />
|
||||
{:else}
|
||||
<VideoNativeViewer {loopVideo} {checksum} {assetId} on:onVideoEnded on:onVideoStarted />
|
||||
<VideoNativeViewer
|
||||
{loopVideo}
|
||||
{checksum}
|
||||
{assetId}
|
||||
{onPreviousAsset}
|
||||
{onNextAsset}
|
||||
on:onVideoEnded
|
||||
on:onVideoStarted
|
||||
/>
|
||||
{/if}
|
||||
|
|
Loading…
Reference in a new issue