diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 7a2f97bb655ee..ea5d6e92759cb 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -8,7 +8,6 @@ import { updateNumberOfComments } from '$lib/stores/activity.store'; import { closeEditorCofirm } from '$lib/stores/asset-editor.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import type { AssetStore } from '$lib/stores/assets.store'; import { isShowDetail } from '$lib/stores/preferences.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { user } from '$lib/stores/user.store'; @@ -49,8 +48,9 @@ import VideoViewer from './video-wrapper-viewer.svelte'; import ImagePanoramaViewer from './image-panorama-viewer.svelte'; + type HasAsset = boolean; + interface Props { - assetStore?: AssetStore | null; asset: AssetResponseDto; preloadAssets?: AssetResponseDto[]; showNavigation?: boolean; @@ -61,13 +61,13 @@ onAction?: OnAction | undefined; reactions?: ActivityResponseDto[]; onClose: (dto: { asset: AssetResponseDto }) => void; - onNext: () => void; - onPrevious: () => void; + onNext: () => Promise; + onPrevious: () => Promise; + onRandom: () => Promise; copyImage?: () => Promise; } let { - assetStore = null, asset = $bindable(), preloadAssets = $bindable([]), showNavigation = true, @@ -80,6 +80,7 @@ onClose, onNext, onPrevious, + onRandom, copyImage = $bindable(), }: Props = $props(); @@ -271,22 +272,6 @@ }); }; - const navigateAssetRandom = async () => { - if (!assetStore) { - return; - } - - const asset = await assetStore.getRandomAsset(); - if (!asset) { - return; - } - - slideshowHistory.queue(asset); - - setAsset(asset); - $restartSlideshowProgress = true; - }; - const navigateAsset = async (order?: 'previous' | 'next', e?: Event) => { if (!order) { if ($slideshowState === SlideshowState.PlaySlideshow) { @@ -296,23 +281,30 @@ } } + e?.stopPropagation(); + + let hasNext = false; + if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) { - return (order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next()) || navigateAssetRandom(); + 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 onPrevious() : await onNext(); } - if ($slideshowState === SlideshowState.PlaySlideshow && assetStore) { - const hasNext = - order === 'previous' ? await assetStore.getPreviousAsset(asset) : await assetStore.getNextAsset(asset); + if ($slideshowState === SlideshowState.PlaySlideshow) { if (hasNext) { $restartSlideshowProgress = true; } else { await handleStopSlideshow(); } } - - e?.stopPropagation(); - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - order === 'previous' ? onPrevious() : onNext(); }; // const showEditorHandler = () => { @@ -435,7 +427,7 @@ {person} {stack} showDetailButton={enableDetailPanel} - showSlideshow={!!assetStore} + showSlideshow={true} onZoomImage={zoomToggle} onCopyImage={copyImage} onAction={handleAction} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 55f935c8ddff7..fd98f7e6a3a10 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -527,6 +527,18 @@ return !!nextAsset; }; + const handleRandom = async () => { + const randomAsset = await $assetStore.getRandomAsset(); + + if (randomAsset) { + const preloadAsset = await $assetStore.getNextAsset(randomAsset); + assetViewingStore.setAsset(randomAsset, preloadAsset ? [preloadAsset] : []); + await navigate({ targetRoute: 'current', assetId: randomAsset.id }); + } + + return randomAsset; + }; + const handleClose = async ({ asset }: { asset: AssetResponseDto }) => { assetViewingStore.showAssetViewer(false); showSkeleton = true; @@ -911,7 +923,6 @@ {#await import('../asset-viewer/asset-viewer.svelte') then { default: AssetViewer }} {/await} diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 8f8a067a902ba..192a6a00c5719 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -34,6 +34,7 @@ isShowDeleteConfirmation?: boolean; onPrevious?: (() => Promise) | undefined; onNext?: (() => Promise) | undefined; + onRandom?: (() => Promise) | undefined; } let { @@ -47,6 +48,7 @@ isShowDeleteConfirmation = $bindable(false), onPrevious = undefined, onNext = undefined, + onRandom = undefined, }: Props = $props(); let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore; @@ -202,35 +204,71 @@ })(), ); - const handleNext = async () => { + const handleNext = async (): Promise => { try { let asset: AssetResponseDto | undefined; if (onNext) { asset = await onNext(); } else { - currentViewAssetIndex = Math.min(currentViewAssetIndex + 1, assets.length - 1); - asset = assets[currentViewAssetIndex]; + currentViewAssetIndex = currentViewAssetIndex + 1; + asset = currentViewAssetIndex < assets.length ? assets[currentViewAssetIndex] : undefined; + } + + if (!asset) { + return false; + } + + await navigateToAsset(asset); + return true; + } catch (error) { + handleError(error, $t('errors.cannot_navigate_next_asset')); + return false; + } + }; + + const handleRandom = async (): Promise => { + try { + let asset: AssetResponseDto | undefined; + if (onRandom) { + asset = await onRandom(); + } else { + if (assets.length > 0) { + const randomIndex = Math.floor(Math.random() * assets.length); + asset = assets[randomIndex]; + } + } + + if (!asset) { + return null; } await navigateToAsset(asset); + return asset; } catch (error) { handleError(error, $t('errors.cannot_navigate_next_asset')); + return null; } }; - const handlePrevious = async () => { + const handlePrevious = async (): Promise => { try { let asset: AssetResponseDto | undefined; if (onPrevious) { asset = await onPrevious(); } else { - currentViewAssetIndex = Math.max(currentViewAssetIndex - 1, 0); - asset = assets[currentViewAssetIndex]; + currentViewAssetIndex = currentViewAssetIndex - 1; + asset = currentViewAssetIndex >= 0 ? assets[currentViewAssetIndex] : undefined; + } + + if (!asset) { + return false; } await navigateToAsset(asset); + return true; } catch (error) { handleError(error, $t('errors.cannot_navigate_previous_asset')); + return false; } }; @@ -372,6 +410,7 @@ onAction={handleAction} onPrevious={handlePrevious} onNext={handleNext} + onRandom={handleRandom} onClose={() => { assetViewingStore.showAssetViewer(false); handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); diff --git a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte index 2afeffb6e4ef1..8464dad9a446b 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte @@ -41,6 +41,34 @@ assetViewingStore.showAssetViewer(false); }); + const onNext = () => { + const index = getAssetIndex($viewingAsset.id) + 1; + if (index >= assets.length) { + return Promise.resolve(false); + } + setAsset(assets[index]); + return Promise.resolve(true); + }; + + const onPrevious = () => { + const index = getAssetIndex($viewingAsset.id) - 1; + if (index < 0) { + return Promise.resolve(false); + } + setAsset(assets[index]); + return Promise.resolve(true); + }; + + const onRandom = () => { + if (assets.length <= 0) { + return Promise.resolve(null); + } + const index = Math.floor(Math.random() * assets.length); + const asset = assets[index]; + setAsset(asset); + return Promise.resolve(asset); + }; + const onSelectAsset = (asset: AssetResponseDto) => { if (selectedAssetIds.has(asset.id)) { selectedAssetIds.delete(asset.id); @@ -152,14 +180,9 @@ 1} - onNext={() => { - const index = getAssetIndex($viewingAsset.id) + 1; - setAsset(assets[index % assets.length]); - }} - onPrevious={() => { - const index = getAssetIndex($viewingAsset.id) - 1 + assets.length; - setAsset(assets[index % assets.length]); - }} + {onNext} + {onPrevious} + {onRandom} onClose={() => { assetViewingStore.showAssetViewer(false); handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); diff --git a/web/src/lib/stores/asset-viewing.store.ts b/web/src/lib/stores/asset-viewing.store.ts index 2e6e44511d36b..689556b52242a 100644 --- a/web/src/lib/stores/asset-viewing.store.ts +++ b/web/src/lib/stores/asset-viewing.store.ts @@ -15,9 +15,10 @@ function createAssetViewingStore() { viewState.set(true); }; - const setAssetId = async (id: string) => { + const setAssetId = async (id: string): Promise => { const asset = await getAssetInfo({ id, key: getKey() }); setAsset(asset); + return asset; }; const showAssetViewer = (show: boolean) => { diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte index 613ae4d66bed7..2239a21cd5699 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -107,14 +107,28 @@ if (viewingAssetCursor < viewingAssets.length - 1) { await setAssetId(viewingAssets[++viewingAssetCursor]); await navigate({ targetRoute: 'current', assetId: $viewingAsset.id }); + return true; } + return false; } async function navigatePrevious() { if (viewingAssetCursor > 0) { await setAssetId(viewingAssets[--viewingAssetCursor]); await navigate({ targetRoute: 'current', assetId: $viewingAsset.id }); + return true; } + return false; + } + + async function navigateRandom() { + if (viewingAssets.length <= 0) { + return null; + } + const index = Math.floor(Math.random() * viewingAssets.length); + const asset = await setAssetId(viewingAssets[index]); + await navigate({ targetRoute: 'current', assetId: $viewingAsset.id }); + return asset; } @@ -132,6 +146,7 @@ showNavigation={viewingAssets.length > 1} onNext={navigateNext} onPrevious={navigatePrevious} + onRandom={navigateRandom} onClose={() => { assetViewingStore.showAssetViewer(false); handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));