|
@@ -16,13 +16,17 @@
|
|
|
import { ProjectionType } from '$lib/constants';
|
|
|
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
|
|
import ProfileImageCropper from '../shared-components/profile-image-cropper.svelte';
|
|
|
-
|
|
|
+ import Pause from 'svelte-material-icons/Pause.svelte';
|
|
|
+ import Play from 'svelte-material-icons/Play.svelte';
|
|
|
import { isShowDetail } from '$lib/stores/preferences.store';
|
|
|
import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
|
|
|
import NavigationArea from './navigation-area.svelte';
|
|
|
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 Close from 'svelte-material-icons/Close.svelte';
|
|
|
+ import ProgressBar, { ProgressBarStatus } from '../shared-components/progress-bar/progress-bar.svelte';
|
|
|
|
|
|
export let assetStore: AssetStore | null = null;
|
|
|
export let asset: AssetResponseDto;
|
|
@@ -47,6 +51,7 @@
|
|
|
let isShowProfileImageCrop = false;
|
|
|
let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : true;
|
|
|
let canCopyImagesToClipboard: boolean;
|
|
|
+
|
|
|
const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo.key, keyInfo.shiftKey);
|
|
|
|
|
|
onMount(async () => {
|
|
@@ -125,12 +130,25 @@
|
|
|
|
|
|
const closeViewer = () => dispatch('close');
|
|
|
|
|
|
- const navigateAssetForward = (e?: Event) => {
|
|
|
+ const navigateAssetForward = async (e?: Event) => {
|
|
|
+ if (isSlideshowMode && assetStore && progressBar) {
|
|
|
+ const hasNext = await assetStore.getNextAssetId(asset.id);
|
|
|
+ if (hasNext) {
|
|
|
+ progressBar.restart(true);
|
|
|
+ } else {
|
|
|
+ handleStopSlideshow();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
e?.stopPropagation();
|
|
|
dispatch('next');
|
|
|
};
|
|
|
|
|
|
const navigateAssetBackward = (e?: Event) => {
|
|
|
+ if (isSlideshowMode && progressBar) {
|
|
|
+ progressBar.restart(true);
|
|
|
+ }
|
|
|
+
|
|
|
e?.stopPropagation();
|
|
|
dispatch('previous');
|
|
|
};
|
|
@@ -263,36 +281,104 @@
|
|
|
handleError(error, `Unable to submit job`);
|
|
|
}
|
|
|
};
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Slide show mode
|
|
|
+ */
|
|
|
+
|
|
|
+ let isSlideshowMode = false;
|
|
|
+ let assetViewerHtmlElement: HTMLElement;
|
|
|
+ let progressBar: ProgressBar;
|
|
|
+ let progressBarStatus: ProgressBarStatus;
|
|
|
+
|
|
|
+ const handleVideoStarted = () => {
|
|
|
+ if (isSlideshowMode) {
|
|
|
+ progressBar.restart(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleVideoEnded = async () => {
|
|
|
+ if (isSlideshowMode) {
|
|
|
+ await navigateAssetForward();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handlePlaySlideshow = async () => {
|
|
|
+ try {
|
|
|
+ await assetViewerHtmlElement.requestFullscreen();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error entering fullscreen', error);
|
|
|
+ } finally {
|
|
|
+ isSlideshowMode = true;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleStopSlideshow = async () => {
|
|
|
+ try {
|
|
|
+ await document.exitFullscreen();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error exiting fullscreen', error);
|
|
|
+ } finally {
|
|
|
+ isSlideshowMode = false;
|
|
|
+ progressBar.restart(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
</script>
|
|
|
|
|
|
<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">
|
|
|
- <AssetViewerNavBar
|
|
|
- {asset}
|
|
|
- isMotionPhotoPlaying={shouldPlayMotionPhoto}
|
|
|
- showCopyButton={canCopyImagesToClipboard && asset.type === AssetTypeEnum.Image}
|
|
|
- showZoomButton={asset.type === AssetTypeEnum.Image}
|
|
|
- showMotionPlayButton={!!asset.livePhotoVideoId}
|
|
|
- showDownloadButton={shouldShowDownloadButton}
|
|
|
- on:goBack={closeViewer}
|
|
|
- on:showDetail={showDetailInfoHandler}
|
|
|
- on:download={() => downloadFile(asset)}
|
|
|
- on:delete={() => (isShowDeleteConfirmation = true)}
|
|
|
- on:favorite={toggleFavorite}
|
|
|
- on:addToAlbum={() => openAlbumPicker(false)}
|
|
|
- on:addToSharedAlbum={() => openAlbumPicker(true)}
|
|
|
- on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)}
|
|
|
- on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)}
|
|
|
- on:toggleArchive={toggleArchive}
|
|
|
- on:asProfileImage={() => (isShowProfileImageCrop = true)}
|
|
|
- on:runJob={({ detail: job }) => handleRunJob(job)}
|
|
|
- />
|
|
|
+ {#if isSlideshowMode}
|
|
|
+ <!-- SlideShowController -->
|
|
|
+ <div class="flex">
|
|
|
+ <div class="m-4 flex gap-2">
|
|
|
+ <CircleIconButton logo={Close} on:click={handleStopSlideshow} title="Exit Slideshow" />
|
|
|
+ <CircleIconButton
|
|
|
+ logo={progressBarStatus === ProgressBarStatus.Paused ? Play : Pause}
|
|
|
+ on:click={() => (progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())}
|
|
|
+ title={progressBarStatus === ProgressBarStatus.Paused ? 'Play' : 'Pause'}
|
|
|
+ />
|
|
|
+ <CircleIconButton logo={ChevronLeft} on:click={navigateAssetBackward} title="Previous" />
|
|
|
+ <CircleIconButton logo={ChevronRight} on:click={navigateAssetForward} title="Next" />
|
|
|
+ </div>
|
|
|
+ <ProgressBar
|
|
|
+ autoplay
|
|
|
+ bind:this={progressBar}
|
|
|
+ bind:status={progressBarStatus}
|
|
|
+ on:done={navigateAssetForward}
|
|
|
+ duration={5000}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ {:else}
|
|
|
+ <AssetViewerNavBar
|
|
|
+ {asset}
|
|
|
+ isMotionPhotoPlaying={shouldPlayMotionPhoto}
|
|
|
+ showCopyButton={canCopyImagesToClipboard && asset.type === AssetTypeEnum.Image}
|
|
|
+ showZoomButton={asset.type === AssetTypeEnum.Image}
|
|
|
+ showMotionPlayButton={!!asset.livePhotoVideoId}
|
|
|
+ showDownloadButton={shouldShowDownloadButton}
|
|
|
+ showSlideshow={!!assetStore}
|
|
|
+ on:goBack={closeViewer}
|
|
|
+ on:showDetail={showDetailInfoHandler}
|
|
|
+ on:download={() => downloadFile(asset)}
|
|
|
+ on:delete={() => (isShowDeleteConfirmation = true)}
|
|
|
+ on:favorite={toggleFavorite}
|
|
|
+ on:addToAlbum={() => openAlbumPicker(false)}
|
|
|
+ on:addToSharedAlbum={() => openAlbumPicker(true)}
|
|
|
+ on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)}
|
|
|
+ on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)}
|
|
|
+ on:toggleArchive={toggleArchive}
|
|
|
+ on:asProfileImage={() => (isShowProfileImageCrop = true)}
|
|
|
+ on:runJob={({ detail: job }) => handleRunJob(job)}
|
|
|
+ on:playSlideShow={handlePlaySlideshow}
|
|
|
+ />
|
|
|
+ {/if}
|
|
|
</div>
|
|
|
|
|
|
- {#if showNavigation}
|
|
|
+ {#if !isSlideshowMode && showNavigation}
|
|
|
<div class="column-span-1 z-[999] col-start-1 row-span-1 row-start-2 mb-[60px] justify-self-start">
|
|
|
<NavigationArea on:click={navigateAssetBackward}><ChevronLeft size="36" /></NavigationArea>
|
|
|
</div>
|
|
@@ -323,18 +409,23 @@
|
|
|
<PhotoViewer {asset} on:close={closeViewer} />
|
|
|
{/if}
|
|
|
{:else}
|
|
|
- <VideoViewer assetId={asset.id} on:close={closeViewer} />
|
|
|
+ <VideoViewer
|
|
|
+ assetId={asset.id}
|
|
|
+ on:close={closeViewer}
|
|
|
+ on:onVideoEnded={handleVideoEnded}
|
|
|
+ on:onVideoStarted={handleVideoStarted}
|
|
|
+ />
|
|
|
{/if}
|
|
|
{/key}
|
|
|
</div>
|
|
|
|
|
|
- {#if showNavigation}
|
|
|
+ {#if !isSlideshowMode && showNavigation}
|
|
|
<div class="z-[999] col-span-1 col-start-4 row-span-1 row-start-2 mb-[60px] justify-self-end">
|
|
|
<NavigationArea on:click={navigateAssetForward}><ChevronRight size="36" /></NavigationArea>
|
|
|
</div>
|
|
|
{/if}
|
|
|
|
|
|
- {#if $isShowDetail}
|
|
|
+ {#if !isSlideshowMode && $isShowDetail}
|
|
|
<div
|
|
|
transition:fly={{ duration: 150 }}
|
|
|
id="detail-panel"
|