mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(web): shuffle slideshow order (#4277)
* feat(web): shuffle slideshow order * Fix play/stop issues * Enter/exit fullscreen mode * Prevent navigation to the next asset after exiting slideshow mode * Fix entering the slideshow mode from an album page * Simplify markup of the AssetViewer Group viewer area and navigation (prev/next/slideshow bar) controls together * Select a random asset from a random bucket * Preserve assets order in random mode * Exit fullscreen mode only if it is active * Extract SlideshowHistory class * Use traditional functions instead of arrow functions * Refactor SlideshowHistory class * Extract SlideshowBar component * Fix comments * Hide Say something in slideshow mode --------- Co-authored-by: brighteyed <sergey.kondrikov@gmail.com>
This commit is contained in:
		| @@ -29,25 +29,24 @@ | ||||
|   import { browser } from '$app/environment'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import type { AssetStore } from '$lib/stores/assets.store'; | ||||
|   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; | ||||
|   import ProgressBar, { ProgressBarStatus } from '../shared-components/progress-bar/progress-bar.svelte'; | ||||
|   import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; | ||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||
|   import { SlideshowHistory } from '$lib/utils/slideshow-history'; | ||||
|   import { featureFlags } from '$lib/stores/server-config.store'; | ||||
|   import { | ||||
|     mdiChevronLeft, | ||||
|     mdiHeartOutline, | ||||
|     mdiHeart, | ||||
|     mdiCommentOutline, | ||||
|     mdiChevronLeft, | ||||
|     mdiChevronRight, | ||||
|     mdiClose, | ||||
|     mdiImageBrokenVariant, | ||||
|     mdiPause, | ||||
|     mdiPlay, | ||||
|   } from '@mdi/js'; | ||||
|   import Icon from '$lib/components/elements/icon.svelte'; | ||||
|   import Thumbnail from '../assets/thumbnail/thumbnail.svelte'; | ||||
|   import { stackAssetsStore } from '$lib/stores/stacked-asset.store'; | ||||
|   import ActivityViewer from './activity-viewer.svelte'; | ||||
|   import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; | ||||
|   import SlideshowBar from './slideshow-bar.svelte'; | ||||
|  | ||||
|   export let assetStore: AssetStore | null = null; | ||||
|   export let asset: AssetResponseDto; | ||||
| @@ -62,6 +61,14 @@ | ||||
|  | ||||
|   let reactions: ActivityResponseDto[] = []; | ||||
|  | ||||
|   const { setAssetId } = assetViewingStore; | ||||
|   const { | ||||
|     restartProgress: restartSlideshowProgress, | ||||
|     stopProgress: stopSlideshowProgress, | ||||
|     slideshowShuffle, | ||||
|     slideshowState, | ||||
|   } = slideshowStore; | ||||
|  | ||||
|   const dispatch = createEventDispatcher<{ | ||||
|     archived: AssetResponseDto; | ||||
|     unarchived: AssetResponseDto; | ||||
| @@ -82,6 +89,8 @@ | ||||
|   let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline; | ||||
|   let shouldShowDetailButton = asset.hasMetadata; | ||||
|   let canCopyImagesToClipboard: boolean; | ||||
|   let slideshowStateUnsubscribe: () => void; | ||||
|   let shuffleSlideshowUnsubscribe: () => void; | ||||
|   let previewStackedAsset: AssetResponseDto | undefined; | ||||
|   let isShowActivity = false; | ||||
|   let isLiked: ActivityResponseDto | null = null; | ||||
| @@ -162,6 +171,23 @@ | ||||
|   onMount(async () => { | ||||
|     document.addEventListener('keydown', onKeyboardPress); | ||||
|  | ||||
|     slideshowStateUnsubscribe = slideshowState.subscribe((value) => { | ||||
|       if (value === SlideshowState.PlaySlideshow) { | ||||
|         slideshowHistory.reset(); | ||||
|         slideshowHistory.queue(asset.id); | ||||
|         handlePlaySlideshow(); | ||||
|       } else if (value === SlideshowState.StopSlideshow) { | ||||
|         handleStopSlideshow(); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     shuffleSlideshowUnsubscribe = slideshowShuffle.subscribe((value) => { | ||||
|       if (value) { | ||||
|         slideshowHistory.reset(); | ||||
|         slideshowHistory.queue(asset.id); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     if (!sharedLink) { | ||||
|       await getAllAlbums(); | ||||
|     } | ||||
| @@ -185,6 +211,14 @@ | ||||
|     if (browser) { | ||||
|       document.removeEventListener('keydown', onKeyboardPress); | ||||
|     } | ||||
|  | ||||
|     if (slideshowStateUnsubscribe) { | ||||
|       slideshowStateUnsubscribe(); | ||||
|     } | ||||
|  | ||||
|     if (shuffleSlideshowUnsubscribe) { | ||||
|       shuffleSlideshowUnsubscribe(); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   $: asset.id && !sharedLink && getAllAlbums(); // Update the album information when the asset ID changes | ||||
| @@ -263,11 +297,31 @@ | ||||
|  | ||||
|   const closeViewer = () => dispatch('close'); | ||||
|  | ||||
|   const navigateAssetRandom = async () => { | ||||
|     if (!assetStore) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const asset = await assetStore.getRandomAsset(); | ||||
|     if (!asset) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     slideshowHistory.queue(asset.id); | ||||
|  | ||||
|     setAssetId(asset.id); | ||||
|     $restartSlideshowProgress = true; | ||||
|   }; | ||||
|  | ||||
|   const navigateAssetForward = async (e?: Event) => { | ||||
|     if (isSlideshowMode && assetStore && progressBar) { | ||||
|     if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowShuffle) { | ||||
|       return slideshowHistory.next() || navigateAssetRandom(); | ||||
|     } | ||||
|  | ||||
|     if ($slideshowState === SlideshowState.PlaySlideshow && assetStore) { | ||||
|       const hasNext = await assetStore.getNextAssetId(asset.id); | ||||
|       if (hasNext) { | ||||
|         progressBar.restart(true); | ||||
|         $restartSlideshowProgress = true; | ||||
|       } else { | ||||
|         await handleStopSlideshow(); | ||||
|       } | ||||
| @@ -278,8 +332,13 @@ | ||||
|   }; | ||||
|  | ||||
|   const navigateAssetBackward = (e?: Event) => { | ||||
|     if (isSlideshowMode && progressBar) { | ||||
|       progressBar.restart(true); | ||||
|     if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowShuffle) { | ||||
|       slideshowHistory.previous(); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if ($slideshowState === SlideshowState.PlaySlideshow) { | ||||
|       $restartSlideshowProgress = true; | ||||
|     } | ||||
|  | ||||
|     e?.stopPropagation(); | ||||
| @@ -427,19 +486,21 @@ | ||||
|    * Slide show mode | ||||
|    */ | ||||
|  | ||||
|   let isSlideshowMode = false; | ||||
|   let assetViewerHtmlElement: HTMLElement; | ||||
|   let progressBar: ProgressBar; | ||||
|   let progressBarStatus: ProgressBarStatus; | ||||
|  | ||||
|   const slideshowHistory = new SlideshowHistory((assetId: string) => { | ||||
|     setAssetId(assetId); | ||||
|     $restartSlideshowProgress = true; | ||||
|   }); | ||||
|  | ||||
|   const handleVideoStarted = () => { | ||||
|     if (isSlideshowMode) { | ||||
|       progressBar.restart(false); | ||||
|     if ($slideshowState === SlideshowState.PlaySlideshow) { | ||||
|       $stopSlideshowProgress = true; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleVideoEnded = async () => { | ||||
|     if (isSlideshowMode) { | ||||
|     if ($slideshowState === SlideshowState.PlaySlideshow) { | ||||
|       await navigateAssetForward(); | ||||
|     } | ||||
|   }; | ||||
| @@ -449,19 +510,20 @@ | ||||
|       await assetViewerHtmlElement.requestFullscreen(); | ||||
|     } catch (error) { | ||||
|       console.error('Error entering fullscreen', error); | ||||
|     } finally { | ||||
|       isSlideshowMode = true; | ||||
|       $slideshowState = SlideshowState.StopSlideshow; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleStopSlideshow = async () => { | ||||
|     try { | ||||
|       if (document.fullscreenElement) { | ||||
|         await document.exitFullscreen(); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       console.error('Error exiting fullscreen', error); | ||||
|     } finally { | ||||
|       isSlideshowMode = false; | ||||
|       progressBar.restart(false); | ||||
|       $stopSlideshowProgress = true; | ||||
|       $slideshowState = SlideshowState.None; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
| @@ -498,31 +560,10 @@ | ||||
| <section | ||||
|   id="immich-asset-viewer" | ||||
|   class="fixed left-0 top-0 z-[1001] grid h-screen w-screen grid-cols-4 grid-rows-[64px_1fr] overflow-y-hidden bg-black" | ||||
|   bind:this={assetViewerHtmlElement} | ||||
| > | ||||
|   <div class="z-[1000] col-span-4 col-start-1 row-span-1 row-start-1 transition-transform"> | ||||
|     {#if isSlideshowMode} | ||||
|       <!-- SlideShowController --> | ||||
|       <div class="flex"> | ||||
|         <div class="m-4 flex gap-2"> | ||||
|           <CircleIconButton icon={mdiClose} on:click={handleStopSlideshow} title="Exit Slideshow" /> | ||||
|           <CircleIconButton | ||||
|             icon={progressBarStatus === ProgressBarStatus.Paused ? mdiPlay : mdiPause} | ||||
|             on:click={() => (progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())} | ||||
|             title={progressBarStatus === ProgressBarStatus.Paused ? 'Play' : 'Pause'} | ||||
|           /> | ||||
|           <CircleIconButton icon={mdiChevronLeft} on:click={navigateAssetBackward} title="Previous" /> | ||||
|           <CircleIconButton icon={mdiChevronRight} on:click={navigateAssetForward} title="Next" /> | ||||
|         </div> | ||||
|         <ProgressBar | ||||
|           autoplay | ||||
|           bind:this={progressBar} | ||||
|           bind:status={progressBarStatus} | ||||
|           on:done={navigateAssetForward} | ||||
|           duration={5000} | ||||
|         /> | ||||
|       </div> | ||||
|     {:else} | ||||
|   <!-- Top navigation bar --> | ||||
|   {#if $slideshowState === SlideshowState.None} | ||||
|     <div class="z-[1002] col-span-4 col-start-1 row-span-1 row-start-1 transition-transform"> | ||||
|       <AssetViewerNavBar | ||||
|         {asset} | ||||
|         isMotionPhotoPlaying={shouldPlayMotionPhoto} | ||||
| @@ -545,19 +586,30 @@ | ||||
|         on:toggleArchive={toggleArchive} | ||||
|         on:asProfileImage={() => (isShowProfileImageCrop = true)} | ||||
|         on:runJob={({ detail: job }) => handleRunJob(job)} | ||||
|         on:playSlideShow={handlePlaySlideshow} | ||||
|         on:playSlideShow={() => ($slideshowState = SlideshowState.PlaySlideshow)} | ||||
|         on:unstack={handleUnstack} | ||||
|       /> | ||||
|     {/if} | ||||
|     </div> | ||||
|   {/if} | ||||
|  | ||||
|   {#if !isSlideshowMode && showNavigation} | ||||
|     <div class="column-span-1 z-[999] col-start-1 row-span-1 row-start-2 mb-[60px] justify-self-start"> | ||||
|   {#if $slideshowState === SlideshowState.None && showNavigation} | ||||
|     <div class="z-[1001] column-span-1 col-start-1 row-span-1 row-start-2 mb-[60px] justify-self-start"> | ||||
|       <NavigationArea on:click={navigateAssetBackward}><Icon path={mdiChevronLeft} size="36" /></NavigationArea> | ||||
|     </div> | ||||
|   {/if} | ||||
|  | ||||
|   <!-- Asset Viewer --> | ||||
|   <div class="relative col-span-4 col-start-1 row-span-full row-start-1"> | ||||
|   <div class="z-[1000] relative col-start-1 col-span-4 row-start-1 row-span-full" bind:this={assetViewerHtmlElement}> | ||||
|     {#if $slideshowState != SlideshowState.None} | ||||
|       <div class="z-[1000] absolute w-full flex"> | ||||
|         <SlideshowBar | ||||
|           on:prev={navigateAssetBackward} | ||||
|           on:next={navigateAssetForward} | ||||
|           on:close={() => ($slideshowState = SlideshowState.StopSlideshow)} | ||||
|         /> | ||||
|       </div> | ||||
|     {/if} | ||||
|  | ||||
|     {#if previewStackedAsset} | ||||
|       {#key previewStackedAsset.id} | ||||
|         {#if previewStackedAsset.type === AssetTypeEnum.Image} | ||||
| @@ -603,7 +655,7 @@ | ||||
|             on:onVideoStarted={handleVideoStarted} | ||||
|           /> | ||||
|         {/if} | ||||
|         {#if isShared} | ||||
|         {#if $slideshowState === SlideshowState.None && isShared} | ||||
|           <div class="z-[9999] absolute bottom-0 right-0 mb-6 mr-6 justify-self-end"> | ||||
|             <div | ||||
|               class="w-full h-14 flex p-4 text-white items-center justify-center rounded-full gap-4 bg-immich-dark-bg bg-opacity-60" | ||||
| @@ -665,19 +717,17 @@ | ||||
|     {/if} | ||||
|   </div> | ||||
|  | ||||
|   <!-- Stack & Stack Controller --> | ||||
|  | ||||
|   {#if !isSlideshowMode && showNavigation} | ||||
|     <div class="z-[999] col-span-1 col-start-4 row-span-1 row-start-2 mb-[60px] justify-self-end"> | ||||
|   {#if $slideshowState === SlideshowState.None && showNavigation} | ||||
|     <div class="z-[1001] col-span-1 col-start-4 row-span-1 row-start-2 mb-[60px] justify-self-end"> | ||||
|       <NavigationArea on:click={navigateAssetForward}><Icon path={mdiChevronRight} size="36" /></NavigationArea> | ||||
|     </div> | ||||
|   {/if} | ||||
|  | ||||
|   {#if !isSlideshowMode && $isShowDetail} | ||||
|   {#if $slideshowState === SlideshowState.None && $isShowDetail} | ||||
|     <div | ||||
|       transition:fly={{ duration: 150 }} | ||||
|       id="detail-panel" | ||||
|       class="z-[1002] row-start-1 row-span-5 w-[360px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg" | ||||
|       class="z-[1002] row-start-1 row-span-4 w-[360px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg" | ||||
|       translate="yes" | ||||
|     > | ||||
|       <DetailPanel | ||||
|   | ||||
							
								
								
									
										78
									
								
								web/src/lib/components/asset-viewer/slideshow-bar.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								web/src/lib/components/asset-viewer/slideshow-bar.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| <script lang="ts"> | ||||
|   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; | ||||
|   import ProgressBar, { ProgressBarStatus } from '../shared-components/progress-bar/progress-bar.svelte'; | ||||
|   import { slideshowStore } from '$lib/stores/slideshow.store'; | ||||
|   import { createEventDispatcher, onDestroy, onMount } from 'svelte'; | ||||
|   import { | ||||
|     mdiChevronLeft, | ||||
|     mdiChevronRight, | ||||
|     mdiClose, | ||||
|     mdiPause, | ||||
|     mdiPlay, | ||||
|     mdiShuffle, | ||||
|     mdiShuffleDisabled, | ||||
|   } from '@mdi/js'; | ||||
|  | ||||
|   const { slideshowShuffle } = slideshowStore; | ||||
|   const { restartProgress, stopProgress } = slideshowStore; | ||||
|  | ||||
|   let progressBarStatus: ProgressBarStatus; | ||||
|   let progressBar: ProgressBar; | ||||
|  | ||||
|   let unsubscribeRestart: () => void; | ||||
|   let unsubscribeStop: () => void; | ||||
|  | ||||
|   const dispatch = createEventDispatcher<{ | ||||
|     next: void; | ||||
|     prev: void; | ||||
|     close: void; | ||||
|   }>(); | ||||
|  | ||||
|   onMount(() => { | ||||
|     unsubscribeRestart = restartProgress.subscribe((value) => { | ||||
|       if (value) { | ||||
|         progressBar.restart(value); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     unsubscribeStop = stopProgress.subscribe((value) => { | ||||
|       if (value) { | ||||
|         progressBar.restart(false); | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   onDestroy(() => { | ||||
|     if (unsubscribeRestart) { | ||||
|       unsubscribeRestart(); | ||||
|     } | ||||
|  | ||||
|     if (unsubscribeStop) { | ||||
|       unsubscribeStop(); | ||||
|     } | ||||
|   }); | ||||
| </script> | ||||
|  | ||||
| <div class="m-4 flex gap-2"> | ||||
|   <CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} title="Exit Slideshow" /> | ||||
|   {#if $slideshowShuffle} | ||||
|     <CircleIconButton icon={mdiShuffle} on:click={() => ($slideshowShuffle = false)} title="Shuffle" /> | ||||
|   {:else} | ||||
|     <CircleIconButton icon={mdiShuffleDisabled} on:click={() => ($slideshowShuffle = true)} title="No shuffle" /> | ||||
|   {/if} | ||||
|   <CircleIconButton | ||||
|     icon={progressBarStatus === ProgressBarStatus.Paused ? mdiPlay : mdiPause} | ||||
|     on:click={() => (progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())} | ||||
|     title={progressBarStatus === ProgressBarStatus.Paused ? 'Play' : 'Pause'} | ||||
|   /> | ||||
|   <CircleIconButton icon={mdiChevronLeft} on:click={() => dispatch('prev')} title="Previous" /> | ||||
|   <CircleIconButton icon={mdiChevronRight} on:click={() => dispatch('next')} title="Next" /> | ||||
| </div> | ||||
|  | ||||
| <ProgressBar | ||||
|   autoplay | ||||
|   bind:this={progressBar} | ||||
|   bind:status={progressBarStatus} | ||||
|   on:done={() => dispatch('next')} | ||||
|   duration={5000} | ||||
| /> | ||||
| @@ -304,6 +304,19 @@ export class AssetStore { | ||||
|     return this.assetToBucket[assetId]?.bucketIndex ?? null; | ||||
|   } | ||||
|  | ||||
|   async getRandomAsset(): Promise<AssetResponseDto | null> { | ||||
|     const bucket = this.buckets[Math.floor(Math.random() * this.buckets.length)] || null; | ||||
|     if (!bucket) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     if (bucket.assets.length === 0) { | ||||
|       await this.loadBucket(bucket.bucketDate, BucketPosition.Unknown); | ||||
|     } | ||||
|  | ||||
|     return bucket.assets[Math.floor(Math.random() * bucket.assets.length)] || null; | ||||
|   } | ||||
|  | ||||
|   updateAsset(_asset: AssetResponseDto) { | ||||
|     const asset = this.assets.find((asset) => asset.id === _asset.id); | ||||
|     if (!asset) { | ||||
|   | ||||
							
								
								
									
										45
									
								
								web/src/lib/stores/slideshow.store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								web/src/lib/stores/slideshow.store.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import { persisted } from 'svelte-local-storage-store'; | ||||
| import { writable } from 'svelte/store'; | ||||
|  | ||||
| export enum SlideshowState { | ||||
|   PlaySlideshow = 'play-slideshow', | ||||
|   StopSlideshow = 'stop-slideshow', | ||||
|   None = 'none', | ||||
| } | ||||
|  | ||||
| function createSlideshowStore() { | ||||
|   const restartState = writable<boolean>(false); | ||||
|   const stopState = writable<boolean>(false); | ||||
|  | ||||
|   const slideshowShuffle = persisted<boolean>('slideshow-shuffle', true); | ||||
|   const slideshowState = writable<SlideshowState>(SlideshowState.None); | ||||
|  | ||||
|   return { | ||||
|     restartProgress: { | ||||
|       subscribe: restartState.subscribe, | ||||
|       set: (value: boolean) => { | ||||
|         // Trigger an action whenever the restartProgress is set to true. Automatically | ||||
|         // reset the restart state after that | ||||
|         if (value) { | ||||
|           restartState.set(true); | ||||
|           restartState.set(false); | ||||
|         } | ||||
|       }, | ||||
|     }, | ||||
|     stopProgress: { | ||||
|       subscribe: stopState.subscribe, | ||||
|       set: (value: boolean) => { | ||||
|         // Trigger an action whenever the stopProgress is set to true. Automatically | ||||
|         // reset the stop state after that | ||||
|         if (value) { | ||||
|           stopState.set(true); | ||||
|           stopState.set(false); | ||||
|         } | ||||
|       }, | ||||
|     }, | ||||
|     slideshowShuffle, | ||||
|     slideshowState, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export const slideshowStore = createSlideshowStore(); | ||||
							
								
								
									
										40
									
								
								web/src/lib/utils/slideshow-history.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								web/src/lib/utils/slideshow-history.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| export class SlideshowHistory { | ||||
|   private history: string[] = []; | ||||
|   private index = 0; | ||||
|  | ||||
|   constructor(private onChange: (assetId: string) => void) {} | ||||
|  | ||||
|   reset() { | ||||
|     this.history = []; | ||||
|     this.index = 0; | ||||
|   } | ||||
|  | ||||
|   queue(assetId: string) { | ||||
|     this.history.push(assetId); | ||||
|  | ||||
|     // If we were at the end of the slideshow history, move the index to the new end | ||||
|     if (this.index === this.history.length - 2) { | ||||
|       this.index++; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   next(): boolean { | ||||
|     if (this.index === this.history.length - 1) { | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     this.index++; | ||||
|     this.onChange(this.history[this.index]); | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   previous(): boolean { | ||||
|     if (this.index === 0) { | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     this.index--; | ||||
|     this.onChange(this.history[this.index]); | ||||
|     return true; | ||||
|   } | ||||
| } | ||||
| @@ -29,6 +29,7 @@ | ||||
|   import { AppRoute, dateFormats } from '$lib/constants'; | ||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; | ||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||
|   import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; | ||||
|   import { AssetStore } from '$lib/stores/assets.store'; | ||||
|   import { locale } from '$lib/stores/preferences.store'; | ||||
|   import { downloadArchive } from '$lib/utils/asset-utils'; | ||||
| @@ -52,7 +53,8 @@ | ||||
|  | ||||
|   export let data: PageData; | ||||
|  | ||||
|   let { isViewing: showAssetViewer } = assetViewingStore; | ||||
|   let { isViewing: showAssetViewer, setAssetId } = assetViewingStore; | ||||
|   let { slideshowState, slideshowShuffle } = slideshowStore; | ||||
|  | ||||
|   let album = data.album; | ||||
|   $: album = data.album; | ||||
| @@ -108,6 +110,14 @@ | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   const handleStartSlideshow = async () => { | ||||
|     const asset = $slideshowShuffle ? await assetStore.getRandomAsset() : assetStore.assets[0]; | ||||
|     if (asset) { | ||||
|       setAssetId(asset.id); | ||||
|       $slideshowState = SlideshowState.PlaySlideshow; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleEscape = () => { | ||||
|     if (viewMode === ViewMode.SELECT_USERS) { | ||||
|       viewMode = ViewMode.VIEW; | ||||
| @@ -365,6 +375,9 @@ | ||||
|                 <CircleIconButton title="Album options" on:click={handleOpenAlbumOptions} icon={mdiDotsVertical}> | ||||
|                   {#if viewMode === ViewMode.ALBUM_OPTIONS} | ||||
|                     <ContextMenu {...contextMenuPosition}> | ||||
|                       {#if album.assetCount !== 0} | ||||
|                         <MenuOption on:click={handleStartSlideshow} text="Slideshow" /> | ||||
|                       {/if} | ||||
|                       <MenuOption on:click={() => (viewMode = ViewMode.SELECT_THUMBNAIL)} text="Set album cover" /> | ||||
|                     </ContextMenu> | ||||
|                   {/if} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user