|
@@ -1,8 +1,9 @@
|
|
|
<script lang="ts">
|
|
|
+ import { browser } from '$app/environment';
|
|
|
import { memoryStore } from '$lib/stores/memory.store';
|
|
|
import { DateTime } from 'luxon';
|
|
|
- import { onMount } from 'svelte';
|
|
|
- import { MemoryLaneResponseDto, api } from '@api';
|
|
|
+ import { onDestroy, onMount } from 'svelte';
|
|
|
+ import { api } from '@api';
|
|
|
import { goto } from '$app/navigation';
|
|
|
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
|
|
import Play from 'svelte-material-icons/Play.svelte';
|
|
@@ -19,15 +20,31 @@
|
|
|
import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
|
|
|
import { fade } from 'svelte/transition';
|
|
|
|
|
|
- let currentIndex = 0;
|
|
|
- let currentMemory: MemoryLaneResponseDto;
|
|
|
- let nextMemory: MemoryLaneResponseDto;
|
|
|
- let lastMemory: MemoryLaneResponseDto;
|
|
|
+ let memoryIndex: number;
|
|
|
+ $: {
|
|
|
+ const index = parseInt($page.url.searchParams.get('memory') ?? '') || 0;
|
|
|
+ memoryIndex = index < $memoryStore?.length ? index : 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ $: previousMemory = $memoryStore?.[memoryIndex - 1] || null;
|
|
|
+ $: currentMemory = $memoryStore?.[memoryIndex] || null;
|
|
|
+ $: nextMemory = $memoryStore?.[memoryIndex + 1] || null;
|
|
|
+
|
|
|
+ let assetIndex: number;
|
|
|
+ $: {
|
|
|
+ const index = parseInt($page.url.searchParams.get('asset') ?? '') || 0;
|
|
|
+ assetIndex = index < currentMemory?.assets.length ? index : 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ $: previousAsset = currentMemory?.assets[assetIndex - 1] || null;
|
|
|
+ $: currentAsset = currentMemory?.assets[assetIndex] || null;
|
|
|
+ $: nextAsset = currentMemory?.assets[assetIndex + 1] || null;
|
|
|
|
|
|
- let lastIndex = 0;
|
|
|
- let nextIndex = 0;
|
|
|
- $: showNextMemory = nextIndex <= $memoryStore?.length - 1;
|
|
|
- $: showPreviousMemory = currentIndex != 0;
|
|
|
+ $: canAdvance = !!(nextMemory || nextAsset);
|
|
|
+
|
|
|
+ $: if (!canAdvance && browser) {
|
|
|
+ pause();
|
|
|
+ }
|
|
|
|
|
|
let memoryGallery: HTMLElement;
|
|
|
let memoryWrapper: HTMLElement;
|
|
@@ -40,123 +57,69 @@
|
|
|
});
|
|
|
$memoryStore = data;
|
|
|
}
|
|
|
+ });
|
|
|
|
|
|
- const queryIndex = $page.url.searchParams.get('index');
|
|
|
- if (queryIndex != null) {
|
|
|
- currentIndex = parseInt(queryIndex);
|
|
|
- if (isNaN(currentIndex) || currentIndex > $memoryStore.length - 1) {
|
|
|
- currentIndex = 0;
|
|
|
- }
|
|
|
- }
|
|
|
+ onDestroy(() => browser && pause());
|
|
|
|
|
|
- currentMemory = $memoryStore[currentIndex];
|
|
|
+ const toPreviousMemory = () => previousMemory && goto(`?memory=${memoryIndex - 1}`);
|
|
|
|
|
|
- nextIndex = currentIndex + 1;
|
|
|
- nextMemory = $memoryStore[nextIndex];
|
|
|
+ const toNextMemory = () => nextMemory && goto(`?memory=${memoryIndex + 1}`);
|
|
|
|
|
|
- if (currentIndex > 0) {
|
|
|
- lastMemory = $memoryStore[lastIndex];
|
|
|
- }
|
|
|
- });
|
|
|
+ const toPreviousAsset = () =>
|
|
|
+ previousAsset ? goto(`?memory=${memoryIndex}&asset=${assetIndex - 1}`) : toPreviousMemory();
|
|
|
|
|
|
- const toNextMemory = (): boolean => {
|
|
|
- if (showNextMemory) {
|
|
|
- resetAutoPlay();
|
|
|
+ const toNextAsset = () =>
|
|
|
+ nextAsset ? goto(`?memory=${memoryIndex}&asset=${assetIndex + 1}`) : toNextMemory();
|
|
|
|
|
|
- currentIndex++;
|
|
|
- nextIndex = currentIndex + 1;
|
|
|
- lastIndex = currentIndex - 1;
|
|
|
+ const duration = 5000; // 5 seconds
|
|
|
|
|
|
- currentMemory = $memoryStore[currentIndex];
|
|
|
- nextMemory = $memoryStore[nextIndex];
|
|
|
- lastMemory = $memoryStore[lastIndex];
|
|
|
+ let paused = true;
|
|
|
+ let progress = 0;
|
|
|
+ let animationFrameRequest: number;
|
|
|
+ let start: number | null = null;
|
|
|
|
|
|
- return true;
|
|
|
- }
|
|
|
+ const requestDraw = () => (animationFrameRequest = requestAnimationFrame(draw));
|
|
|
|
|
|
- return false;
|
|
|
- };
|
|
|
+ const draw = (now: number) => {
|
|
|
+ requestDraw();
|
|
|
|
|
|
- const toPreviousMemory = () => {
|
|
|
- if (showPreviousMemory) {
|
|
|
- resetAutoPlay();
|
|
|
+ start ??= now - progress * duration;
|
|
|
|
|
|
- currentIndex--;
|
|
|
- nextIndex = currentIndex + 1;
|
|
|
- lastIndex = currentIndex - 1;
|
|
|
+ const elapsed = now - start;
|
|
|
+ progress = Math.min(1, elapsed / duration);
|
|
|
|
|
|
- currentMemory = $memoryStore[currentIndex];
|
|
|
- nextMemory = $memoryStore[nextIndex];
|
|
|
- lastMemory = $memoryStore[lastIndex];
|
|
|
+ if (progress !== 1) {
|
|
|
+ return;
|
|
|
}
|
|
|
- };
|
|
|
|
|
|
- let autoPlayInterval: NodeJS.Timeout;
|
|
|
- let autoPlay = false;
|
|
|
- let autoPlaySpeed = 5000;
|
|
|
- let autoPlayProgress = 0;
|
|
|
- let autoPlayIndex = 0;
|
|
|
- let canPlayNext = true;
|
|
|
-
|
|
|
- const toggleAutoPlay = () => {
|
|
|
- autoPlay = !autoPlay;
|
|
|
- if (autoPlay) {
|
|
|
- autoPlayInterval = setInterval(() => {
|
|
|
- if (!canPlayNext) return;
|
|
|
-
|
|
|
- window.requestAnimationFrame(() => {
|
|
|
- autoPlayProgress++;
|
|
|
- });
|
|
|
-
|
|
|
- if (autoPlayProgress > 100) {
|
|
|
- autoPlayProgress = 0;
|
|
|
- canPlayNext = false;
|
|
|
- autoPlayTransition();
|
|
|
- }
|
|
|
- }, autoPlaySpeed / 100);
|
|
|
- } else {
|
|
|
- clearInterval(autoPlayInterval);
|
|
|
- }
|
|
|
+ toNextAsset();
|
|
|
+ start = now;
|
|
|
};
|
|
|
|
|
|
- const autoPlayTransition = () => {
|
|
|
- if (autoPlayIndex < currentMemory.assets.length - 1) {
|
|
|
- autoPlayIndex++;
|
|
|
- } else {
|
|
|
- const canAdvance = toNextMemory();
|
|
|
- if (!canAdvance) {
|
|
|
- autoPlay = false;
|
|
|
- clearInterval(autoPlayInterval);
|
|
|
- return;
|
|
|
- }
|
|
|
+ const play = () => {
|
|
|
+ if (!canAdvance) {
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- // Delay for nicer animation of the progress bar
|
|
|
- setTimeout(() => {
|
|
|
- canPlayNext = true;
|
|
|
- }, 250);
|
|
|
+ paused = false;
|
|
|
+ requestDraw();
|
|
|
};
|
|
|
|
|
|
- const resetAutoPlay = () => {
|
|
|
- autoPlayIndex = 0;
|
|
|
- autoPlayProgress = 0;
|
|
|
+ const pause = () => {
|
|
|
+ paused = true;
|
|
|
+ cancelAnimationFrame(animationFrameRequest);
|
|
|
+ resetStart();
|
|
|
};
|
|
|
|
|
|
- const toNextCurrentAsset = () => {
|
|
|
- autoPlayIndex++;
|
|
|
-
|
|
|
- if (autoPlayIndex > currentMemory.assets.length - 1) {
|
|
|
- toNextMemory();
|
|
|
- }
|
|
|
+ const resetProgress = () => {
|
|
|
+ progress = 0;
|
|
|
+ resetStart();
|
|
|
};
|
|
|
|
|
|
- const toPreviousCurrentAsset = () => {
|
|
|
- autoPlayIndex--;
|
|
|
+ const resetStart = () => (start = null);
|
|
|
|
|
|
- if (autoPlayIndex < 0) {
|
|
|
- toPreviousMemory();
|
|
|
- }
|
|
|
- };
|
|
|
+ // Progress should be reset when the current memory or asset changes.
|
|
|
+ $: memoryIndex, assetIndex, resetProgress();
|
|
|
</script>
|
|
|
|
|
|
<section id="memory-viewer" class="w-full bg-immich-dark-gray" bind:this={memoryWrapper}>
|
|
@@ -170,19 +133,20 @@
|
|
|
|
|
|
{#if !galleryInView}
|
|
|
<div class="flex place-items-center place-content-center overflow-hidden gap-2">
|
|
|
- <CircleIconButton logo={autoPlay ? Pause : Play} forceDark on:click={toggleAutoPlay} />
|
|
|
+ <CircleIconButton
|
|
|
+ logo={paused ? Play : Pause}
|
|
|
+ forceDark
|
|
|
+ on:click={paused ? play : pause}
|
|
|
+ />
|
|
|
|
|
|
<div class="relative w-full">
|
|
|
<span class="absolute left-0 w-full h-[2px] bg-gray-500" />
|
|
|
- <span
|
|
|
- class="absolute left-0 h-[2px] bg-white transition-all"
|
|
|
- style:width={`${autoPlayProgress}%`}
|
|
|
- />
|
|
|
+ <span class="absolute left-0 h-[2px] bg-white" style:width={`${progress * 100}%`} />
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
<p class="text-small">
|
|
|
- {autoPlayIndex + 1}/{currentMemory.assets.length}
|
|
|
+ {assetIndex + 1}/{currentMemory.assets.length}
|
|
|
</p>
|
|
|
</div>
|
|
|
</div>
|
|
@@ -211,28 +175,28 @@
|
|
|
<!-- PREVIOUS MEMORY -->
|
|
|
<div
|
|
|
class="rounded-2xl w-[20vw] h-1/2"
|
|
|
- class:opacity-25={showPreviousMemory}
|
|
|
- class:opacity-0={!showPreviousMemory}
|
|
|
- class:hover:opacity-70={showPreviousMemory}
|
|
|
+ class:opacity-25={previousMemory}
|
|
|
+ class:opacity-0={!previousMemory}
|
|
|
+ class:hover:opacity-70={previousMemory}
|
|
|
>
|
|
|
<button
|
|
|
class="rounded-2xl h-full w-full relative"
|
|
|
- disabled={!showPreviousMemory}
|
|
|
+ disabled={!previousMemory}
|
|
|
on:click={toPreviousMemory}
|
|
|
>
|
|
|
<img
|
|
|
class="rounded-2xl h-full w-full object-cover"
|
|
|
- src={showPreviousMemory && lastMemory
|
|
|
- ? api.getAssetThumbnailUrl(lastMemory.assets[0].id, 'JPEG')
|
|
|
+ src={previousMemory
|
|
|
+ ? api.getAssetThumbnailUrl(previousMemory.assets[0].id, 'JPEG')
|
|
|
: noThumbnailUrl}
|
|
|
alt=""
|
|
|
draggable="false"
|
|
|
/>
|
|
|
|
|
|
- {#if showPreviousMemory}
|
|
|
+ {#if previousMemory}
|
|
|
<div class="absolute right-4 bottom-4 text-white text-left">
|
|
|
<p class="font-semibold text-xs text-gray-200">PREVIOUS</p>
|
|
|
- <p class="text-xl">{lastMemory.title}</p>
|
|
|
+ <p class="text-xl">{previousMemory.title}</p>
|
|
|
</div>
|
|
|
{/if}
|
|
|
</button>
|
|
@@ -247,29 +211,33 @@
|
|
|
<div class="absolute h-full flex justify-between w-full">
|
|
|
<div class="flex h-full flex-col place-content-center place-items-center ml-4">
|
|
|
<div class="inline-block">
|
|
|
- <CircleIconButton
|
|
|
- logo={ChevronLeft}
|
|
|
- backgroundColor="#202123"
|
|
|
- on:click={toPreviousCurrentAsset}
|
|
|
- />
|
|
|
+ {#if previousMemory || previousAsset}
|
|
|
+ <CircleIconButton
|
|
|
+ logo={ChevronLeft}
|
|
|
+ backgroundColor="#202123"
|
|
|
+ on:click={toPreviousAsset}
|
|
|
+ />
|
|
|
+ {/if}
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="flex h-full flex-col place-content-center place-items-center mr-4">
|
|
|
<div class="inline-block">
|
|
|
- <CircleIconButton
|
|
|
- logo={ChevronRight}
|
|
|
- backgroundColor="#202123"
|
|
|
- on:click={toNextCurrentAsset}
|
|
|
- />
|
|
|
+ {#if canAdvance}
|
|
|
+ <CircleIconButton
|
|
|
+ logo={ChevronRight}
|
|
|
+ backgroundColor="#202123"
|
|
|
+ on:click={toNextAsset}
|
|
|
+ />
|
|
|
+ {/if}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- {#key currentMemory.assets[autoPlayIndex].id}
|
|
|
+ {#key currentAsset.id}
|
|
|
<img
|
|
|
transition:fade|local
|
|
|
class="rounded-2xl w-full h-full object-contain transition-all"
|
|
|
- src={api.getAssetThumbnailUrl(currentMemory.assets[autoPlayIndex].id, 'JPEG')}
|
|
|
+ src={api.getAssetThumbnailUrl(currentAsset.id, 'JPEG')}
|
|
|
alt=""
|
|
|
draggable="false"
|
|
|
/>
|
|
@@ -282,8 +250,8 @@
|
|
|
)}
|
|
|
</p>
|
|
|
<p>
|
|
|
- {currentMemory.assets[autoPlayIndex].exifInfo?.city || ''}
|
|
|
- {currentMemory.assets[autoPlayIndex].exifInfo?.country || ''}
|
|
|
+ {currentAsset.exifInfo?.city || ''}
|
|
|
+ {currentAsset.exifInfo?.country || ''}
|
|
|
</p>
|
|
|
</div>
|
|
|
</div>
|
|
@@ -292,25 +260,25 @@
|
|
|
<!-- NEXT MEMORY -->
|
|
|
<div
|
|
|
class="rounded-xl w-[20vw] h-1/2"
|
|
|
- class:opacity-25={showNextMemory}
|
|
|
- class:opacity-0={!showNextMemory}
|
|
|
- class:hover:opacity-70={showNextMemory}
|
|
|
+ class:opacity-25={nextMemory}
|
|
|
+ class:opacity-0={!nextMemory}
|
|
|
+ class:hover:opacity-70={nextMemory}
|
|
|
>
|
|
|
<button
|
|
|
class="rounded-2xl h-full w-full relative"
|
|
|
on:click={toNextMemory}
|
|
|
- disabled={!showNextMemory}
|
|
|
+ disabled={!nextMemory}
|
|
|
>
|
|
|
<img
|
|
|
class="rounded-2xl h-full w-full object-cover"
|
|
|
- src={showNextMemory
|
|
|
+ src={nextMemory
|
|
|
? api.getAssetThumbnailUrl(nextMemory.assets[0].id, 'JPEG')
|
|
|
: noThumbnailUrl}
|
|
|
alt=""
|
|
|
draggable="false"
|
|
|
/>
|
|
|
|
|
|
- {#if showNextMemory}
|
|
|
+ {#if nextMemory}
|
|
|
<div class="absolute left-4 bottom-4 text-white text-left">
|
|
|
<p class="font-semibold text-xs text-gray-200">UP NEXT</p>
|
|
|
<p class="text-xl">{nextMemory.title}</p>
|