fix(web): memory viewer (#12649)

refactor(web): memory viewer
This commit is contained in:
Jason Rasmussen 2024-09-13 12:27:10 -04:00 committed by GitHub
parent cdbc673a59
commit a373d50c31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 242 additions and 180 deletions

View File

@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { intersectionObserver } from '$lib/actions/intersection-observer';
import { resizeObserver } from '$lib/actions/resize-observer';
import { shortcuts } from '$lib/actions/shortcut'; import { shortcuts } from '$lib/actions/shortcut';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
@ -12,16 +13,18 @@
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
import { AppRoute, QueryParameter } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { type Viewport } from '$lib/stores/assets.store'; import { type Viewport } from '$lib/stores/assets.store';
import { memoryStore } from '$lib/stores/memory.store'; import { memoryStore } from '$lib/stores/memory.store';
import { locale } from '$lib/stores/preferences.store';
import { getAssetThumbnailUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils'; import { getAssetThumbnailUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils';
import { fromLocalDateTime } from '$lib/utils/timeline-util'; import { fromLocalDateTime } from '$lib/utils/timeline-util';
import { AssetMediaSize, getMemoryLane, type AssetResponseDto } from '@immich/sdk'; import { AssetMediaSize, getMemoryLane, type AssetResponseDto, type MemoryLaneResponseDto } from '@immich/sdk';
import { import {
mdiChevronDown, mdiChevronDown,
mdiChevronLeft, mdiChevronLeft,
@ -34,100 +37,154 @@
mdiPlus, mdiPlus,
mdiSelectAll, mdiSelectAll,
} from '@mdi/js'; } from '@mdi/js';
import type { NavigationTarget } from '@sveltejs/kit';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { tweened } from 'svelte/motion';
import { fade } from 'svelte/transition';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { intersectionObserver } from '$lib/actions/intersection-observer'; import { tweened } from 'svelte/motion';
import { resizeObserver } from '$lib/actions/resize-observer'; import { derived } from 'svelte/store';
import { locale } from '$lib/stores/preferences.store'; import { fade } from 'svelte/transition';
const parseIndex = (s: string | null, max: number | null) => type MemoryIndex = {
Math.max(Math.min(Number.parseInt(s ?? '') || 0, max ?? 0), 0); memoryIndex: number;
assetIndex: number;
};
$: memoryIndex = parseIndex($page.url.searchParams.get(QueryParameter.MEMORY_INDEX), $memoryStore?.length - 1); type MemoryAsset = MemoryIndex & {
$: assetIndex = parseIndex($page.url.searchParams.get(QueryParameter.ASSET_INDEX), currentMemory?.assets.length - 1); memory: MemoryLaneResponseDto;
asset: AssetResponseDto;
$: previousMemory = $memoryStore?.[memoryIndex - 1]; previousMemory?: MemoryLaneResponseDto;
$: currentMemory = $memoryStore?.[memoryIndex]; previous?: MemoryAsset;
$: nextMemory = $memoryStore?.[memoryIndex + 1]; next?: MemoryAsset;
nextMemory?: MemoryLaneResponseDto;
$: previousAsset = currentMemory?.assets[assetIndex - 1]; };
$: currentAsset = currentMemory?.assets[assetIndex];
$: nextAsset = currentMemory?.assets[assetIndex + 1];
$: canGoForward = !!(nextMemory || nextAsset);
$: canGoBack = !!(previousMemory || previousAsset);
const viewport: Viewport = { width: 0, height: 0 };
const toNextMemory = () => goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex + 1}`);
const toPreviousMemory = () => goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex - 1}`);
const toNextAsset = () =>
goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex}&${QueryParameter.ASSET_INDEX}=${assetIndex + 1}`);
const toPreviousAsset = () =>
goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex}&${QueryParameter.ASSET_INDEX}=${assetIndex - 1}`);
const toNext = () => (nextAsset ? toNextAsset() : toNextMemory());
const toPrevious = () => (previousAsset ? toPreviousAsset() : toPreviousMemory());
const progress = tweened<number>(0, {
duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0),
});
const play = () => progress.set(1);
const pause = () => progress.set($progress);
let resetPromise = Promise.resolve();
const reset = () => (resetPromise = progress.set(0));
let paused = false;
// Play or pause progress when the paused state changes.
$: {
if (paused) {
handlePromiseError(pause());
} else {
handlePromiseError(play());
}
}
// Progress should be paused when it's no longer possible to advance.
$: paused ||= !canGoForward || galleryInView;
// Advance to the next asset or memory when progress is complete.
$: {
if ($progress === 1) {
handlePromiseError(toNext());
}
}
// Progress should be resumed when reset and not paused.
$: {
if (!$progress && !paused) {
handlePromiseError(play());
}
}
// Progress should be reset when the current memory or asset changes.
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
$: memoryIndex, assetIndex, handlePromiseError(reset());
let selectedAssets: Set<AssetResponseDto> = new Set();
$: isMultiSelectionMode = selectedAssets.size > 0;
let memoryGallery: HTMLElement; let memoryGallery: HTMLElement;
let memoryWrapper: HTMLElement; let memoryWrapper: HTMLElement;
let galleryInView = false; let galleryInView = false;
let paused = false;
let selectedAssets: Set<AssetResponseDto> = new Set();
let current: MemoryAsset | undefined = undefined;
// let memories: MemoryAsset[] = [];
let resetPromise = Promise.resolve();
const { isViewing } = assetViewingStore;
const viewport: Viewport = { width: 0, height: 0 };
const progress = tweened<number>(0, { duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0) });
const memories = derived(memoryStore, (memories) => {
memories = memories ?? [];
const memoryAssets: MemoryAsset[] = [];
let previous: MemoryAsset | undefined;
for (const [memoryIndex, memory] of memories.entries()) {
for (const [assetIndex, asset] of memory.assets.entries()) {
const current = {
memory,
memoryIndex,
previousMemory: memories[memoryIndex - 1],
nextMemory: memories[memoryIndex + 1],
asset,
assetIndex,
previous,
};
memoryAssets.push(current);
if (previous) {
previous.next = current;
}
previous = current;
}
}
return memoryAssets;
});
$: isMultiSelectionMode = selectedAssets.size > 0;
$: isAllArchived = [...selectedAssets].every((asset) => asset.isArchived); $: isAllArchived = [...selectedAssets].every((asset) => asset.isArchived);
$: isAllFavorite = [...selectedAssets].every((asset) => asset.isFavorite); $: isAllFavorite = [...selectedAssets].every((asset) => asset.isFavorite);
$: { $: selectedAssets = galleryInView ? selectedAssets : new Set();
if (!galleryInView) { $: handlePromiseError(handleProgress($progress));
selectedAssets = new Set(); $: handlePromiseError(handleAction(galleryInView ? 'pause' : 'play'));
const loadFromParams = (memories: MemoryAsset[], page: typeof $page | NavigationTarget | null) => {
const assetId = page?.params?.assetId ?? page?.url.searchParams.get(QueryParameter.ID) ?? undefined;
handlePromiseError(handleAction($isViewing ? 'pause' : 'reset'));
return memories.find((memory) => memory.asset.id === assetId) ?? memories[0];
};
const asHref = (asset: AssetResponseDto) => `?${QueryParameter.ID}=${asset.id}`;
const handleNavigate = async (asset?: AssetResponseDto) => {
if ($isViewing) {
return asset;
} }
}
await handleAction('reset');
if (!asset) {
return;
}
await goto(asHref(asset));
};
const handleNextAsset = () => handleNavigate(current?.next?.asset);
const handlePreviousAsset = () => handleNavigate(current?.previous?.asset);
const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]);
const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]);
const handleEscape = async () => goto(AppRoute.PHOTOS);
const handleSelectAll = () => (selectedAssets = new Set(current?.memory.assets || []));
const handleAction = async (action: 'reset' | 'pause' | 'play') => {
switch (action) {
case 'play': {
paused = false;
await progress.set(1);
break;
}
case 'pause': {
paused = true;
await progress.set($progress);
break;
}
case 'reset': {
paused = false;
resetPromise = progress.set(0);
break;
}
}
};
const handleProgress = async (progress: number) => {
if (progress === 0 && !paused) {
await handleAction('play');
return;
}
if (progress === 1) {
await (current?.next ? handleNextAsset() : handleAction('pause'));
}
};
const handleUpdate = () => {
if (!current) {
return;
}
current.memory.assets = current.memory.assets;
};
const handleRemove = (ids: string[]) => {
if (!current) {
return;
}
const idSet = new Set(ids);
current.memory.assets = current.memory.assets.filter((asset) => !idSet.has(asset.id));
init();
};
const init = () => {
$memoryStore = $memoryStore.filter((memory) => memory.assets.length > 0);
if ($memoryStore.length === 0) {
return handlePromiseError(goto(AppRoute.PHOTOS));
}
current = loadFromParams($memories, $page);
};
onMount(async () => { onMount(async () => {
if (!$memoryStore) { if (!$memoryStore) {
@ -137,28 +194,34 @@
day: localTime.getDate(), day: localTime.getDate(),
}); });
} }
init();
}); });
const triggerAssetUpdate = () => (currentMemory.assets = currentMemory.assets); afterNavigate(({ from, to }) => {
let target = null;
if (to?.params?.assetId) {
target = to;
} else if (from?.params?.assetId) {
target = from;
} else {
target = $page;
}
const onAssetDelete = (assetIds: string[]) => { current = loadFromParams($memories, target);
const assetIdSet = new Set(assetIds); });
currentMemory.assets = currentMemory.assets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id));
};
const handleSelectAll = () => {
selectedAssets = new Set(currentMemory.assets);
};
</script> </script>
<svelte:window <svelte:window
use:shortcuts={[ use:shortcuts={$isViewing
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => canGoForward && toNext() }, ? []
{ shortcut: { key: 'd' }, onShortcut: () => canGoForward && toNext() }, : [
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => canGoBack && toPrevious() }, { shortcut: { key: 'ArrowRight' }, onShortcut: () => handleNextAsset() },
{ shortcut: { key: 'a' }, onShortcut: () => canGoBack && toPrevious() }, { shortcut: { key: 'd' }, onShortcut: () => handleNextAsset() },
{ shortcut: { key: 'Escape' }, onShortcut: () => goto(AppRoute.PHOTOS) }, { shortcut: { key: 'ArrowLeft' }, onShortcut: () => handlePreviousAsset() },
]} { shortcut: { key: 'a' }, onShortcut: () => handlePreviousAsset() },
{ shortcut: { key: 'Escape' }, onShortcut: () => handleEscape() },
]}
/> />
{#if isMultiSelectionMode} {#if isMultiSelectionMode}
@ -172,61 +235,56 @@
<AddToAlbum shared /> <AddToAlbum shared />
</ButtonContextMenu> </ButtonContextMenu>
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={triggerAssetUpdate} /> <FavoriteAction removeFavorite={isAllFavorite} onFavorite={handleUpdate} />
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
<DownloadAction menuItem /> <DownloadAction menuItem />
<ChangeDate menuItem /> <ChangeDate menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<ArchiveAction menuItem unarchive={isAllArchived} onArchive={triggerAssetUpdate} /> <ArchiveAction menuItem unarchive={isAllArchived} onArchive={handleRemove} />
<DeleteAssets menuItem {onAssetDelete} /> <DeleteAssets menuItem onAssetDelete={handleRemove} />
</ButtonContextMenu> </ButtonContextMenu>
</AssetSelectControlBar> </AssetSelectControlBar>
</div> </div>
{/if} {/if}
<section id="memory-viewer" class="w-full bg-immich-dark-gray" bind:this={memoryWrapper}> <section id="memory-viewer" class="w-full bg-immich-dark-gray" bind:this={memoryWrapper}>
{#if currentMemory} {#if current && current.memory.assets.length > 0}
<ControlAppBar on:close={() => goto(AppRoute.PHOTOS)} forceDark> <ControlAppBar on:close={() => goto(AppRoute.PHOTOS)} forceDark>
<svelte:fragment slot="leading"> <svelte:fragment slot="leading">
<p class="text-lg"> <p class="text-lg">
{$memoryLaneTitle(currentMemory.yearsAgo)} {$memoryLaneTitle(current.memory.yearsAgo)}
</p> </p>
</svelte:fragment> </svelte:fragment>
{#if canGoForward} <div class="flex place-content-center place-items-center gap-2 overflow-hidden">
<div class="flex place-content-center place-items-center gap-2 overflow-hidden"> <CircleIconButton
<CircleIconButton title={paused ? $t('play_memories') : $t('pause_memories')}
title={paused ? $t('play_memories') : $t('pause_memories')} icon={paused ? mdiPlay : mdiPause}
icon={paused ? mdiPlay : mdiPause} on:click={() => handleAction(paused ? 'play' : 'pause')}
on:click={() => (paused = !paused)} class="hover:text-black"
class="hover:text-black" />
/>
{#each currentMemory.assets as _, index} {#each current.memory.assets as asset, index}
<a <a class="relative w-full py-2" href={asHref(asset)}>
class="relative w-full py-2" <span class="absolute left-0 h-[2px] w-full bg-gray-500" />
href="?{QueryParameter.MEMORY_INDEX}={memoryIndex}&{QueryParameter.ASSET_INDEX}={index}" {#await resetPromise}
> <span class="absolute left-0 h-[2px] bg-white" style:width={`${index < current.assetIndex ? 100 : 0}%`} />
<span class="absolute left-0 h-[2px] w-full bg-gray-500" /> {:then}
{#await resetPromise} <span
<span class="absolute left-0 h-[2px] bg-white" style:width={`${index < assetIndex ? 100 : 0}%`} /> class="absolute left-0 h-[2px] bg-white"
{:then} style:width={`${index < current.assetIndex ? 100 : index > current.assetIndex ? 0 : $progress * 100}%`}
<span />
class="absolute left-0 h-[2px] bg-white" {/await}
style:width={`${index < assetIndex ? 100 : index > assetIndex ? 0 : $progress * 100}%`} </a>
/> {/each}
{/await}
</a>
{/each}
<div> <div>
<p class="text-small"> <p class="text-small">
{(assetIndex + 1).toLocaleString($locale)}/{currentMemory.assets.length.toLocaleString($locale)} {(current.assetIndex + 1).toLocaleString($locale)}/{current.memory.assets.length.toLocaleString($locale)}
</p> </p>
</div>
</div> </div>
{/if} </div>
</ControlAppBar> </ControlAppBar>
{#if galleryInView} {#if galleryInView}
@ -250,22 +308,17 @@
class="ml-[-100%] box-border flex h-[calc(100vh_-_180px)] w-[300%] items-center justify-center gap-10 overflow-hidden" class="ml-[-100%] box-border flex h-[calc(100vh_-_180px)] w-[300%] items-center justify-center gap-10 overflow-hidden"
> >
<!-- PREVIOUS MEMORY --> <!-- PREVIOUS MEMORY -->
<div <div class="h-1/2 w-[20vw] rounded-2xl {current.previousMemory ? 'opacity-25 hover:opacity-70' : 'opacity-0'}">
class="h-1/2 w-[20vw] rounded-2xl"
class:opacity-25={previousMemory}
class:opacity-0={!previousMemory}
class:hover:opacity-70={previousMemory}
>
<button <button
type="button" type="button"
class="relative h-full w-full rounded-2xl" class="relative h-full w-full rounded-2xl"
disabled={!previousMemory} disabled={!current.previousMemory}
on:click={toPreviousMemory} on:click={handlePreviousMemory}
> >
{#if previousMemory} {#if current.previousMemory && current.previousMemory.assets.length > 0}
<img <img
class="h-full w-full rounded-2xl object-cover" class="h-full w-full rounded-2xl object-cover"
src={getAssetThumbnailUrl({ id: previousMemory.assets[0].id, size: AssetMediaSize.Preview })} src={getAssetThumbnailUrl({ id: current.previousMemory.assets[0].id, size: AssetMediaSize.Preview })}
alt={$t('previous_memory')} alt={$t('previous_memory')}
draggable="false" draggable="false"
/> />
@ -279,10 +332,10 @@
/> />
{/if} {/if}
{#if previousMemory} {#if current.previousMemory}
<div class="absolute bottom-4 right-4 text-left text-white"> <div class="absolute bottom-4 right-4 text-left text-white">
<p class="text-xs font-semibold text-gray-200">{$t('previous').toUpperCase()}</p> <p class="text-xs font-semibold text-gray-200">{$t('previous').toUpperCase()}</p>
<p class="text-xl">{$memoryLaneTitle(previousMemory.yearsAgo)}</p> <p class="text-xl">{$memoryLaneTitle(current.previousMemory.yearsAgo)}</p>
</div> </div>
{/if} {/if}
</button> </button>
@ -293,12 +346,12 @@
class="main-view relative flex h-full w-[70vw] place-content-center place-items-center rounded-2xl bg-black" class="main-view relative flex h-full w-[70vw] place-content-center place-items-center rounded-2xl bg-black"
> >
<div class="relative h-full w-full rounded-2xl bg-black"> <div class="relative h-full w-full rounded-2xl bg-black">
{#key currentAsset.id} {#key current.asset.id}
<img <img
transition:fade transition:fade
class="h-full w-full rounded-2xl object-contain transition-all" class="h-full w-full rounded-2xl object-contain transition-all"
src={getAssetThumbnailUrl({ id: currentAsset.id, size: AssetMediaSize.Preview })} src={getAssetThumbnailUrl({ id: current.asset.id, size: AssetMediaSize.Preview })}
alt={currentAsset.exifInfo?.description} alt={current.asset.exifInfo?.description}
draggable="false" draggable="false"
/> />
{/key} {/key}
@ -309,59 +362,59 @@
class:opacity-100={!galleryInView} class:opacity-100={!galleryInView}
> >
<CircleIconButton <CircleIconButton
href="{AppRoute.PHOTOS}?at={currentAsset.id}" href="{AppRoute.PHOTOS}?at={current.asset.id}"
icon={mdiImageSearch} icon={mdiImageSearch}
title={$t('view_in_timeline')} title={$t('view_in_timeline')}
color="light" color="light"
/> />
</div> </div>
<!-- CONTROL BUTTONS --> <!-- CONTROL BUTTONS -->
{#if canGoBack} {#if current.previous}
<div class="absolute top-1/2 left-0 ml-4"> <div class="absolute top-1/2 left-0 ml-4">
<CircleIconButton <CircleIconButton
title={$t('previous_memory')} title={$t('previous_memory')}
icon={mdiChevronLeft} icon={mdiChevronLeft}
color="dark" color="dark"
on:click={toPrevious} on:click={handlePreviousAsset}
/> />
</div> </div>
{/if} {/if}
{#if canGoForward} {#if current.next}
<div class="absolute top-1/2 right-0 mr-4"> <div class="absolute top-1/2 right-0 mr-4">
<CircleIconButton title={$t('next_memory')} icon={mdiChevronRight} color="dark" on:click={toNext} /> <CircleIconButton
title={$t('next_memory')}
icon={mdiChevronRight}
color="dark"
on:click={handleNextAsset}
/>
</div> </div>
{/if} {/if}
<div class="absolute left-8 top-4 text-sm font-medium text-white"> <div class="absolute left-8 top-4 text-sm font-medium text-white">
<p> <p>
{fromLocalDateTime(currentMemory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL)} {fromLocalDateTime(current.memory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL)}
</p> </p>
<p> <p>
{currentAsset.exifInfo?.city || ''} {current.asset.exifInfo?.city || ''}
{currentAsset.exifInfo?.country || ''} {current.asset.exifInfo?.country || ''}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<!-- NEXT MEMORY --> <!-- NEXT MEMORY -->
<div <div class="h-1/2 w-[20vw] rounded-2xl {current.nextMemory ? 'opacity-25 hover:opacity-70' : 'opacity-0'}">
class="h-1/2 w-[20vw] rounded-xl"
class:opacity-25={nextMemory}
class:opacity-0={!nextMemory}
class:hover:opacity-70={nextMemory}
>
<button <button
type="button" type="button"
class="relative h-full w-full rounded-2xl" class="relative h-full w-full rounded-2xl"
on:click={toNextMemory} on:click={handleNextMemory}
disabled={!nextMemory} disabled={!current.nextMemory}
> >
{#if nextMemory} {#if current.nextMemory && current.nextMemory.assets.length > 0}
<img <img
class="h-full w-full rounded-2xl object-cover" class="h-full w-full rounded-2xl object-cover"
src={getAssetThumbnailUrl({ id: nextMemory.assets[0].id, size: AssetMediaSize.Preview })} src={getAssetThumbnailUrl({ id: current.nextMemory.assets[0].id, size: AssetMediaSize.Preview })}
alt={$t('next_memory')} alt={$t('next_memory')}
draggable="false" draggable="false"
/> />
@ -375,10 +428,10 @@
/> />
{/if} {/if}
{#if nextMemory} {#if current.nextMemory}
<div class="absolute bottom-4 left-4 text-left text-white"> <div class="absolute bottom-4 left-4 text-left text-white">
<p class="text-xs font-semibold text-gray-200">{$t('up_next').toUpperCase()}</p> <p class="text-xs font-semibold text-gray-200">{$t('up_next').toUpperCase()}</p>
<p class="text-xl">{$memoryLaneTitle(nextMemory.yearsAgo)}</p> <p class="text-xl">{$memoryLaneTitle(current.nextMemory.yearsAgo)}</p>
</div> </div>
{/if} {/if}
</button> </button>
@ -411,7 +464,13 @@
use:resizeObserver={({ height, width }) => ((viewport.height = height), (viewport.width = width))} use:resizeObserver={({ height, width }) => ((viewport.height = height), (viewport.width = width))}
bind:this={memoryGallery} bind:this={memoryGallery}
> >
<GalleryViewer assets={currentMemory.assets} {viewport} bind:selectedAssets /> <GalleryViewer
onNext={handleNextAsset}
onPrevious={handlePreviousAsset}
assets={current.memory.assets}
{viewport}
bind:selectedAssets
/>
</div> </div>
</section> </section>
{/if} {/if}

View File

@ -69,11 +69,11 @@
</div> </div>
{/if} {/if}
<div class="inline-block" use:resizeObserver={({ width }) => (innerWidth = width)}> <div class="inline-block" use:resizeObserver={({ width }) => (innerWidth = width)}>
{#each $memoryStore as memory, index (memory.yearsAgo)} {#each $memoryStore as memory (memory.yearsAgo)}
{#if memory.assets.length > 0} {#if memory.assets.length > 0}
<a <a
class="memory-card relative mr-8 inline-block aspect-video h-[215px] rounded-xl" class="memory-card relative mr-8 inline-block aspect-video h-[215px] rounded-xl"
href="{AppRoute.MEMORY}?{QueryParameter.MEMORY_INDEX}={index}" href="{AppRoute.MEMORY}?{QueryParameter.ID}={memory.assets[0].id}"
> >
<img <img
class="h-full w-full rounded-xl object-cover" class="h-full w-full rounded-xl object-cover"

View File

@ -24,6 +24,8 @@
export let viewport: Viewport; export let viewport: Viewport;
export let onIntersected: (() => void) | undefined = undefined; export let onIntersected: (() => void) | undefined = undefined;
export let showAssetName = false; export let showAssetName = false;
export let onPrevious: (() => Promise<AssetResponseDto | undefined>) | undefined = undefined;
export let onNext: (() => Promise<AssetResponseDto | undefined>) | undefined = undefined;
let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore; let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore;
@ -50,8 +52,9 @@
const handleNext = async () => { const handleNext = async () => {
try { try {
if (currentViewAssetIndex < assets.length - 1) { const asset = onNext ? await onNext() : assets[++currentViewAssetIndex];
setAsset(assets[++currentViewAssetIndex]); if (asset) {
setAsset(asset);
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id }); await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
} }
} catch (error) { } catch (error) {
@ -61,8 +64,9 @@
const handlePrevious = async () => { const handlePrevious = async () => {
try { try {
if (currentViewAssetIndex > 0) { const asset = onPrevious ? await onPrevious() : assets[--currentViewAssetIndex];
setAsset(assets[--currentViewAssetIndex]); if (asset) {
setAsset(asset);
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id }); await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
} }
} catch (error) { } catch (error) {

View File

@ -71,9 +71,8 @@ export const dateFormats = {
export enum QueryParameter { export enum QueryParameter {
ACTION = 'action', ACTION = 'action',
ASSET_INDEX = 'assetIndex', ID = 'id',
IS_OPEN = 'isOpen', IS_OPEN = 'isOpen',
MEMORY_INDEX = 'memoryIndex',
ONBOARDING_STEP = 'step', ONBOARDING_STEP = 'step',
OPEN_SETTING = 'openSetting', OPEN_SETTING = 'openSetting',
PREVIOUS_ROUTE = 'previousRoute', PREVIOUS_ROUTE = 'previousRoute',